@@ -55,9 +55,12 @@ final class TranscriptStore {
5555 private let sessionsURL : URL
5656 private let textPipeline : TextPipeline ?
5757 private let audioDecoder : any ImportedAudioDecoding
58+ private let maxRecordingDuration : TimeInterval
5859 private var recordingStartDate : Date ?
5960 private var initialized = false
6061 private var warmupTask : Task < Void , Never > ?
62+ private var recordingLimitTask : Task < Void , Never > ?
63+ private var explicitlyUnloaded = false
6164
6265 init (
6366 preferences: Preferences ,
@@ -66,7 +69,8 @@ final class TranscriptStore {
6669 textPipeline: TextPipeline ? = nil ,
6770 audioDecoder: any ImportedAudioDecoding = ImportedAudioDecoder ( ) ,
6871 recorder: AudioRecorder = AudioRecorder ( ) ,
69- runner: any RunnerBridgeClient = RunnerBridge ( )
72+ runner: any RunnerBridgeClient = RunnerBridge ( ) ,
73+ maxRecordingDuration: TimeInterval = 30 * 60
7074 ) {
7175 self . recorder = recorder
7276 self . runner = runner
@@ -75,6 +79,7 @@ final class TranscriptStore {
7579 self . sessionsURL = sessionsURL
7680 self . textPipeline = textPipeline
7781 self . audioDecoder = audioDecoder
82+ self . maxRecordingDuration = maxRecordingDuration
7883 loadSessions ( )
7984 }
8085
@@ -230,10 +235,12 @@ final class TranscriptStore {
230235 }
231236
232237 func preloadModel( ) async {
238+ explicitlyUnloaded = false
233239 await performHelperWarmupIfNeeded ( updateStatusMessage: true )
234240 }
235241
236242 func unloadModel( ) async {
243+ explicitlyUnloaded = true
237244 warmupTask? . cancel ( )
238245 warmupTask = nil
239246 await runner. shutdown ( )
@@ -279,6 +286,9 @@ final class TranscriptStore {
279286 currentError = nil
280287 sessionState = . recording
281288 recordingStartDate = . now
289+ scheduleRecordingLimit {
290+ await self . stopRecordingAndTranscribe ( )
291+ }
282292
283293 do {
284294 try await recorder. startRecording ( selectedMicrophoneID: preferences. selectedMicrophoneID) { [ weak self] level in
@@ -288,9 +298,11 @@ final class TranscriptStore {
288298 }
289299 startBackgroundWarmupIfNeeded ( )
290300 } catch let error as RunnerError {
301+ cancelRecordingLimit ( )
291302 currentError = error
292303 sessionState = . idle
293304 } catch {
305+ cancelRecordingLimit ( )
294306 currentError = . launchFailed( description: error. localizedDescription)
295307 sessionState = . idle
296308 }
@@ -344,11 +356,13 @@ final class TranscriptStore {
344356 storeLog. info ( " Dictation capture started " )
345357 return true
346358 } catch let error as RunnerError {
359+ cancelRecordingLimit ( )
347360 storeLog. error ( " Dictation capture failed to start: \( error. localizedDescription, privacy: . public) " )
348361 currentError = error
349362 resetLiveState ( status: " Ready " )
350363 return false
351364 } catch {
365+ cancelRecordingLimit ( )
352366 storeLog. error ( " Dictation capture failed with unexpected error: \( error. localizedDescription, privacy: . public) " )
353367 currentError = . launchFailed( description: error. localizedDescription)
354368 resetLiveState ( status: " Ready " )
@@ -365,6 +379,7 @@ final class TranscriptStore {
365379 sessionState = . transcribing
366380 statusMessage = " Transcribing... "
367381 audioLevel = 0
382+ cancelRecordingLimit ( )
368383 storeLog. info ( " Dictation capture stopping after duration= \( duration, format: . fixed( precision: 3 ) ) s " )
369384
370385 do {
@@ -388,6 +403,7 @@ final class TranscriptStore {
388403 sessionState = . transcribing
389404 statusMessage = " Finalizing recording... "
390405 audioLevel = 0
406+ cancelRecordingLimit ( )
391407
392408 do {
393409 let pcmData = try await recorder. stopRecording ( )
@@ -634,7 +650,11 @@ final class TranscriptStore {
634650 case . completed( let result) :
635651 finalResult = result
636652 storeLog. info ( " Runner completed event textLength= \( result. text. count) stdoutLength= \( result. stdout. count) stderrLength= \( result. stderr. count) " )
637- storeLog. info ( " Parakeet transcript: \( result. text, privacy: . public) " )
653+ if DiagnosticLogging . shouldLogTranscriptsPublicly {
654+ storeLog. info ( " Parakeet transcript: \( result. text, privacy: . public) " )
655+ } else {
656+ storeLog. info ( " Parakeet transcript: \( result. text, privacy: . private) " )
657+ }
638658 if let runtimeProfile = result. runtimeProfile {
639659 storeLog. info ( " Runner runtime profile: \( runtimeProfile, privacy: . public) " )
640660 }
@@ -659,6 +679,7 @@ final class TranscriptStore {
659679 }
660680
661681 private func autoPreloadModelIfReady( ) async {
682+ guard !explicitlyUnloaded else { return }
662683 guard resourcesReady else { return }
663684 guard helperState == . unloaded || helperState == . failed else { return }
664685 await performHelperWarmupIfNeeded ( updateStatusMessage: false )
@@ -691,6 +712,7 @@ final class TranscriptStore {
691712 )
692713 helperState = . warm
693714 helperStatusMessage = " Model preloaded "
715+ logResidentMemory ( context: " Parakeet helper preloaded " )
694716 if updateStatusMessage && !hasActiveSession {
695717 statusMessage = " Ready "
696718 }
@@ -728,6 +750,15 @@ final class TranscriptStore {
728750 }
729751 }
730752
753+ private func logResidentMemory( context: String ) {
754+ guard UserDefaults . standard. bool ( forKey: DiagnosticLogging . transcriptDebugKey) ,
755+ let bytes = DiagnosticLogging . residentMemoryBytes ( )
756+ else {
757+ return
758+ }
759+ storeLog. info ( " \( context, privacy: . public) residentMemoryBytes= \( bytes) " )
760+ }
761+
731762 @discardableResult
732763 private func processCompletedTranscription(
733764 rawText: String ,
@@ -764,12 +795,30 @@ final class TranscriptStore {
764795 }
765796
766797 private func resetLiveState( status: String = " Ready " ) {
798+ cancelRecordingLimit ( )
767799 audioLevel = 0
768800 sessionState = . idle
769801 recordingStartDate = nil
770802 statusMessage = status
771803 }
772804
805+ private func scheduleRecordingLimit( onLimit: @escaping @MainActor ( ) async -> Void ) {
806+ cancelRecordingLimit ( )
807+ guard maxRecordingDuration > 0 else { return }
808+ recordingLimitTask = Task { @MainActor [ weak self] in
809+ try ? await Task . sleep ( for: . seconds( maxRecordingDuration) )
810+ guard let self, self . sessionState == . recording else { return }
811+ self . statusMessage = " Maximum recording duration reached "
812+ self . currentError = . transcriptionFailed( description: " Maximum recording duration reached. " )
813+ await onLimit ( )
814+ }
815+ }
816+
817+ private func cancelRecordingLimit( ) {
818+ recordingLimitTask? . cancel ( )
819+ recordingLimitTask = nil
820+ }
821+
773822 private var formatterAssetsReady : Bool {
774823 let fm = FileManager . default
775824 return fm. fileExists ( atPath: preferences. formatterModelPath)
0 commit comments