bep40 commited on
Commit
186d7cb
·
verified ·
1 Parent(s): cc2198c

Fix: Restore stable logic, keep newsHook {title,link} support

Browse files
Files changed (1) hide show
  1. 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 && newsHook.title
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
- let greeting = preFetchedGreeting;
309
- if (!greeting) { try { greeting = await getHotNewsGreeting(); } catch { greeting = null; } }
310
- try {
311
- const c = new S2sWsRealtimeClient({
312
- ...(config.lb ? { sessionUrl: "api/session" } : { directUrl: settings.directUrl }),
313
- voice: settings.voice, instructions: effectiveInstructions(greeting),
314
- micStream, audioContext: audioCtx, outputNode: voiceSink,
315
- workletBaseUrl: "/worklets/", tools: TOOL_DEFS, _textOnly: false,
316
- });
317
- client = c; _attachClientEvents(c);
318
- setCaption("REQUESTING A SLOT…");
319
- const ok = await connectSession(c);
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
- let newsHook = preFetchedGreeting;
342
- if (!newsHook) { try { newsHook = await getHotNewsGreeting(); } catch { newsHook = null; } }
343
- try {
344
- const c = new S2sWsRealtimeClient({
345
- ...(config.lb ? { sessionUrl: "api/session" } : { directUrl: settings.directUrl }),
346
- voice: settings.voice, instructions: effectiveInstructions(newsHook),
347
- audioContext: audioCtx, outputNode: voiceSink,
348
- workletBaseUrl: "/worklets/", tools: TOOL_DEFS, _textOnly: true,
349
- });
350
- client = c; _attachClientEvents(c);
351
- textMode = true; showTextChat(true); textModeBtn.classList.add("active");
352
- const ok = await connectSession(c);
353
- if (!ok) return;
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"); setCaption(CAPTIONS.idle); setMainButton("start", "Start talking");
 
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