跳转至

Session Review — Developer Guide

Technical documentation for the session review system.

Overview

The session review screen (session_review_screen.dart) provides post-session analysis of bird detection data. It operates on a LiveSession object and supports playback, editing, manual species addition, annotations, recording trimming, and export.

File Structure

lib/features/history/
  session_review_screen.dart          # Main screen + state management
  session_export.dart                 # Export logic (CSV, JSON, Raven Pro, ZIP)
  widgets/
    session_review_widgets.dart       # Private widget classes (part file)

Data Models

DetectionRecord

Stored in lib/features/live/live_session.dart:

Field Type Description
scientificName String Species scientific name
commonName String Species common name (English)
confidence double 0.0–1.0 confidence score
timestamp DateTime Wall-clock detection time
audioClipPath String? Path to audio clip (optional)
source DetectionSource auto (model) or manual (user-added)

Static constants unknownSpeciesName and unknownCommonName are used for unknown/unidentifiable species.

SessionAnnotation

Field Type Description
text String Free-form annotation text
createdAt DateTime When the annotation was created
offsetInRecording double? Seconds from session start (null = global)

LiveSession Extensions

Field Type Default Description
annotations List<SessionAnnotation> [] User annotations
trimStartSec double? null Trim start offset (seconds)
trimEndSec double? null Trim end offset (seconds)

Detection Counting (Live Mode)

Detection accumulation during live sessions uses card-visibility-based counting rather than time-window merging:

  1. The LiveController maintains _activeCardSpecies: Map<String, DetectionRecord> tracking species with visible cards.
  2. Each inference cycle determines three sets:
  3. Appeared: species in current detections but not in active set → new DetectionRecord
  4. Ongoing: species in both → update confidence if higher
  5. Disappeared: species in active set but not current → remove from tracking
  6. Only appeared species create new detection records. As a result, a bird calling continuously counts as a single detection, while a gap (card removal followed by reappearance) creates a second detection.

Spectrogram

The review spectrogram is computed dynamically to balance RAM usage and CPU performance, especially for long sessions:

  • Standard Rendering (Sessions < 128 MB Decoded PCM):
  • The entire audio file is decoded and resampled to the model's sample rate (32 kHz).
  • The spectrogram is pre-computed as a single full-session ui.Image using a 2048-point FFT (yielding ~12 Hz/bin frequency resolution, showing formants and harmonics clearly) with a 1024-point hop.
  • _ReviewSpectrogramPainter blits a 10-second viewport centered on the playback position, with a Ticker animating playhead movements at up to 60 fps.

  • Lazy Chunk Loading (Sessions ≥ 128 MB Decoded PCM): To prevent massive RAM allocations and out-of-memory crashes on hour-long sessions, the app uses a lazy-loading tile pipeline:

  • The session is divided into 30-second chunks. Only chunks visible in (or immediately adjacent to) the current viewport are loaded.
  • Up to 12 chunks are cached in memory; older/farthest chunks are evicted dynamically.
  • The decode and FFT calculations run inside a background isolate (Isolate.run / _runSpectrogramChunkIsolate) to prevent the UI thread from dropping frames during scroll or pinch-to-zoom actions.

  • Trim editor image: The trim editor needs one image spanning the whole recording. It is rendered through renderSpectrogramInBatches(), which decodes the file a minute at a time (reusing the FLAC seek index when there is one) and renders each batch with the same renderSpectrogram(); batch boundaries fall on exact source samples, so the result is pixel-identical to a single pass while only one batch of PCM is ever resident. Recordings above 128 MB of decoded PCM (_maxFullSpectrogramPcmBytes) still use the overlay editor.

  • FLAC-to-WAV Transcoding Optimization: FLAC files have neither a constant-bitrate layout nor a seek table, so range-decoding requires sequentially decoding the file from the beginning—leading to O(N²) time complexity for lazy chunk loading on long sessions. To avoid this:

  • If a session is large (≥ 128 MB PCM) and recorded in FLAC format, the app runs a one-time sequential transcode to a temporary WAV file in a background isolate (_runFlacTranscodeIsolate).
  • The lazy spectrogram pipeline then uses the temporary WAV file, enabling fast O(1) random-access seek-and-read operations for individual chunks.
  • The temporary WAV file is session-scoped and deleted when the review screen is disposed.

Trim System

Trimming is metadata-only — the original recording is not modified:

  1. _TrimOverlay renders draggable start/end handles over the spectrogram.
  2. _TrimOverlayPainter draws dimmed regions and handle graphics.
  3. Apply Trim: removes detections outside [trimStartSec, trimEndSec] from _detections, updates _speciesGroups, marks dirty.
  4. Reset Trim: sets both offsets to null.
  5. The export system includes trimStartSec/trimEndSec in JSON output.
  6. commitSessionTrim() (on save) and writeTrimmedAudioFile() (on export and detection share) turn the range into real audio. The slicer streams the source frame by frame and runs inside Isolate.run: the UI isolate is the platform thread on Android and iOS, and re-encoding an hour of FLAC takes tens of seconds of pure Dart work. Only paths and numbers cross the isolate boundary. AudioExportNormalizer.normalizeFileInPlace() (quiet shared audio) follows the same rule.

Add Species Overlay

AddSpeciesOverlay is a full-screen route using TaxonomyService.search():

  • Search: real-time substring match on common + scientific names (limit 30).
  • Insert modes: global (at session start), at timestamp (playhead position), replace (swaps an existing detection).
  • Confirm step: tapping a result (or finishing the "Other (specify)" dialog) opens _ConfirmSpeciesSheet rather than popping the route. The sheet previews the chosen species and carries two independent heard / seen checkboxes, collapsed into a DetectionEvidence? on confirm (heard, seen, heardAndSeen, or null when neither box is ticked). Cancelling returns to the search. Checkbox state lives on _AddSpeciesOverlayState, so it survives a cancel and carries over to the next species; it defaults to heard, or seeds from the target record in a locked replace.
  • Unknown/Other: quick action using DetectionRecord.unknownSpeciesName.
  • Returns AddSpeciesResult with the chosen species, mode, and evidence.

DetectionRecord.evidence is only ever set on manual records. Consumers must read null as not specified rather than neither heard nor seen — model detections, sessions saved before the field existed, and manual entries with both boxes clear all share that state. The shared hearing / visibility iconography lives in DetectionEvidenceBadge (lib/shared/widgets/detection_evidence_badge.dart), whose aggregateEvidence() helper ORs the flags for rows that stand in for several records (species headers, clustered timestamp rows). Exports surface the field as an Evidence column (CSV / Raven, values heard, seen, heard+seen, emitted only when some detection carries one) and as an evidence key in JSON.

Annotations

_AnnotationsSection provides a collapsible section:

  • Input field with global/timestamp toggle.
  • Each annotation stored as SessionAnnotation in the session.
  • Exported as annotations.txt in ZIP bundles.
  • Persisted to JSON alongside detections.

Export Pipeline

session_export.dart builds export files:

Format Function Notes
CSV _buildCsvExport() Header + rows, tab-delimited
JSON buildJsonExport() Full session data including annotations, trim, source
Raven Pro _buildRavenExport() Selection table format
ZIP Bundle mode Recording + CSV + JSON + Raven + annotations.txt

The _buildAnnotationsText() helper formats annotations as plain text with timestamp labels.

Audio never passes through memory as a whole. A trimmed recording and any FLAC-to-WAV conversion (the "share audio as WAV" setting) are written to a staging directory by the streaming slicer in session_audio_trim.dart, added to the ZIP by path, and deleted once writeExportZip() has streamed them into the archive in its own isolate. Only generated documents are held in memory.

Session Lifecycle (App Focus)

WidgetsBindingObserver on _LiveScreenState:

  • paused/inactive → auto-pause session, set _pausedByLifecycle = true
  • resumed → auto-resume if _pausedByLifecycle
  • 10-minute Timer triggers a continue/stop dialog

Localization

All user-facing strings use ARB keys prefixed with session:

  • sessionAddSpecies, sessionSearchSpecies, sessionInsertGlobally, etc.
  • sessionAnnotations, sessionAddAnnotation, sessionAnnotationGlobal
  • sessionTrimRecording, sessionTrimApply, sessionTrimReset, sessionTrimWarning
  • sessionHelpTitle, sessionHelpOverview, sessionHelpAddSpecies, etc.
  • sessionDurationWarningTitle, sessionDurationWarningMessage, sessionContinue

Translations are provided in all UI locale ARB files: English, German, Czech, Spanish, French, Italian, Portuguese, Dutch, Norwegian Bokmål, Polish, Russian, and Simplified Chinese.