Recording produced no transcript, and no record of why #56

Open
opened 2026-08-10 15:09:18 -04:00 by agent · 0 comments
Member

A 62-minute recording produced audio and a track but no .vtt, and left nothing on disk saying why.

What happened

Capture 2026-08-10-135350-53qbru, recorded 2026-08-10 13:53, ended 14:56.

On the device, in Documents/:

2026-08-10-135350-53qbru_audio.m4a     23.3 MB   8/10/26, 2:56 PM
2026-08-10-135350-53qbru_track.gpx      1.4 MB   8/10/26, 2:56 PM
                                     <- no _transcription.vtt

Wes's description: "a pretty straightforward recording with no weird transitions."

What was ruled out

The audio is fine. Pulled off the device and inspected: m4af, 1 ch, 48000 Hz AAC, estimated duration 3738.58 sec (62:18), 175249 packets, 48851 bits/sec. Not truncated, not damaged.

The app did not crash. xcrun devicectl device info files --domain-type systemCrashLogs shows the most recent Stash-*.ips on 2026-08-06. Nothing on 08-10, no JetsamEvent on 08-10.

Length is not the trigger. The capture immediately before it the same morning, 2026-08-10-100318-q4vmmt, ran 3679.48 sec (61:19) — within a minute of the failing one — and transcribed cleanly to a 38 KB VTT whose last cue is 01:01:13.800 --> 01:01:18.540. 2026-08-10-121036-7iglz5 (11:11) also transcribed cleanly.

Assembly did not fail. There is no _transcription.N.vtt part, no parts folder, and no .assembling directory anywhere in the container. TranscriptAssembly keeps the parts folder on every assembly failure by design, and RecordingTranscription.assemble says so directly: "every failure here ends with the parts still on the phone and a message saying so." An empty container means assembly never had anything to assemble.

Where this points

RecordingTranscription.assemble(unwrittenParts:) early-returns on guard let directory = partsDirectory else { return }, and partsDirectory is only set inside begin() after the TranscriptPartWriter is constructed. No parts folder at all means begin() never reached that line — so transcription never started, rather than starting and producing nothing.

Two paths reach that outcome, both of which set status and return before a writer exists:

  • usableLocale() returning nil — .needsModel, .downloading, or .unsupported from SpeechModel.availability(for:)
  • the catch at RecordingTranscription.swift:126, setting .unavailable(reason:) from a thrown TranscriptionError

Worth checking whether the speech model was evicted or was mid-redownload at 13:53. Library/Caches/com.apple.speech.localspeechrecognition in the container is an empty directory last modified 8/6 — not conclusive on its own, but it is the right place to look. Two clean transcriptions earlier the same day mean whatever changed, changed between 12:21 and 13:53.

The part that made this hard to diagnose, and is arguably the real bug

The reason is computed and then discarded. status is in-memory on RecordingTranscription and is reset by reset() on the next recording. Once the recording ends, nothing on disk records why transcription did not run. MemoDetailView already concedes this in Stash/Views/MemoDetailView.swift:312-322 — the comment reads "Nothing on disk says which of them it was," and the user-facing copy has to hedge across three unrelated causes:

Nothing was transcribed for this memo. Usually that means nobody spoke, or the speech model was not on the phone when it was recorded; a transcript removed from the recordings folder looks the same.

A capture that silently produces no transcript is the failure this app most needs to be loud about, and right now the evidence is gone by the time anyone looks. Persisting the .unavailable / .needsModel reason beside the memo — a sidecar the detail screen reads back — would turn this from an unanswerable question into a one-line answer.

Possibly the same failure

2026-08-09-191821-t954x1_audio.m4a (33 KB, ~30 s, 8/9 7:18 PM) also has no .vtt and no parts folder. Short enough that "nobody spoke" is plausible, so it is not confirmed to be the same bug — noting it because if it is, the failure predates today.

Recovering the content

The audio was transcribed out-of-band with the retired whisper-cpp pipeline (16 kHz mono → 30 s segments → ggml-medium), since the memo has material Wes needs to act on. That is a recovery step, not a fix, and it does not tell us why the app produced nothing.

A 62-minute recording produced audio and a track but no `.vtt`, and left nothing on disk saying why. ## What happened Capture `2026-08-10-135350-53qbru`, recorded 2026-08-10 13:53, ended 14:56. On the device, in `Documents/`: ``` 2026-08-10-135350-53qbru_audio.m4a 23.3 MB 8/10/26, 2:56 PM 2026-08-10-135350-53qbru_track.gpx 1.4 MB 8/10/26, 2:56 PM <- no _transcription.vtt ``` Wes's description: "a pretty straightforward recording with no weird transitions." ## What was ruled out **The audio is fine.** Pulled off the device and inspected: `m4af`, 1 ch, 48000 Hz AAC, estimated duration 3738.58 sec (62:18), 175249 packets, 48851 bits/sec. Not truncated, not damaged. **The app did not crash.** `xcrun devicectl device info files --domain-type systemCrashLogs` shows the most recent `Stash-*.ips` on 2026-08-06. Nothing on 08-10, no JetsamEvent on 08-10. **Length is not the trigger.** The capture immediately before it the same morning, `2026-08-10-100318-q4vmmt`, ran 3679.48 sec (61:19) — within a minute of the failing one — and transcribed cleanly to a 38 KB VTT whose last cue is `01:01:13.800 --> 01:01:18.540`. `2026-08-10-121036-7iglz5` (11:11) also transcribed cleanly. **Assembly did not fail.** There is no `_transcription.N.vtt` part, no parts folder, and no `.assembling` directory anywhere in the container. `TranscriptAssembly` keeps the parts folder on every assembly failure by design, and `RecordingTranscription.assemble` says so directly: "every failure here ends with the parts still on the phone and a message saying so." An empty container means assembly never had anything to assemble. ## Where this points `RecordingTranscription.assemble(unwrittenParts:)` early-returns on `guard let directory = partsDirectory else { return }`, and `partsDirectory` is only set inside `begin()` after the `TranscriptPartWriter` is constructed. No parts folder at all means `begin()` never reached that line — so transcription never started, rather than starting and producing nothing. Two paths reach that outcome, both of which set `status` and return before a writer exists: - `usableLocale()` returning nil — `.needsModel`, `.downloading`, or `.unsupported` from `SpeechModel.availability(for:)` - the `catch` at `RecordingTranscription.swift:126`, setting `.unavailable(reason:)` from a thrown `TranscriptionError` Worth checking whether the speech model was evicted or was mid-redownload at 13:53. `Library/Caches/com.apple.speech.localspeechrecognition` in the container is an empty directory last modified 8/6 — not conclusive on its own, but it is the right place to look. Two clean transcriptions earlier the same day mean whatever changed, changed between 12:21 and 13:53. ## The part that made this hard to diagnose, and is arguably the real bug **The reason is computed and then discarded.** `status` is in-memory on `RecordingTranscription` and is reset by `reset()` on the next recording. Once the recording ends, nothing on disk records why transcription did not run. `MemoDetailView` already concedes this in `Stash/Views/MemoDetailView.swift:312-322` — the comment reads "Nothing on disk says which of them it was," and the user-facing copy has to hedge across three unrelated causes: > Nothing was transcribed for this memo. Usually that means nobody spoke, or the speech model was not on the phone when it was recorded; a transcript removed from the recordings folder looks the same. A capture that silently produces no transcript is the failure this app most needs to be loud about, and right now the evidence is gone by the time anyone looks. Persisting the `.unavailable` / `.needsModel` reason beside the memo — a sidecar the detail screen reads back — would turn this from an unanswerable question into a one-line answer. ## Possibly the same failure `2026-08-09-191821-t954x1_audio.m4a` (33 KB, ~30 s, 8/9 7:18 PM) also has no `.vtt` and no parts folder. Short enough that "nobody spoke" is plausible, so it is not confirmed to be the same bug — noting it because if it is, the failure predates today. ## Recovering the content The audio was transcribed out-of-band with the retired whisper-cpp pipeline (16 kHz mono → 30 s segments → `ggml-medium`), since the memo has material Wes needs to act on. That is a recovery step, not a fix, and it does not tell us why the app produced nothing.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Stash/stash-ios#56
No description provided.