diff --git a/app/RealtimeTranslation.tsx b/app/RealtimeTranslation.tsx index 7e2fdeb..b21b3f9 100644 --- a/app/RealtimeTranslation.tsx +++ b/app/RealtimeTranslation.tsx @@ -40,6 +40,7 @@ import VmWebrtcTranslatorModule, { closeTranslationConnectionAsync, muteUnmuteOutgoingAudio, openTranslationConnectionAsync, + updateOutputLanguage, type TranslationTranscriptEventPayload, } from "../modules/vm-webrtc/src/VmWebrtcTranslatorModule"; @@ -378,7 +379,14 @@ export function RealtimeTranslation({ const handleSelectLanguage = useCallback((code: string) => { setOutputLanguage(code); saveOutputLanguage(code); - }, []); + if (isSessionActive) { + try { + updateOutputLanguage(code); + } catch (e) { + log.warn("[RealtimeTranslation] updateOutputLanguage unavailable", {}, { error: e }); + } + } + }, [isSessionActive]); const isSpeakerphone = audioOutput === "speakerphone"; @@ -386,7 +394,6 @@ export function RealtimeTranslation({ setLanguageModalVisible(true)} /> diff --git a/components/realtime/LanguagePickerRow.tsx b/components/realtime/LanguagePickerRow.tsx index c77e990..a4e8b46 100644 --- a/components/realtime/LanguagePickerRow.tsx +++ b/components/realtime/LanguagePickerRow.tsx @@ -7,14 +7,12 @@ import { type Props = { outputLanguage: string; - isSessionActive: boolean; onSelectLanguage: (code: string) => void; onOpenModal: () => void; }; export function LanguagePickerRow({ outputLanguage, - isSessionActive, onSelectLanguage, onOpenModal, }: Props) { @@ -29,11 +27,10 @@ export function LanguagePickerRow({ return ( !isSessionActive && onSelectLanguage(lang.code)} + onPress={() => onSelectLanguage(lang.code)} style={[ styles.langChip, isSelected && styles.langChipSelected, - isSessionActive && styles.langChipDisabled, ]} > {lang.flag} @@ -49,11 +46,10 @@ export function LanguagePickerRow({ ); })} !isSessionActive && onOpenModal()} + onPress={() => onOpenModal()} style={[ styles.langChip, styles.langChipMore, - isSessionActive && styles.langChipDisabled, !FEATURED_LANGUAGE_CODES.includes(outputLanguage) && styles.langChipSelected, ]} @@ -123,9 +119,6 @@ const styles = StyleSheet.create({ langChipMore: { backgroundColor: "#F2F2F7", }, - langChipDisabled: { - opacity: 0.45, - }, langFlag: { fontSize: 18, }, diff --git a/modules/vm-webrtc/ios/OpenAIWebRTCTranslatorClient.swift b/modules/vm-webrtc/ios/OpenAIWebRTCTranslatorClient.swift index efd705f..801a51b 100644 --- a/modules/vm-webrtc/ios/OpenAIWebRTCTranslatorClient.swift +++ b/modules/vm-webrtc/ios/OpenAIWebRTCTranslatorClient.swift @@ -132,6 +132,23 @@ final class OpenAIWebRTCTranslatorClient: OpenAIWebRTCBase { return result } + // MARK: Language update + + @MainActor + func updateOutputLanguage(_ language: String) { + let trimmed = language.trimmingCharacters(in: .whitespacesAndNewlines) + guard !trimmed.isEmpty else { return } + outputLanguage = trimmed + logger.log( + "[VmWebrtc][Translator] Sending language update", + attributes: logAttributes(for: .info, metadata: ["role": role, "language": trimmed]) + ) + _ = sendEvent([ + "type": "session.update", + "session": ["audio": ["output": ["language": trimmed]]] + ]) + } + // MARK: Virtual hook overrides override func dataChannelDidOpen() { diff --git a/modules/vm-webrtc/ios/WebRTCTranslatorModule.swift b/modules/vm-webrtc/ios/WebRTCTranslatorModule.swift index 32fdf16..8fb70c0 100644 --- a/modules/vm-webrtc/ios/WebRTCTranslatorModule.swift +++ b/modules/vm-webrtc/ios/WebRTCTranslatorModule.swift @@ -125,5 +125,11 @@ public class WebRTCTranslatorModule: Module { self.reverseClient.setOutgoingAudioMuted(shouldMute) } } + + Function("updateOutputLanguage") { (language: String) in + Task { @MainActor in + self.primaryClient.updateOutputLanguage(language) + } + } } } diff --git a/modules/vm-webrtc/src/VmWebrtcTranslatorModule.ts b/modules/vm-webrtc/src/VmWebrtcTranslatorModule.ts index 7792b84..41a49c4 100644 --- a/modules/vm-webrtc/src/VmWebrtcTranslatorModule.ts +++ b/modules/vm-webrtc/src/VmWebrtcTranslatorModule.ts @@ -39,6 +39,7 @@ declare class VmWebrtcTranslatorModule extends NativeModule; closeTranslationConnectionAsync(): Promise; muteUnmuteOutgoingAudio(shouldMute: boolean): void; + updateOutputLanguage(language: string): void; } const makeUnavailableError = () => @@ -84,4 +85,10 @@ export const muteUnmuteOutgoingAudio = (shouldMute: boolean): void => { module.muteUnmuteOutgoingAudio(shouldMute); }; +export const updateOutputLanguage = (language: string): void => { + if (!module) throw makeUnavailableError(); + log.debug(`[${MODULE_NAME}] updateOutputLanguage`, {}, { language }); + module.updateOutputLanguage(language); +}; + export default module ?? ({} as VmWebrtcTranslatorModule);