feat: connect Qwen3-ASR-API voice manager and custom OpenAI-compatible TTS support with UI configurations

This commit is contained in:
Michael committed 2026-06-08 19:15:46 +02:00
1 parent 115996c7d9
commit 7c10d633ca
2 files changed
+506 -10

No files matched your search

+249 -5
View File
@@ -892,7 +892,18 @@ export const App: React.FC = () => {
const [selectedVoice, setSelectedVoice] = useState<string>(''); const [selectedVoice, setSelectedVoice] = useState<string>('');
const [ttsRate, setTtsRate] = useState<number>(1.15); const [ttsRate, setTtsRate] = useState<number>(1.15);
const [sttLang, setSttLang] = useState<string>('fr-FR'); const [sttLang, setSttLang] = useState<string>('fr-FR');
const [ttsProvider, setTtsProvider] = useState<'system' | 'google-free'>('google-free'); const [ttsProvider, setTtsProvider] = useState<'system' | 'google-free' | 'openai-tts'>('google-free');
// Configuration personnalisée de l'API Speech-To-Text (ASR) et Text-To-Speech (TTS)
const [sttProvider, setSttProvider] = useState<'browser' | 'qwen3-asr'>('browser');
const [sttApiUrl, setSttApiUrl] = useState<string>('http://127.0.0.1:8000/v1');
const [sttApiKey, setSttApiKey] = useState<string>('');
const [sttModel, setSttModel] = useState<string>('qwen3-asr');
const [ttsApiUrl, setTtsApiUrl] = useState<string>('http://127.0.0.1:8000/v1');
const [ttsApiKey, setTtsApiKey] = useState<string>('');
const [ttsModel, setTtsModel] = useState<string>('tts-1');
const activeAvatar = AVATAR_PROFILES[avatarId]; const activeAvatar = AVATAR_PROFILES[avatarId];
const hermesUnreadRuns = hermesRunHistory.filter(run => !run.read).length; const hermesUnreadRuns = hermesRunHistory.filter(run => !run.read).length;
const hermesSyncLabel = hermesSyncState === 'offline' const hermesSyncLabel = hermesSyncState === 'offline'
@@ -1136,6 +1147,8 @@ export const App: React.FC = () => {
// --- INITIALISATION --- // --- INITIALISATION ---
useEffect(() => { useEffect(() => {
const service = new AudioService(); const service = new AudioService();
service.updateSTTConfig(sttProvider, sttApiUrl, sttApiKey, sttModel);
service.updateTTSConfig(ttsApiUrl, ttsApiKey, ttsModel);
audioServiceRef.current = service; audioServiceRef.current = service;
// Charger la configuration depuis le store persistant (IPC fichier > localStorage) // Charger la configuration depuis le store persistant (IPC fichier > localStorage)
@@ -1159,11 +1172,33 @@ export const App: React.FC = () => {
}); });
persistRead('eveflow_tts_provider').then(savedProvider => { persistRead('eveflow_tts_provider').then(savedProvider => {
if (savedProvider === 'system' || savedProvider === 'google-free') { if (savedProvider === 'system' || savedProvider === 'google-free' || savedProvider === 'openai-tts') {
setTtsProvider(savedProvider as 'system' | 'google-free'); setTtsProvider(savedProvider as 'system' | 'google-free' | 'openai-tts');
} }
}); });
persistRead('eveflow_stt_provider').then(val => {
if (val === 'browser' || val === 'qwen3-asr') setSttProvider(val as 'browser' | 'qwen3-asr');
});
persistRead('eveflow_stt_api_url').then(val => {
if (val) setSttApiUrl(val);
});
persistRead('eveflow_stt_api_key').then(val => {
if (val) setSttApiKey(val);
});
persistRead('eveflow_stt_model').then(val => {
if (val) setSttModel(val);
});
persistRead('eveflow_tts_api_url').then(val => {
if (val) setTtsApiUrl(val);
});
persistRead('eveflow_tts_api_key').then(val => {
if (val) setTtsApiKey(val);
});
persistRead('eveflow_tts_model').then(val => {
if (val) setTtsModel(val);
});
persistRead('eveflow_avatar_id').then(savedAvatar => { persistRead('eveflow_avatar_id').then(savedAvatar => {
if (savedAvatar && savedAvatar in AVATAR_PROFILES) { if (savedAvatar && savedAvatar in AVATAR_PROFILES) {
setAvatarId(savedAvatar as EveAvatar); setAvatarId(savedAvatar as EveAvatar);
@@ -1221,6 +1256,49 @@ export const App: React.FC = () => {
return undefined; return undefined;
}, []); }, []);
// Synchroniser la configuration de AudioService avec l'état React
useEffect(() => {
if (audioServiceRef.current) {
audioServiceRef.current.updateSTTConfig(sttProvider, sttApiUrl, sttApiKey, sttModel);
}
}, [sttProvider, sttApiUrl, sttApiKey, sttModel]);
useEffect(() => {
if (audioServiceRef.current) {
audioServiceRef.current.updateTTSConfig(ttsApiUrl, ttsApiKey, ttsModel);
}
}, [ttsApiUrl, ttsApiKey, ttsModel]);
const handleSttProviderChange = (val: 'browser' | 'qwen3-asr') => {
setSttProvider(val);
persistWrite('eveflow_stt_provider', val);
};
const handleSttApiUrlChange = (val: string) => {
setSttApiUrl(val);
persistWrite('eveflow_stt_api_url', val);
};
const handleSttApiKeyChange = (val: string) => {
setSttApiKey(val);
persistWrite('eveflow_stt_api_key', val);
};
const handleSttModelChange = (val: string) => {
setSttModel(val);
persistWrite('eveflow_stt_model', val);
};
const handleTtsApiUrlChange = (val: string) => {
setTtsApiUrl(val);
persistWrite('eveflow_tts_api_url', val);
};
const handleTtsApiKeyChange = (val: string) => {
setTtsApiKey(val);
persistWrite('eveflow_tts_api_key', val);
};
const handleTtsModelChange = (val: string) => {
setTtsModel(val);
persistWrite('eveflow_tts_model', val);
};
// Défilement automatique // Défilement automatique
useEffect(() => { useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
@@ -2069,7 +2147,7 @@ export const App: React.FC = () => {
</div> </div>
{/* B. TTS / STT */} {/* B. TTS (Synthese Vocale / Voice Reader) */}
<div className="settings-card"> <div className="settings-card">
<h3 className="label-title" style={{ display: 'flex', alignItems: 'center', gap: '6px' }}> <h3 className="label-title" style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
<Volume2 style={{ width: '14px', height: '14px' }} /> Voix de {activeAvatar.name} (Text-to-Speech) <Volume2 style={{ width: '14px', height: '14px' }} /> Voix de {activeAvatar.name} (Text-to-Speech)
@@ -2078,7 +2156,7 @@ export const App: React.FC = () => {
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}> <div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
<div> <div>
<label className="label-title">Type de Synthèse Vocale</label> <label className="label-title">Type de Synthèse Vocale</label>
<div className="grid-2" style={{ gap: '10px', marginTop: '4px' }}> <div className="grid-3" style={{ gap: '10px', marginTop: '4px' }}>
<button <button
onClick={() => { onClick={() => {
setTtsProvider('google-free'); setTtsProvider('google-free');
@@ -2113,6 +2191,23 @@ export const App: React.FC = () => {
> >
🔌 Voix Système (Local) 🔌 Voix Système (Local)
</button> </button>
<button
onClick={() => {
setTtsProvider('openai-tts');
persistWrite('eveflow_tts_provider', 'openai-tts');
}}
className="neon-btn"
style={{
padding: '8px 12px',
fontSize: '11px',
background: ttsProvider === 'openai-tts' ? '' : 'transparent',
border: ttsProvider === 'openai-tts' ? 'none' : '1px solid #cbd5e1',
color: ttsProvider === 'openai-tts' ? 'white' : '#64748b',
boxShadow: ttsProvider === 'openai-tts' ? '' : 'none'
}}
>
🌐 API Personnalisée
</button>
</div> </div>
</div> </div>
@@ -2143,6 +2238,63 @@ export const App: React.FC = () => {
</div> </div>
)} )}
{ttsProvider === 'openai-tts' && (
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
<div>
<label className="label-title">URL de l'API TTS</label>
<input
type="text"
value={ttsApiUrl}
onChange={(e) => handleTtsApiUrlChange(e.target.value)}
className="glow-input"
style={{ padding: '8px 12px', fontSize: '12px' }}
placeholder="Ex: http://127.0.0.1:8000/v1"
/>
<span style={{ display: 'block', marginTop: '5px', fontSize: '10px', fontFamily: 'var(--font-mono)', color: 'var(--text-secondary)', lineHeight: 1.5 }}>
ℹ️ L'endpoint <code>/v1/audio/speech</code> sera utilisé pour la synthèse.
</span>
</div>
<div className="grid-2">
<div>
<label className="label-title">Modèle TTS</label>
<input
type="text"
value={ttsModel}
onChange={(e) => handleTtsModelChange(e.target.value)}
className="glow-input"
style={{ padding: '8px 12px', fontSize: '12px' }}
placeholder="tts-1"
/>
</div>
<div>
<label className="label-title">Clé API (Optionnel)</label>
<input
type="password"
value={ttsApiKey}
onChange={(e) => handleTtsApiKeyChange(e.target.value)}
className="glow-input"
style={{ padding: '8px 12px', fontSize: '12px' }}
placeholder="Bearer token (API_KEY)"
/>
</div>
</div>
<div>
<label className="label-title">Nom de la Voix (ex: alloy, onyx, nova)</label>
<input
type="text"
value={selectedVoice}
onChange={(e) => {
setSelectedVoice(e.target.value);
persistWrite('eveflow_voice', e.target.value);
}}
className="glow-input"
style={{ padding: '8px 12px', fontSize: '12px' }}
placeholder="alloy"
/>
</div>
</div>
)}
<div className="grid-2"> <div className="grid-2">
<div> <div>
<label className="label-title">Vitesse: {ttsRate.toFixed(2)}</label> <label className="label-title">Vitesse: {ttsRate.toFixed(2)}</label>
@@ -2185,6 +2337,98 @@ export const App: React.FC = () => {
</button> </button>
</div> </div>
</div> </div>
{/* C. STT (Reconnaissance Vocale) */}
<div className="settings-card">
<h3 className="label-title" style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
<Mic style={{ width: '14px', height: '14px' }} /> Reconnaissance Vocale (Speech-to-Text)
</h3>
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
<div>
<label className="label-title">Moteur de Reconnaissance Vocale</label>
<div className="grid-2" style={{ gap: '10px', marginTop: '4px' }}>
<button
onClick={() => handleSttProviderChange('browser')}
className="neon-btn"
style={{
padding: '8px 12px',
fontSize: '11px',
background: sttProvider === 'browser' ? '' : 'transparent',
border: sttProvider === 'browser' ? 'none' : '1px solid #cbd5e1',
color: sttProvider === 'browser' ? 'white' : '#64748b',
boxShadow: sttProvider === 'browser' ? '' : 'none'
}}
>
🟢 Moteur Navigateur (Local)
</button>
<button
onClick={() => handleSttProviderChange('qwen3-asr')}
className="neon-btn"
style={{
padding: '8px 12px',
fontSize: '11px',
background: sttProvider === 'qwen3-asr' ? '' : 'transparent',
border: sttProvider === 'qwen3-asr' ? 'none' : '1px solid #cbd5e1',
color: sttProvider === 'qwen3-asr' ? 'white' : '#64748b',
boxShadow: sttProvider === 'qwen3-asr' ? '' : 'none'
}}
>
🌐 Qwen3 ASR (API)
</button>
</div>
</div>
{sttProvider === 'browser' && (
<div style={{ padding: '8px 12px', backgroundColor: 'var(--accent-light)', border: '1px solid rgba(0,212,245,0.2)', borderRadius: '10px', fontSize: '11px', fontFamily: 'var(--font-mono)', color: 'var(--text-neon)', lineHeight: 1.5 }}>
ℹ️ **Mode Navigateur Actif** : Utilise l'API de reconnaissance vocale standard intégrée à votre système (locale, rapide et gratuite).
</div>
)}
{sttProvider === 'qwen3-asr' && (
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
<div>
<label className="label-title">URL de l'API STT</label>
<input
type="text"
value={sttApiUrl}
onChange={(e) => handleSttApiUrlChange(e.target.value)}
className="glow-input"
style={{ padding: '8px 12px', fontSize: '12px' }}
placeholder="Ex: http://127.0.0.1:8000/v1"
/>
<span style={{ display: 'block', marginTop: '5px', fontSize: '10px', fontFamily: 'var(--font-mono)', color: 'var(--text-secondary)', lineHeight: 1.5 }}>
ℹ️ L'endpoint <code>/v1/audio/transcriptions</code> sera utilisé pour la transcription.
</span>
</div>
<div className="grid-2">
<div>
<label className="label-title">Modèle ASR</label>
<input
type="text"
value={sttModel}
onChange={(e) => handleSttModelChange(e.target.value)}
className="glow-input"
style={{ padding: '8px 12px', fontSize: '12px' }}
placeholder="qwen3-asr"
/>
</div>
<div>
<label className="label-title">Clé API (Optionnel)</label>
<input
type="password"
value={sttApiKey}
onChange={(e) => handleSttApiKeyChange(e.target.value)}
className="glow-input"
style={{ padding: '8px 12px', fontSize: '12px' }}
placeholder="Bearer token"
/>
</div>
</div>
</div>
)}
</div>
</div>
</section> </section>
)} )}
+257 -5
View File
@@ -55,10 +55,22 @@ export class AudioService {
private recognition: any = null; private recognition: any = null;
private isListeningActive = false; private isListeningActive = false;
private currentAudioElement: HTMLAudioElement | null = null; private currentAudioElement: HTMLAudioElement | null = null;
private audioQueue: { text: string; voiceName?: string; rate: number; pitch: number; provider: 'system' | 'google-free' }[] = []; private audioQueue: { text: string; voiceName?: string; rate: number; pitch: number; provider: 'system' | 'google-free' | 'openai-tts' }[] = [];
private isPlayingQueue = false; private isPlayingQueue = false;
private activeReject: ((err: any) => void) | null = null; private activeReject: ((err: any) => void) | null = null;
// ASR/STT & TTS Custom API settings
private sttProvider: 'browser' | 'qwen3-asr' = 'browser';
private sttApiUrl = 'http://127.0.0.1:8000/v1';
private sttApiKey = '';
private sttModel = 'qwen3-asr';
private mediaRecorder: MediaRecorder | null = null;
private audioChunks: Blob[] = [];
private ttsApiUrl = 'http://127.0.0.1:8000/v1';
private ttsApiKey = '';
private ttsModel = 'tts-1';
public get isSpeakingActive(): boolean { public get isSpeakingActive(): boolean {
return this.isPlayingQueue || this.audioQueue.length > 0; return this.isPlayingQueue || this.audioQueue.length > 0;
} }
@@ -94,6 +106,21 @@ export class AudioService {
this.initSTT(); this.initSTT();
} }
// Permet de mettre à jour dynamiquement la configuration de la reconnaissance vocale
public updateSTTConfig(provider: 'browser' | 'qwen3-asr', apiUrl: string, apiKey: string, model: string) {
this.sttProvider = provider;
this.sttApiUrl = apiUrl;
this.sttApiKey = apiKey;
this.sttModel = model;
}
// Permet de mettre à jour dynamiquement la configuration du lecteur de voix personnalisé
public updateTTSConfig(apiUrl: string, apiKey: string, model: string) {
this.ttsApiUrl = apiUrl;
this.ttsApiKey = apiKey;
this.ttsModel = model;
}
// Initialisation de la reconnaissance vocale locale et gratuite (Web Speech API) // Initialisation de la reconnaissance vocale locale et gratuite (Web Speech API)
private initSTT() { private initSTT() {
const SpeechRecognition = (window as any).SpeechRecognition || (window as any).webkitSpeechRecognition; const SpeechRecognition = (window as any).SpeechRecognition || (window as any).webkitSpeechRecognition;
@@ -339,6 +366,98 @@ export class AudioService {
} }
} }
// Synthese vocale personnalisee via API OpenAI /v1/audio/speech
private async speakCustomAPI(text: string, voiceName?: string, rate: number = 1.0): Promise<void> {
const cleanText = this._cleanForTTS(text);
if (!cleanText.trim()) return;
if (!this.ttsApiUrl) {
throw new Error("L'URL de l'API TTS n'est pas configurée.");
}
const base = this.ttsApiUrl.replace(/\/$/, '');
let endpoint: string;
if (base.endsWith('/speech')) {
endpoint = base;
} else if (base.endsWith('/v1')) {
endpoint = `${base}/audio/speech`;
} else {
endpoint = `${base}/v1/audio/speech`;
}
const body = {
model: this.ttsModel || 'tts-1',
input: cleanText,
voice: voiceName || 'alloy'
};
const headers: Record<string, string> = {
'Content-Type': 'application/json'
};
if (this.ttsApiKey) {
headers['Authorization'] = `Bearer ${this.ttsApiKey.trim()}`;
}
const response = await fetch(endpoint, {
method: 'POST',
headers,
body: JSON.stringify(body)
});
if (!response.ok) {
const rawErr = await response.text().catch(() => '(no body)');
let errMsg = `Erreur TTS API: ${response.status}`;
try {
const parsed = JSON.parse(rawErr);
errMsg = parsed.error?.message || parsed.message || rawErr;
} catch {}
throw new Error(errMsg);
}
const blob = await response.blob();
const url = URL.createObjectURL(blob);
await new Promise<void>((resolve, reject) => {
const audio = new Audio(url);
this.currentAudioElement = audio;
audio.playbackRate = rate;
this.activeReject = reject;
const timeout = setTimeout(() => {
if (this.currentAudioElement === audio) this.currentAudioElement = null;
this.activeReject = null;
audio.pause();
reject(new Error("playback_stopped"));
}, 60000); // 60 secondes max pour une portion
audio.onended = () => {
clearTimeout(timeout);
if (this.currentAudioElement === audio) this.currentAudioElement = null;
this.activeReject = null;
URL.revokeObjectURL(url);
resolve();
};
audio.onerror = () => {
clearTimeout(timeout);
if (this.currentAudioElement === audio) this.currentAudioElement = null;
this.activeReject = null;
URL.revokeObjectURL(url);
const code = audio.error ? audio.error.code : 'UNKNOWN';
const msg = audio.error ? audio.error.message : 'No message';
reject(new Error(`Erreur de lecture audio API (code: ${code}, msg: ${msg})`));
};
audio.play().catch((err) => {
clearTimeout(timeout);
if (this.currentAudioElement === audio) this.currentAudioElement = null;
this.activeReject = null;
URL.revokeObjectURL(url);
reject(err);
});
});
}
// File de traitement asynchrone des phrases de synthèse vocale (Zero Trust & Zero Dependency) // File de traitement asynchrone des phrases de synthèse vocale (Zero Trust & Zero Dependency)
private async processQueue(): Promise<void> { private async processQueue(): Promise<void> {
if (this.isPlayingQueue) return; if (this.isPlayingQueue) return;
@@ -351,6 +470,8 @@ export class AudioService {
try { try {
if (item.provider === 'google-free') { if (item.provider === 'google-free') {
await this.speakGoogleFree(item.text, item.rate); await this.speakGoogleFree(item.text, item.rate);
} else if (item.provider === 'openai-tts') {
await this.speakCustomAPI(item.text, item.voiceName, item.rate);
} else { } else {
await new Promise<void>((resolve, reject) => { await new Promise<void>((resolve, reject) => {
if (!this.ttsSynth) { reject('TTS non supporté'); return; } if (!this.ttsSynth) { reject('TTS non supporté'); return; }
@@ -371,7 +492,7 @@ export class AudioService {
break; break;
} }
this.logWarn("AudioService", "Échec de la lecture de la phrase dans la file", err); this.logWarn("AudioService", "Échec de la lecture de la phrase dans la file", err);
if (item.provider === 'google-free') { if (item.provider === 'google-free' || item.provider === 'openai-tts') {
// Repli automatique transparent sur la voix locale hors-ligne pour ce segment précis en cas de défaillance // Repli automatique transparent sur la voix locale hors-ligne pour ce segment précis en cas de défaillance
try { try {
await new Promise<void>((resolve, reject) => { await new Promise<void>((resolve, reject) => {
@@ -406,7 +527,7 @@ export class AudioService {
voiceName?: string, voiceName?: string,
rate: number = 1.0, rate: number = 1.0,
pitch: number = 1.1, pitch: number = 1.1,
provider: 'system' | 'google-free' = 'google-free' provider: 'system' | 'google-free' | 'openai-tts' = 'google-free'
): Promise<void> { ): Promise<void> {
this.stopSpeaking(); this.stopSpeaking();
this.queueSentence(text, voiceName, rate, pitch, provider); this.queueSentence(text, voiceName, rate, pitch, provider);
@@ -423,7 +544,7 @@ export class AudioService {
voiceName?: string, voiceName?: string,
rate: number = 1.0, rate: number = 1.0,
pitch: number = 1.1, pitch: number = 1.1,
provider: 'system' | 'google-free' = 'google-free' provider: 'system' | 'google-free' | 'openai-tts' = 'google-free'
): void { ): void {
const clean = text.trim(); const clean = text.trim();
if (!clean) return; if (!clean) return;
@@ -463,6 +584,126 @@ export class AudioService {
} }
} }
// Démarrer l'écoute vocale API via MediaRecorder
private async startListeningAPI(
onResult: (text: string) => void,
onStart: () => void,
onEnd: () => void,
onError: (err: string) => void,
lang: string
) {
if (this.isListeningActive) {
this.stopListening();
}
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
this.audioChunks = [];
let options = {};
if (MediaRecorder.isTypeSupported('audio/webm;codecs=opus')) {
options = { mimeType: 'audio/webm;codecs=opus' };
} else if (MediaRecorder.isTypeSupported('audio/webm')) {
options = { mimeType: 'audio/webm' };
}
this.mediaRecorder = new MediaRecorder(stream, options);
this.isListeningActive = true;
this.mediaRecorder.ondataavailable = (event) => {
if (event.data && event.data.size > 0) {
this.audioChunks.push(event.data);
}
};
this.mediaRecorder.onstart = () => {
onStart();
};
this.mediaRecorder.onstop = async () => {
this.isListeningActive = false;
onEnd();
// Stopper toutes les pistes pour éteindre le voyant du microphone
stream.getTracks().forEach(track => track.stop());
if (this.audioChunks.length === 0) {
onError("Aucune donnée audio capturée");
return;
}
const audioBlob = new Blob(this.audioChunks, { type: this.mediaRecorder?.mimeType || 'audio/webm' });
try {
const text = await this.transcribeAudioWithAPI(audioBlob, lang);
onResult(text);
} catch (err: any) {
onError(err.message || "Erreur de transcription de l'API");
}
};
this.mediaRecorder.start();
} catch (err: any) {
this.isListeningActive = false;
onError(err.message || "Impossible d'accéder au microphone.");
}
}
// Requête API Speech-To-Text compatible OpenAI /v1/audio/transcriptions
private async transcribeAudioWithAPI(audioBlob: Blob, lang: string): Promise<string> {
if (!this.sttApiUrl) {
throw new Error("L'URL de l'API STT n'est pas configurée.");
}
const base = this.sttApiUrl.replace(/\/$/, '');
let endpoint: string;
if (base.endsWith('/transcriptions')) {
endpoint = base;
} else if (base.endsWith('/v1')) {
endpoint = `${base}/audio/transcriptions`;
} else {
endpoint = `${base}/v1/audio/transcriptions`;
}
const formData = new FormData();
const ext = audioBlob.type.includes('wav') ? 'wav' : 'webm';
formData.append('file', audioBlob, `audio.${ext}`);
formData.append('model', this.sttModel || 'qwen3-asr');
if (lang) {
const isoLang = lang.split('-')[0];
formData.append('language', isoLang);
}
const headers: Record<string, string> = {};
if (this.sttApiKey) {
headers['Authorization'] = `Bearer ${this.sttApiKey.trim()}`;
}
const response = await fetch(endpoint, {
method: 'POST',
headers,
body: formData
});
if (!response.ok) {
const rawErr = await response.text().catch(() => '(no body)');
let errMsg = `Erreur STT API: ${response.status}`;
try {
const parsed = JSON.parse(rawErr);
errMsg = parsed.error?.message || parsed.message || rawErr;
} catch {}
throw new Error(errMsg);
}
const data = await response.json();
if (data && typeof data.text === 'string') {
return data.text;
}
throw new Error("Format de réponse de l'API STT invalide.");
}
// Lancer l'écoute vocale (STT - Speech-To-Text) // Lancer l'écoute vocale (STT - Speech-To-Text)
public startListening( public startListening(
onResult: (text: string) => void, onResult: (text: string) => void,
@@ -471,8 +712,11 @@ export class AudioService {
onError: (err: string) => void, onError: (err: string) => void,
lang: string = 'fr-FR' lang: string = 'fr-FR'
) { ) {
if (this.sttProvider === 'qwen3-asr') {
this.startListeningAPI(onResult, onStart, onEnd, onError, lang);
} else {
if (!this.recognition) { if (!this.recognition) {
onError("STT non supporté"); onError("STT non supporté par ce navigateur");
return; return;
} }
@@ -507,12 +751,20 @@ export class AudioService {
onError(e.message || "Erreur de démarrage de l'écoute"); onError(e.message || "Erreur de démarrage de l'écoute");
} }
} }
}
// Arrêter l'écoute vocale // Arrêter l'écoute vocale
public stopListening() { public stopListening() {
if (this.sttProvider === 'qwen3-asr') {
if (this.mediaRecorder && this.mediaRecorder.state !== 'inactive') {
this.mediaRecorder.stop();
this.isListeningActive = false;
}
} else {
if (this.recognition && this.isListeningActive) { if (this.recognition && this.isListeningActive) {
this.recognition.stop(); this.recognition.stop();
this.isListeningActive = false; this.isListeningActive = false;
} }
} }
}
} }