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:
- The
LiveControllermaintains_activeCardSpecies: Map<String, DetectionRecord>tracking species with visible cards. - Each inference cycle determines three sets:
- Appeared: species in current detections but not in active set → new
DetectionRecord - Ongoing: species in both → update confidence if higher
- Disappeared: species in active set but not current → remove from tracking
- Only
appearedspecies 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.Imageusing a 2048-point FFT (yielding ~12 Hz/bin frequency resolution, showing formants and harmonics clearly) with a 1024-point hop. -
_ReviewSpectrogramPainterblits a 10-second viewport centered on the playback position, with aTickeranimating 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 samerenderSpectrogram(); 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:
_TrimOverlayrenders draggable start/end handles over the spectrogram._TrimOverlayPainterdraws dimmed regions and handle graphics.- Apply Trim: removes detections outside
[trimStartSec, trimEndSec]from_detections, updates_speciesGroups, marks dirty. - Reset Trim: sets both offsets to null.
- The export system includes
trimStartSec/trimEndSecin JSON output. commitSessionTrim()(on save) andwriteTrimmedAudioFile()(on export and detection share) turn the range into real audio. The slicer streams the source frame by frame and runs insideIsolate.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
_ConfirmSpeciesSheetrather than popping the route. The sheet previews the chosen species and carries two independent heard / seen checkboxes, collapsed into aDetectionEvidence?on confirm (heard,seen,heardAndSeen, ornullwhen 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
AddSpeciesResultwith 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
SessionAnnotationin the session. - Exported as
annotations.txtin 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 = trueresumed→ auto-resume if_pausedByLifecycle- 10-minute
Timertriggers a continue/stop dialog
Localization¶
All user-facing strings use ARB keys prefixed with session:
sessionAddSpecies,sessionSearchSpecies,sessionInsertGlobally, etc.sessionAnnotations,sessionAddAnnotation,sessionAnnotationGlobalsessionTrimRecording,sessionTrimApply,sessionTrimReset,sessionTrimWarningsessionHelpTitle,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.