From fb75c23e5cd08553f6a35f22d03b6494ede339a3 Mon Sep 17 00:00:00 2001 From: Cale Shapera <25466659+cshape@users.noreply.github.com> Date: Tue, 5 May 2026 12:10:21 -0700 Subject: [PATCH 1/7] Migrate to inworld-tts-2 + Soniox STT, add 54 languages - TTS: switch realtime + REST calls to inworld-tts-2; pass BCP-47 via session.providerData.tts.language and the REST language field so cross-lingual voices stay grounded in the target locale. - STT: switch to soniox/stt-rt-v4 with ISO 639-1 language hint via transcription.language. Soniox emits cumulative partial deltas, so replace the buffer per delta instead of appending (fixes "hi hi there hi there this..." UI bug). - Languages: reshuffle LanguageConfig to make BCP-47 canonical (drop sttLanguageCode + ttsConfig.languageCode); add 54 Soniox-supported languages with alternating Sarah/Jason TTS-2 voices and templated personas. Existing 6 curated personas/voices/topics preserved. - Frontend: bump welcome modal copy to "60+ languages" and let the language dropdown scroll (max-height: 60vh, overflow-y: auto). --- .../src/__tests__/config/languages.test.ts | 2 +- backend/src/__tests__/inworld-llm.test.ts | 11 +- backend/src/__tests__/session-manager.test.ts | 13 +- backend/src/config/languages.ts | 986 +++++++++++++++++- backend/src/config/server.ts | 2 +- backend/src/helpers/tts-audio-generator.ts | 3 +- backend/src/services/inworld-llm.ts | 9 +- backend/src/services/session-manager.ts | 14 +- backend/src/services/websocket-handler.ts | 3 +- frontend/src/components/WelcomeModal.tsx | 2 +- frontend/src/styles/main.css | 4 +- 11 files changed, 986 insertions(+), 63 deletions(-) diff --git a/backend/src/__tests__/config/languages.test.ts b/backend/src/__tests__/config/languages.test.ts index 74c6e77..34f9753 100644 --- a/backend/src/__tests__/config/languages.test.ts +++ b/backend/src/__tests__/config/languages.test.ts @@ -48,7 +48,7 @@ describe('languages config', () => { expect(config.name).toBeTruthy(); expect(config.nativeName).toBeTruthy(); expect(config.flag).toBeTruthy(); - expect(config.sttLanguageCode).toBeTruthy(); + expect(config.bcp47).toBeTruthy(); expect(config.ttsConfig).toBeDefined(); expect(config.ttsConfig.speakerId).toBeTruthy(); expect(config.ttsConfig.modelId).toBeTruthy(); diff --git a/backend/src/__tests__/inworld-llm.test.ts b/backend/src/__tests__/inworld-llm.test.ts index 97226d1..37c1bb4 100644 --- a/backend/src/__tests__/inworld-llm.test.ts +++ b/backend/src/__tests__/inworld-llm.test.ts @@ -295,7 +295,7 @@ describe('InworldLLM', () => { json: async () => ({ audioContent: 'base64audiodata==' }), } as Response); - const audio = await llm.pronounce('Hola', 'Rafael'); + const audio = await llm.pronounce('Hola', 'Rafael', 'es-MX'); expect(audio).toBe('base64audiodata=='); }); @@ -305,7 +305,7 @@ describe('InworldLLM', () => { json: async () => ({ audioContent: 'audio' }), } as Response); - await llm.pronounce('perro', 'Rafael'); + await llm.pronounce('perro', 'Rafael', 'es-MX'); expect(fetchSpy).toHaveBeenCalledWith( 'https://api.inworld.ai/tts/v1/voice', @@ -315,7 +315,8 @@ describe('InworldLLM', () => { const body = JSON.parse(fetchSpy.mock.calls[0][1]!.body as string); expect(body.text).toBe('perro'); expect(body.voice_id).toBe('Rafael'); - expect(body.model_id).toBe('inworld-tts-1.5-max'); + expect(body.model_id).toBe('inworld-tts-2'); + expect(body.language).toBe('es-MX'); expect(body.audio_config.audio_encoding).toBe('LINEAR16'); expect(body.audio_config.sample_rate_hertz).toBe(24000); }); @@ -326,7 +327,7 @@ describe('InworldLLM', () => { status: 500, } as Response); - const audio = await llm.pronounce('Hola', 'Rafael'); + const audio = await llm.pronounce('Hola', 'Rafael', 'es-MX'); expect(audio).toBeNull(); }); @@ -334,7 +335,7 @@ describe('InworldLLM', () => { process.env.INWORLD_API_KEY = ''; const noKeyLlm = new InworldLLM(); - const audio = await noKeyLlm.pronounce('Hola', 'Rafael'); + const audio = await noKeyLlm.pronounce('Hola', 'Rafael', 'es-MX'); expect(audio).toBeNull(); }); }); diff --git a/backend/src/__tests__/session-manager.test.ts b/backend/src/__tests__/session-manager.test.ts index 5205b52..6ab0291 100644 --- a/backend/src/__tests__/session-manager.test.ts +++ b/backend/src/__tests__/session-manager.test.ts @@ -281,7 +281,7 @@ describe('SessionManager', () => { }); describe('streaming STT events', () => { - it('should accumulate transcription deltas incrementally', () => { + it('should treat transcription deltas as cumulative (Soniox)', () => { const clientWs = createMockClientWs(); const mgr = new SessionManager({ sessionId: 'test-stt-1', @@ -298,12 +298,12 @@ describe('SessionManager', () => { handler.call(mgr, { type: 'conversation.item.input_audio_transcription.delta', - delta: 'Hola, ', + delta: 'Hola', }); handler.call(mgr, { type: 'conversation.item.input_audio_transcription.delta', - delta: 'me llamo Cale.', + delta: 'Hola, me llamo Cale.', }); const sent = (clientWs as unknown as { _messages: string[] })._messages; @@ -313,7 +313,7 @@ describe('SessionManager', () => { (m: Record) => m.type === 'partial_transcript' ); expect(partials).toHaveLength(2); - expect(partials[0].text).toBe('Hola, '); + expect(partials[0].text).toBe('Hola'); expect(partials[1].text).toBe('Hola, me llamo Cale.'); }); @@ -430,9 +430,10 @@ describe('SessionManager', () => { const sent = JSON.parse(mockInworldWs.send.mock.calls[0][0]); expect(sent.type).toBe('session.update'); expect(sent.session.audio.input.transcription.model).toBe( - 'assemblyai/u3-rt-pro' + 'soniox/stt-rt-v4' ); - expect(sent.session.audio.input.transcription.language).toBe('es-MX'); + expect(sent.session.audio.input.transcription.language).toBe('es'); + expect(sent.session.providerData.tts.language).toBe('es-MX'); expect(sent.session.model).toBe('openai/gpt-4.1-nano'); }); }); diff --git a/backend/src/config/languages.ts b/backend/src/config/languages.ts index 032033b..8542c43 100644 --- a/backend/src/config/languages.ts +++ b/backend/src/config/languages.ts @@ -1,10 +1,15 @@ /** * Language Configuration System * - * This module provides a centralized configuration for all supported languages. - * To add a new language: - * 1. Add a new entry to SUPPORTED_LANGUAGES with all required fields - * 2. The rest of the app will automatically support the new language + * Centralized configuration for all supported languages. + * + * Wire-format conventions: + * - `bcp47` is the canonical form ("es-ES", "fi-FI"). Used for TTS-2 via + * `session.providerData.tts.language` (and the REST `/tts/v1/voice` `language` field). + * - `code` is ISO 639-1 ("es", "fi"). Used as the map key, dropdown value, + * and Soniox STT hint via `transcription.language`. + * + * To add a new language: add a new entry to SUPPORTED_LANGUAGES. */ import { createLogger } from '../utils/logger.js'; @@ -23,53 +28,42 @@ export interface TTSConfig { modelId: string; speakingRate: number; temperature: number; - languageCode?: string; // Optional TTS language code (e.g., 'ja-JP') } export interface LanguageConfig { - // Identifier - code: string; // e.g., 'es', 'ja', 'fr' + /** ISO 639-1 (e.g., 'es', 'ja', 'fr') — map key, dropdown value, Soniox STT hint. */ + code: string; + /** BCP-47 with uppercase region (e.g., 'es-ES', 'fi-FI') — TTS-2 language hint. */ + bcp47: string; - // Display names name: string; // English name: "Spanish" nativeName: string; // Native name: "Español" flag: string; // Emoji flag - // STT configuration - sttLanguageCode: string; // Language code for speech-to-text - - // TTS configuration ttsConfig: TTSConfig; - - // Teacher persona for this language teacherPersona: TeacherPersona; - - // Example conversation topics specific to this language's culture exampleTopics: string[]; } /** * Supported Languages Configuration * - * Each language defines everything needed for: - * - Speech recognition (STT) - * - Text-to-speech (TTS) - * - Teacher persona and conversation style - * - Cultural context and example topics + * The first 6 entries are curated personas (existing). All other languages + * use Sarah/Jason TTS-2 voices alternated, with concise templated personas. */ export const SUPPORTED_LANGUAGES: Record = { + // ── Curated languages ──────────────────────────────────────── en: { code: 'en', + bcp47: 'en-US', name: 'English', nativeName: 'English', flag: '🇺🇸', - sttLanguageCode: 'en-US', ttsConfig: { speakerId: 'Lauren', - modelId: 'inworld-tts-1.5-max', + modelId: 'inworld-tts-2', speakingRate: 1, temperature: 1.1, - languageCode: 'en-US', }, teacherPersona: { name: 'Ms. Sarah Mitchell', @@ -89,16 +83,15 @@ export const SUPPORTED_LANGUAGES: Record = { es: { code: 'es', + bcp47: 'es-MX', name: 'Spanish', nativeName: 'Español', flag: '🇲🇽', - sttLanguageCode: 'es-MX', // Mexican Spanish ttsConfig: { speakerId: 'Rafael', - modelId: 'inworld-tts-1.5-max', + modelId: 'inworld-tts-2', speakingRate: 1, temperature: 1.1, - languageCode: 'es-MX', }, teacherPersona: { name: 'Señor Gael Herrera', @@ -118,16 +111,15 @@ export const SUPPORTED_LANGUAGES: Record = { fr: { code: 'fr', + bcp47: 'fr-FR', name: 'French', nativeName: 'Français', flag: '🇫🇷', - sttLanguageCode: 'fr-FR', ttsConfig: { speakerId: 'Alain', - modelId: 'inworld-tts-1.5-max', + modelId: 'inworld-tts-2', speakingRate: 1, temperature: 1.1, - languageCode: 'fr-FR', }, teacherPersona: { name: 'Monsieur Lucien Dubois', @@ -148,16 +140,15 @@ export const SUPPORTED_LANGUAGES: Record = { de: { code: 'de', + bcp47: 'de-DE', name: 'German', nativeName: 'Deutsch', flag: '🇩🇪', - sttLanguageCode: 'de-DE', ttsConfig: { speakerId: 'Josef', - modelId: 'inworld-tts-1.5-max', + modelId: 'inworld-tts-2', speakingRate: 1, temperature: 0.7, - languageCode: 'de-DE', }, teacherPersona: { name: 'Herr Klaus Weber', @@ -178,16 +169,15 @@ export const SUPPORTED_LANGUAGES: Record = { it: { code: 'it', + bcp47: 'it-IT', name: 'Italian', nativeName: 'Italiano', flag: '🇮🇹', - sttLanguageCode: 'it-IT', ttsConfig: { speakerId: 'Orietta', - modelId: 'inworld-tts-1.5-max', + modelId: 'inworld-tts-2', speakingRate: 1, temperature: 1.1, - languageCode: 'it-IT', }, teacherPersona: { name: 'Signora Maria Rossi', @@ -208,16 +198,15 @@ export const SUPPORTED_LANGUAGES: Record = { pt: { code: 'pt', + bcp47: 'pt-BR', name: 'Portuguese', nativeName: 'Português', flag: '🇧🇷', - sttLanguageCode: 'pt-BR', // Brazilian Portuguese ttsConfig: { speakerId: 'Heitor', - modelId: 'inworld-tts-1.5-max', + modelId: 'inworld-tts-2', speakingRate: 1, temperature: 0.7, - languageCode: 'pt-BR', }, teacherPersona: { name: 'Senhor João Silva', @@ -235,6 +224,925 @@ export const SUPPORTED_LANGUAGES: Record = { 'the Amazon and Brazilian nature', ], }, + + // ── Soniox-supported languages (alphabetical, alternating Sarah/Jason) ─ + af: { + code: 'af', + bcp47: 'af-ZA', + name: 'Afrikaans', + nativeName: 'Afrikaans', + flag: '🇿🇦', + ttsConfig: { speakerId: 'Sarah', modelId: 'inworld-tts-2', speakingRate: 1, temperature: 1 }, + teacherPersona: { + name: 'Pieter', + age: 36, + nationality: 'South African', + description: + 'a South African tutor who loves teaching Afrikaans through Cape Town life, braai culture, and Karoo road trips', + }, + exampleTopics: ['Cape Town and Table Mountain', 'braai culture and South African food', 'Afrikaans music and writers'], + }, + + sq: { + code: 'sq', + bcp47: 'sq-AL', + name: 'Albanian', + nativeName: 'Shqip', + flag: '🇦🇱', + ttsConfig: { speakerId: 'Jason', modelId: 'inworld-tts-2', speakingRate: 1, temperature: 1 }, + teacherPersona: { + name: 'Arta', + age: 32, + nationality: 'Albanian', + description: + 'an Albanian tutor passionate about Tirana, the Albanian Riviera, and traditional cuisine', + }, + exampleTopics: ['Tirana street life', 'the Albanian Riviera and Ksamil', 'traditional dishes like tavë kosi'], + }, + + ar: { + code: 'ar', + bcp47: 'ar-SA', + name: 'Arabic', + nativeName: 'العربية', + flag: '🇸🇦', + ttsConfig: { speakerId: 'Sarah', modelId: 'inworld-tts-2', speakingRate: 1, temperature: 1 }, + teacherPersona: { + name: 'Layla', + age: 33, + nationality: 'Saudi', + description: + 'a Saudi tutor who loves teaching Arabic through Middle Eastern history, classical poetry, and modern culture', + }, + exampleTopics: ['Arabic poetry and proverbs', 'food across the Levant and Gulf', 'travel to Petra, Cairo, and Riyadh'], + }, + + az: { + code: 'az', + bcp47: 'az-AZ', + name: 'Azerbaijani', + nativeName: 'Azərbaycanca', + flag: '🇦🇿', + ttsConfig: { speakerId: 'Jason', modelId: 'inworld-tts-2', speakingRate: 1, temperature: 1 }, + teacherPersona: { + name: 'Elmira', + age: 34, + nationality: 'Azerbaijani', + description: + 'an Azerbaijani tutor who loves Baku, the Caspian coast, and Caucasus cuisine', + }, + exampleTopics: ['Baku old city and modern skyline', 'plov and traditional Azerbaijani food', 'mugham music'], + }, + + eu: { + code: 'eu', + bcp47: 'eu-ES', + name: 'Basque', + nativeName: 'Euskara', + flag: '🏴', + ttsConfig: { speakerId: 'Sarah', modelId: 'inworld-tts-2', speakingRate: 1, temperature: 1 }, + teacherPersona: { + name: 'Iker', + age: 35, + nationality: 'Basque', + description: + 'a Basque tutor who loves teaching Euskara through San Sebastián pintxos and Bilbao culture', + }, + exampleTopics: ['pintxo bars in Donostia', 'the Guggenheim and Bilbao', 'Basque mythology and rural life'], + }, + + be: { + code: 'be', + bcp47: 'be-BY', + name: 'Belarusian', + nativeName: 'Беларуская', + flag: '🇧🇾', + ttsConfig: { speakerId: 'Jason', modelId: 'inworld-tts-2', speakingRate: 1, temperature: 1 }, + teacherPersona: { + name: 'Hanna', + age: 31, + nationality: 'Belarusian', + description: + 'a Belarusian tutor passionate about Minsk, traditional folk songs, and Belarusian literature', + }, + exampleTopics: ['Minsk and Belarusian cities', 'draniki and traditional cuisine', 'Belarusian folk music'], + }, + + bn: { + code: 'bn', + bcp47: 'bn-BD', + name: 'Bengali', + nativeName: 'বাংলা', + flag: '🇧🇩', + ttsConfig: { speakerId: 'Sarah', modelId: 'inworld-tts-2', speakingRate: 1, temperature: 1 }, + teacherPersona: { + name: 'Anika', + age: 30, + nationality: 'Bangladeshi', + description: + 'a Bangladeshi tutor who loves teaching Bengali through Dhaka life, Tagore poetry, and the Sundarbans', + }, + exampleTopics: ['Dhaka street food', 'Tagore and Bengali literature', 'Sundarbans and rural Bengal'], + }, + + bs: { + code: 'bs', + bcp47: 'bs-BA', + name: 'Bosnian', + nativeName: 'Bosanski', + flag: '🇧🇦', + ttsConfig: { speakerId: 'Jason', modelId: 'inworld-tts-2', speakingRate: 1, temperature: 1 }, + teacherPersona: { + name: 'Edin', + age: 37, + nationality: 'Bosnian', + description: + 'a Bosnian tutor passionate about Sarajevo, ćevapi, and Balkan history', + }, + exampleTopics: ['Sarajevo old town', 'ćevapi and Bosnian cuisine', 'Mostar and the Stari Most bridge'], + }, + + bg: { + code: 'bg', + bcp47: 'bg-BG', + name: 'Bulgarian', + nativeName: 'Български', + flag: '🇧🇬', + ttsConfig: { speakerId: 'Sarah', modelId: 'inworld-tts-2', speakingRate: 1, temperature: 1 }, + teacherPersona: { + name: 'Boyana', + age: 33, + nationality: 'Bulgarian', + description: + 'a Bulgarian tutor who loves Sofia, Rila monasteries, and Black Sea summers', + }, + exampleTopics: ['Sofia and the Vitosha mountains', 'banitsa and shopska salad', 'Bulgarian folk music and dance'], + }, + + ca: { + code: 'ca', + bcp47: 'ca-ES', + name: 'Catalan', + nativeName: 'Català', + flag: '🏴', + ttsConfig: { speakerId: 'Jason', modelId: 'inworld-tts-2', speakingRate: 1, temperature: 1 }, + teacherPersona: { + name: 'Jordi', + age: 36, + nationality: 'Catalan', + description: + 'a Catalan tutor who loves Barcelona, Gaudí, and Mediterranean coastal life', + }, + exampleTopics: ['Barcelona neighborhoods', 'castellers and Catalan traditions', 'pa amb tomàquet and Catalan food'], + }, + + zh: { + code: 'zh', + bcp47: 'zh-CN', + name: 'Chinese', + nativeName: '中文', + flag: '🇨🇳', + ttsConfig: { speakerId: 'Sarah', modelId: 'inworld-tts-2', speakingRate: 1, temperature: 1 }, + teacherPersona: { + name: 'Mei', + age: 32, + nationality: 'Chinese', + description: + 'a Beijing tutor who loves teaching Mandarin through tea culture, classical poetry, and modern Chinese cinema', + }, + exampleTopics: ['Beijing hutongs and street food', 'Chinese tea culture', 'classical poetry and modern films'], + }, + + hr: { + code: 'hr', + bcp47: 'hr-HR', + name: 'Croatian', + nativeName: 'Hrvatski', + flag: '🇭🇷', + ttsConfig: { speakerId: 'Jason', modelId: 'inworld-tts-2', speakingRate: 1, temperature: 1 }, + teacherPersona: { + name: 'Ivana', + age: 34, + nationality: 'Croatian', + description: + 'a Croatian tutor passionate about Dubrovnik, Dalmatian islands, and Adriatic seafood', + }, + exampleTopics: ['Dalmatian coast and islands', 'Plitvice Lakes', 'peka and Croatian seafood'], + }, + + cs: { + code: 'cs', + bcp47: 'cs-CZ', + name: 'Czech', + nativeName: 'Čeština', + flag: '🇨🇿', + ttsConfig: { speakerId: 'Sarah', modelId: 'inworld-tts-2', speakingRate: 1, temperature: 1 }, + teacherPersona: { + name: 'Pavel', + age: 38, + nationality: 'Czech', + description: + 'a Czech tutor who loves teaching through Prague history, Bohemian beer halls, and Czech literature', + }, + exampleTopics: ['Prague castle and the old town', 'Czech pivo and beer culture', 'Kafka and Czech cinema'], + }, + + da: { + code: 'da', + bcp47: 'da-DK', + name: 'Danish', + nativeName: 'Dansk', + flag: '🇩🇰', + ttsConfig: { speakerId: 'Jason', modelId: 'inworld-tts-2', speakingRate: 1, temperature: 1 }, + teacherPersona: { + name: 'Mette', + age: 33, + nationality: 'Danish', + description: + 'a Danish tutor passionate about Copenhagen, hygge, and Nordic design', + }, + exampleTopics: ['Copenhagen and Nyhavn', 'hygge and Scandinavian design', 'smørrebrød and new Nordic cuisine'], + }, + + nl: { + code: 'nl', + bcp47: 'nl-NL', + name: 'Dutch', + nativeName: 'Nederlands', + flag: '🇳🇱', + ttsConfig: { speakerId: 'Sarah', modelId: 'inworld-tts-2', speakingRate: 1, temperature: 1 }, + teacherPersona: { + name: 'Sanne', + age: 31, + nationality: 'Dutch', + description: + 'a Dutch tutor who loves teaching through Amsterdam canals, cycling culture, and Dutch design', + }, + exampleTopics: ['Amsterdam canals and museums', 'cycling and Dutch daily life', 'stroopwafels and bitterballen'], + }, + + et: { + code: 'et', + bcp47: 'et-EE', + name: 'Estonian', + nativeName: 'Eesti', + flag: '🇪🇪', + ttsConfig: { speakerId: 'Jason', modelId: 'inworld-tts-2', speakingRate: 1, temperature: 1 }, + teacherPersona: { + name: 'Kaarel', + age: 34, + nationality: 'Estonian', + description: + 'an Estonian tutor passionate about Tallinn old town, e-Estonia, and Baltic forests', + }, + exampleTopics: ['Tallinn medieval old town', 'Estonian saunas and forest culture', 'e-Estonia and digital society'], + }, + + tl: { + code: 'tl', + bcp47: 'fil-PH', + name: 'Filipino', + nativeName: 'Filipino', + flag: '🇵🇭', + ttsConfig: { speakerId: 'Sarah', modelId: 'inworld-tts-2', speakingRate: 1, temperature: 1 }, + teacherPersona: { + name: 'Liza', + age: 30, + nationality: 'Filipino', + description: + 'a Filipino tutor who loves teaching Tagalog through Manila life, island hopping, and family traditions', + }, + exampleTopics: ['Manila and Cebu', 'adobo and lechon', 'Philippine islands and beaches'], + }, + + fi: { + code: 'fi', + bcp47: 'fi-FI', + name: 'Finnish', + nativeName: 'Suomi', + flag: '🇫🇮', + ttsConfig: { speakerId: 'Jason', modelId: 'inworld-tts-2', speakingRate: 1, temperature: 1 }, + teacherPersona: { + name: 'Aino', + age: 33, + nationality: 'Finnish', + description: + 'a Finnish tutor who loves teaching Finnish through Helsinki life and Nordic culture', + }, + exampleTopics: ['sauna culture', 'Finnish design and architecture', 'life in Helsinki and Lapland'], + }, + + gl: { + code: 'gl', + bcp47: 'gl-ES', + name: 'Galician', + nativeName: 'Galego', + flag: '🏴', + ttsConfig: { speakerId: 'Sarah', modelId: 'inworld-tts-2', speakingRate: 1, temperature: 1 }, + teacherPersona: { + name: 'Brais', + age: 36, + nationality: 'Galician', + description: + 'a Galician tutor passionate about Santiago de Compostela, Atlantic coast, and Galician seafood', + }, + exampleTopics: ['Camino de Santiago', 'pulpo a la gallega and seafood', 'Galician folk music'], + }, + + el: { + code: 'el', + bcp47: 'el-GR', + name: 'Greek', + nativeName: 'Ελληνικά', + flag: '🇬🇷', + ttsConfig: { speakerId: 'Jason', modelId: 'inworld-tts-2', speakingRate: 1, temperature: 1 }, + teacherPersona: { + name: 'Eleni', + age: 35, + nationality: 'Greek', + description: + 'a Greek tutor who loves teaching through Athens history, the Aegean islands, and Greek philosophy', + }, + exampleTopics: ['Athens and the Acropolis', 'Greek islands like Santorini and Crete', 'Greek philosophy and mythology'], + }, + + gu: { + code: 'gu', + bcp47: 'gu-IN', + name: 'Gujarati', + nativeName: 'ગુજરાતી', + flag: '🇮🇳', + ttsConfig: { speakerId: 'Sarah', modelId: 'inworld-tts-2', speakingRate: 1, temperature: 1 }, + teacherPersona: { + name: 'Priya', + age: 32, + nationality: 'Gujarati', + description: + 'a Gujarati tutor passionate about Ahmedabad, vegetarian cuisine, and Garba dance', + }, + exampleTopics: ['Ahmedabad and Gujarati culture', 'dhokla, thepla and vegetarian thalis', 'Navratri and Garba'], + }, + + he: { + code: 'he', + bcp47: 'he-IL', + name: 'Hebrew', + nativeName: 'עברית', + flag: '🇮🇱', + ttsConfig: { speakerId: 'Jason', modelId: 'inworld-tts-2', speakingRate: 1, temperature: 1 }, + teacherPersona: { + name: 'Noa', + age: 31, + nationality: 'Israeli', + description: + 'an Israeli tutor who loves teaching Hebrew through Tel Aviv beach life, Jerusalem history, and modern Israeli culture', + }, + exampleTopics: ['Tel Aviv and Jaffa', 'Jerusalem and the Old City', 'shakshuka and Israeli cuisine'], + }, + + hi: { + code: 'hi', + bcp47: 'hi-IN', + name: 'Hindi', + nativeName: 'हिन्दी', + flag: '🇮🇳', + ttsConfig: { speakerId: 'Sarah', modelId: 'inworld-tts-2', speakingRate: 1, temperature: 1 }, + teacherPersona: { + name: 'Aarav', + age: 34, + nationality: 'Indian', + description: + 'an Indian tutor who loves teaching Hindi through Bollywood, street food, and travel across India', + }, + exampleTopics: ['Delhi and Mumbai life', 'Bollywood films and music', 'Indian street food and chai'], + }, + + hu: { + code: 'hu', + bcp47: 'hu-HU', + name: 'Hungarian', + nativeName: 'Magyar', + flag: '🇭🇺', + ttsConfig: { speakerId: 'Jason', modelId: 'inworld-tts-2', speakingRate: 1, temperature: 1 }, + teacherPersona: { + name: 'Zsófia', + age: 33, + nationality: 'Hungarian', + description: + 'a Hungarian tutor passionate about Budapest, thermal baths, and Magyar literature', + }, + exampleTopics: ['Budapest and the Danube', 'gulyás and Hungarian cuisine', 'thermal baths and ruin pubs'], + }, + + id: { + code: 'id', + bcp47: 'id-ID', + name: 'Indonesian', + nativeName: 'Indonesia', + flag: '🇮🇩', + ttsConfig: { speakerId: 'Sarah', modelId: 'inworld-tts-2', speakingRate: 1, temperature: 1 }, + teacherPersona: { + name: 'Budi', + age: 35, + nationality: 'Indonesian', + description: + 'an Indonesian tutor who loves teaching through Bali, Javanese culture, and the diverse archipelago', + }, + exampleTopics: ['Bali and Java', 'nasi goreng and Indonesian street food', 'island hopping across Indonesia'], + }, + + ja: { + code: 'ja', + bcp47: 'ja-JP', + name: 'Japanese', + nativeName: '日本語', + flag: '🇯🇵', + ttsConfig: { speakerId: 'Jason', modelId: 'inworld-tts-2', speakingRate: 1, temperature: 1 }, + teacherPersona: { + name: 'Yuki', + age: 32, + nationality: 'Japanese', + description: + 'a Japanese tutor who loves teaching through Tokyo neighborhoods, tea ceremony, and modern pop culture', + }, + exampleTopics: ['Tokyo neighborhoods and Kyoto temples', 'sushi, ramen, and izakaya culture', 'anime, manga, and J-pop'], + }, + + kn: { + code: 'kn', + bcp47: 'kn-IN', + name: 'Kannada', + nativeName: 'ಕನ್ನಡ', + flag: '🇮🇳', + ttsConfig: { speakerId: 'Sarah', modelId: 'inworld-tts-2', speakingRate: 1, temperature: 1 }, + teacherPersona: { + name: 'Kavya', + age: 30, + nationality: 'Kannadiga', + description: + 'a Karnataka tutor passionate about Bengaluru, Mysuru palaces, and South Indian cuisine', + }, + exampleTopics: ['Bengaluru tech and café culture', 'Mysuru palace and Hampi ruins', 'masala dosa and South Indian food'], + }, + + kk: { + code: 'kk', + bcp47: 'kk-KZ', + name: 'Kazakh', + nativeName: 'Қазақ', + flag: '🇰🇿', + ttsConfig: { speakerId: 'Jason', modelId: 'inworld-tts-2', speakingRate: 1, temperature: 1 }, + teacherPersona: { + name: 'Aigerim', + age: 32, + nationality: 'Kazakh', + description: + 'a Kazakh tutor who loves teaching through Almaty, the steppes, and Central Asian traditions', + }, + exampleTopics: ['Almaty and Astana', 'beshbarmak and steppe cuisine', 'eagle hunting and Kazakh traditions'], + }, + + ko: { + code: 'ko', + bcp47: 'ko-KR', + name: 'Korean', + nativeName: '한국어', + flag: '🇰🇷', + ttsConfig: { speakerId: 'Sarah', modelId: 'inworld-tts-2', speakingRate: 1, temperature: 1 }, + teacherPersona: { + name: 'Min-jun', + age: 31, + nationality: 'Korean', + description: + 'a Korean tutor who loves teaching through Seoul life, K-pop, and Korean food culture', + }, + exampleTopics: ['Seoul neighborhoods and Jeju island', 'K-pop, K-dramas, and Korean cinema', 'kimchi, bibimbap, and Korean BBQ'], + }, + + lv: { + code: 'lv', + bcp47: 'lv-LV', + name: 'Latvian', + nativeName: 'Latviešu', + flag: '🇱🇻', + ttsConfig: { speakerId: 'Jason', modelId: 'inworld-tts-2', speakingRate: 1, temperature: 1 }, + teacherPersona: { + name: 'Liene', + age: 33, + nationality: 'Latvian', + description: + 'a Latvian tutor passionate about Riga art nouveau, Baltic forests, and folk traditions', + }, + exampleTopics: ['Riga old town and art nouveau', 'Latvian folk songs (dainas)', 'midsummer Jāņi celebrations'], + }, + + lt: { + code: 'lt', + bcp47: 'lt-LT', + name: 'Lithuanian', + nativeName: 'Lietuvių', + flag: '🇱🇹', + ttsConfig: { speakerId: 'Sarah', modelId: 'inworld-tts-2', speakingRate: 1, temperature: 1 }, + teacherPersona: { + name: 'Tomas', + age: 35, + nationality: 'Lithuanian', + description: + 'a Lithuanian tutor who loves Vilnius old town, Curonian Spit dunes, and Baltic history', + }, + exampleTopics: ['Vilnius and Trakai castle', 'cepelinai and Lithuanian cuisine', 'Curonian Spit and the Baltic coast'], + }, + + mk: { + code: 'mk', + bcp47: 'mk-MK', + name: 'Macedonian', + nativeName: 'Македонски', + flag: '🇲🇰', + ttsConfig: { speakerId: 'Jason', modelId: 'inworld-tts-2', speakingRate: 1, temperature: 1 }, + teacherPersona: { + name: 'Nikola', + age: 36, + nationality: 'Macedonian', + description: + 'a Macedonian tutor passionate about Skopje, Lake Ohrid, and Balkan history', + }, + exampleTopics: ['Skopje and Lake Ohrid', 'tavče gravče and Macedonian cuisine', 'Balkan folk music'], + }, + + ms: { + code: 'ms', + bcp47: 'ms-MY', + name: 'Malay', + nativeName: 'Melayu', + flag: '🇲🇾', + ttsConfig: { speakerId: 'Sarah', modelId: 'inworld-tts-2', speakingRate: 1, temperature: 1 }, + teacherPersona: { + name: 'Aisyah', + age: 32, + nationality: 'Malaysian', + description: + 'a Malaysian tutor who loves teaching through KL street food, Penang heritage, and Borneo nature', + }, + exampleTopics: ['Kuala Lumpur and Penang', 'nasi lemak and Malaysian street food', 'Borneo rainforests'], + }, + + ml: { + code: 'ml', + bcp47: 'ml-IN', + name: 'Malayalam', + nativeName: 'മലയാളം', + flag: '🇮🇳', + ttsConfig: { speakerId: 'Jason', modelId: 'inworld-tts-2', speakingRate: 1, temperature: 1 }, + teacherPersona: { + name: 'Anjali', + age: 31, + nationality: 'Malayali', + description: + 'a Kerala tutor passionate about backwater houseboats, Kathakali, and Keralan cuisine', + }, + exampleTopics: ['Kerala backwaters and Kochi', 'sadya and coconut-based cooking', 'Kathakali and traditional arts'], + }, + + mr: { + code: 'mr', + bcp47: 'mr-IN', + name: 'Marathi', + nativeName: 'मराठी', + flag: '🇮🇳', + ttsConfig: { speakerId: 'Sarah', modelId: 'inworld-tts-2', speakingRate: 1, temperature: 1 }, + teacherPersona: { + name: 'Rohan', + age: 33, + nationality: 'Marathi', + description: + 'a Maharashtrian tutor who loves teaching through Mumbai life, Pune culture, and Marathi cinema', + }, + exampleTopics: ['Mumbai and the Western Ghats', 'vada pav and Maharashtrian street food', 'Marathi theatre and cinema'], + }, + + no: { + code: 'no', + bcp47: 'nb-NO', + name: 'Norwegian', + nativeName: 'Norsk', + flag: '🇳🇴', + ttsConfig: { speakerId: 'Jason', modelId: 'inworld-tts-2', speakingRate: 1, temperature: 1 }, + teacherPersona: { + name: 'Sigrid', + age: 32, + nationality: 'Norwegian', + description: + 'a Norwegian tutor passionate about Oslo, fjord hikes, and Nordic outdoor life', + }, + exampleTopics: ['Oslo and the Norwegian fjords', 'friluftsliv and outdoor culture', 'brunost and Norwegian cuisine'], + }, + + fa: { + code: 'fa', + bcp47: 'fa-IR', + name: 'Persian', + nativeName: 'فارسی', + flag: '🇮🇷', + ttsConfig: { speakerId: 'Sarah', modelId: 'inworld-tts-2', speakingRate: 1, temperature: 1 }, + teacherPersona: { + name: 'Darius', + age: 36, + nationality: 'Iranian', + description: + 'an Iranian tutor who loves teaching Persian through Tehran life, classical poetry, and Iranian cuisine', + }, + exampleTopics: ['Tehran and Isfahan', 'Hafez, Rumi and Persian poetry', 'kebabs, stews, and Persian rice dishes'], + }, + + pl: { + code: 'pl', + bcp47: 'pl-PL', + name: 'Polish', + nativeName: 'Polski', + flag: '🇵🇱', + ttsConfig: { speakerId: 'Jason', modelId: 'inworld-tts-2', speakingRate: 1, temperature: 1 }, + teacherPersona: { + name: 'Kasia', + age: 33, + nationality: 'Polish', + description: + 'a Polish tutor passionate about Kraków old town, Polish cinema, and pierogi traditions', + }, + exampleTopics: ['Kraków and Warsaw', 'pierogi and Polish home cooking', 'Polish cinema and history'], + }, + + pa: { + code: 'pa', + bcp47: 'pa-IN', + name: 'Punjabi', + nativeName: 'ਪੰਜਾਬੀ', + flag: '🇮🇳', + ttsConfig: { speakerId: 'Sarah', modelId: 'inworld-tts-2', speakingRate: 1, temperature: 1 }, + teacherPersona: { + name: 'Harpreet', + age: 34, + nationality: 'Punjabi', + description: + 'a Punjabi tutor who loves teaching through Amritsar, bhangra, and Punjabi food culture', + }, + exampleTopics: ['Amritsar and the Golden Temple', 'butter chicken, sarson da saag, and Punjabi food', 'bhangra and Punjabi music'], + }, + + ro: { + code: 'ro', + bcp47: 'ro-RO', + name: 'Romanian', + nativeName: 'Română', + flag: '🇷🇴', + ttsConfig: { speakerId: 'Jason', modelId: 'inworld-tts-2', speakingRate: 1, temperature: 1 }, + teacherPersona: { + name: 'Andrei', + age: 35, + nationality: 'Romanian', + description: + 'a Romanian tutor passionate about Bucharest, Transylvanian castles, and Carpathian villages', + }, + exampleTopics: ['Bucharest and Transylvania', 'sarmale and Romanian home cooking', 'Carpathian mountains and folklore'], + }, + + ru: { + code: 'ru', + bcp47: 'ru-RU', + name: 'Russian', + nativeName: 'Русский', + flag: '🇷🇺', + ttsConfig: { speakerId: 'Sarah', modelId: 'inworld-tts-2', speakingRate: 1, temperature: 1 }, + teacherPersona: { + name: 'Anastasia', + age: 34, + nationality: 'Russian', + description: + 'a Russian tutor who loves teaching through Moscow life, classical literature, and Russian cuisine', + }, + exampleTopics: ['Moscow and St. Petersburg', 'Tolstoy, Dostoevsky and Russian literature', 'borscht, pelmeni and Russian food'], + }, + + sr: { + code: 'sr', + bcp47: 'sr-RS', + name: 'Serbian', + nativeName: 'Српски', + flag: '🇷🇸', + ttsConfig: { speakerId: 'Jason', modelId: 'inworld-tts-2', speakingRate: 1, temperature: 1 }, + teacherPersona: { + name: 'Miloš', + age: 36, + nationality: 'Serbian', + description: + 'a Serbian tutor passionate about Belgrade nightlife, Balkan music, and Serbian traditions', + }, + exampleTopics: ['Belgrade nightlife and Novi Sad', 'ćevapi, ajvar and Serbian food', 'Exit Festival and Balkan music'], + }, + + sk: { + code: 'sk', + bcp47: 'sk-SK', + name: 'Slovak', + nativeName: 'Slovenčina', + flag: '🇸🇰', + ttsConfig: { speakerId: 'Sarah', modelId: 'inworld-tts-2', speakingRate: 1, temperature: 1 }, + teacherPersona: { + name: 'Zuzana', + age: 32, + nationality: 'Slovak', + description: + 'a Slovak tutor who loves Bratislava, the Tatra mountains, and Slovak folk traditions', + }, + exampleTopics: ['Bratislava and the High Tatras', 'bryndzové halušky and Slovak cuisine', 'wooden churches and folk music'], + }, + + sl: { + code: 'sl', + bcp47: 'sl-SI', + name: 'Slovenian', + nativeName: 'Slovenščina', + flag: '🇸🇮', + ttsConfig: { speakerId: 'Jason', modelId: 'inworld-tts-2', speakingRate: 1, temperature: 1 }, + teacherPersona: { + name: 'Maja', + age: 31, + nationality: 'Slovenian', + description: + 'a Slovenian tutor passionate about Ljubljana, Lake Bled, and Julian Alps hiking', + }, + exampleTopics: ['Ljubljana and Lake Bled', 'potica and Slovenian cuisine', 'Julian Alps and Postojna caves'], + }, + + sw: { + code: 'sw', + bcp47: 'sw-KE', + name: 'Swahili', + nativeName: 'Kiswahili', + flag: '🇰🇪', + ttsConfig: { speakerId: 'Sarah', modelId: 'inworld-tts-2', speakingRate: 1, temperature: 1 }, + teacherPersona: { + name: 'Amani', + age: 33, + nationality: 'Kenyan', + description: + 'a Kenyan tutor who loves teaching Swahili through Nairobi life, coastal Lamu, and East African culture', + }, + exampleTopics: ['Nairobi and the Maasai Mara', 'ugali, nyama choma and East African food', 'Swahili coastal culture and Lamu'], + }, + + sv: { + code: 'sv', + bcp47: 'sv-SE', + name: 'Swedish', + nativeName: 'Svenska', + flag: '🇸🇪', + ttsConfig: { speakerId: 'Jason', modelId: 'inworld-tts-2', speakingRate: 1, temperature: 1 }, + teacherPersona: { + name: 'Erik', + age: 35, + nationality: 'Swedish', + description: + 'a Swedish tutor passionate about Stockholm, fika culture, and the Swedish countryside', + }, + exampleTopics: ['Stockholm archipelago', 'fika and Swedish coffee culture', 'midsummer and Swedish traditions'], + }, + + ta: { + code: 'ta', + bcp47: 'ta-IN', + name: 'Tamil', + nativeName: 'தமிழ்', + flag: '🇮🇳', + ttsConfig: { speakerId: 'Sarah', modelId: 'inworld-tts-2', speakingRate: 1, temperature: 1 }, + teacherPersona: { + name: 'Karthik', + age: 34, + nationality: 'Tamil', + description: + 'a Tamil tutor who loves teaching through Chennai life, Tamil cinema, and South Indian temples', + }, + exampleTopics: ['Chennai and Madurai temples', 'idli, dosa and Tamil cuisine', 'Tamil cinema and Carnatic music'], + }, + + te: { + code: 'te', + bcp47: 'te-IN', + name: 'Telugu', + nativeName: 'తెలుగు', + flag: '🇮🇳', + ttsConfig: { speakerId: 'Jason', modelId: 'inworld-tts-2', speakingRate: 1, temperature: 1 }, + teacherPersona: { + name: 'Lakshmi', + age: 32, + nationality: 'Telugu', + description: + 'a Telugu tutor passionate about Hyderabad, biryani, and Tollywood cinema', + }, + exampleTopics: ['Hyderabad and the Charminar', 'Hyderabadi biryani and Andhra cuisine', 'Tollywood films and Telugu poetry'], + }, + + th: { + code: 'th', + bcp47: 'th-TH', + name: 'Thai', + nativeName: 'ไทย', + flag: '🇹🇭', + ttsConfig: { speakerId: 'Sarah', modelId: 'inworld-tts-2', speakingRate: 1, temperature: 1 }, + teacherPersona: { + name: 'Siriporn', + age: 31, + nationality: 'Thai', + description: + 'a Thai tutor who loves teaching through Bangkok markets, island life, and Thai food culture', + }, + exampleTopics: ['Bangkok and Chiang Mai', 'pad thai, tom yum and Thai street food', 'Thai islands and beaches'], + }, + + tr: { + code: 'tr', + bcp47: 'tr-TR', + name: 'Turkish', + nativeName: 'Türkçe', + flag: '🇹🇷', + ttsConfig: { speakerId: 'Jason', modelId: 'inworld-tts-2', speakingRate: 1, temperature: 1 }, + teacherPersona: { + name: 'Emre', + age: 35, + nationality: 'Turkish', + description: + 'a Turkish tutor passionate about Istanbul, Anatolian history, and Turkish cuisine', + }, + exampleTopics: ['Istanbul and the Bosphorus', 'kebabs, mezes and Turkish breakfasts', 'Cappadocia and Turkish coast'], + }, + + uk: { + code: 'uk', + bcp47: 'uk-UA', + name: 'Ukrainian', + nativeName: 'Українська', + flag: '🇺🇦', + ttsConfig: { speakerId: 'Sarah', modelId: 'inworld-tts-2', speakingRate: 1, temperature: 1 }, + teacherPersona: { + name: 'Olena', + age: 33, + nationality: 'Ukrainian', + description: + 'a Ukrainian tutor who loves teaching through Kyiv, Lviv coffee houses, and Ukrainian folk traditions', + }, + exampleTopics: ['Kyiv and Lviv', 'borscht, varenyky and Ukrainian cuisine', 'Ukrainian folk songs and embroidery'], + }, + + ur: { + code: 'ur', + bcp47: 'ur-PK', + name: 'Urdu', + nativeName: 'اردو', + flag: '🇵🇰', + ttsConfig: { speakerId: 'Jason', modelId: 'inworld-tts-2', speakingRate: 1, temperature: 1 }, + teacherPersona: { + name: 'Zara', + age: 32, + nationality: 'Pakistani', + description: + 'a Pakistani tutor passionate about Lahore, Urdu poetry (ghazals), and Mughlai cuisine', + }, + exampleTopics: ['Lahore and Karachi', 'Urdu ghazals and shayari', 'biryani, nihari and Mughlai food'], + }, + + vi: { + code: 'vi', + bcp47: 'vi-VN', + name: 'Vietnamese', + nativeName: 'Tiếng Việt', + flag: '🇻🇳', + ttsConfig: { speakerId: 'Sarah', modelId: 'inworld-tts-2', speakingRate: 1, temperature: 1 }, + teacherPersona: { + name: 'Linh', + age: 30, + nationality: 'Vietnamese', + description: + 'a Vietnamese tutor who loves teaching through Hanoi street food, Hạ Long Bay, and Vietnamese coffee culture', + }, + exampleTopics: ['Hanoi and Ho Chi Minh City', 'phở, bánh mì and Vietnamese street food', 'Hạ Long Bay and the Mekong Delta'], + }, + + cy: { + code: 'cy', + bcp47: 'cy-GB', + name: 'Welsh', + nativeName: 'Cymraeg', + flag: '🏴󠁧󠁢󠁷󠁬󠁳󠁿', + ttsConfig: { speakerId: 'Jason', modelId: 'inworld-tts-2', speakingRate: 1, temperature: 1 }, + teacherPersona: { + name: 'Rhys', + age: 36, + nationality: 'Welsh', + description: + 'a Welsh tutor passionate about Cardiff, Snowdonia hiking, and Welsh poetry traditions', + }, + exampleTopics: ['Cardiff and the Welsh valleys', 'Snowdonia and the coastal path', 'cawl, Welsh cakes and male voice choirs'], + }, }; /** diff --git a/backend/src/config/server.ts b/backend/src/config/server.ts index 81eeea7..22f06be 100644 --- a/backend/src/config/server.ts +++ b/backend/src/config/server.ts @@ -18,5 +18,5 @@ export const serverConfig = { 'wss://api.inworld.ai/api/v1/realtime/session', /** TTS voice model */ - ttsModel: process.env.TTS_MODEL || 'inworld-tts-1.5-max', + ttsModel: process.env.TTS_MODEL || 'inworld-tts-2', } as const; diff --git a/backend/src/helpers/tts-audio-generator.ts b/backend/src/helpers/tts-audio-generator.ts index fa39be9..556e1c0 100644 --- a/backend/src/helpers/tts-audio-generator.ts +++ b/backend/src/helpers/tts-audio-generator.ts @@ -82,7 +82,8 @@ async function generateTTSAudio( body: JSON.stringify({ text: text.trim(), voice_id: voiceId, - model_id: 'inworld-tts-1.5-max', + model_id: 'inworld-tts-2', + language: langConfig.bcp47, audio_config: { audio_encoding: 'LINEAR16', sample_rate_hertz: 24000, diff --git a/backend/src/services/inworld-llm.ts b/backend/src/services/inworld-llm.ts index 8fd9a3d..9302c0f 100644 --- a/backend/src/services/inworld-llm.ts +++ b/backend/src/services/inworld-llm.ts @@ -129,7 +129,11 @@ Text: ${text}`; * Pronounce text using Inworld TTS API. * Returns base64-encoded LINEAR16 audio at 24kHz, or null on failure. */ - async pronounce(text: string, voiceId: string): Promise { + async pronounce( + text: string, + voiceId: string, + bcp47: string + ): Promise { if (!this.apiKey) return null; try { @@ -142,7 +146,8 @@ Text: ${text}`; body: JSON.stringify({ text, voice_id: voiceId, - model_id: 'inworld-tts-1.5-max', + model_id: 'inworld-tts-2', + language: bcp47, audio_config: { audio_encoding: 'LINEAR16', sample_rate_hertz: 24000, diff --git a/backend/src/services/session-manager.ts b/backend/src/services/session-manager.ts index 25a4624..77fa6d4 100644 --- a/backend/src/services/session-manager.ts +++ b/backend/src/services/session-manager.ts @@ -236,10 +236,11 @@ export class SessionManager { break; case 'conversation.item.input_audio_transcription.delta': { - // Deltas are INCREMENTAL — accumulate into buffer (matches Inworld playground) + // Soniox emits CUMULATIVE deltas — each delta is the full transcript + // so far, not just new tokens. Replace the buffer rather than appending. const partialDelta = event.delta as string; if (partialDelta) { - this.userTextBuffer += partialDelta; + this.userTextBuffer = partialDelta; this.wsSend({ type: 'partial_transcript', text: this.userTextBuffer, @@ -348,7 +349,7 @@ export class SessionManager { } private sendSessionUpdate(): void { - const { teacherPersona, name, exampleTopics, ttsConfig, sttLanguageCode } = + const { teacherPersona, name, exampleTopics, ttsConfig, code, bcp47 } = this.langConfig; const memoryContext = this.memory.getContext(); @@ -381,8 +382,8 @@ export class SessionManager { audio: { input: { transcription: { - model: 'assemblyai/u3-rt-pro', - language: sttLanguageCode, + model: 'soniox/stt-rt-v4', + language: code, }, turn_detection: { type: 'semantic_vad', @@ -397,6 +398,9 @@ export class SessionManager { speed: ttsConfig.speakingRate, }, }, + providerData: { + tts: { language: bcp47 }, + }, }, }); } diff --git a/backend/src/services/websocket-handler.ts b/backend/src/services/websocket-handler.ts index c147adc..ae42883 100644 --- a/backend/src/services/websocket-handler.ts +++ b/backend/src/services/websocket-handler.ts @@ -126,7 +126,8 @@ export function setupWebSocketHandlers(wss: WebSocketServer): void { const langConfig = getLanguageConfig(languageCode); const audio = await llm.pronounce( msg.text, - langConfig.ttsConfig.speakerId + langConfig.ttsConfig.speakerId, + langConfig.bcp47 ); if (audio) { wsSend(ws, { diff --git a/frontend/src/components/WelcomeModal.tsx b/frontend/src/components/WelcomeModal.tsx index 46cc8cd..edb89fe 100644 --- a/frontend/src/components/WelcomeModal.tsx +++ b/frontend/src/components/WelcomeModal.tsx @@ -51,7 +51,7 @@ export function WelcomeModal() { Auto flashcards
- 6 languages + 60+ languages