diff --git a/src/api/teamsbotApi.ts b/src/api/teamsbotApi.ts index ded65f4..8a3c6e3 100644 --- a/src/api/teamsbotApi.ts +++ b/src/api/teamsbotApi.ts @@ -57,7 +57,7 @@ export interface TeamsbotConfig { responseMode: 'auto' | 'manual' | 'transcribeOnly'; language: string; voiceId?: string; - bridgeUrl?: string; + browserBotUrl?: string; triggerIntervalSeconds: number; triggerCooldownSeconds: number; contextWindowSegments: number; @@ -85,7 +85,7 @@ export interface ConfigUpdateRequest { responseMode?: 'auto' | 'manual' | 'transcribeOnly'; language?: string; voiceId?: string; - bridgeUrl?: string; + browserBotUrl?: string; triggerIntervalSeconds?: number; triggerCooldownSeconds?: number; contextWindowSegments?: number; @@ -172,6 +172,23 @@ export async function updateConfig(instanceId: string, updates: ConfigUpdateRequ return response.data; } +/** + * Test TTS voice with a sample text. Returns base64-encoded audio. + */ +export async function testVoice( + instanceId: string, + text: string, + language: string, + voiceId?: string, +): Promise<{ success: boolean; audio?: string; format?: string; error?: string }> { + const response = await api.post(`/api/teamsbot/${instanceId}/voice/test`, { + text, + language, + voiceId, + }); + return response.data; +} + /** * 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 c706716..911c2a8 100644 --- a/src/pages/views/teamsbot/TeamsbotSettingsView.tsx +++ b/src/pages/views/teamsbot/TeamsbotSettingsView.tsx @@ -1,9 +1,55 @@ -import React, { useState, useEffect, useCallback } from 'react'; +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 { 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?', +}; + /** * TeamsbotSettingsView - Bot configuration for a feature instance. */ @@ -14,8 +60,10 @@ export const TeamsbotSettingsView: React.FC = () => { const [_config, setConfig] = useState(null); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); + const [testingVoice, setTestingVoice] = useState(false); const [error, setError] = useState(null); const [successMsg, setSuccessMsg] = useState(null); + const audioRef = useRef(null); // Form state const [formData, setFormData] = useState({}); @@ -62,6 +110,49 @@ export const TeamsbotSettingsView: React.FC = () => { setFormData(prev => ({ ...prev, [field]: value })); }; + const _handleLanguageChange = (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); + } + }; + + const _handleTestVoice = async () => { + if (!instanceId) return; + setTestingVoice(true); + try { + const language = formData.language || 'de-DE'; + const testText = TEST_TEXTS[language] || TEST_TEXTS['de-DE']; + const result = await teamsbotApi.testVoice(instanceId, testText, language, formData.voiceId); + + if (result.success && result.audio) { + // Play audio from base64 + const audioBlob = new Blob( + [Uint8Array.from(atob(result.audio), c => c.charCodeAt(0))], + { type: 'audio/mp3' } + ); + const audioUrl = URL.createObjectURL(audioBlob); + if (audioRef.current) { + audioRef.current.pause(); + } + const audio = new Audio(audioUrl); + audioRef.current = audio; + audio.play(); + audio.onended = () => URL.revokeObjectURL(audioUrl); + } else { + setError(result.error || 'Stimmtest fehlgeschlagen'); + setTimeout(() => setError(null), 3000); + } + } catch (err: any) { + setError(err.message || 'Stimmtest fehlgeschlagen'); + setTimeout(() => setError(null), 3000); + } finally { + setTestingVoice(false); + } + }; + if (loading) return
Lade Konfiguration...
; return ( @@ -140,7 +231,7 @@ export const TeamsbotSettingsView: React.FC = () => { + Sprache fuer Captions und Sprachausgabe
- - _updateField('voiceId', e.target.value)} - placeholder="de-DE-Standard-A" - /> - Google TTS Voice ID fuer die Sprachausgabe + +
+ + +
+ Google TTS Stimme - klicke Test um eine Vorschau zu hoeren
@@ -210,15 +318,15 @@ export const TeamsbotSettingsView: React.FC = () => {
- + _updateField('bridgeUrl', e.target.value)} - placeholder="https://bridge.example.com" + value={formData.browserBotUrl || ''} + onChange={(e) => _updateField('browserBotUrl', e.target.value)} + placeholder="Automatisch aus Umgebungskonfiguration" /> - URL des .NET Media Bridge Service + URL des Browser Bot Service. Leer lassen fuer Standard-Konfiguration.