feat(aihr): harden M0-M2 mobile training flows
This commit is contained in:
@@ -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 : '语音生成失败'));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user