From 02a8bcb19f7c0853656ff7ddea507a2f19040190 Mon Sep 17 00:00:00 2001 From: ValueOn AG Date: Sun, 15 Feb 2026 03:31:15 +0100 Subject: [PATCH] feat(teamsbot): dynamic language and voice selection from Google TTS API - Replaced static VOICE_PRESETS with dynamic API calls to /voice-google/languages and /voice-google/voices - Languages and voices loaded from Google Cloud TTS at runtime - Voice dropdown updates automatically when language changes - Added fetchLanguages() and fetchVoices() API functions with VoiceLanguage/VoiceOption types - Test button uses bot name in sample text for personalized preview - Fallback to basic language options if API unavailable Co-authored-by: Cursor --- src/api/teamsbotApi.ts | 39 ++++++ .../views/teamsbot/TeamsbotSettingsView.tsx | 127 +++++++++--------- 2 files changed, 101 insertions(+), 65 deletions(-) diff --git a/src/api/teamsbotApi.ts b/src/api/teamsbotApi.ts index 8a3c6e3..3a9fe26 100644 --- a/src/api/teamsbotApi.ts +++ b/src/api/teamsbotApi.ts @@ -91,6 +91,19 @@ export interface ConfigUpdateRequest { contextWindowSegments?: number; } +// Voice/Language Types (from Google TTS API) +export interface VoiceLanguage { + code: string; + name: string; +} + +export interface VoiceOption { + name: string; + languageCodes: string[]; + ssmlGender: string; + naturalSampleRateHertz: number; +} + // SSE Event Types export interface TeamsbotSSEEvent { type: 'transcript' | 'botResponse' | 'analysis' | 'suggestedResponse' | 'statusChange' | 'error' | 'ping' | 'sessionState'; @@ -189,6 +202,32 @@ export async function testVoice( return response.data; } +/** + * Fetch available TTS languages from Google Cloud. + */ +export async function fetchLanguages(): Promise { + try { + const response = await api.get('/voice-google/languages'); + return response.data?.languages || []; + } catch { + return []; + } +} + +/** + * Fetch available TTS voices for a language from Google Cloud. + */ +export async function fetchVoices(languageCode: string): Promise { + try { + const response = await api.get('/voice-google/voices', { + params: { languageCode }, + }); + return response.data?.voices || []; + } catch { + return []; + } +} + /** * Create an SSE EventSource for live session streaming. * Returns the EventSource instance for the caller to manage. diff --git a/src/pages/views/teamsbot/TeamsbotSettingsView.tsx b/src/pages/views/teamsbot/TeamsbotSettingsView.tsx index 911c2a8..83e1487 100644 --- a/src/pages/views/teamsbot/TeamsbotSettingsView.tsx +++ b/src/pages/views/teamsbot/TeamsbotSettingsView.tsx @@ -1,54 +1,18 @@ import React, { useState, useEffect, useCallback, useRef } from 'react'; import { useCurrentInstance } from '../../../hooks/useCurrentInstance'; import * as teamsbotApi from '../../../api/teamsbotApi'; -import type { TeamsbotConfig, ConfigUpdateRequest } from '../../../api/teamsbotApi'; +import type { TeamsbotConfig, ConfigUpdateRequest, VoiceLanguage, VoiceOption } from '../../../api/teamsbotApi'; import { FaPlay, FaSpinner } from 'react-icons/fa'; import styles from './Teamsbot.module.css'; -// Common Google TTS voices grouped by language -const VOICE_PRESETS: Record> = { - 'de-DE': [ - { id: 'de-DE-Wavenet-A', label: 'Frau A (Wavenet)' }, - { id: 'de-DE-Wavenet-B', label: 'Mann B (Wavenet)' }, - { id: 'de-DE-Wavenet-C', label: 'Frau C (Wavenet)' }, - { id: 'de-DE-Wavenet-D', label: 'Mann D (Wavenet)' }, - { id: 'de-DE-Neural2-A', label: 'Frau A (Neural)' }, - { id: 'de-DE-Neural2-B', label: 'Mann B (Neural)' }, - ], - 'de-CH': [ - { id: 'de-DE-Wavenet-A', label: 'Frau A (Wavenet)' }, - { id: 'de-DE-Wavenet-B', label: 'Mann B (Wavenet)' }, - ], - 'en-US': [ - { id: 'en-US-Wavenet-C', label: 'Female C (Wavenet)' }, - { id: 'en-US-Wavenet-D', label: 'Male D (Wavenet)' }, - { id: 'en-US-Neural2-C', label: 'Female C (Neural)' }, - { id: 'en-US-Neural2-D', label: 'Male D (Neural)' }, - ], - 'en-GB': [ - { id: 'en-GB-Wavenet-A', label: 'Female A (Wavenet)' }, - { id: 'en-GB-Wavenet-B', label: 'Male B (Wavenet)' }, - ], - 'fr-FR': [ - { id: 'fr-FR-Wavenet-A', label: 'Femme A (Wavenet)' }, - { id: 'fr-FR-Wavenet-B', label: 'Homme B (Wavenet)' }, - { id: 'fr-FR-Neural2-A', label: 'Femme A (Neural)' }, - { id: 'fr-FR-Neural2-D', label: 'Homme D (Neural)' }, - ], - 'it-IT': [ - { id: 'it-IT-Wavenet-A', label: 'Donna A (Wavenet)' }, - { id: 'it-IT-Wavenet-C', label: 'Uomo C (Wavenet)' }, - ], -}; - -const TEST_TEXTS: Record = { - 'de-DE': 'Hallo, ich bin der AI-Assistent. Wie kann ich Ihnen helfen?', - 'de-CH': 'Grüezi, ich bin der AI-Assistent. Wie chan ich Ihne helfe?', - 'en-US': 'Hello, I am the AI assistant. How can I help you?', - 'en-GB': 'Hello, I am the AI assistant. How may I help you?', - 'fr-FR': 'Bonjour, je suis l\'assistant IA. Comment puis-je vous aider?', - 'it-IT': 'Ciao, sono l\'assistente AI. Come posso aiutarla?', -}; +/** Format voice name for display: "de-DE-Wavenet-A" -> "Wavenet A" + gender */ +function _formatVoiceName(voice: VoiceOption): string { + const parts = voice.name.split('-'); + const type = parts[2] || ''; // Wavenet, Neural2, Standard, etc. + const variant = parts[3] || ''; + const gender = voice.ssmlGender === 'FEMALE' ? 'Frau' : voice.ssmlGender === 'MALE' ? 'Mann' : ''; + return `${gender} ${variant} (${type})`.trim(); +} /** * TeamsbotSettingsView - Bot configuration for a feature instance. @@ -68,13 +32,26 @@ export const TeamsbotSettingsView: React.FC = () => { // Form state const [formData, setFormData] = useState({}); + // Dynamic voice data from Google TTS API + const [languages, setLanguages] = useState([]); + const [voices, setVoices] = useState([]); + const [loadingVoices, setLoadingVoices] = useState(false); + const _loadConfig = useCallback(async () => { if (!instanceId) return; try { setLoading(true); - const result = await teamsbotApi.getConfig(instanceId); - setConfig(result.config); - setFormData(result.config); + const [configResult, languagesResult] = await Promise.all([ + teamsbotApi.getConfig(instanceId), + teamsbotApi.fetchLanguages(), + ]); + setConfig(configResult.config); + setFormData(configResult.config); + setLanguages(languagesResult); + // Load voices for the current language + const lang = configResult.config.language || 'de-DE'; + const voicesResult = await teamsbotApi.fetchVoices(lang); + setVoices(voicesResult); setError(null); } catch (err: any) { setError(err.message || 'Fehler beim Laden der Konfiguration'); @@ -110,12 +87,21 @@ export const TeamsbotSettingsView: React.FC = () => { setFormData(prev => ({ ...prev, [field]: value })); }; - const _handleLanguageChange = (language: string) => { + const _handleLanguageChange = async (language: string) => { _updateField('language', language); - // Auto-select first voice for the new language - const voices = VOICE_PRESETS[language] || []; - if (voices.length > 0 && !voices.find(v => v.id === formData.voiceId)) { - _updateField('voiceId', voices[0].id); + // Load voices for the new language dynamically + setLoadingVoices(true); + try { + const voicesResult = await teamsbotApi.fetchVoices(language); + setVoices(voicesResult); + // Auto-select first voice if current voice doesn't match + if (voicesResult.length > 0 && !voicesResult.find(v => v.name === formData.voiceId)) { + _updateField('voiceId', voicesResult[0].name); + } + } catch { + setVoices([]); + } finally { + setLoadingVoices(false); } }; @@ -124,7 +110,8 @@ export const TeamsbotSettingsView: React.FC = () => { setTestingVoice(true); try { const language = formData.language || 'de-DE'; - const testText = TEST_TEXTS[language] || TEST_TEXTS['de-DE']; + const botName = formData.botName || 'AI Assistant'; + const testText = `Hallo, ich bin ${botName}. So klinge ich in diesem Meeting.`; const result = await teamsbotApi.testVoice(instanceId, testText, language, formData.voiceId); if (result.success && result.audio) { @@ -233,14 +220,19 @@ export const TeamsbotSettingsView: React.FC = () => { value={formData.language || 'de-DE'} onChange={(e) => _handleLanguageChange(e.target.value)} > - - - - - - + {languages.length > 0 ? ( + languages.map(lang => ( + + )) + ) : ( + <> + + + + + )} - Sprache fuer Captions und Sprachausgabe + Sprache fuer Captions und Sprachausgabe ({languages.length} Sprachen verfuegbar)
@@ -251,16 +243,19 @@ export const TeamsbotSettingsView: React.FC = () => { style={{ flex: 1 }} value={formData.voiceId || ''} onChange={(e) => _updateField('voiceId', e.target.value)} + disabled={loadingVoices} > - {(VOICE_PRESETS[formData.language || 'de-DE'] || []).map(voice => ( - + {voices.map(voice => ( + ))}
- Google TTS Stimme - klicke Test um eine Vorschau zu hoeren + + {loadingVoices ? 'Lade Stimmen...' : `${voices.length} Stimmen verfuegbar - klicke Test fuer Vorschau`} +