import Foundation /// Translation entry point. Mirrors Android `feature/translation/TranslationFeature.kt`. /// /// Dispatches `start(modeId:args:)` to the matching mode handler. The set of /// supported modes mirrors Android — call translation (mode 3 / 6) plus the /// record-only path used internally by AssistantBridge / DeviceRecordFeature /// (mode 1). /// /// The `feedTranslatedAudio` / `feedTranslationResult` paths route to the /// active handler / bridge, exactly like Android. class TranslationFeature: NSObject { weak var server: JieliHomeServer? private var bridge: TranslationAudioBridge? private var handlers: [Int: TranslationModeHandler] = [:] private var current: TranslationModeHandler? init(server: JieliHomeServer) { self.server = server super.init() let defaultBridge = EventChannelAudioBridge( dispatcher: server.dispatcher, injector: { [weak self] modeId, streamId, pcm, fmt, isFinal in return self?.feedTranslatedAudio( streamId: streamId, pcm: pcm, sampleRate: fmt.sampleRate, channels: fmt.channels, bitsPerSample: fmt.bitsPerSample, isFinal: isFinal ) ?? false } ) setBridge(defaultBridge) } /// Replace the active bridge — used by host code that wants to keep /// audio inside native (mirrors Android `setTranslationBridge`). func setBridge(_ bridge: TranslationAudioBridge) { self.bridge = bridge guard let server = server else { return } handlers = [ TranslationModeIds.MODE_CALL_TRANSLATION: CallTranslationModeHandler(server: server, bridge: bridge), TranslationModeIds.MODE_CALL_TRANSLATION_WITH_STEREO: StereoCallTranslationModeHandler(server: server, bridge: bridge), TranslationModeIds.MODE_RECORD: RecordOnlyModeHandler(server: server, bridge: bridge), ] } // MARK: - Public API func isWorking() -> Bool { current?.isWorking ?? false } func currentModeId() -> Int? { guard let h = current, h.isWorking else { return nil } return h.modeId } func currentInputStreams() -> [String] { current?.isWorking == true ? (current?.inputStreams ?? []) : [] } func currentOutputStreams() -> [String] { current?.isWorking == true ? (current?.outputStreams ?? []) : [] } func start(modeId: Int, args: [String: Any]) throws { // Match Android logic — auto-upgrade mode=3 + OPUS to mode=6 stereo when // the device supports it (and caller did not opt out). let bypassStereo = (args["bypassStereoUpgrade"] as? Bool) ?? false let codecRaw = args["audioType"] let codecIsOpus: Bool = { if codecRaw == nil { return true } if let s = codecRaw as? String { return s.lowercased() == "opus" } if let n = codecRaw as? Int { return n == 2 } return false }() var effectiveModeId = modeId if !bypassStereo && modeId == TranslationModeIds.MODE_CALL_TRANSLATION && codecIsOpus && isSupportCallTranslationWithStereo(address: args["address"] as? String) { effectiveModeId = TranslationModeIds.MODE_CALL_TRANSLATION_WITH_STEREO } guard let handler = handlers[effectiveModeId] else { throw PluginError("TRANSLATION_ERR", "unknown modeId=\(effectiveModeId)") } current?.stop() try handler.start(args: args) current = handler } func stop() { current?.stop() current = nil } @discardableResult func feedTranslatedAudio( streamId: String, pcm: Data, sampleRate: Int, channels: Int, bitsPerSample: Int, isFinal: Bool ) -> Bool { guard let h = current, h.isWorking else { return false } if !h.outputStreams.contains(streamId) { return false } return h.onTranslatedAudio( outputStreamId: streamId, pcm: pcm, format: TranslationAudioFormat( sampleRate: sampleRate, channels: channels, bitsPerSample: bitsPerSample ), isFinal: isFinal ) } func feedTranslationResult( srcLang: String?, srcText: String?, destLang: String?, destText: String?, requestId: String? ) { guard let mid = current?.modeId else { return } bridge?.emitTranslationResult( modeId: mid, srcLang: srcLang, srcText: srcText, destLang: destLang, destText: destText, requestId: requestId ) } /// Mirrors Android `isSupportCallTranslationWithStereo`. The iOS SDK does /// not expose a direct boolean for this; we use `trIsPlayWithA2dp` /// (which is true on TWS earphones that support the stereo path) plus the /// `JLDeviceConfigTws.isSupportOpusStereo` flag when available. As a /// pragmatic default we just return `trIsPlayWithA2dp()`, matching the /// demo's `setupModeForCall` heuristic. func isSupportCallTranslationWithStereo(address: String?) -> Bool { #if canImport(JL_BLEKit) guard let server = server else { return false } return server.translationCoordinator.manager?.trIsPlayWithA2dp() ?? false #else return false #endif } /// Audio-translation mode (file-driven) is not yet wired on iOS — the /// dedicated handler maps to MODE_AUDIO_TRANSLATION which the demo drives /// by starting recording-and-play. Returns `false` until implemented to /// keep the Dart contract intact. func feedAudioFilePcm(pcm: Data, sampleRate: Int) -> Bool { false } }