feat(speech): add qwen tts voice preferences

This commit is contained in:
2026-07-24 01:54:37 +08:00
parent faa2a77e9a
commit 474d46e728
8 changed files with 523 additions and 30 deletions
+20 -9
View File
@@ -1,6 +1,8 @@
import type { AsrResponse, PracticeTtsContext, SpeechPlaybackStatus, SpeechSelectedFile, SpeechVoiceProfile, TtsResponse } from '@/types/api';
import { apiRequest, apiUrl, authHeaders, readTextPayload } from './api';
import { getAuth } from './auth';
import type { SpeechCapture } from './speech-capture';
import { applyTtsVoicePreference, getTtsDialectPreference, getTtsVoicePreference } from './tts-voice-preference';
const audioExtensions = ['.mp3', '.wav', '.m4a', '.webm', '.aac', '.ogg'];
@@ -103,11 +105,20 @@ export const transcribeSpeechCapture = (
? transcribeSpeechBlob(capture.blob, filename, registerAbort)
: transcribeSpeechFile(capture.file, registerAbort);
export const resolveTtsVoiceProfile = (voiceProfile?: SpeechVoiceProfile) => {
const auth = getAuth();
return applyTtsVoicePreference(
voiceProfile,
getTtsVoicePreference(auth.tenantId, auth.phone),
getTtsDialectPreference(auth.tenantId, auth.phone)
);
};
export const synthesizeSpeech = (text: string, voiceProfile?: SpeechVoiceProfile, practiceContext?: PracticeTtsContext) =>
apiRequest<TtsResponse>({
url: '/api/ai/tts',
method: 'POST',
data: { text: text.trim().slice(0, 300), voiceProfile, practiceContext },
data: { text: text.trim().slice(0, 300), voiceProfile: resolveTtsVoiceProfile(voiceProfile), practiceContext },
timeout: 60000
});
@@ -118,7 +129,7 @@ export interface SpeechPlaybackSnapshot {
const browserDialectLanguage = (dialect: SpeechVoiceProfile['dialect']) => {
if (dialect === 'cantonese') return 'zh-HK';
if (dialect === 'sichuanese') return '';
if (dialect && dialect !== 'mandarin') return '';
return 'zh-CN';
};
@@ -221,8 +232,8 @@ export const createSpeechPlaybackController = (
stop();
const value = text.trim();
if (!value) return;
const profile = voiceProfile || defaultVoiceProfile;
const sourceKey = cacheKey(value, profile, practiceContext);
const effectiveProfile = resolveTtsVoiceProfile(voiceProfile || defaultVoiceProfile);
const sourceKey = cacheKey(value, effectiveProfile, practiceContext);
const currentGeneration = generation;
publish(key, 'loading');
try {
@@ -233,7 +244,7 @@ export const createSpeechPlaybackController = (
playAudioSource(key, cachedSource, sourceKey, currentGeneration);
return;
}
const result = await synthesizeSpeech(value, profile, practiceContext);
const result = await synthesizeSpeech(value, effectiveProfile, practiceContext);
if (currentGeneration !== generation || activeKey !== key) return;
const source = result.inlineAudioUrl || result.audioUrl;
if (!source) throw new Error('语音合成未返回音频');
@@ -246,7 +257,7 @@ export const createSpeechPlaybackController = (
playAudioSource(key, source, sourceKey, currentGeneration);
} catch (error) {
if (currentGeneration !== generation) return;
const browserFallback = playWithBrowserSpeech(key, value, currentGeneration, profile);
const browserFallback = playWithBrowserSpeech(key, value, currentGeneration, effectiveProfile);
if (browserFallback.played) return;
stop();
onError(browserFallback.message || (error instanceof Error ? error.message : '语音生成失败'));
@@ -261,12 +272,12 @@ export const createSpeechPlaybackController = (
const preload = async (text: string, voiceProfile?: SpeechVoiceProfile, practiceContext?: PracticeTtsContext): Promise<string | null> => {
const value = text.trim();
if (!value) return null;
const profile = voiceProfile || defaultVoiceProfile;
const sourceKey = cacheKey(value, profile, practiceContext);
const effectiveProfile = resolveTtsVoiceProfile(voiceProfile || defaultVoiceProfile);
const sourceKey = cacheKey(value, effectiveProfile, practiceContext);
const cached = sourceCache.get(sourceKey);
if (cached) return cached;
try {
const result = await synthesizeSpeech(value, profile, practiceContext);
const result = await synthesizeSpeech(value, effectiveProfile, practiceContext);
const source = result.inlineAudioUrl || result.audioUrl;
if (!source) return null;
sourceCache.set(sourceKey, source);