@@ -5,7 +5,7 @@ import Foundation
55
66struct Stream : AsyncParsableCommand {
77 static let configuration = CommandConfiguration (
8- abstract: " Stream live audio transcription from microphone or system audio . "
8+ abstract: " Stream live audio transcription from microphone. "
99 )
1010
1111 @Option ( name: . long, help: " Output format: text or jsonl. " )
@@ -20,16 +20,8 @@ struct Stream: AsyncParsableCommand {
2020 func run( ) async throws {
2121 if verbose { log ( " Initializing streaming ASR (Parakeet)... " ) }
2222
23- // Low-latency config: 3s chunks for fast output, 1s hypothesis for immediate feedback
24- let config = SlidingWindowAsrConfig (
25- chunkSeconds: 3.0 ,
26- hypothesisChunkSeconds: 1.0 ,
27- leftContextSeconds: 3.0 ,
28- rightContextSeconds: 0.5 ,
29- minContextForConfirmation: 3.0 ,
30- confirmationThreshold: 0.5
31- )
32- let streamManager = SlidingWindowAsrManager ( config: config)
23+ // Use the library's streaming config (11s chunks + 1s hypothesis updates)
24+ let streamManager = SlidingWindowAsrManager ( config: . streaming)
3325 try await streamManager. start ( source: . microphone)
3426
3527 // Set up microphone capture
@@ -41,24 +33,22 @@ struct Stream: AsyncParsableCommand {
4133 log ( " Microphone: \( inputFormat. sampleRate) Hz, \( inputFormat. channelCount) ch " )
4234 }
4335
44- // Track state for output
4536 let startTime = Date ( )
4637 var outputFile : FileHandle ? = nil
47- var utteranceCount = 0
48- var lastEmittedText = " "
38+ var lastConfirmedText = " "
39+ var lastVolatileText = " "
4940
5041 if let outputPath = output {
5142 FileManager . default. createFile ( atPath: outputPath, contents: nil )
5243 outputFile = FileHandle ( forWritingAtPath: outputPath)
5344 }
5445
55- // Set up signal handler for clean Ctrl+C
5646 signal ( SIGINT) { _ in
5747 FileHandle . standardError. write ( Data ( " \n " . utf8) )
5848 Darwin . exit ( 0 )
5949 }
6050
61- // Install tap on microphone — feed buffers to ASR
51+ // Install tap on microphone
6252 inputNode. installTap ( onBus: 0 , bufferSize: 4096 , format: inputFormat) { buffer, _ in
6353 nonisolated ( unsafe) let buf = buffer
6454 streamManager. streamAudio ( buf)
@@ -73,37 +63,39 @@ struct Stream: AsyncParsableCommand {
7363 let text = update. text. trimmingCharacters ( in: . whitespaces)
7464 guard !text. isEmpty else { continue }
7565
76- // Skip if this is the same text we already emitted (dedup)
77- guard text != lastEmittedText else { continue }
78- lastEmittedText = text
79-
80- utteranceCount += 1
8166 let elapsed = Date ( ) . timeIntervalSince ( startTime)
8267
83- let line : String
84- switch format {
85- case . text:
86- let ts = formatTimestamp ( elapsed)
87- line = " [ \( ts) ] \( text) "
88- case . jsonl:
89- let jsonObj : [ String : Any ] = [
90- " time " : round ( elapsed * 10 ) / 10 ,
91- " text " : text,
92- " confirmed " : update. isConfirmed,
93- ]
94- if let data = try ? JSONSerialization . data ( withJSONObject: jsonObj, options: [ . sortedKeys] ) ,
95- let jsonStr = String ( data: data, encoding: . utf8) {
96- line = jsonStr
97- } else {
98- continue
99- }
100- }
68+ if update. isConfirmed {
69+ // Confirmed: stable, final text — print as a permanent line
70+ guard text != lastConfirmedText else { continue }
71+ lastConfirmedText = text
72+ lastVolatileText = " " // reset volatile tracking
10173
102- print ( line)
103- fflush ( stdout)
74+ let line = formatLine ( text: text, elapsed: elapsed, confirmed: true )
75+ print ( line)
76+ fflush ( stdout)
10477
105- if let file = outputFile {
106- file. write ( Data ( ( line + " \n " ) . utf8) )
78+ if let file = outputFile {
79+ file. write ( Data ( ( line + " \n " ) . utf8) )
80+ }
81+ } else {
82+ // Volatile: hypothesis, may change — show as ephemeral line
83+ guard text != lastVolatileText else { continue }
84+ lastVolatileText = text
85+
86+ switch format {
87+ case . text:
88+ // Overwrite current line with \r for live feel
89+ let ts = formatTimestamp ( elapsed)
90+ let preview = String ( text. suffix ( 80 ) )
91+ let line = " \r [ \( ts) ] \( preview) "
92+ FileHandle . standardError. write ( Data ( line. utf8) )
93+ case . jsonl:
94+ // In JSONL mode, emit volatile updates too (marked as unconfirmed)
95+ let line = formatLine ( text: text, elapsed: elapsed, confirmed: false )
96+ print ( line)
97+ fflush ( stdout)
98+ }
10799 }
108100 }
109101
@@ -113,11 +105,9 @@ struct Stream: AsyncParsableCommand {
113105 _ = try await streamManager. finish ( )
114106
115107 let totalElapsed = Date ( ) . timeIntervalSince ( startTime)
116-
117108 FileHandle . standardError. write ( Data ( " \n --- Stream ended --- \n " . utf8) )
118109 FileHandle . standardError. write ( Data ( String ( format: " Duration: %dm %ds \n " ,
119110 Int ( totalElapsed) / 60 , Int ( totalElapsed) % 60 ) . utf8) )
120- FileHandle . standardError. write ( Data ( " Utterances: \( utteranceCount) \n " . utf8) )
121111
122112 if let outputPath = output {
123113 FileHandle . standardError. write ( Data ( " Saved to: \( outputPath) \n " . utf8) )
@@ -126,6 +116,25 @@ struct Stream: AsyncParsableCommand {
126116 outputFile? . closeFile ( )
127117 }
128118
119+ private func formatLine( text: String , elapsed: Double , confirmed: Bool ) -> String {
120+ switch format {
121+ case . text:
122+ let ts = formatTimestamp ( elapsed)
123+ return " [ \( ts) ] \( text) "
124+ case . jsonl:
125+ let jsonObj : [ String : Any ] = [
126+ " time " : round ( elapsed * 10 ) / 10 ,
127+ " text " : text,
128+ " confirmed " : confirmed,
129+ ]
130+ if let data = try ? JSONSerialization . data ( withJSONObject: jsonObj, options: [ . sortedKeys] ) ,
131+ let jsonStr = String ( data: data, encoding: . utf8) {
132+ return jsonStr
133+ }
134+ return " { \" text \" : \" \( text) \" } "
135+ }
136+ }
137+
129138 private func log( _ message: String ) {
130139 FileHandle . standardError. write ( Data ( " [scribe] \( message) \n " . utf8) )
131140 }
0 commit comments