Spaces:
Paused
Paused
Update app.py
Browse files
app.py
CHANGED
|
@@ -109,14 +109,20 @@ def _parse_avatar_config(avatar_dir):
|
|
| 109 |
def _resolve_voice(config: dict) -> str:
|
| 110 |
"""
|
| 111 |
Pick the TTS voice from an avatar config.
|
| 112 |
-
Precedence:
|
| 113 |
-
1.
|
| 114 |
-
2.
|
| 115 |
-
3.
|
| 116 |
-
|
| 117 |
-
|
|
|
|
| 118 |
"""
|
| 119 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
|
| 121 |
|
| 122 |
def _switch_avatar(avatar_name: str):
|
|
@@ -1072,6 +1078,7 @@ const btnClear=document.getElementById('btnClear');
|
|
| 1072 |
// WEBSOCKET FRAME STREAMING
|
| 1073 |
// ======================================================================
|
| 1074 |
let ws=null, frameImg=new Image();
|
|
|
|
| 1075 |
|
| 1076 |
function drawFrame(data){
|
| 1077 |
const blob=new Blob([data], {type:'image/jpeg'});
|
|
@@ -1083,6 +1090,15 @@ function drawFrame(data){
|
|
| 1083 |
frameImg.src=url;
|
| 1084 |
}
|
| 1085 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1086 |
function connectWebSocket(){
|
| 1087 |
const proto=location.protocol==='https:'?'wss:':'ws:';
|
| 1088 |
ws=new WebSocket(proto+'//'+location.host+'/ws/video');
|
|
@@ -1094,15 +1110,20 @@ function connectWebSocket(){
|
|
| 1094 |
if(typeof e.data==='string'){
|
| 1095 |
const msg=JSON.parse(e.data);
|
| 1096 |
if(msg.type==='speech_start' && msg.audio_url){
|
| 1097 |
-
|
| 1098 |
-
|
| 1099 |
-
|
| 1100 |
-
|
| 1101 |
-
}
|
| 1102 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1103 |
}
|
| 1104 |
} else {
|
| 1105 |
-
// Always draw frames immediately β server paces at 25fps
|
| 1106 |
drawFrame(e.data);
|
| 1107 |
}
|
| 1108 |
};
|
|
@@ -1158,6 +1179,10 @@ async function sendMessage(){
|
|
| 1158 |
typing.contentEl.classList.remove('typing-dots');
|
| 1159 |
typing.contentEl.textContent=data.text;
|
| 1160 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1161 |
// Audio + lipsync frames arrive via WebSocket automatically
|
| 1162 |
// Wait for speech duration before allowing next message
|
| 1163 |
if(data.audio_duration){
|
|
|
|
| 109 |
def _resolve_voice(config: dict) -> str:
|
| 110 |
"""
|
| 111 |
Pick the TTS voice from an avatar config.
|
| 112 |
+
Precedence (first wins):
|
| 113 |
+
1. cartesiavoicename β Cartesia voice UUID (e.g. "e07c00bc-4134-...")
|
| 114 |
+
2. edgettsname β raw edge-tts voice ID (e.g. "en-GB-RyanNeural")
|
| 115 |
+
3. groqttsname β legacy short name, mapped via VOICE_MAP in tts.py
|
| 116 |
+
4. fallback default
|
| 117 |
+
set_voice() in tts.py detects the format and routes to the right backend:
|
| 118 |
+
UUID β Cartesia API, else β edge-tts (raw or via VOICE_MAP).
|
| 119 |
"""
|
| 120 |
+
return (
|
| 121 |
+
config.get("cartesiavoicename")
|
| 122 |
+
or config.get("edgettsname")
|
| 123 |
+
or config.get("groqttsname")
|
| 124 |
+
or "diana"
|
| 125 |
+
)
|
| 126 |
|
| 127 |
|
| 128 |
def _switch_avatar(avatar_name: str):
|
|
|
|
| 1078 |
// WEBSOCKET FRAME STREAMING
|
| 1079 |
// ======================================================================
|
| 1080 |
let ws=null, frameImg=new Image();
|
| 1081 |
+
let pendingAudio=null, pendingAudioUrl=null;
|
| 1082 |
|
| 1083 |
function drawFrame(data){
|
| 1084 |
const blob=new Blob([data], {type:'image/jpeg'});
|
|
|
|
| 1090 |
frameImg.src=url;
|
| 1091 |
}
|
| 1092 |
|
| 1093 |
+
function preloadAudio(url){
|
| 1094 |
+
// Called from chat_stream response β gives ~2s headstart on HTTP fetch + buffer
|
| 1095 |
+
// so audio is ready to play instantly when speech_start fires.
|
| 1096 |
+
if(!url) return;
|
| 1097 |
+
pendingAudio=new Audio(url);
|
| 1098 |
+
pendingAudioUrl=url;
|
| 1099 |
+
pendingAudio.load();
|
| 1100 |
+
}
|
| 1101 |
+
|
| 1102 |
function connectWebSocket(){
|
| 1103 |
const proto=location.protocol==='https:'?'wss:':'ws:';
|
| 1104 |
ws=new WebSocket(proto+'//'+location.host+'/ws/video');
|
|
|
|
| 1110 |
if(typeof e.data==='string'){
|
| 1111 |
const msg=JSON.parse(e.data);
|
| 1112 |
if(msg.type==='speech_start' && msg.audio_url){
|
| 1113 |
+
if(pendingAudio && pendingAudioUrl===msg.audio_url){
|
| 1114 |
+
// Audio was preloaded after /api/chat/stream returned β play instantly
|
| 1115 |
+
pendingAudio.play().catch(err=>console.warn('[AUDIO]',err));
|
| 1116 |
+
pendingAudio=null; pendingAudioUrl=null;
|
| 1117 |
+
} else {
|
| 1118 |
+
// Fallback (greeting / no preload) β load + wait for canplaythrough
|
| 1119 |
+
const audio=new Audio(msg.audio_url);
|
| 1120 |
+
audio.addEventListener('canplaythrough', ()=>{
|
| 1121 |
+
audio.play().catch(err=>console.warn('[AUDIO]',err));
|
| 1122 |
+
}, {once:true});
|
| 1123 |
+
audio.load();
|
| 1124 |
+
}
|
| 1125 |
}
|
| 1126 |
} else {
|
|
|
|
| 1127 |
drawFrame(e.data);
|
| 1128 |
}
|
| 1129 |
};
|
|
|
|
| 1179 |
typing.contentEl.classList.remove('typing-dots');
|
| 1180 |
typing.contentEl.textContent=data.text;
|
| 1181 |
|
| 1182 |
+
// Start fetching the TTS audio NOW β gives ~2s headstart so it's
|
| 1183 |
+
// buffered and ready to play instantly when WS emits speech_start.
|
| 1184 |
+
if(data.audio_url) preloadAudio(data.audio_url);
|
| 1185 |
+
|
| 1186 |
// Audio + lipsync frames arrive via WebSocket automatically
|
| 1187 |
// Wait for speech duration before allowing next message
|
| 1188 |
if(data.audio_duration){
|