Spaces:
Paused
Paused
Fix: Restore stable logic, keep newsHook {title,link} support
Browse files- src/app.js +30 -43
src/app.js
CHANGED
|
@@ -24,7 +24,6 @@ const DEFAULT_VOICE = "Sohee";
|
|
| 24 |
/**
|
| 25 |
* Fetch a SHORT hot news headline to weave into the greeting.
|
| 26 |
* Short enough that the AI says it as ONE sentence.
|
| 27 |
-
* Returns {title, link} or null.
|
| 28 |
*/
|
| 29 |
async function getHotNewsGreeting() {
|
| 30 |
let hotItem = null;
|
|
@@ -48,6 +47,7 @@ const DEFAULT_INSTRUCTIONS = [
|
|
| 48 |
"You are a friendly voice assistant with a visible, human-like 3D avatar.",
|
| 49 |
"CRITICAL: Always respond in the SAME LANGUAGE the user writes or speaks.",
|
| 50 |
"If Vietnamese, respond in Vietnamese. If English, respond in English.",
|
|
|
|
| 51 |
"Keep replies short, natural, warm. Never list-like.",
|
| 52 |
"You can control avatar body with tools: set_mood, make_hand_gesture, make_facial_expression. Use them naturally.",
|
| 53 |
"NEVER guess. Use search_web or search_wikipedia for ANY factual question.",
|
|
@@ -141,7 +141,7 @@ function effectiveInstructions(newsHook) {
|
|
| 141 |
const extra = settings.instructions.trim();
|
| 142 |
const introLine = "Tôi tên là Vương đến V.AI STUDIO.";
|
| 143 |
// newsHook is either null or {title, link}
|
| 144 |
-
const newsInst = newsHook
|
| 145 |
? `\n\nWhen you start the conversation, greet the user naturally and briefly mention today's news hook: "${newsHook.title}"${newsHook.link ? ` (xem chi tiết: ${newsHook.link})` : ""}\nThen wait for their response. Keep your greeting to ONE short sentence.`
|
| 146 |
: "";
|
| 147 |
const base = `${dateLine}\n\n${introLine}\n\n${DEFAULT_INSTRUCTIONS}${newsInst}`;
|
|
@@ -305,25 +305,18 @@ async function startVoiceSession() {
|
|
| 305 |
else { try { micStream = await navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: true, noiseSuppression: true, autoGainControl: true } }); } catch { setCaption("MIC BLOCKED", "error"); sessionInProgress = false; return; } }
|
| 306 |
const audioCtx = stage.audioCtx; const voiceSink = stage.voiceSink;
|
| 307 |
if (!audioCtx || !voiceSink) { sessionInProgress = false; return; }
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
if (!ok) return;
|
| 321 |
-
if (!autoGreetingSent) { c.requestResponse(); }
|
| 322 |
-
} catch (err) {
|
| 323 |
-
console.error("Session start error:", err);
|
| 324 |
-
setCaption("CONNECT ERROR, TAP TO RETRY", "error");
|
| 325 |
-
await endSession(true);
|
| 326 |
-
}
|
| 327 |
}
|
| 328 |
|
| 329 |
/**
|
|
@@ -338,26 +331,19 @@ async function startTextSession(initialText) {
|
|
| 338 |
await stage.resume();
|
| 339 |
const audioCtx = stage.audioCtx; const voiceSink = stage.voiceSink;
|
| 340 |
if (!audioCtx || !voiceSink) { sessionInProgress = false; return; }
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
if (!autoGreetingSent) { c.requestResponse(); }
|
| 355 |
-
if (initialText) { c.sendUserText(initialText); c.requestResponse(); }
|
| 356 |
-
} catch (err) {
|
| 357 |
-
console.error("Session start error:", err);
|
| 358 |
-
setCaption("CONNECT ERROR, TAP TO RETRY", "error");
|
| 359 |
-
await endSession(true);
|
| 360 |
-
}
|
| 361 |
}
|
| 362 |
|
| 363 |
function _attachClientEvents(c) {
|
|
@@ -404,7 +390,7 @@ settingsBtn.addEventListener("click", () => { inputVoice.value = settings.voice;
|
|
| 404 |
settingsDialog.addEventListener("close", () => {
|
| 405 |
settings = { voice: inputVoice.value || DEFAULT_VOICE, avatar: settingsAvatarSelect.value || "", instructions: inputInstructions.value, directUrl: inputDirectUrl.value.trim(), subtitles: inputSubtitles.checked };
|
| 406 |
if (settings.avatar !== "vuong.glb") autoGreetingSent = false;
|
| 407 |
-
saveSettings(); chatAvatarSelect.value = settings.avatar; if (!settings.subtitles) subtitles.classList.remove("visible"); client?.updateSession({ voice: settings.voice, instructions: effectiveInstructions(preFetchedGreeting) });
|
| 408 |
});
|
| 409 |
window.addEventListener("beforeunload", () => { client?.close(); });
|
| 410 |
|
|
@@ -422,7 +408,8 @@ async function boot() {
|
|
| 422 |
try { await stage.init({ avatarUrl: settings.avatar ? `/avatars/${settings.avatar}` : undefined, onprogress: (ev) => { if (ev.lengthComputable) loading.textContent = `Loading avatar ${Math.min(100, Math.round((ev.loaded / ev.total) * 100))}%`; } }); }
|
| 423 |
catch (err) { console.error(err); loading.textContent = "Avatar failed to load. Reload."; setCaption("AVATAR FAILED", "error"); return; }
|
| 424 |
preFetchedGreeting = await newsPromise;
|
| 425 |
-
loading.classList.add("done");
|
|
|
|
| 426 |
|
| 427 |
// NO auto-greeting here. On direct hf.space domains AudioContext is
|
| 428 |
// "suspended" before first click — any audio routed through a suspended
|
|
|
|
| 24 |
/**
|
| 25 |
* Fetch a SHORT hot news headline to weave into the greeting.
|
| 26 |
* Short enough that the AI says it as ONE sentence.
|
|
|
|
| 27 |
*/
|
| 28 |
async function getHotNewsGreeting() {
|
| 29 |
let hotItem = null;
|
|
|
|
| 47 |
"You are a friendly voice assistant with a visible, human-like 3D avatar.",
|
| 48 |
"CRITICAL: Always respond in the SAME LANGUAGE the user writes or speaks.",
|
| 49 |
"If Vietnamese, respond in Vietnamese. If English, respond in English.",
|
| 50 |
+
"When Vietnamese: write dates as 'ngày 9 tháng 7 năm 2026' (NOT '9/7/2026'),",
|
| 51 |
"Keep replies short, natural, warm. Never list-like.",
|
| 52 |
"You can control avatar body with tools: set_mood, make_hand_gesture, make_facial_expression. Use them naturally.",
|
| 53 |
"NEVER guess. Use search_web or search_wikipedia for ANY factual question.",
|
|
|
|
| 141 |
const extra = settings.instructions.trim();
|
| 142 |
const introLine = "Tôi tên là Vương đến V.AI STUDIO.";
|
| 143 |
// newsHook is either null or {title, link}
|
| 144 |
+
const newsInst = newsHook
|
| 145 |
? `\n\nWhen you start the conversation, greet the user naturally and briefly mention today's news hook: "${newsHook.title}"${newsHook.link ? ` (xem chi tiết: ${newsHook.link})` : ""}\nThen wait for their response. Keep your greeting to ONE short sentence.`
|
| 146 |
: "";
|
| 147 |
const base = `${dateLine}\n\n${introLine}\n\n${DEFAULT_INSTRUCTIONS}${newsInst}`;
|
|
|
|
| 305 |
else { try { micStream = await navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: true, noiseSuppression: true, autoGainControl: true } }); } catch { setCaption("MIC BLOCKED", "error"); sessionInProgress = false; return; } }
|
| 306 |
const audioCtx = stage.audioCtx; const voiceSink = stage.voiceSink;
|
| 307 |
if (!audioCtx || !voiceSink) { sessionInProgress = false; return; }
|
| 308 |
+
const greeting = preFetchedGreeting || (await getHotNewsGreeting());
|
| 309 |
+
const c = new S2sWsRealtimeClient({
|
| 310 |
+
...(config.lb ? { sessionUrl: "api/session" } : { directUrl: settings.directUrl }),
|
| 311 |
+
voice: settings.voice, instructions: effectiveInstructions(greeting),
|
| 312 |
+
micStream, audioContext: audioCtx, outputNode: voiceSink,
|
| 313 |
+
workletBaseUrl: "/worklets/", tools: TOOL_DEFS, _textOnly: false,
|
| 314 |
+
});
|
| 315 |
+
client = c; _attachClientEvents(c);
|
| 316 |
+
setCaption("REQUESTING A SLOT…");
|
| 317 |
+
const ok = await connectSession(c);
|
| 318 |
+
if (!ok) return;
|
| 319 |
+
if (!autoGreetingSent) { c.requestResponse(); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 320 |
}
|
| 321 |
|
| 322 |
/**
|
|
|
|
| 331 |
await stage.resume();
|
| 332 |
const audioCtx = stage.audioCtx; const voiceSink = stage.voiceSink;
|
| 333 |
if (!audioCtx || !voiceSink) { sessionInProgress = false; return; }
|
| 334 |
+
const newsHook = preFetchedGreeting || (await getHotNewsGreeting());
|
| 335 |
+
const c = new S2sWsRealtimeClient({
|
| 336 |
+
...(config.lb ? { sessionUrl: "api/session" } : { directUrl: settings.directUrl }),
|
| 337 |
+
voice: settings.voice, instructions: effectiveInstructions(newsHook),
|
| 338 |
+
audioContext: audioCtx, outputNode: voiceSink,
|
| 339 |
+
workletBaseUrl: "/worklets/", tools: TOOL_DEFS, _textOnly: true,
|
| 340 |
+
});
|
| 341 |
+
client = c; _attachClientEvents(c);
|
| 342 |
+
textMode = true; showTextChat(true); textModeBtn.classList.add("active");
|
| 343 |
+
const ok = await connectSession(c);
|
| 344 |
+
if (!ok) return;
|
| 345 |
+
if (!autoGreetingSent) { c.requestResponse(); }
|
| 346 |
+
if (initialText) { c.sendUserText(initialText); c.requestResponse(); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 347 |
}
|
| 348 |
|
| 349 |
function _attachClientEvents(c) {
|
|
|
|
| 390 |
settingsDialog.addEventListener("close", () => {
|
| 391 |
settings = { voice: inputVoice.value || DEFAULT_VOICE, avatar: settingsAvatarSelect.value || "", instructions: inputInstructions.value, directUrl: inputDirectUrl.value.trim(), subtitles: inputSubtitles.checked };
|
| 392 |
if (settings.avatar !== "vuong.glb") autoGreetingSent = false;
|
| 393 |
+
saveSettings(); chatAvatarSelect.value = settings.avatar; if (!settings.subtitles) subtitles.classList.remove("visible"); client?.updateSession({ voice: settings.voice, instructions: effectiveInstructions(preFetchedGreeting || "") });
|
| 394 |
});
|
| 395 |
window.addEventListener("beforeunload", () => { client?.close(); });
|
| 396 |
|
|
|
|
| 408 |
try { await stage.init({ avatarUrl: settings.avatar ? `/avatars/${settings.avatar}` : undefined, onprogress: (ev) => { if (ev.lengthComputable) loading.textContent = `Loading avatar ${Math.min(100, Math.round((ev.loaded / ev.total) * 100))}%`; } }); }
|
| 409 |
catch (err) { console.error(err); loading.textContent = "Avatar failed to load. Reload."; setCaption("AVATAR FAILED", "error"); return; }
|
| 410 |
preFetchedGreeting = await newsPromise;
|
| 411 |
+
loading.classList.add("done");
|
| 412 |
+
setCaption(CAPTIONS.idle); setMainButton("start", "Start talking");
|
| 413 |
|
| 414 |
// NO auto-greeting here. On direct hf.space domains AudioContext is
|
| 415 |
// "suspended" before first click — any audio routed through a suspended
|