diff --git a/OpenOats/Sources/OpenOats/Audio/AudioRecorder.swift b/OpenOats/Sources/OpenOats/Audio/AudioRecorder.swift index 74845b7d..2426a96f 100644 --- a/OpenOats/Sources/OpenOats/Audio/AudioRecorder.swift +++ b/OpenOats/Sources/OpenOats/Audio/AudioRecorder.swift @@ -1,4 +1,5 @@ @preconcurrency import AVFoundation +import os /// Records mic and system audio to temporary CAF files during a session, /// then merges and encodes them into a single M4A (AAC) file on finalization. @@ -67,14 +68,14 @@ final class AudioRecorder: @unchecked Sendable { guard let monoFormat = AVAudioFormat( standardFormatWithSampleRate: buffer.format.sampleRate, channels: 1 ) else { - diagLog("[RECORDER] mic file SKIP: cannot create mono format at \(buffer.format.sampleRate)Hz") + Log.recorder.error("Mic file SKIP: cannot create mono format at \(buffer.format.sampleRate, privacy: .public)Hz") return } do { micFile = try AVAudioFile(forWriting: url, settings: monoFormat.settings) - diagLog("[RECORDER] mic file created: \(url.lastPathComponent) mono at \(buffer.format.sampleRate)Hz") + Log.recorder.info("Mic file created: \(url.lastPathComponent, privacy: .private(mask: .hash)) mono at \(buffer.format.sampleRate, privacy: .public)Hz") } catch { - diagLog("[RECORDER] mic file creation FAILED: \(error)") + Log.recorder.error("Mic file creation failed: \(error.localizedDescription, privacy: .public)") return } } @@ -156,19 +157,19 @@ final class AudioRecorder: @unchecked Sendable { } } } else { - diagLog("[RECORDER] mic write SKIP: unsupported buffer format \(buffer.format.commonFormat.rawValue)") + Log.recorder.error("Mic write SKIP: unsupported buffer format \(buffer.format.commonFormat.rawValue, privacy: .public)") return } micWriteCount += 1 if micWriteCount <= 5 || micWriteCount % 100 == 0 { let peak = Self.peakLevel(monoBuf) - diagLog("[RECORDER] mic write #\(micWriteCount): frames=\(frames) peak=\(peak)") + Log.recorder.debug("Mic write #\(self.micWriteCount, privacy: .public): frames=\(frames, privacy: .public) peak=\(peak, privacy: .public)") } do { try micFile?.write(from: monoBuf) } catch { - diagLog("[RECORDER] mic write ERROR: \(error)") + Log.recorder.error("Mic write error: \(error.localizedDescription, privacy: .public)") } } } @@ -185,7 +186,7 @@ final class AudioRecorder: @unchecked Sendable { interleaved: buffer.format.isInterleaved ) } catch { - diagLog("[RECORDER] sys file creation FAILED: \(error)") + Log.recorder.error("Sys file creation failed: \(error.localizedDescription, privacy: .public)") return } } @@ -204,7 +205,7 @@ final class AudioRecorder: @unchecked Sendable { do { try sysFile?.write(from: buffer) } catch { - diagLog("[RECORDER] sys write ERROR: \(error)") + Log.recorder.error("Sys write error: \(error.localizedDescription, privacy: .public)") } } } @@ -310,7 +311,7 @@ final class AudioRecorder: @unchecked Sendable { }() guard micReader != nil || sysReader != nil else { - diagLog("[RECORDER] No audio data recorded") + Log.recorder.info("No audio data recorded") return } @@ -318,12 +319,12 @@ final class AudioRecorder: @unchecked Sendable { guard let targetFormat = AVAudioFormat(standardFormatWithSampleRate: targetRate, channels: 1) else { return } if let mic = micReader { - diagLog("[RECORDER] mic temp: \(mic.length) frames, format=\(mic.processingFormat)") + Log.recorder.info("Mic temp: \(mic.length, privacy: .public) frames, format=\(mic.processingFormat, privacy: .public)") } if let sys = sysReader { - diagLog("[RECORDER] sys temp: \(sys.length) frames, format=\(sys.processingFormat)") + Log.recorder.info("Sys temp: \(sys.length, privacy: .public) frames, format=\(sys.processingFormat, privacy: .public)") if let eff = sysEffectiveRate { - diagLog("[RECORDER] sys effective sample rate: \(eff) Hz (declared: \(sys.processingFormat.sampleRate) Hz)") + Log.recorder.info("Sys effective sample rate: \(eff, privacy: .public) Hz (declared: \(sys.processingFormat.sampleRate, privacy: .public) Hz)") } } @@ -334,7 +335,7 @@ final class AudioRecorder: @unchecked Sendable { let effectiveRate = sysEffectiveRate, abs(effectiveRate - sysReader.processingFormat.sampleRate) > 1000 { - diagLog("[RECORDER] sys rate mismatch: effective=\(effectiveRate) vs declared=\(sysReader.processingFormat.sampleRate), resampling from effective rate") + Log.recorder.info("Sys rate mismatch: effective=\(effectiveRate, privacy: .public) vs declared=\(sysReader.processingFormat.sampleRate, privacy: .public), resampling from effective rate") sysSamples = Self.readAllMono( file: sysReader, targetRate: targetRate, @@ -347,7 +348,7 @@ final class AudioRecorder: @unchecked Sendable { let micPeak = micSamples.reduce(Float(0)) { max($0, abs($1)) } let sysPeak = sysSamples.reduce(Float(0)) { max($0, abs($1)) } - diagLog("[RECORDER] after readAllMono: micSamples=\(micSamples.count) micPeak=\(micPeak) sysSamples=\(sysSamples.count) sysPeak=\(sysPeak)") + Log.recorder.info("After readAllMono: micSamples=\(micSamples.count, privacy: .public) micPeak=\(micPeak, privacy: .public) sysSamples=\(sysSamples.count, privacy: .public) sysPeak=\(sysPeak, privacy: .public)") let length = max(micSamples.count, sysSamples.count) guard length > 0 else { return } @@ -364,7 +365,7 @@ final class AudioRecorder: @unchecked Sendable { commonFormat: .pcmFormatFloat32, interleaved: false ) else { - diagLog("[RECORDER] Failed to create output file") + Log.recorder.error("Failed to create output file") return } @@ -387,7 +388,7 @@ final class AudioRecorder: @unchecked Sendable { offset += count } - diagLog("[RECORDER] Saved \(outputURL.lastPathComponent) (\(length) frames)") + Log.recorder.info("Saved \(outputURL.lastPathComponent, privacy: .private(mask: .hash)) (\(length, privacy: .public) frames)") } private static func readAllMono( diff --git a/OpenOats/Sources/OpenOats/Audio/MicCapture.swift b/OpenOats/Sources/OpenOats/Audio/MicCapture.swift index c01451f2..0a5c937a 100644 --- a/OpenOats/Sources/OpenOats/Audio/MicCapture.swift +++ b/OpenOats/Sources/OpenOats/Audio/MicCapture.swift @@ -4,7 +4,6 @@ import CoreAudio import Foundation import os -private let micLog = Logger(subsystem: "com.openoats", category: "MicCapture") /// Captures microphone audio via AVAudioEngine and streams PCM buffers. final class MicCapture: @unchecked Sendable { @@ -55,21 +54,21 @@ final class MicCapture: @unchecked Sendable { errorHolder.value = nil self._hasCapturedFrames.value = false - diagLog("[MIC-1] bufferStream called, deviceID=\(String(describing: deviceID))") + Log.mic.info("bufferStream called, deviceID=\(String(describing: deviceID), privacy: .public)") let engine = self.makeFreshEngine() - diagLog("[MIC-1a] fresh engine created") + Log.mic.info("Fresh engine created") let inputNode = engine.inputNode - diagLog("[MIC-1b] input node ready") + Log.mic.info("Input node ready") // Enable voice processing (AEC + noise suppression) if requested if echoCancellation { do { try inputNode.setVoiceProcessingEnabled(true) - diagLog("[MIC-1c] voice processing (AEC) enabled") + Log.mic.info("Voice processing (AEC) enabled") } catch { - diagLog("[MIC-1c] failed to enable voice processing: \(error.localizedDescription)") + Log.mic.error("Failed to enable voice processing: \(error.localizedDescription, privacy: .public)") } } @@ -78,7 +77,7 @@ final class MicCapture: @unchecked Sendable { if let id = deviceID { guard let inAU = inputNode.audioUnit else { let msg = "inputNode has no audio unit after prepare" - diagLog("[MIC-2-FAIL] \(msg)") + Log.mic.error("\(msg, privacy: .public)") errorHolder.value = msg continuation.finish() return @@ -92,10 +91,10 @@ final class MicCapture: @unchecked Sendable { &devID, UInt32(MemoryLayout.size) ) - diagLog("[MIC-2] setInputDevice status=\(inStatus) (0=ok)") + Log.mic.info("setInputDevice status=\(inStatus, privacy: .public) (0=ok)") resolvedDeviceID = id } else { - diagLog("[MIC-2] no deviceID, using system default") + Log.mic.info("No deviceID, using system default") resolvedDeviceID = Self.defaultInputDeviceID() } @@ -108,15 +107,15 @@ final class MicCapture: @unchecked Sendable { if let devID = resolvedDeviceID, let hwRate = Self.deviceNominalSampleRate(for: devID), hwRate > 0, hwRate != sampleRate { - diagLog("[MIC-3] hardware sr=\(hwRate) differs from inputNode sr=\(sampleRate), using hardware rate") + Log.mic.info("Hardware sr=\(hwRate, privacy: .public) differs from inputNode sr=\(sampleRate, privacy: .public), using hardware rate") sampleRate = hwRate } - diagLog("[MIC-3] inputNode format: sr=\(format.sampleRate) ch=\(format.channelCount) interleaved=\(format.isInterleaved) commonFormat=\(format.commonFormat.rawValue), effective sr=\(sampleRate)") + Log.mic.info("inputNode format: sr=\(format.sampleRate, privacy: .public) ch=\(format.channelCount, privacy: .public) interleaved=\(format.isInterleaved, privacy: .public) commonFormat=\(format.commonFormat.rawValue, privacy: .public), effective sr=\(sampleRate, privacy: .public)") guard sampleRate > 0 && format.channelCount > 0 else { let msg = "Invalid audio format: sr=\(sampleRate) ch=\(format.channelCount)" - diagLog("[MIC-3-FAIL] \(msg)") + Log.mic.error("\(msg, privacy: .public)") errorHolder.value = msg continuation.finish() return @@ -130,14 +129,14 @@ final class MicCapture: @unchecked Sendable { tapFormat = f } else if sampleRate != format.sampleRate, let f = AVAudioFormat(standardFormatWithSampleRate: format.sampleRate, channels: format.channelCount) { - diagLog("[MIC-4] hardware-rate format failed, using node rate \(format.sampleRate)") + Log.mic.info("Hardware-rate format failed, using node rate \(format.sampleRate, privacy: .public)") tapFormat = f } else { - diagLog("[MIC-4] standard formats failed, using native input format") + Log.mic.info("Standard formats failed, using native input format") tapFormat = format } - diagLog("[MIC-4] tapFormat: sr=\(tapFormat.sampleRate) ch=\(tapFormat.channelCount)") + Log.mic.info("tapFormat: sr=\(tapFormat.sampleRate, privacy: .public) ch=\(tapFormat.channelCount, privacy: .public)") let muted = self._muted var tapCallCount = 0 @@ -148,7 +147,7 @@ final class MicCapture: @unchecked Sendable { level.value = min(rms * 25, 1.0) if tapCallCount <= 5 || tapCallCount % 100 == 0 { - diagLog("[MIC-6] tap #\(tapCallCount): frames=\(buffer.frameLength) rms=\(rms) level=\(level.value)") + Log.mic.debug("tap #\(tapCallCount, privacy: .public): frames=\(buffer.frameLength, privacy: .public) rms=\(rms, privacy: .public) level=\(level.value, privacy: .public)") } guard !muted.value else { return } @@ -156,21 +155,21 @@ final class MicCapture: @unchecked Sendable { } self.hasTapInstalled = true - diagLog("[MIC-5] tap installed, preparing engine...") + Log.mic.info("Tap installed, preparing engine") continuation.onTermination = { _ in - diagLog("[MIC-TERM] stream terminated") + Log.mic.info("Stream terminated") // Audio hardware teardown handled by stop() — not here, // so finishStream() can drain without premature engine shutdown. } do { - diagLog("[MIC-7] engine prepared, starting...") + Log.mic.info("Engine prepared, starting") try engine.start() - diagLog("[MIC-8] engine started successfully, isRunning=\(engine.isRunning)") + Log.mic.info("Engine started successfully, isRunning=\(engine.isRunning, privacy: .public)") } catch { let msg = "Mic failed: \(error.localizedDescription)" - print("[MIC-8-FAIL] \(msg)") + Log.mic.error("\(msg, privacy: .public)") errorHolder.value = msg self.hasTapInstalled = false continuation.finish() diff --git a/OpenOats/Sources/OpenOats/Models/TranscriptStore.swift b/OpenOats/Sources/OpenOats/Models/TranscriptStore.swift index 390e554a..b1495d4e 100644 --- a/OpenOats/Sources/OpenOats/Models/TranscriptStore.swift +++ b/OpenOats/Sources/OpenOats/Models/TranscriptStore.swift @@ -1,5 +1,6 @@ import Foundation import Observation +import os @Observable @MainActor @@ -203,11 +204,12 @@ final class TranscriptStore { guard similarity >= acousticEchoSimilarityThreshold || containsOther else { continue } - diagLog( - "[TRANSCRIPT-ECHO] dropped mic utterance as system-audio echo " + - "dt=\(String(format: "%.2f", timeDelta)) " + - "similarity=\(String(format: "%.2f", similarity)) " + - "you='\(utterance.text.prefix(80))' them='\(candidate.text.prefix(80))'" + let dtFormatted = String(format: "%.2f", timeDelta) + let simFormatted = String(format: "%.2f", similarity) + let youSnippet = String(utterance.text.prefix(80)) + let themSnippet = String(candidate.text.prefix(80)) + Log.transcript.info( + "Dropped mic utterance as system-audio echo dt=\(dtFormatted, privacy: .public) similarity=\(simFormatted, privacy: .public) you='\(youSnippet, privacy: .private)' them='\(themSnippet, privacy: .private)'" ) return true } diff --git a/OpenOats/Sources/OpenOats/Transcription/AcousticEchoFilter.swift b/OpenOats/Sources/OpenOats/Transcription/AcousticEchoFilter.swift index af9ba9d0..46c238de 100644 --- a/OpenOats/Sources/OpenOats/Transcription/AcousticEchoFilter.swift +++ b/OpenOats/Sources/OpenOats/Transcription/AcousticEchoFilter.swift @@ -1,4 +1,5 @@ import Foundation +import os /// Shared acoustic echo suppression logic. /// Detects when mic (YOU) utterances are echoes of system (THEM) audio based on @@ -37,11 +38,12 @@ enum AcousticEchoFilter { normalizedThem.contains(normalizedYou) if similarity >= similarityThreshold || containsOther { - diagLog( - "[ECHO-FILTER] suppressed mic record as echo " + - "dt=\(String(format: "%.2f", timeDelta)) " + - "sim=\(String(format: "%.2f", similarity)) " + - "mic='\(micRecord.text.prefix(80))' sys='\(sysRecord.text.prefix(80))'" + let dtFormatted = String(format: "%.2f", timeDelta) + let simFormatted = String(format: "%.2f", similarity) + let micSnippet = String(micRecord.text.prefix(80)) + let sysSnippet = String(sysRecord.text.prefix(80)) + Log.echo.info( + "Suppressed mic record as echo dt=\(dtFormatted, privacy: .public) sim=\(simFormatted, privacy: .public) mic='\(micSnippet, privacy: .private)' sys='\(sysSnippet, privacy: .private)'" ) return true } diff --git a/OpenOats/Sources/OpenOats/Transcription/StreamingTranscriber.swift b/OpenOats/Sources/OpenOats/Transcription/StreamingTranscriber.swift index b225f017..08802676 100644 --- a/OpenOats/Sources/OpenOats/Transcription/StreamingTranscriber.swift +++ b/OpenOats/Sources/OpenOats/Transcription/StreamingTranscriber.swift @@ -11,7 +11,6 @@ final class StreamingTranscriber: @unchecked Sendable { private let speaker: Speaker private let onPartial: @Sendable (String) -> Void private let onFinal: @Sendable (String) -> Void - private let log = Logger(subsystem: "com.openoats", category: "StreamingTranscriber") /// Resampler from source format to 16kHz mono Float32. private var converter: AVAudioConverter? @@ -68,14 +67,14 @@ final class StreamingTranscriber: @unchecked Sendable { bufferCount += 1 if bufferCount <= 3 { let fmt = buffer.format - diagLog("[\(speaker.storageKey)] buffer #\(bufferCount): frames=\(buffer.frameLength) sr=\(fmt.sampleRate) ch=\(fmt.channelCount) interleaved=\(fmt.isInterleaved) common=\(fmt.commonFormat.rawValue)") + Log.streaming.debug("[\(self.speaker.storageKey, privacy: .public)] buffer #\(bufferCount, privacy: .public): frames=\(buffer.frameLength, privacy: .public) sr=\(fmt.sampleRate, privacy: .public) ch=\(fmt.channelCount, privacy: .public) interleaved=\(fmt.isInterleaved, privacy: .public) common=\(fmt.commonFormat.rawValue, privacy: .public)") } guard let samples = extractSamples(buffer) else { continue } if bufferCount <= 3 { let maxVal = samples.max() ?? 0 - diagLog("[\(speaker.storageKey)] samples: count=\(samples.count) max=\(maxVal)") + Log.streaming.debug("[\(self.speaker.storageKey, privacy: .public)] samples: count=\(samples.count, privacy: .public) max=\(maxVal, privacy: .public)") } vadBuffer.append(contentsOf: samples) @@ -110,7 +109,7 @@ final class StreamingTranscriber: @unchecked Sendable { isSpeaking = true startedSpeech = true speechSamples = recentChunks.suffix(Self.prerollChunkCount).flatMap { $0 } - diagLog("[\(self.speaker.storageKey)] speech start") + Log.streaming.info("[\(self.speaker.storageKey, privacy: .public)] speech start") } case .speechEnd: @@ -131,7 +130,7 @@ final class StreamingTranscriber: @unchecked Sendable { if endedSpeech { isSpeaking = false isRunningPartial = false - diagLog("[\(self.speaker.storageKey)] speech end, samples=\(speechSamples.count)") + Log.streaming.info("[\(self.speaker.storageKey, privacy: .public)] speech end, samples=\(speechSamples.count, privacy: .public)") if speechSamples.count > Self.minimumSpeechSamples { let segment = speechSamples speechSamples.removeAll(keepingCapacity: true) @@ -170,7 +169,7 @@ final class StreamingTranscriber: @unchecked Sendable { } } } catch { - log.error("VAD error: \(error.localizedDescription)") + Log.streaming.error("VAD error: \(error.localizedDescription, privacy: .public)") } } } @@ -188,13 +187,13 @@ final class StreamingTranscriber: @unchecked Sendable { do { let text = try await backend.transcribe(samples, locale: locale, previousContext: previousContext) guard !text.isEmpty else { return } - log.info("[\(self.speaker.storageKey)] transcribed: \(text.prefix(80))") + Log.streaming.info("[\(self.speaker.storageKey, privacy: .public)] transcribed: \(text.prefix(80), privacy: .private)") // Store trailing words for cross-segment context let words = text.split(separator: " ") previousContext = words.suffix(Self.contextWordCount).joined(separator: " ") onFinal(text) } catch { - log.error("ASR error: \(error.localizedDescription)") + Log.streaming.error("ASR error: \(error.localizedDescription, privacy: .public)") } } @@ -269,7 +268,7 @@ final class StreamingTranscriber: @unchecked Sendable { } if let error { - log.error("Resample error: \(error.localizedDescription)") + Log.streaming.error("Resample error: \(error.localizedDescription, privacy: .public)") return nil } diff --git a/OpenOats/Sources/OpenOats/Transcription/TranscriptionEngine.swift b/OpenOats/Sources/OpenOats/Transcription/TranscriptionEngine.swift index f749f183..e48da036 100644 --- a/OpenOats/Sources/OpenOats/Transcription/TranscriptionEngine.swift +++ b/OpenOats/Sources/OpenOats/Transcription/TranscriptionEngine.swift @@ -4,19 +4,6 @@ import FluidAudio import Observation import os -/// Simple file logger for diagnostics — writes to /tmp/openoats.log -func diagLog(_ msg: String) { - let line = "\(Date()): \(msg)\n" - let path = "/tmp/openoats.log" - if let fh = FileHandle(forWritingAtPath: path) { - fh.seekToEndOfFile() - fh.write(line.data(using: .utf8)!) - fh.closeFile() - } else { - FileManager.default.createFile(atPath: path, contents: line.data(using: .utf8)) - } -} - enum TranscriptionEngineError: LocalizedError { case transcriberNotInitialized @@ -188,7 +175,7 @@ final class TranscriptionEngine { inputDeviceID: AudioDeviceID = 0, transcriptionModel: TranscriptionModel ) async { - diagLog("[ENGINE-0] start() called, isRunning=\(isRunning)") + Log.transcription.info("start() called, isRunning=\(self.isRunning, privacy: .public)") guard !isRunning else { return } lastError = nil refreshModelAvailability() @@ -232,7 +219,7 @@ final class TranscriptionEngine { downloadTotalBytes = transcriptionModel.estimatedDownloadBytes downloadDetail = DownloadProgressDetail(fraction: 0, sizeText: nil, speedText: nil, etaText: nil) } - diagLog("[ENGINE-1] loading transcription model \(transcriptionModel.rawValue)...") + Log.transcription.info("Loading transcription model \(transcriptionModel.rawValue, privacy: .public)") do { let vocab = settings.transcriptionCustomVocabulary let mic = transcriptionModel.makeBackend(customVocabulary: vocab) @@ -262,19 +249,19 @@ final class TranscriptionEngine { } assetStatus = "Loading VAD model..." - diagLog("[ENGINE-1b] loading VAD model...") + Log.transcription.info("Loading VAD model") let vad = try await VadManager() self.vadManager = vad // Optionally load speaker diarization model if settings.enableDiarization { assetStatus = "Loading diarization model..." - diagLog("[ENGINE-1c] loading LS-EEND diarization model...") + Log.transcription.info("Loading LS-EEND diarization model") let dm = DiarizationManager() let variant = LSEENDVariant(rawValue: settings.diarizationVariant.rawValue) ?? .dihard3 try await dm.load(variant: variant) self.diarizationManager = dm - diagLog("[ENGINE-1c] diarization model loaded") + Log.transcription.info("Diarization model loaded") } else { self.diarizationManager = nil } @@ -286,10 +273,10 @@ final class TranscriptionEngine { downloadStartTime = nil downloadTotalBytes = nil assetStatus = "Models ready" - diagLog("[ENGINE-2] transcription model loaded") + Log.transcription.info("Transcription model loaded") } catch { let msg = "Failed to load models: \(error.localizedDescription)" - diagLog("[ENGINE-2-FAIL] \(msg)") + Log.transcription.error("Failed to load models: \(msg, privacy: .public)") lastError = msg assetStatus = "Ready" isRunning = false @@ -299,7 +286,7 @@ final class TranscriptionEngine { downloadTotalBytes = nil // Clear corrupt cache so the next attempt triggers a fresh download settings.transcriptionModel.makeBackend().clearModelCache() - diagLog("[ENGINE-2-FAIL] cleared model cache for \(settings.transcriptionModel.rawValue)") + Log.transcription.info("Cleared model cache for \(self.settings.transcriptionModel.rawValue, privacy: .public)") needsModelDownload = true downloadConfirmed = false return @@ -311,7 +298,7 @@ final class TranscriptionEngine { userSelectedDeviceID = inputDeviceID guard let targetMicID = resolvedMicDeviceID(for: inputDeviceID) else { let msg = unavailableMicMessage(for: inputDeviceID) - diagLog("[ENGINE-3-FAIL] \(msg)") + Log.transcription.error("Mic unavailable: \(msg, privacy: .public)") lastError = msg assetStatus = "Ready" isRunning = false @@ -324,10 +311,10 @@ final class TranscriptionEngine { // AEC must be disabled to prevent capture failures. let useAEC = false if settings.enableEchoCancellation { - diagLog("[ENGINE-3] AEC disabled — conflicts with system audio capture") + Log.transcription.info("AEC disabled - conflicts with system audio capture") } - diagLog("[ENGINE-3] starting mic capture, targetMicID=\(String(describing: targetMicID)), aec=\(useAEC)") + Log.transcription.info("Starting mic capture, targetMicID=\(targetMicID, privacy: .public), aec=\(useAEC, privacy: .public)") startMicStream( locale: locale, vadManager: vadManager, @@ -337,7 +324,7 @@ final class TranscriptionEngine { // Check for immediate mic capture failure if let micError = micCapture.captureError { - diagLog("[ENGINE-3-FAIL] mic capture error: \(micError)") + Log.transcription.error("Mic capture error: \(micError, privacy: .public)") lastError = micError } @@ -348,7 +335,7 @@ final class TranscriptionEngine { guard let self, self.isRunning else { return } if !self.micCapture.hasCapturedFrames && self.micCapture.captureError == nil { if useAEC { - diagLog("[ENGINE-HEALTH] no mic audio after 5s with AEC, retrying without") + Log.transcription.error("No mic audio after 5s with AEC, retrying without") self.micCapture.finishStream() await self.micTask?.value self.micTask = nil @@ -360,7 +347,7 @@ final class TranscriptionEngine { echoCancellation: false ) } else { - diagLog("[ENGINE-HEALTH] no mic audio after 5s") + Log.transcription.error("No mic audio after 5s") self.lastError = "Microphone is not producing audio. Check your input device in System Settings." } } @@ -370,7 +357,7 @@ final class TranscriptionEngine { await startSystemAudioStream(locale: locale, vadManager: vadManager) assetStatus = "Transcribing (\(micBackend?.displayName ?? transcriptionModel.displayName))" - diagLog("[ENGINE-6] all transcription tasks started") + Log.transcription.info("All transcription tasks started") // Install CoreAudio listeners for live device routing changes installDefaultDeviceListener() @@ -385,7 +372,7 @@ final class TranscriptionEngine { pendingMicDeviceID = inputDeviceID if micRestartTask != nil { - diagLog("[ENGINE-MIC-SWAP] queued restart for device \(inputDeviceID)") + Log.transcription.info("Queued mic restart for device \(inputDeviceID, privacy: .public)") return } @@ -596,17 +583,17 @@ final class TranscriptionEngine { guard let targetMicID = resolvedMicDeviceID(for: inputDeviceID) else { let msg = unavailableMicMessage(for: inputDeviceID) - diagLog("[ENGINE-MIC-SWAP-FAIL] \(msg)") + Log.transcription.error("Mic swap failed: \(msg, privacy: .public)") lastError = msg return } guard targetMicID != currentMicDeviceID else { - diagLog("[ENGINE-MIC-SWAP] same device \(targetMicID), skipping") + Log.transcription.debug("Mic swap skipped, same device \(targetMicID, privacy: .public)") return } - diagLog("[ENGINE-MIC-SWAP] switching mic from \(currentMicDeviceID) to \(targetMicID)") + Log.transcription.info("Switching mic from \(self.currentMicDeviceID, privacy: .public) to \(targetMicID, privacy: .public)") micCapture.finishStream() await micTask?.value @@ -625,7 +612,7 @@ final class TranscriptionEngine { currentMicDeviceID = targetMicID lastError = nil - diagLog("[ENGINE-MIC-SWAP] mic restarted on device \(targetMicID)") + Log.transcription.info("Mic restarted on device \(targetMicID, privacy: .public)") } private func restartSystemAudio() { @@ -633,7 +620,7 @@ final class TranscriptionEngine { pendingSystemAudioRestart = true if sysRestartTask != nil { - diagLog("[ENGINE-SYS-SWAP] queued restart") + Log.transcription.info("Queued system audio restart") return } @@ -651,7 +638,7 @@ final class TranscriptionEngine { private func performSystemAudioRestart() async { guard isRunning, let vadManager else { return } - diagLog("[ENGINE-SYS-SWAP] restarting system audio stream") + Log.transcription.info("Restarting system audio stream") systemCapture.finishStream() await sysTask?.value @@ -664,7 +651,7 @@ final class TranscriptionEngine { await systemCapture.stop() await startSystemAudioStream(locale: settings.locale, vadManager: vadManager) - diagLog("[ENGINE-SYS-SWAP] system audio stream restarted") + Log.transcription.info("System audio stream restarted") } private func startMicStream( @@ -708,16 +695,16 @@ final class TranscriptionEngine { locale: Locale, vadManager: VadManager ) async { - diagLog("[ENGINE-4] starting system audio capture...") + Log.transcription.info("Starting system audio capture") let sysStreams: SystemAudioCapture.CaptureStreams do { sysStreams = try await systemCapture.bufferStream() - diagLog("[ENGINE-5] system audio capture started OK") + Log.transcription.info("System audio capture started") clearSystemAudioErrorIfPresent() } catch { let msg = "Failed to start system audio: \(error.localizedDescription)" - diagLog("[ENGINE-5-FAIL] \(msg)") + Log.transcription.error("Failed to start system audio: \(msg, privacy: .public)") lastError = msg return } @@ -804,7 +791,7 @@ final class TranscriptionEngine { ) -> StreamingTranscriber? { let backend = speaker == .you ? micBackend : systemBackend guard let backend else { - diagLog("[ENGINE] makeTranscriber called without initialized backend for \(speaker.storageKey)") + Log.transcription.error("makeTranscriber called without initialized backend for \(speaker.storageKey, privacy: .public)") return nil } return StreamingTranscriber( diff --git a/OpenOats/Sources/OpenOats/Transcription/WhisperKitManager.swift b/OpenOats/Sources/OpenOats/Transcription/WhisperKitManager.swift index 415e2e8c..61aed5d0 100644 --- a/OpenOats/Sources/OpenOats/Transcription/WhisperKitManager.swift +++ b/OpenOats/Sources/OpenOats/Transcription/WhisperKitManager.swift @@ -1,6 +1,5 @@ import Foundation import WhisperKit -import os /// Wraps WhisperKit for use as a transcription backend. /// Handles model download, initialization, and transcription of Float32 audio samples. @@ -26,7 +25,6 @@ final class WhisperKitManager: @unchecked Sendable { private let variant: Variant private var pipe: WhisperKit? - private let log = Logger(subsystem: "com.openoats", category: "WhisperKitManager") init(variant: Variant) { self.variant = variant diff --git a/OpenOats/Sources/OpenOats/Utils/Logging.swift b/OpenOats/Sources/OpenOats/Utils/Logging.swift new file mode 100644 index 00000000..eabe2f59 --- /dev/null +++ b/OpenOats/Sources/OpenOats/Utils/Logging.swift @@ -0,0 +1,14 @@ +import Foundation +import os + +enum Log { + static let mic = Logger(subsystem: subsystem, category: "MicCapture") + static let recorder = Logger(subsystem: subsystem, category: "AudioRecorder") + static let transcription = Logger(subsystem: subsystem, category: "TranscriptionEngine") + static let streaming = Logger(subsystem: subsystem, category: "StreamingTranscriber") + static let transcript = Logger(subsystem: subsystem, category: "TranscriptStore") + static let echo = Logger(subsystem: subsystem, category: "AcousticEchoFilter") + static let whisperkit = Logger(subsystem: subsystem, category: "WhisperKitManager") + + private static let subsystem = Bundle.main.bundleIdentifier ?? "com.openoats.app" +}