Simplify to plain text transcription
No speaker diarization in SpeechAnalyzer, so timestamps and JSON
add complexity without value. Output is now one segment per line
as plain text. Added a backlog item for contextual strings.

Assisted-by: Claude Opus 4.6 via pi
change wsrmnmtkqnoowttyxyowunnzyxottxnz
commit 2394d58b33409f310f45779e10bda1c03c9933aa
author Alpha Chen <alpha@kejadlen.dev>
date
parent umuplmwu
diff --git a/lib/feed.rb b/lib/feed.rb
index 84b054a..1465eb7 100644
--- a/lib/feed.rb
+++ b/lib/feed.rb
@@ -21,7 +21,7 @@ module CookingIssues
     end
 
     def transcript_path
-      audio_path.pathmap("%{^cache/audio/,transcripts/}X.json")
+      audio_path.pathmap("%{^cache/audio/,transcripts/}X.txt")
     end
   end
 
diff --git a/sous_chef/PROMPT.md b/sous_chef/PROMPT.md
index 8a680df..768d8ca 100644
--- a/sous_chef/PROMPT.md
+++ b/sous_chef/PROMPT.md
@@ -24,11 +24,8 @@ care about is `SpeechTranscriber`, which does speech-to-text.
   and audio input, controls the session lifecycle.
 - `SpeechTranscriber` — a module that produces transcription results.
   Created with a locale and a preset.
-- `SpeechTranscriber.Preset` — predefined configurations. The relevant
-  ones for offline file transcription:
-  - `.transcription` — basic accurate transcription, no timestamps
-  - `.timeIndexedTranscriptionWithAlternatives` — transcription with
-    audio time ranges and alternative interpretations
+- `SpeechTranscriber.Preset` — predefined configurations. We use
+  `.transcription` for basic accurate transcription with no timestamps.
 - `SpeechTranscriber.Result` — a phrase of transcribed speech. Has:
   - `.text` — an `AttributedString` with the best interpretation. Can
     carry `audioTimeRange` attributes when the preset includes them.
@@ -95,23 +92,8 @@ To get plain text: `String(result.text.characters)`
 
 ## Output format
 
-Write JSON to the output path. Structure:
-
-```json
-{
-  "segments": [
-    {
-      "text": "transcribed text for this segment",
-      "start": 0.0,
-      "end": 5.23
-    }
-  ]
-}
-```
-
-Each segment corresponds to one `SpeechTranscriber.Result`. Times are
-in seconds from the start of the audio file. If time range attributes
-are unavailable, omit `start` and `end`.
+Write plain text to the output path. Each `SpeechTranscriber.Result`
+becomes one line, joined with newlines.
 
 ## Build
 
diff --git a/sous_chef/Sources/SousChef.swift b/sous_chef/Sources/SousChef.swift
index f8971d0..91b7b21 100644
--- a/sous_chef/Sources/SousChef.swift
+++ b/sous_chef/Sources/SousChef.swift
@@ -12,7 +12,7 @@ struct SousChef: AsyncParsableCommand {
     @Argument(help: "Path to the input audio file.")
     var input: String
 
-    @Argument(help: "Path to write the output JSON transcript.")
+    @Argument(help: "Path to write the output transcript.")
     var output: String
 
     func run() async throws {
@@ -23,7 +23,7 @@ struct SousChef: AsyncParsableCommand {
         guard let locale = await SpeechTranscriber.supportedLocale(equivalentTo: Locale(identifier: "en-US")) else {
             throw TranscriptionError.unsupportedLocale
         }
-        let transcriber = SpeechTranscriber(locale: locale, preset: .timeIndexedTranscriptionWithAlternatives)
+        let transcriber = SpeechTranscriber(locale: locale, preset: .transcription)
 
         // Install assets if needed.
         if let request = try await AssetInventory.assetInstallationRequest(supporting: [transcriber]) {
@@ -38,16 +38,14 @@ struct SousChef: AsyncParsableCommand {
         let analyzer = SpeechAnalyzer(modules: [transcriber])
 
         // Collect results in a separate task.
-        var segments: [Segment] = []
         let resultsTask = Task {
-            var collected: [Segment] = []
+            var segments: [String] = []
             for try await result in transcriber.results {
                 let text = String(result.text.characters)
-                let timeRange = extractTimeRange(from: result.text)
-                collected.append(Segment(text: text, start: timeRange?.start, end: timeRange?.end))
+                segments.append(text)
                 log("  \(text)")
             }
-            return collected
+            return segments
         }
 
         // Run the analysis.
@@ -61,36 +59,15 @@ struct SousChef: AsyncParsableCommand {
             await analyzer.cancelAndFinishNow()
         }
 
-        segments = try await resultsTask.value
+        let segments = try await resultsTask.value
 
-        // Write JSON output.
-        let transcript = Transcript(segments: segments)
-        let encoder = JSONEncoder()
-        encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
-        let data = try encoder.encode(transcript)
-        try data.write(to: outputURL)
+        // Write plain text output.
+        let text = segments.joined(separator: "\n")
+        try text.write(to: outputURL, atomically: true, encoding: .utf8)
 
         log("Done. Wrote \(segments.count) segments to \(output).")
     }
 
-    private func extractTimeRange(from text: AttributedString) -> (start: Double, end: Double)? {
-        // Walk the attributed string looking for time range attributes.
-        var earliest: Double?
-        var latest: Double?
-
-        for run in text.runs {
-            if let timeRange = run.audioTimeRange {
-                let start = CMTimeGetSeconds(timeRange.start)
-                let end = CMTimeGetSeconds(timeRange.start + timeRange.duration)
-                if earliest == nil || start < earliest! { earliest = start }
-                if latest == nil || end > latest! { latest = end }
-            }
-        }
-
-        guard let start = earliest, let end = latest else { return nil }
-        return (start, end)
-    }
-
     private func log(_ message: String) {
         FileHandle.standardError.write(Data((message + "\n").utf8))
     }
@@ -106,13 +83,3 @@ enum TranscriptionError: Error, CustomStringConvertible {
         }
     }
 }
-
-struct Transcript: Encodable {
-    let segments: [Segment]
-}
-
-struct Segment: Encodable {
-    let text: String
-    let start: Double?
-    let end: Double?
-}