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
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?
-}