josephrw commited on
Commit
2ae31c3
Β·
verified Β·
1 Parent(s): fd4fad1

Upload folder using huggingface_hub

Browse files
Files changed (2) hide show
  1. app.py +74 -0
  2. frontend/index.html +153 -22
app.py CHANGED
@@ -272,6 +272,80 @@ async def generate_patch(request: Request):
272
  }
273
 
274
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
275
  # In-memory artifact store: patch_hash -> {code, output, timestamp}
276
  artifact_store: dict[str, dict] = {}
277
 
 
272
  }
273
 
274
 
275
+ @app.post("/audio/generate")
276
+ async def audio_generate(request: Request):
277
+ """Generate code from an audio transcript without requiring an existing session.
278
+ Creates a temporary session, injects the transcript as audio evidence,
279
+ runs observer β†’ builder β†’ receipt."""
280
+ body = {}
281
+ try:
282
+ body = await request.json()
283
+ except Exception:
284
+ pass
285
+ transcript = body.get("transcript", "") or request.query_params.get("transcript", "")
286
+ audio_file = body.get("audio_file", "") or request.query_params.get("audio_file", "")
287
+ mode = body.get("mode", "continuous_code") or request.query_params.get("mode", "continuous_code")
288
+ if not transcript:
289
+ return JSONResponse({"error": "transcript required"}, 400)
290
+
291
+ # Create a temporary session
292
+ state = SessionState()
293
+ state.mode = mode
294
+ sessions[state.session_id] = state
295
+
296
+ # Inject transcript as an audio chunk
297
+ chunk = process_transcript(transcript, state)
298
+ state.add_audio_chunk(chunk)
299
+
300
+ # Compress and run the full pipeline
301
+ compact = compress_state(state)
302
+ ev_result = evidence_gate("", compact)
303
+ log_gate_decision("evidence_gate", ev_result, patch_hash="")
304
+ observation = observe(state, compact)
305
+ state.observer_state = observation
306
+ state.last_observer_ts = time.time()
307
+ ev_result2 = evidence_gate(observation.get("observer_output", ""), compact)
308
+ log_gate_decision("evidence_gate", ev_result2, patch_hash="")
309
+ result = build(state, observation, gate_result=ev_result2)
310
+ state.current_artifact = result["patch_output"]
311
+ state.last_patch_hash = result["patch_hash"]
312
+ state.last_builder_ts = time.time()
313
+ state.receipt_count += 1
314
+ save_receipt(result["receipt"])
315
+
316
+ patch_output = result["patch_output"]
317
+ extracted_code = extract_code_from_output(patch_output)
318
+ if not extracted_code:
319
+ extracted_code = patch_output
320
+ artifact_data = {
321
+ "patch_hash": result["patch_hash"],
322
+ "code": extracted_code,
323
+ "observer_output": patch_output,
324
+ "timestamp": time.time(),
325
+ "session_id": state.session_id,
326
+ "fallback_level": result.get("fallback_level", 0),
327
+ "artifact_type": result.get("artifact_type", ""),
328
+ "sensory_channels": ev_result2.sensory_channels,
329
+ "feature_attribution": ev_result2.feature_attribution,
330
+ "audio_file": audio_file,
331
+ "transcript": transcript,
332
+ }
333
+ artifact_store[result["patch_hash"]] = artifact_data
334
+ save_artifact_disk(result["patch_hash"], artifact_data)
335
+
336
+ return {
337
+ **result,
338
+ "session_id": state.session_id,
339
+ "gate": {
340
+ "fallback_level": ev_result2.fallback_level,
341
+ "artifact_type": ev_result2.artifact_type,
342
+ "sensory_channels": ev_result2.sensory_channels,
343
+ "feature_attribution": ev_result2.feature_attribution,
344
+ "reason": ev_result2.reason,
345
+ },
346
+ }
347
+
348
+
349
  # In-memory artifact store: patch_hash -> {code, output, timestamp}
350
  artifact_store: dict[str, dict] = {}
351
 
frontend/index.html CHANGED
@@ -1738,6 +1738,24 @@
1738
  <div class="etl-stage-status" id="etlReceiptStatus">idle</div>
1739
  </div>
1740
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1741
  </div>
1742
 
1743
  <!-- CONTROLS -->
@@ -1930,6 +1948,7 @@
1930
  if (view) view.classList.add('active');
1931
  const link = document.querySelector(`.nav-link[data-view="${name}"]`);
1932
  if (link) link.classList.add('active');
 
1933
  }
1934
 
1935
  // ── STATUS ──
@@ -2181,43 +2200,86 @@
2181
  }
2182
  }
2183
 
 
 
 
2184
  async function startAudioRecording() {
2185
  try {
2186
  audioStream = await navigator.mediaDevices.getUserMedia({ audio: true });
2187
  audioChunks = [];
2188
  currentAudioHash = null;
 
2189
  mediaRecorder = new MediaRecorder(audioStream);
2190
  mediaRecorder.ondataavailable = (e) => { if (e.data.size > 0) audioChunks.push(e.data); };
2191
  mediaRecorder.onstop = () => {
2192
  if (audioStream) { audioStream.getTracks().forEach(t => t.stop()); audioStream = null; }
2193
- if (currentAudioHash) finishAudioUpload(currentAudioHash);
2194
- else {
2195
- // Standalone upload β€” no patch hash needed
2196
- if (audioChunks.length > 0) {
2197
- const blob = new Blob(audioChunks, { type: "audio/webm" });
2198
- const reader = new FileReader();
2199
- reader.onloadend = () => {
2200
- const b64 = reader.result.split(",")[1];
2201
- if (b64) {
2202
- fetch("/audio/store", {
2203
- method: "POST",
2204
- headers: { "Content-Type": "application/json" },
2205
- body: JSON.stringify({ audio_b64: b64, label: "standalone" })
2206
- }).then(() => {
2207
- audioChunks = [];
2208
- document.getElementById("recStatus").textContent = "Stored";
2209
- document.getElementById("audioStatus").textContent = "stored";
2210
- }).catch(() => { });
2211
- }
2212
- };
2213
- reader.readAsDataURL(blob);
2214
  }
2215
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2216
  };
2217
  mediaRecorder.start();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2218
  document.getElementById("recButton").classList.add("recording");
2219
  document.getElementById("recStatus").textContent = "Recording...";
2220
  document.getElementById("audioStatus").textContent = "recording";
 
 
2221
  setEtlStage('capture', 'active');
2222
  recSeconds = 0;
2223
  recTimer = setInterval(() => {
@@ -2234,12 +2296,81 @@
2234
  bar.style.height = (Math.random() * 36 + 4) + "px";
2235
  });
2236
  }, 100);
2237
- setStatus("Audio recording started", "dot-active");
2238
  } catch (e) {
2239
  setStatus("Audio recording error: " + e.message, "dot-error");
2240
  }
2241
  }
2242
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2243
  function finishAudioUpload(patchHash) {
2244
  if (audioChunks.length === 0) return;
2245
  const blob = new Blob(audioChunks, { type: "audio/webm" });
 
1738
  <div class="etl-stage-status" id="etlReceiptStatus">idle</div>
1739
  </div>
1740
  </div>
1741
+ <!-- LIVE TRANSCRIPT DURING RECORDING -->
1742
+ <div id="liveTranscript"
1743
+ style="margin-top:12px;padding:10px 14px;background:var(--bg2);border:1px solid var(--border);border-radius:8px;font-family:var(--mono);font-size:12px;color:var(--text2);min-height:40px;display:none">
1744
+ <div style="font-size:9px;color:var(--text3);text-transform:uppercase;letter-spacing:1px;margin-bottom:4px">
1745
+ Live Transcript</div>
1746
+ <div id="liveTranscriptText" style="line-height:1.6">Waiting for speech...</div>
1747
+ </div>
1748
+
1749
+ <!-- RECORDED AUDIO LIST -->
1750
+ <div id="audioListPanel" style="margin-top:12px">
1751
+ <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:6px">
1752
+ <div style="font-size:10px;color:var(--text3);text-transform:uppercase;letter-spacing:1px;font-weight:700">
1753
+ Recorded Audio Files</div>
1754
+ <button class="toolbar-btn" onclick="loadAudioList()"
1755
+ style="font-size:10px;padding:2px 8px">Refresh</button>
1756
+ </div>
1757
+ <div id="audioList" style="display:flex;flex-direction:column;gap:4px"></div>
1758
+ </div>
1759
  </div>
1760
 
1761
  <!-- CONTROLS -->
 
1948
  if (view) view.classList.add('active');
1949
  const link = document.querySelector(`.nav-link[data-view="${name}"]`);
1950
  if (link) link.classList.add('active');
1951
+ if (name === 'engine') loadAudioList();
1952
  }
1953
 
1954
  // ── STATUS ──
 
2200
  }
2201
  }
2202
 
2203
+ let recRecognition = null;
2204
+ let recTranscriptText = "";
2205
+
2206
  async function startAudioRecording() {
2207
  try {
2208
  audioStream = await navigator.mediaDevices.getUserMedia({ audio: true });
2209
  audioChunks = [];
2210
  currentAudioHash = null;
2211
+ recTranscriptText = "";
2212
  mediaRecorder = new MediaRecorder(audioStream);
2213
  mediaRecorder.ondataavailable = (e) => { if (e.data.size > 0) audioChunks.push(e.data); };
2214
  mediaRecorder.onstop = () => {
2215
  if (audioStream) { audioStream.getTracks().forEach(t => t.stop()); audioStream = null; }
2216
+ if (recRecognition) { try { recRecognition.stop(); } catch (e) { } recRecognition = null; }
2217
+ // Show transcript
2218
+ if (recTranscriptText.trim()) {
2219
+ document.getElementById("liveTranscriptText").textContent = recTranscriptText;
2220
+ setEtlStage('transcribe', 'done');
2221
+ // Send transcript to WebSocket to trigger observer β†’ builder
2222
+ if (ws && ws.readyState === WebSocket.OPEN) {
2223
+ ws.send(JSON.stringify({ type: "transcript", text: recTranscriptText.trim() }));
2224
+ setEtlStage('observe', 'active');
2225
+ setStatus("Transcript sent to observer LLM", "dot-thinking");
 
 
 
 
 
 
 
 
 
 
 
2226
  }
2227
  }
2228
+ // Upload audio file
2229
+ if (currentAudioHash) finishAudioUpload(currentAudioHash);
2230
+ else if (audioChunks.length > 0) {
2231
+ const blob = new Blob(audioChunks, { type: "audio/webm" });
2232
+ const reader = new FileReader();
2233
+ reader.onloadend = () => {
2234
+ const b64 = reader.result.split(",")[1];
2235
+ if (b64) {
2236
+ fetch("/audio/store", {
2237
+ method: "POST",
2238
+ headers: { "Content-Type": "application/json" },
2239
+ body: JSON.stringify({ audio_b64: b64, label: "standalone", transcript: recTranscriptText })
2240
+ }).then(r => r.json()).then(data => {
2241
+ audioChunks = [];
2242
+ document.getElementById("recStatus").textContent = "Stored";
2243
+ document.getElementById("audioStatus").textContent = "stored";
2244
+ loadAudioList();
2245
+ // If we have transcript but no WS, trigger generate directly
2246
+ if (recTranscriptText.trim() && (!ws || ws.readyState !== WebSocket.OPEN)) {
2247
+ triggerGenerateFromAudio(recTranscriptText.trim(), data.audio_file);
2248
+ }
2249
+ }).catch(() => { });
2250
+ }
2251
+ };
2252
+ reader.readAsDataURL(blob);
2253
+ }
2254
  };
2255
  mediaRecorder.start();
2256
+ // Start speech recognition in parallel for live transcription
2257
+ const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
2258
+ if (SR) {
2259
+ recRecognition = new SR();
2260
+ recRecognition.lang = "en-US";
2261
+ recRecognition.continuous = true;
2262
+ recRecognition.interimResults = true;
2263
+ recRecognition.onresult = (event) => {
2264
+ let interim = "";
2265
+ let final = "";
2266
+ for (let i = event.resultIndex; i < event.results.length; i++) {
2267
+ if (event.results[i].isFinal) final += event.results[i][0].transcript + " ";
2268
+ else interim += event.results[i][0].transcript;
2269
+ }
2270
+ recTranscriptText += final;
2271
+ const display = recTranscriptText + (interim ? '<span style="color:var(--text3)">' + interim + '</span>' : '');
2272
+ document.getElementById("liveTranscriptText").innerHTML = display || "Listening...";
2273
+ if (final.trim()) setEtlStage('transcribe', 'active');
2274
+ };
2275
+ recRecognition.onerror = (e) => { console.warn('Speech error during recording:', e.error); };
2276
+ try { recRecognition.start(); } catch (e) { console.warn('SR already started'); }
2277
+ }
2278
  document.getElementById("recButton").classList.add("recording");
2279
  document.getElementById("recStatus").textContent = "Recording...";
2280
  document.getElementById("audioStatus").textContent = "recording";
2281
+ document.getElementById("liveTranscript").style.display = "block";
2282
+ document.getElementById("liveTranscriptText").textContent = "Listening...";
2283
  setEtlStage('capture', 'active');
2284
  recSeconds = 0;
2285
  recTimer = setInterval(() => {
 
2296
  bar.style.height = (Math.random() * 36 + 4) + "px";
2297
  });
2298
  }, 100);
2299
+ setStatus("Audio recording + transcription started", "dot-active");
2300
  } catch (e) {
2301
  setStatus("Audio recording error: " + e.message, "dot-error");
2302
  }
2303
  }
2304
 
2305
+ // Trigger code generation from audio transcript when WS not connected
2306
+ async function triggerGenerateFromAudio(transcript, audioFile) {
2307
+ setEtlStage('observe', 'active');
2308
+ setStatus("Sending audio transcript to LLM...", "dot-thinking");
2309
+ try {
2310
+ const resp = await fetch('/audio/generate', {
2311
+ method: 'POST',
2312
+ headers: { 'Content-Type': 'application/json' },
2313
+ body: JSON.stringify({ transcript: transcript, audio_file: audioFile, mode: document.getElementById('mode')?.value || 'continuous_code' })
2314
+ });
2315
+ const data = await resp.json();
2316
+ if (data.patch_output) {
2317
+ setEtlStage('observe', 'done');
2318
+ setEtlStage('build', 'done');
2319
+ setEtlStage('receipt', 'done');
2320
+ addPatchEntry(data.patch_output, data.patch_hash, data.receipt, undefined);
2321
+ patchCount++;
2322
+ document.getElementById("patchCount").textContent = patchCount + " patch" + (patchCount !== 1 ? "es" : "");
2323
+ setStatus("Code patch generated from audio", "dot-active");
2324
+ // Store artifact
2325
+ const sections = parseSections(data.patch_output);
2326
+ const codeText = (sections.CODE || "").replace(/^```python\s*/i, "").replace(/^```\s*/, "").replace(/```$/, "").trim();
2327
+ if (codeText && data.patch_hash) {
2328
+ fetch("/store-artifact?patch_hash=" + data.patch_hash + "&code=" + encodeURIComponent(codeText) + "&reasoning=" + encodeURIComponent(sections.REASONING || "") + "&evidence=" + encodeURIComponent(sections.EVIDENCE || "") + "&observer_output=" + encodeURIComponent(data.patch_output), { method: "POST" }).catch(() => { });
2329
+ // Link audio to this artifact
2330
+ if (audioChunks.length > 0) {
2331
+ currentAudioHash = data.patch_hash;
2332
+ finishAudioUpload(data.patch_hash);
2333
+ }
2334
+ if (document.getElementById("autoRun") && document.getElementById("autoRun").checked) {
2335
+ setTimeout(() => runCode(data.patch_hash, null), 800);
2336
+ }
2337
+ }
2338
+ } else if (data.error) {
2339
+ setStatus("LLM error: " + data.error, "dot-error");
2340
+ }
2341
+ } catch (e) {
2342
+ setStatus("Generate error: " + e.message, "dot-error");
2343
+ }
2344
+ }
2345
+
2346
+ // Load recorded audio files from server
2347
+ async function loadAudioList() {
2348
+ try {
2349
+ const resp = await fetch('/audio/list');
2350
+ const data = await resp.json();
2351
+ const el = document.getElementById('audioList');
2352
+ if (data.total === 0) {
2353
+ el.innerHTML = '<div style="font-size:11px;color:var(--text3);padding:8px;font-family:var(--mono)">No recordings yet.</div>';
2354
+ return;
2355
+ }
2356
+ el.innerHTML = data.files.map(f => {
2357
+ const sizeKB = (f.size_bytes / 1024).toFixed(1);
2358
+ const date = new Date(f.modified * 1000).toLocaleTimeString();
2359
+ return '<div style="display:flex;align-items:center;gap:10px;padding:8px 12px;background:var(--panel2);border:1px solid var(--border);border-radius:8px">' +
2360
+ '<span style="font-size:16px">🎡</span>' +
2361
+ '<div style="flex:1;min-width:0">' +
2362
+ '<div style="font-size:11px;font-family:var(--mono);color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + f.filename + '</div>' +
2363
+ '<div style="font-size:9px;color:var(--text3)">' + sizeKB + 'KB Β· ' + date + '</div>' +
2364
+ '</div>' +
2365
+ '<audio controls preload="none" src="/audio/' + f.filename + '" style="height:28px;width:200px" />' +
2366
+ '<a href="/audio/' + f.filename + '" download style="font-size:11px;color:var(--accent);text-decoration:none">Download</a>' +
2367
+ '</div>';
2368
+ }).join('');
2369
+ } catch (e) {
2370
+ document.getElementById('audioList').innerHTML = '<div style="font-size:11px;color:var(--red);padding:8px">Error: ' + escapeHtml(e.message) + '</div>';
2371
+ }
2372
+ }
2373
+
2374
  function finishAudioUpload(patchHash) {
2375
  if (audioChunks.length === 0) return;
2376
  const blob = new Blob(audioChunks, { type: "audio/webm" });