feat(speech): add role-aware emotional TTS profiles
This commit is contained in:
@@ -348,6 +348,7 @@ import type {
|
||||
PracticePrepCard,
|
||||
PracticeAssignment,
|
||||
PracticeRole,
|
||||
SpeechVoiceProfile,
|
||||
SpeechPlaybackStatus,
|
||||
PracticeTurn,
|
||||
PracticeRecord
|
||||
@@ -357,7 +358,7 @@ import { getSelectedPosition, isLoggedIn, rememberLoginRedirect } from '@/servic
|
||||
import { searchKnowledge } from '@/services/knowledge';
|
||||
import { resetPageScroll } from '@/services/navigation';
|
||||
import { ensureEmployeePosition } from '@/services/position';
|
||||
import { chooseSpeechAudio, createSpeechPlaybackController, measureAudioDuration, normalizeOssId, transcribeSpeechBlob, transcribeSpeechFile } from '@/services/speech';
|
||||
import { chooseSpeechAudio, createSpeechPlaybackController, customerVoiceProfile, measureAudioDuration, MENTOR_VOICE_PROFILE, normalizeOssId, transcribeSpeechBlob, transcribeSpeechFile } from '@/services/speech';
|
||||
import { createPageRequestScope, currentAccountKey, type RequestScopeTicket } from '@/services/request-scope';
|
||||
import { stripInternalCodes } from '@/services/text';
|
||||
|
||||
@@ -433,7 +434,8 @@ const speechPlayback = createSpeechPlaybackController(
|
||||
(text) => {
|
||||
message.value = text;
|
||||
uni.showToast({ title: text, icon: 'none' });
|
||||
}
|
||||
},
|
||||
MENTOR_VOICE_PROFILE
|
||||
);
|
||||
const busy = computed(() => status.value === 'starting' || status.value === 'submitting');
|
||||
const practiceView = computed<'prep' | 'active' | 'result'>(() => {
|
||||
@@ -517,12 +519,19 @@ interface TurnVoice {
|
||||
duration: number | null;
|
||||
unread: boolean;
|
||||
open: boolean;
|
||||
profile: SpeechVoiceProfile;
|
||||
}
|
||||
|
||||
const turnVoices = ref<Record<number, TurnVoice>>({});
|
||||
|
||||
const turnVoice = (index: number): TurnVoice =>
|
||||
turnVoices.value[index] || { status: 'unavailable', duration: null, unread: false, open: false };
|
||||
turnVoices.value[index] || {
|
||||
status: 'unavailable',
|
||||
duration: null,
|
||||
unread: false,
|
||||
open: false,
|
||||
profile: customerVoiceProfile()
|
||||
};
|
||||
|
||||
const patchTurnVoice = (index: number, patch: Partial<TurnVoice>) => {
|
||||
turnVoices.value = { ...turnVoices.value, [index]: { ...turnVoice(index), ...patch } };
|
||||
@@ -530,11 +539,12 @@ const patchTurnVoice = (index: number, patch: Partial<TurnVoice>) => {
|
||||
|
||||
/** Customer lines arrive as WeChat-style voice messages: pre-synthesize so the
|
||||
* bubble shows a duration and an unread dot; fall back to text when TTS is off. */
|
||||
const prepareTurnVoice = async (index: number, text: string) => {
|
||||
const prepareTurnVoice = async (index: number, text: string, emotion = 0) => {
|
||||
const spoken = (text || '').trim();
|
||||
if (!spoken) return;
|
||||
patchTurnVoice(index, { status: 'preparing', duration: null, unread: true, open: false });
|
||||
const source = await speechPlayback.preload(spoken);
|
||||
const profile = customerVoiceProfile(emotion);
|
||||
patchTurnVoice(index, { status: 'preparing', duration: null, unread: true, open: false, profile });
|
||||
const source = await speechPlayback.preload(spoken, profile);
|
||||
if (!turnVoices.value[index]) return;
|
||||
if (!source) {
|
||||
patchTurnVoice(index, { status: 'unavailable', unread: false });
|
||||
@@ -560,7 +570,7 @@ const playTurnVoice = (index: number, text: string) => {
|
||||
return;
|
||||
}
|
||||
patchTurnVoice(index, { unread: false });
|
||||
void speechPlayback.toggle(`turn-${index}`, (text || '').trim());
|
||||
void speechPlayback.toggle(`turn-${index}`, (text || '').trim(), turnVoice(index).profile);
|
||||
};
|
||||
|
||||
const toggleTurnTranscript = (index: number) => {
|
||||
@@ -748,11 +758,11 @@ const requireLogin = () => {
|
||||
return false;
|
||||
};
|
||||
|
||||
const appendTurn = (role: PracticeRole, text?: string) => {
|
||||
const appendTurn = (role: PracticeRole, text?: string, emotion = 0) => {
|
||||
if (!text) return;
|
||||
turns.value.push({ role, text });
|
||||
if (role === 'customer') {
|
||||
void prepareTurnVoice(turns.value.length - 1, text);
|
||||
void prepareTurnVoice(turns.value.length - 1, text, emotion);
|
||||
}
|
||||
scrollThreadToBottom();
|
||||
};
|
||||
@@ -1146,7 +1156,7 @@ const start = async (scenarioId = defaultScenarioId, assignmentId?: number) => {
|
||||
emotionScore.value = data.emotion ?? 0;
|
||||
trustScore.value = data.trust ?? 0;
|
||||
sessionId.value = data.sessionId;
|
||||
appendTurn('customer', data.customerText);
|
||||
appendTurn('customer', data.customerText, data.emotion);
|
||||
status.value = 'active';
|
||||
} catch (error) {
|
||||
if (!requests.isCurrent(request)) return;
|
||||
@@ -1267,7 +1277,7 @@ const submit = async () => {
|
||||
await finish(true);
|
||||
return;
|
||||
}
|
||||
appendTurn('customer', data.customerText);
|
||||
appendTurn('customer', data.customerText, data.emotion);
|
||||
roundIndex.value = data.roundIndex;
|
||||
status.value = 'active';
|
||||
} catch (error) {
|
||||
|
||||
Reference in New Issue
Block a user