Spaces:
Paused
Paused
Upload src/app.js
Browse files- src/app.js +15 -6
src/app.js
CHANGED
|
@@ -8,6 +8,9 @@
|
|
| 8 |
* silently failing, we wait for the user to click "Start talking" before
|
| 9 |
* any WebSocket connection — that click resumes AudioContext, audio plays,
|
| 10 |
* and HeadAudio lip-sync works.
|
|
|
|
|
|
|
|
|
|
| 11 |
*/
|
| 12 |
|
| 13 |
import { S2sWsRealtimeClient } from "./s2s/s2s-ws-client.js";
|
|
@@ -20,6 +23,10 @@ const VOICES = [
|
|
| 20 |
];
|
| 21 |
const DEFAULT_VOICE = "Sohee";
|
| 22 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
// ── Greeting ─────────────────────────────────────────────────────────────
|
| 24 |
/**
|
| 25 |
* Fetch a SHORT hot news headline to weave into the greeting.
|
|
@@ -188,7 +195,7 @@ function makeDraggable() {
|
|
| 188 |
let dragging = false; let startX, startY, startLeft, startTop;
|
| 189 |
function getPos(e) { const p = e.changedTouches ? e.changedTouches[0] : e; return { x: p.clientX, y: p.clientY }; }
|
| 190 |
function onStart(e) { if (e.target.closest("#chat-header-actions") || e.target.closest("#chat-avatar-select")) return; const p = getPos(e); dragging = true; const rect = textChat.getBoundingClientRect(); startX = p.x; startY = p.y; startLeft = rect.left; startTop = rect.top; textChat.classList.add("dragging"); e.preventDefault(); }
|
| 191 |
-
function onMove(e) { if (!dragging) return; const p = getPos(e); textChat.style.left = `${startLeft + p.x - startX}px`; textChat.style.top = `${startTop + p.y - startY}px`; textChat.style.right = "auto"; textChat.style.bottom = "auto";
|
| 192 |
function onEnd() { if (!dragging) return; dragging = false; textChat.classList.remove("dragging"); clampRect(); }
|
| 193 |
chatHeader.addEventListener("mousedown", onStart); document.addEventListener("mousemove", onMove); document.addEventListener("mouseup", onEnd);
|
| 194 |
chatHeader.addEventListener("touchstart", onStart, { passive: false }); document.addEventListener("touchmove", onMove, { passive: false }); document.addEventListener("touchend", onEnd);
|
|
@@ -220,7 +227,6 @@ function sendTextMessage() {
|
|
| 220 |
if (!text) return;
|
| 221 |
addChatMessage("user", text); chatInput.value = "";
|
| 222 |
if (!client || !sessionInProgress) {
|
| 223 |
-
const url = new URL(location.href); url.searchParams.set("fakemic", "1"); history.replaceState(null, "", url.href);
|
| 224 |
void startTextSession(text); return;
|
| 225 |
}
|
| 226 |
if (!sendTextViaSession(text)) setCaption("QUEUED…");
|
|
@@ -305,7 +311,7 @@ async function startVoiceSession() {
|
|
| 305 |
// Await AudioContext resume — this runs in user-gesture handler so it resolves
|
| 306 |
await stage.resume();
|
| 307 |
let micStream;
|
| 308 |
-
if (
|
| 309 |
else { try { micStream = await navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: true, noiseSuppression: true, autoGainControl: true } }); } catch { setCaption("MIC BLOCKED", "error"); sessionInProgress = false; return; } }
|
| 310 |
const audioCtx = stage.audioCtx; const voiceSink = stage.voiceSink;
|
| 311 |
if (!audioCtx || !voiceSink) { sessionInProgress = false; return; }
|
|
@@ -375,7 +381,7 @@ mainBtn.addEventListener("click", () => {
|
|
| 375 |
if (mainAction === "start") {
|
| 376 |
// User gesture! AudioContext.resume() will resolve → audio plays + lipsync works.
|
| 377 |
if (sessionInProgress) return;
|
| 378 |
-
if (
|
| 379 |
void startTextSession();
|
| 380 |
} else {
|
| 381 |
void startVoiceSession();
|
|
@@ -405,7 +411,6 @@ async function boot() {
|
|
| 405 |
directUrlRow.hidden = !config.allowDirect;
|
| 406 |
await fetchAvatarList(); populateAvatarSelects(settings.avatar);
|
| 407 |
makeDraggable(); makeResizable();
|
| 408 |
-
const url = new URL(location.href); url.searchParams.set("fakemic", "1"); history.replaceState(null, "", url.href);
|
| 409 |
setCaption("WAKING HER UP…"); setMainButton("busy", "Loading…");
|
| 410 |
// Pre-fetch news hook in parallel with avatar
|
| 411 |
const newsPromise = (settings.avatar === "vuong.glb" || !settings.avatar) ? getHotNewsGreeting().catch(() => null) : Promise.resolve(null);
|
|
@@ -419,5 +424,9 @@ async function boot() {
|
|
| 419 |
// context is silent and HeadAudio won't process any frames.
|
| 420 |
// User must tap "Start talking" → click provides user gesture →
|
| 421 |
// AudioContext.resume() resolves → audio plays → HeadAudio drives lipsync.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 422 |
}
|
| 423 |
-
void boot();
|
|
|
|
| 8 |
* silently failing, we wait for the user to click "Start talking" before
|
| 9 |
* any WebSocket connection — that click resumes AudioContext, audio plays,
|
| 10 |
* and HeadAudio lip-sync works.
|
| 11 |
+
*
|
| 12 |
+
* ?fakemic=1 — opt-in URL query param for testing text-only mode
|
| 13 |
+
* WITHOUT a microphone. Only activates when user explicitly adds it to URL.
|
| 14 |
*/
|
| 15 |
|
| 16 |
import { S2sWsRealtimeClient } from "./s2s/s2s-ws-client.js";
|
|
|
|
| 23 |
];
|
| 24 |
const DEFAULT_VOICE = "Sohee";
|
| 25 |
|
| 26 |
+
// ── Detect fakemic mode from URL (user opt-in only) ──────────────────────
|
| 27 |
+
const _urlParams = new URLSearchParams(location.search);
|
| 28 |
+
const FAKEMIC_MODE = _urlParams.has("fakemic");
|
| 29 |
+
|
| 30 |
// ── Greeting ─────────────────────────────────────────────────────────────
|
| 31 |
/**
|
| 32 |
* Fetch a SHORT hot news headline to weave into the greeting.
|
|
|
|
| 195 |
let dragging = false; let startX, startY, startLeft, startTop;
|
| 196 |
function getPos(e) { const p = e.changedTouches ? e.changedTouches[0] : e; return { x: p.clientX, y: p.clientY }; }
|
| 197 |
function onStart(e) { if (e.target.closest("#chat-header-actions") || e.target.closest("#chat-avatar-select")) return; const p = getPos(e); dragging = true; const rect = textChat.getBoundingClientRect(); startX = p.x; startY = p.y; startLeft = rect.left; startTop = rect.top; textChat.classList.add("dragging"); e.preventDefault(); }
|
| 198 |
+
function onMove(e) { if (!dragging) return; const p = getPos(e); textChat.style.left = `${startLeft + p.x - startX}px`; textChat.style.top = `${startTop + p.y - startY}px`; textChat.style.right = "auto"; textChat.style.bottom = "auto"; e.preventDefault(); }
|
| 199 |
function onEnd() { if (!dragging) return; dragging = false; textChat.classList.remove("dragging"); clampRect(); }
|
| 200 |
chatHeader.addEventListener("mousedown", onStart); document.addEventListener("mousemove", onMove); document.addEventListener("mouseup", onEnd);
|
| 201 |
chatHeader.addEventListener("touchstart", onStart, { passive: false }); document.addEventListener("touchmove", onMove, { passive: false }); document.addEventListener("touchend", onEnd);
|
|
|
|
| 227 |
if (!text) return;
|
| 228 |
addChatMessage("user", text); chatInput.value = "";
|
| 229 |
if (!client || !sessionInProgress) {
|
|
|
|
| 230 |
void startTextSession(text); return;
|
| 231 |
}
|
| 232 |
if (!sendTextViaSession(text)) setCaption("QUEUED…");
|
|
|
|
| 311 |
// Await AudioContext resume — this runs in user-gesture handler so it resolves
|
| 312 |
await stage.resume();
|
| 313 |
let micStream;
|
| 314 |
+
if (FAKEMIC_MODE) { const ctx = /** @type {AudioContext} */ (stage.audioCtx); micStream = ctx.createMediaStreamDestination().stream; }
|
| 315 |
else { try { micStream = await navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: true, noiseSuppression: true, autoGainControl: true } }); } catch { setCaption("MIC BLOCKED", "error"); sessionInProgress = false; return; } }
|
| 316 |
const audioCtx = stage.audioCtx; const voiceSink = stage.voiceSink;
|
| 317 |
if (!audioCtx || !voiceSink) { sessionInProgress = false; return; }
|
|
|
|
| 381 |
if (mainAction === "start") {
|
| 382 |
// User gesture! AudioContext.resume() will resolve → audio plays + lipsync works.
|
| 383 |
if (sessionInProgress) return;
|
| 384 |
+
if (FAKEMIC_MODE) {
|
| 385 |
void startTextSession();
|
| 386 |
} else {
|
| 387 |
void startVoiceSession();
|
|
|
|
| 411 |
directUrlRow.hidden = !config.allowDirect;
|
| 412 |
await fetchAvatarList(); populateAvatarSelects(settings.avatar);
|
| 413 |
makeDraggable(); makeResizable();
|
|
|
|
| 414 |
setCaption("WAKING HER UP…"); setMainButton("busy", "Loading…");
|
| 415 |
// Pre-fetch news hook in parallel with avatar
|
| 416 |
const newsPromise = (settings.avatar === "vuong.glb" || !settings.avatar) ? getHotNewsGreeting().catch(() => null) : Promise.resolve(null);
|
|
|
|
| 424 |
// context is silent and HeadAudio won't process any frames.
|
| 425 |
// User must tap "Start talking" → click provides user gesture →
|
| 426 |
// AudioContext.resume() resolves → audio plays → HeadAudio drives lipsync.
|
| 427 |
+
//
|
| 428 |
+
// ?fakemic=1 is an OPT-IN testing flag. Setting it automatically would
|
| 429 |
+
// break the direct domain (bep40-gemma-avatar.hf.space) by routing to
|
| 430 |
+
// text-only mode without a working backend.
|
| 431 |
}
|
| 432 |
+
void boot();
|