Itachi-42 commited on
Commit
f5e6224
·
verified ·
1 Parent(s): 963788b

Update voice/static/index.html

Browse files
Files changed (1) hide show
  1. voice/static/index.html +93 -43
voice/static/index.html CHANGED
@@ -3,7 +3,7 @@
3
  <head>
4
  <meta charset="utf-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
- <title>CONTEXTUAL-INTEGRITY DISCLOSURE GAME</title>
7
  <link rel="preconnect" href="https://fonts.googleapis.com">
8
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
  <link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&family=Pixelify+Sans:wght@400;500;600;700&family=VT323&family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet">
@@ -1146,8 +1146,8 @@
1146
  </svg>
1147
  </div>
1148
  <div class="brand-text">
1149
- <h1>CTX-DISCLOSURE GAME</h1>
1150
- <div class="sub">TEXT <span class="dim">→</span> VOICE <span class="dim">·</span> ADVERSARY-AWARE</div>
1151
  </div>
1152
  </div>
1153
  <div class="header-meta">
@@ -1761,18 +1761,74 @@ document.getElementById("msg-input").addEventListener("keydown", (e) => {
1761
  if (e.key === "Enter") sendText();
1762
  });
1763
 
1764
- // ─── Voice mode ──────────────────────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1765
  async function toggleRecording() {
1766
  if (!sessionId || terminated) return;
 
 
 
 
1767
 
1768
  if (!isRecording) {
 
 
 
 
 
 
 
 
 
 
1769
  try {
1770
- const stream = await navigator.mediaDevices.getUserMedia({audio: true});
1771
- mediaRecorder = new MediaRecorder(stream, {mimeType: "audio/webm;codecs=opus"});
1772
- recordedChunks = [];
1773
- mediaRecorder.ondataavailable = (e) => { if (e.data.size > 0) recordedChunks.push(e.data); };
1774
- mediaRecorder.onstop = () => sendVoice();
1775
- mediaRecorder.start();
1776
  isRecording = true;
1777
  recordStart = performance.now();
1778
  document.getElementById("mic-btn").classList.add("recording");
@@ -1783,56 +1839,50 @@ async function toggleRecording() {
1783
  }, 50);
1784
  } catch (err) {
1785
  toast("Microphone access denied", "error");
 
1786
  }
1787
  } else {
1788
- mediaRecorder.stop();
1789
- mediaRecorder.stream.getTracks().forEach(t => t.stop());
1790
- isRecording = false;
1791
- clearInterval(recordTimer);
1792
- document.getElementById("mic-btn").classList.remove("recording");
1793
- document.getElementById("rec-indicator").classList.remove("active");
1794
  }
1795
  }
1796
 
1797
- async function sendVoice() {
1798
- const blob = new Blob(recordedChunks, {type: "audio/webm"});
1799
- const log = document.getElementById("chat-log");
1800
- const placeholder = document.createElement("div");
1801
- placeholder.className = "msg you";
1802
- placeholder.innerHTML = `
1803
- <div class="avatar you">P1</div>
1804
- <div class="bubble transcribing"><div class="meta">You</div>transcribing…</div>`;
1805
- log.appendChild(placeholder);
1806
- log.scrollTop = log.scrollHeight;
1807
 
1808
  appendTypingBubble("rp");
1809
-
1810
- const fd = new FormData();
1811
- fd.append("audio", blob, "clip.webm");
1812
  try {
1813
- const res = await fetch(`/api/step-voice/${sessionId}`, {method: "POST", body: fd});
1814
- if (!res.ok) throw new Error("voice step failed");
 
 
 
 
1815
  const j = await res.json();
1816
- placeholder.querySelector(".bubble").classList.remove("transcribing");
1817
- placeholder.querySelector(".bubble").innerHTML = `<div class="meta">You</div>${escapeHtml(j.transcribed || "")}`;
1818
  if (pendingTypingBubble) { pendingTypingBubble.remove(); pendingTypingBubble = null; }
1819
  applyObservation(j.observation);
1820
  } catch (e) {
1821
- placeholder.querySelector(".bubble").innerHTML = `<div class="meta">You</div><em style="color:var(--red)">voice step failed</em>`;
1822
  if (pendingTypingBubble) { pendingTypingBubble.remove(); pendingTypingBubble = null; }
1823
- toast("Voice step failed", "error");
1824
  }
1825
  }
1826
 
1827
- async function playTTS(text, voice) {
 
1828
  try {
1829
- const res = await fetch(`/api/tts?voice=${voice}&text=${encodeURIComponent(text)}`);
1830
- if (!res.ok) return;
1831
- const blob = await res.blob();
1832
- const url = URL.createObjectURL(blob);
1833
- const a = new Audio(url);
1834
- a.play();
1835
- a.onended = () => URL.revokeObjectURL(url);
 
1836
  } catch (e) { /* swallow */ }
1837
  }
1838
 
 
3
  <head>
4
  <meta charset="utf-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>CIPHER Contextual-Integrity Privacy</title>
7
  <link rel="preconnect" href="https://fonts.googleapis.com">
8
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
  <link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&family=Pixelify+Sans:wght@400;500;600;700&family=VT323&family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet">
 
1146
  </svg>
1147
  </div>
1148
  <div class="brand-text">
1149
+ <h1>CIPHER</h1>
1150
+ <div class="sub">CONTEXTUAL-INTEGRITY PRIVACY <span class="dim">·</span> ADVERSARY-AWARE</div>
1151
  </div>
1152
  </div>
1153
  <div class="header-meta">
 
1761
  if (e.key === "Enter") sendText();
1762
  });
1763
 
1764
+ // ─── Voice mode (browser Web Speech API — no server deps) ────────────────────
1765
+ const _SR = window.SpeechRecognition || window.webkitSpeechRecognition;
1766
+ let recognition = null;
1767
+ let voiceTranscript = "";
1768
+ let voicePlaceholder = null;
1769
+
1770
+ if (_SR) {
1771
+ recognition = new _SR();
1772
+ recognition.continuous = true;
1773
+ recognition.interimResults = true;
1774
+ recognition.lang = "en-US";
1775
+ recognition.onresult = (e) => {
1776
+ let interim = "";
1777
+ for (let i = e.resultIndex; i < e.results.length; i++) {
1778
+ const t = e.results[i][0].transcript;
1779
+ if (e.results[i].isFinal) voiceTranscript += t;
1780
+ else interim += t;
1781
+ }
1782
+ if (voicePlaceholder) {
1783
+ const display = (voiceTranscript + interim).trim();
1784
+ voicePlaceholder.querySelector(".bubble").innerHTML =
1785
+ `<div class="meta">You</div>${display ? escapeHtml(display) : "<em>listening…</em>"}`;
1786
+ }
1787
+ };
1788
+ recognition.onerror = (e) => {
1789
+ if (voicePlaceholder) {
1790
+ voicePlaceholder.querySelector(".bubble").innerHTML =
1791
+ `<div class="meta">You</div><em style="color:var(--red)">voice error: ${escapeHtml(e.error || "unknown")}</em>`;
1792
+ voicePlaceholder = null;
1793
+ }
1794
+ if (isRecording) {
1795
+ isRecording = false;
1796
+ clearInterval(recordTimer);
1797
+ document.getElementById("mic-btn").classList.remove("recording");
1798
+ document.getElementById("rec-indicator").classList.remove("active");
1799
+ }
1800
+ };
1801
+ recognition.onend = () => {
1802
+ if (isRecording) {
1803
+ isRecording = false;
1804
+ clearInterval(recordTimer);
1805
+ document.getElementById("mic-btn").classList.remove("recording");
1806
+ document.getElementById("rec-indicator").classList.remove("active");
1807
+ }
1808
+ finalizeVoice();
1809
+ };
1810
+ }
1811
+
1812
  async function toggleRecording() {
1813
  if (!sessionId || terminated) return;
1814
+ if (!recognition) {
1815
+ toast("Voice mode needs Chrome / Edge / Safari", "error");
1816
+ return;
1817
+ }
1818
 
1819
  if (!isRecording) {
1820
+ voiceTranscript = "";
1821
+ const log = document.getElementById("chat-log");
1822
+ voicePlaceholder = document.createElement("div");
1823
+ voicePlaceholder.className = "msg you";
1824
+ voicePlaceholder.innerHTML = `
1825
+ <div class="avatar you">P1</div>
1826
+ <div class="bubble"><div class="meta">You</div><em>listening…</em></div>`;
1827
+ log.appendChild(voicePlaceholder);
1828
+ log.scrollTop = log.scrollHeight;
1829
+
1830
  try {
1831
+ recognition.start();
 
 
 
 
 
1832
  isRecording = true;
1833
  recordStart = performance.now();
1834
  document.getElementById("mic-btn").classList.add("recording");
 
1839
  }, 50);
1840
  } catch (err) {
1841
  toast("Microphone access denied", "error");
1842
+ if (voicePlaceholder) { voicePlaceholder.remove(); voicePlaceholder = null; }
1843
  }
1844
  } else {
1845
+ try { recognition.stop(); } catch (e) {}
 
 
 
 
 
1846
  }
1847
  }
1848
 
1849
+ async function finalizeVoice() {
1850
+ const text = voiceTranscript.trim();
1851
+ if (voicePlaceholder) {
1852
+ voicePlaceholder.querySelector(".bubble").innerHTML =
1853
+ `<div class="meta">You</div>${text ? escapeHtml(text) : "<em>(no speech detected)</em>"}`;
1854
+ voicePlaceholder = null;
1855
+ }
1856
+ if (!text) return;
 
 
1857
 
1858
  appendTypingBubble("rp");
 
 
 
1859
  try {
1860
+ const res = await fetch(`/api/step/${sessionId}`, {
1861
+ method: "POST",
1862
+ headers: {"Content-Type": "application/json"},
1863
+ body: JSON.stringify({message: text}),
1864
+ });
1865
+ if (!res.ok) throw new Error("step failed");
1866
  const j = await res.json();
 
 
1867
  if (pendingTypingBubble) { pendingTypingBubble.remove(); pendingTypingBubble = null; }
1868
  applyObservation(j.observation);
1869
  } catch (e) {
 
1870
  if (pendingTypingBubble) { pendingTypingBubble.remove(); pendingTypingBubble = null; }
1871
+ toast("Step failed", "error");
1872
  }
1873
  }
1874
 
1875
+ function playTTS(text, _voice) {
1876
+ if (!window.speechSynthesis || !text) return;
1877
  try {
1878
+ speechSynthesis.cancel();
1879
+ const u = new SpeechSynthesisUtterance(text);
1880
+ const voices = speechSynthesis.getVoices();
1881
+ const en = voices.find(v => /^en[-_]/i.test(v.lang)) || voices[0];
1882
+ if (en) u.voice = en;
1883
+ u.rate = 1.0;
1884
+ u.pitch = 1.0;
1885
+ speechSynthesis.speak(u);
1886
  } catch (e) { /* swallow */ }
1887
  }
1888