feat(aihr): harden M0-M2 mobile training flows

This commit is contained in:
2026-07-23 21:18:13 +08:00
parent 06322c994b
commit 927458b9e0
44 changed files with 1674 additions and 119 deletions
+27 -12
View File
@@ -4,12 +4,15 @@ import type { SpeechCapture } from './speech-capture';
const audioExtensions = ['.mp3', '.wav', '.m4a', '.webm', '.aac', '.ogg'];
export const MENTOR_VOICE_PROFILE: SpeechVoiceProfile = { role: 'mentor', emotion: 'warm', speed: 0.88 };
export const INTERVIEWER_VOICE_PROFILE: SpeechVoiceProfile = { role: 'interviewer', emotion: 'professional', speed: 0.96 };
export const customerVoiceProfile = (emotion = 0): SpeechVoiceProfile => {
if (emotion >= 80) return { role: 'customer', emotion: 'intense', speed: 1.12 };
if (emotion >= 70) return { role: 'customer', emotion: 'serious', speed: 1.04 };
return { role: 'customer', emotion: 'calm', speed: 0.98 };
export const MENTOR_VOICE_PROFILE: SpeechVoiceProfile = { role: 'mentor', emotion: 'warm', speed: 0.88, dialect: 'mandarin' };
export const INTERVIEWER_VOICE_PROFILE: SpeechVoiceProfile = { role: 'interviewer', emotion: 'professional', speed: 0.96, dialect: 'mandarin' };
export const customerVoiceProfile = (
emotion = 0,
dialect: SpeechVoiceProfile['dialect'] = 'mandarin'
): SpeechVoiceProfile => {
if (emotion >= 80) return { role: 'customer', emotion: 'intense', speed: 1.12, dialect };
if (emotion >= 70) return { role: 'customer', emotion: 'serious', speed: 1.04, dialect };
return { role: 'customer', emotion: 'calm', speed: 0.98, dialect };
};
// OSS 编号是雪花 ID,超出 JS 安全整数范围,必须按字符串透传,禁止 Number() 强转。
@@ -113,6 +116,12 @@ export interface SpeechPlaybackSnapshot {
status: SpeechPlaybackStatus;
}
const browserDialectLanguage = (dialect: SpeechVoiceProfile['dialect']) => {
if (dialect === 'cantonese') return 'zh-HK';
if (dialect === 'sichuanese') return '';
return 'zh-CN';
};
export const createSpeechPlaybackController = (
onState: (snapshot: SpeechPlaybackSnapshot) => void,
onError: (message: string) => void,
@@ -150,15 +159,20 @@ export const createSpeechPlaybackController = (
value: string,
currentGeneration: number,
profile?: SpeechVoiceProfile
) => {
): { played: boolean; message?: string } => {
if (
typeof window === 'undefined'
|| !window.speechSynthesis
|| typeof window.SpeechSynthesisUtterance !== 'function'
) return false;
) return { played: false };
const lang = browserDialectLanguage(profile?.dialect);
if (!lang) {
return { played: false, message: '当前设备语音不支持四川话,请使用服务端语音或文字查看' };
}
const utterance = new window.SpeechSynthesisUtterance(value.slice(0, 600));
utterance.lang = 'zh-CN';
utterance.lang = lang;
utterance.rate = profile?.speed || 0.95;
utterance.pitch = profile?.role === 'mentor' ? 1.06 : profile?.role === 'customer' ? 1.05 : 1;
utterance.onend = () => {
@@ -172,7 +186,7 @@ export const createSpeechPlaybackController = (
browserUtterance = utterance;
publish(key, 'playing');
window.speechSynthesis.speak(utterance);
return true;
return { played: true };
};
const playAudioSource = (key: string, source: string, cacheKey: string, currentGeneration: number) => {
@@ -231,9 +245,10 @@ export const createSpeechPlaybackController = (
playAudioSource(key, source, sourceKey, currentGeneration);
} catch (error) {
if (currentGeneration !== generation) return;
if (playWithBrowserSpeech(key, value, currentGeneration, profile)) return;
const browserFallback = playWithBrowserSpeech(key, value, currentGeneration, profile);
if (browserFallback.played) return;
stop();
onError(error instanceof Error ? error.message : '语音生成失败');
onError(browserFallback.message || (error instanceof Error ? error.message : '语音生成失败'));
}
};