josephrw commited on
Commit
e8a76c8
·
verified ·
1 Parent(s): 654a6ba

Upload folder using huggingface_hub

Browse files
Files changed (4) hide show
  1. app.py +7 -1
  2. frontend/index.html +190 -38
  3. src/builder_llm.py +53 -26
  4. src/consensus.py +476 -0
app.py CHANGED
@@ -33,6 +33,7 @@ from src.gate import builder_gate, evidence_gate, log_gate_decision, get_gate_lo
33
  from src.observer_llm import observe
34
  from src.builder_llm import build, debug_fix
35
  from src.code_receipts import save_receipt, list_receipts, get_receipt
 
36
 
37
  APP_ROOT = Path(__file__).resolve().parent
38
  FRONTEND_INDEX = APP_ROOT / "frontend" / "index.html"
@@ -275,7 +276,7 @@ async def generate_patch(request: Request):
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:
@@ -284,6 +285,7 @@ async def audio_generate(request: Request):
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)
@@ -299,6 +301,9 @@ async def audio_generate(request: Request):
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)
@@ -329,6 +334,7 @@ async def audio_generate(request: Request):
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)
 
33
  from src.observer_llm import observe
34
  from src.builder_llm import build, debug_fix
35
  from src.code_receipts import save_receipt, list_receipts, get_receipt
36
+ from src.consensus import observer_consensus, builder_consensus
37
 
38
  APP_ROOT = Path(__file__).resolve().parent
39
  FRONTEND_INDEX = APP_ROOT / "frontend" / "index.html"
 
276
  @app.post("/audio/generate")
277
  async def audio_generate(request: Request):
278
  """Generate code from an audio transcript without requiring an existing session.
279
+ Creates a temporary session, injects the transcript and audio features as evidence,
280
  runs observer → builder → receipt."""
281
  body = {}
282
  try:
 
285
  pass
286
  transcript = body.get("transcript", "") or request.query_params.get("transcript", "")
287
  audio_file = body.get("audio_file", "") or request.query_params.get("audio_file", "")
288
+ audio_features = body.get("audio_features", {}) or {}
289
  mode = body.get("mode", "continuous_code") or request.query_params.get("mode", "continuous_code")
290
  if not transcript:
291
  return JSONResponse({"error": "transcript required"}, 400)
 
301
 
302
  # Compress and run the full pipeline
303
  compact = compress_state(state)
304
+ # Inject audio features into compact state for the builder
305
+ if audio_features:
306
+ compact["audio_features"] = audio_features
307
  ev_result = evidence_gate("", compact)
308
  log_gate_decision("evidence_gate", ev_result, patch_hash="")
309
  observation = observe(state, compact)
 
334
  "feature_attribution": ev_result2.feature_attribution,
335
  "audio_file": audio_file,
336
  "transcript": transcript,
337
+ "audio_features": audio_features,
338
  }
339
  artifact_store[result["patch_hash"]] = artifact_data
340
  save_artifact_disk(result["patch_hash"], artifact_data)
frontend/index.html CHANGED
@@ -1697,6 +1697,15 @@
1697
  </svg>
1698
  Start Speech
1699
  </button>
 
 
 
 
 
 
 
 
 
1700
  <button class="btn btn-sm btn-ghost" onclick="switchView('terminal')">
1701
  <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1702
  <polyline points="4 17 10 11 4 5" />
@@ -2074,6 +2083,7 @@
2074
  document.getElementById("observerStatus").textContent = "updated";
2075
  setEtlStage('observe', 'done');
2076
  setEtlStage('build', 'active');
 
2077
  break;
2078
  case "patch":
2079
  addPatchEntry(msg.output, msg.patch_hash, msg.receipt, msg.qvd);
@@ -2202,6 +2212,10 @@
2202
 
2203
  let recRecognition = null;
2204
  let recTranscriptText = "";
 
 
 
 
2205
 
2206
  async function startAudioRecording() {
2207
  try {
@@ -2209,25 +2223,38 @@
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 = () => {
@@ -2236,15 +2263,25 @@
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
  }
@@ -2252,8 +2289,9 @@
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();
@@ -2261,22 +2299,36 @@
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...";
@@ -2288,29 +2340,128 @@
2288
  const s = (recSeconds % 60).toString().padStart(2, '0');
2289
  document.getElementById("recTime").textContent = m + ':' + s;
2290
  }, 1000);
2291
- // Waveform animation
 
2292
  const wf = document.getElementById("waveform");
2293
- wf.innerHTML = Array.from({ length: 30 }, () => '<div class="wave-bar" style="height:4px"></div>').join("");
2294
  waveformTimer = setInterval(() => {
2295
- wf.querySelectorAll(".wave-bar").forEach(bar => {
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) {
@@ -2321,6 +2472,7 @@
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();
 
1697
  </svg>
1698
  Start Speech
1699
  </button>
1700
+ <button class="btn btn-sm btn-ghost" id="ttsBtn" onclick="toggleTTS()"
1701
+ style="border-color:var(--accent);color:var(--accent)">
1702
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1703
+ <polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5" />
1704
+ <path d="M19.07 4.93a10 10 0 0 1 0 14.14" />
1705
+ <path d="M15.54 8.46a5 5 0 0 1 0 7.07" />
1706
+ </svg>
1707
+ TTS: On
1708
+ </button>
1709
  <button class="btn btn-sm btn-ghost" onclick="switchView('terminal')">
1710
  <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1711
  <polyline points="4 17 10 11 4 5" />
 
2083
  document.getElementById("observerStatus").textContent = "updated";
2084
  setEtlStage('observe', 'done');
2085
  setEtlStage('build', 'active');
2086
+ speakText(msg.output);
2087
  break;
2088
  case "patch":
2089
  addPatchEntry(msg.output, msg.patch_hash, msg.receipt, msg.qvd);
 
2212
 
2213
  let recRecognition = null;
2214
  let recTranscriptText = "";
2215
+ let recAudioContext = null;
2216
+ let recAnalyser = null;
2217
+ let recFreqData = null;
2218
+ let recFreqSamples = [];
2219
 
2220
  async function startAudioRecording() {
2221
  try {
 
2223
  audioChunks = [];
2224
  currentAudioHash = null;
2225
  recTranscriptText = "";
2226
+ recFreqSamples = [];
2227
+
2228
+ // Real audio analysis via Web Audio API
2229
+ recAudioContext = new (window.AudioContext || window.webkitAudioContext)();
2230
+ const source = recAudioContext.createMediaStreamSource(audioStream);
2231
+ recAnalyser = recAudioContext.createAnalyser();
2232
+ recAnalyser.fftSize = 2048;
2233
+ source.connect(recAnalyser);
2234
+ recFreqData = new Uint8Array(recAnalyser.frequencyBinCount);
2235
+
2236
  mediaRecorder = new MediaRecorder(audioStream);
2237
  mediaRecorder.ondataavailable = (e) => { if (e.data.size > 0) audioChunks.push(e.data); };
2238
  mediaRecorder.onstop = () => {
2239
  if (audioStream) { audioStream.getTracks().forEach(t => t.stop()); audioStream = null; }
2240
  if (recRecognition) { try { recRecognition.stop(); } catch (e) { } recRecognition = null; }
2241
+ if (recAudioContext) { try { recAudioContext.close(); } catch (e) { } recAudioContext = null; }
2242
+
2243
+ // Compute audio features from collected frequency samples
2244
+ const audioFeatures = computeAudioFeatures(recFreqSamples);
2245
+ document.getElementById("liveTranscriptText").innerHTML =
2246
+ (recTranscriptText.trim() ? '<div style="margin-bottom:8px;color:var(--text)">' + escapeHtml(recTranscriptText) + '</div>' : '') +
2247
+ '<div style="font-size:10px;color:var(--text3);border-top:1px solid var(--border);padding-top:8px">' +
2248
+ '<b>Audio Analysis:</b> dominant_freq=' + audioFeatures.dominant_freq + 'Hz' +
2249
+ ' | spectral_centroid=' + audioFeatures.spectral_centroid +
2250
+ ' | rhythm_bpm=' + audioFeatures.estimated_bpm +
2251
+ ' | patterns=' + audioFeatures.patterns.length +
2252
+ ' | samples=' + audioFeatures.sample_count +
2253
+ '</div>';
2254
+ setEtlStage('transcribe', 'done');
2255
+
2256
+ // Upload audio + features + transcript, then generate
2257
+ if (audioChunks.length > 0) {
2258
  const blob = new Blob(audioChunks, { type: "audio/webm" });
2259
  const reader = new FileReader();
2260
  reader.onloadend = () => {
 
2263
  fetch("/audio/store", {
2264
  method: "POST",
2265
  headers: { "Content-Type": "application/json" },
2266
+ body: JSON.stringify({ audio_b64: b64, label: "standalone", transcript: recTranscriptText, audio_features: audioFeatures })
2267
  }).then(r => r.json()).then(data => {
2268
  audioChunks = [];
2269
+ document.getElementById("recStatus").textContent = "Analyzed";
2270
+ document.getElementById("audioStatus").textContent = "analyzed";
2271
  loadAudioList();
2272
+ // Always trigger generation with or without speech
2273
+ const payload = {
2274
+ transcript: recTranscriptText.trim() || "[no speech detected — environmental audio with patterns: " + audioFeatures.patterns.length + " repeating cycles, dominant freq " + audioFeatures.dominant_freq + "Hz, estimated BPM " + audioFeatures.estimated_bpm + "]",
2275
+ audio_file: data.audio_file,
2276
+ audio_features: audioFeatures,
2277
+ mode: document.getElementById('mode')?.value || 'continuous_code'
2278
+ };
2279
+ if (ws && ws.readyState === WebSocket.OPEN) {
2280
+ ws.send(JSON.stringify({ type: "transcript", text: payload.transcript, audio_features: audioFeatures }));
2281
+ setEtlStage('observe', 'active');
2282
+ setStatus("Audio + features sent to observer LLM", "dot-thinking");
2283
+ } else {
2284
+ triggerGenerateFromAudio(payload.transcript, data.audio_file, audioFeatures);
2285
  }
2286
  }).catch(() => { });
2287
  }
 
2289
  reader.readAsDataURL(blob);
2290
  }
2291
  };
2292
+ mediaRecorder.start(1000); // timeslice = 1s chunks
2293
+
2294
+ // Speech recognition with auto-restart (Chrome stops after ~60s)
2295
  const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
2296
  if (SR) {
2297
  recRecognition = new SR();
 
2299
  recRecognition.continuous = true;
2300
  recRecognition.interimResults = true;
2301
  recRecognition.onresult = (event) => {
2302
+ let interim = "", final = "";
 
2303
  for (let i = event.resultIndex; i < event.results.length; i++) {
2304
  if (event.results[i].isFinal) final += event.results[i][0].transcript + " ";
2305
  else interim += event.results[i][0].transcript;
2306
  }
2307
  recTranscriptText += final;
2308
+ const display = recTranscriptText + (interim ? '<span style="color:var(--text3)">' + escapeHtml(interim) + '</span>' : '');
2309
  document.getElementById("liveTranscriptText").innerHTML = display || "Listening...";
2310
  if (final.trim()) setEtlStage('transcribe', 'active');
2311
  };
2312
+ recRecognition.onerror = (e) => {
2313
+ console.warn('SR error:', e.error);
2314
+ if (e.error === 'not-allowed' || e.error === 'service-not-allowed') {
2315
+ document.getElementById("liveTranscriptText").innerHTML =
2316
+ '<span style="color:var(--amber)">Speech transcription unavailable. Audio analysis running — extracting frequencies, rhythms, and environmental patterns.</span>';
2317
+ }
2318
+ };
2319
+ recRecognition.onend = () => {
2320
+ if (mediaRecorder && mediaRecorder.state === 'recording') {
2321
+ try { recRecognition.start(); } catch (e) { }
2322
+ }
2323
+ };
2324
+ try { recRecognition.start(); } catch (e) { console.warn('SR start failed:', e); }
2325
+ } else {
2326
+ document.getElementById("liveTranscriptText").innerHTML =
2327
+ '<span style="color:var(--amber)">SpeechRecognition not available. Audio analysis running — extracting frequencies, rhythms, and patterns from environmental audio.</span>';
2328
  }
2329
+
2330
  document.getElementById("recButton").classList.add("recording");
2331
+ document.getElementById("recStatus").textContent = "Recording + analyzing...";
2332
  document.getElementById("audioStatus").textContent = "recording";
2333
  document.getElementById("liveTranscript").style.display = "block";
2334
  document.getElementById("liveTranscriptText").textContent = "Listening...";
 
2340
  const s = (recSeconds % 60).toString().padStart(2, '0');
2341
  document.getElementById("recTime").textContent = m + ':' + s;
2342
  }, 1000);
2343
+
2344
+ // Real waveform from frequency data + sample for pattern analysis
2345
  const wf = document.getElementById("waveform");
2346
+ wf.innerHTML = Array.from({ length: 40 }, () => '<div class="wave-bar" style="height:4px"></div>').join("");
2347
  waveformTimer = setInterval(() => {
2348
+ if (recAnalyser && recFreqData) {
2349
+ recAnalyser.getByteFrequencyData(recFreqData);
2350
+ if (recSeconds % 2 === 0 && recSeconds > 0) {
2351
+ recFreqSamples.push(Array.from(recFreqData.slice(0, 128)));
2352
+ }
2353
+ const bars = wf.querySelectorAll(".wave-bar");
2354
+ const step = Math.floor(recFreqData.length / bars.length);
2355
+ bars.forEach((bar, i) => {
2356
+ const val = recFreqData[i * step] || 0;
2357
+ bar.style.height = Math.max(4, (val / 255) * 40) + "px";
2358
+ });
2359
+ }
2360
+ }, 50);
2361
+ setStatus("Audio recording + analysis started", "dot-active");
2362
  } catch (e) {
2363
  setStatus("Audio recording error: " + e.message, "dot-error");
2364
  }
2365
  }
2366
 
2367
+ // Compute audio features from collected frequency samples
2368
+ function computeAudioFeatures(samples) {
2369
+ if (!samples || samples.length === 0) {
2370
+ return { dominant_freq: 0, spectral_centroid: 0, estimated_bpm: 0, patterns: [], sample_count: 0, avg_energy: 0 };
2371
+ }
2372
+ const nBins = samples[0].length;
2373
+ const avgSpectrum = new Array(nBins).fill(0);
2374
+ for (const s of samples) for (let i = 0; i < nBins; i++) avgSpectrum[i] += s[i];
2375
+ for (let i = 0; i < nBins; i++) avgSpectrum[i] /= samples.length;
2376
+
2377
+ let maxBin = 0, maxVal = 0;
2378
+ for (let i = 0; i < nBins; i++) { if (avgSpectrum[i] > maxVal) { maxVal = avgSpectrum[i]; maxBin = i; } }
2379
+ const dominantFreq = Math.round(maxBin * 44100 / 2048);
2380
+
2381
+ let sumWeighted = 0, sumMag = 0;
2382
+ for (let i = 0; i < nBins; i++) {
2383
+ const freq = i * 44100 / 2048;
2384
+ sumWeighted += freq * avgSpectrum[i];
2385
+ sumMag += avgSpectrum[i];
2386
+ }
2387
+ const spectralCentroid = sumMag > 0 ? Math.round(sumWeighted / sumMag) : 0;
2388
+
2389
+ const energies = samples.map(s => s.reduce((a, b) => a + b, 0) / s.length);
2390
+ let bpm = 0;
2391
+ if (energies.length > 4) {
2392
+ const peaks = [];
2393
+ for (let i = 1; i < energies.length - 1; i++) {
2394
+ if (energies[i] > energies[i - 1] && energies[i] > energies[i + 1] && energies[i] > 50) peaks.push(i);
2395
+ }
2396
+ if (peaks.length > 1) {
2397
+ const intervals = [];
2398
+ for (let i = 1; i < peaks.length; i++) intervals.push(peaks[i] - peaks[i - 1]);
2399
+ const avgInterval = intervals.reduce((a, b) => a + b, 0) / intervals.length;
2400
+ bpm = avgInterval > 0 ? Math.round(60 / (avgInterval * 2)) : 0;
2401
+ }
2402
+ }
2403
+
2404
+ // Detect repeating patterns via autocorrelation
2405
+ const patterns = [];
2406
+ if (samples.length > 6) {
2407
+ for (let lag = 2; lag < Math.min(samples.length / 2, 10); lag++) {
2408
+ let corr = 0, count = 0;
2409
+ for (let i = 0; i < samples.length - lag; i++) {
2410
+ for (let j = 0; j < nBins; j++) { corr += Math.abs(samples[i][j] - samples[i + lag][j]); count++; }
2411
+ }
2412
+ const avgDiff = corr / count;
2413
+ if (avgDiff < 15) patterns.push({ lag_seconds: lag * 2, correlation: Math.round((1 - avgDiff / 128) * 100) / 100 });
2414
+ }
2415
+ }
2416
+
2417
+ return { dominant_freq: dominantFreq, spectral_centroid: spectralCentroid, estimated_bpm: bpm, patterns, sample_count: samples.length, avg_energy: Math.round(energies.reduce((a, b) => a + b, 0) / energies.length) };
2418
+ }
2419
+
2420
+ // TTS — speak observer reasoning aloud
2421
+ let ttsEnabled = true;
2422
+ let ttsVoice = null;
2423
+ function speakText(text) {
2424
+ if (!ttsEnabled || !text || !('speechSynthesis' in window)) return;
2425
+ window.speechSynthesis.cancel();
2426
+ if (!ttsVoice) {
2427
+ const voices = window.speechSynthesis.getVoices();
2428
+ ttsVoice = voices.find(v => v.lang.startsWith('en') && v.name.includes('Google')) || voices.find(v => v.lang.startsWith('en')) || voices[0];
2429
+ }
2430
+ const sections = parseSections(text);
2431
+ let speak = sections.REASONING || sections.INFERRED_INTENT || text.substring(0, 500);
2432
+ speak = speak.replace(/```[\s\S]*?```/g, ' code omitted ').replace(/[#*\-]/g, ' ').replace(/\s+/g, ' ').trim();
2433
+ if (speak.length > 600) speak = speak.substring(0, 600) + '. That is the key finding.';
2434
+ const utter = new SpeechSynthesisUtterance(speak);
2435
+ if (ttsVoice) utter.voice = ttsVoice;
2436
+ utter.rate = 1.1; utter.pitch = 0.9;
2437
+ window.speechSynthesis.speak(utter);
2438
+ }
2439
+ if ('speechSynthesis' in window) window.speechSynthesis.onvoiceschanged = () => { ttsVoice = null; };
2440
+
2441
+ function toggleTTS() {
2442
+ ttsEnabled = !ttsEnabled;
2443
+ const btn = document.getElementById('ttsBtn');
2444
+ if (ttsEnabled) {
2445
+ btn.style.borderColor = 'var(--accent)';
2446
+ btn.style.color = 'var(--accent)';
2447
+ btn.innerHTML = btn.innerHTML.replace('TTS: Off', 'TTS: On');
2448
+ } else {
2449
+ btn.style.borderColor = 'var(--text3)';
2450
+ btn.style.color = 'var(--text3)';
2451
+ btn.innerHTML = btn.innerHTML.replace('TTS: On', 'TTS: Off');
2452
+ window.speechSynthesis.cancel();
2453
+ }
2454
+ }
2455
+
2456
  // Trigger code generation from audio transcript when WS not connected
2457
+ async function triggerGenerateFromAudio(transcript, audioFile, audioFeatures) {
2458
  setEtlStage('observe', 'active');
2459
+ setStatus("Sending audio + analysis to LLM...", "dot-thinking");
2460
  try {
2461
  const resp = await fetch('/audio/generate', {
2462
  method: 'POST',
2463
  headers: { 'Content-Type': 'application/json' },
2464
+ body: JSON.stringify({ transcript: transcript, audio_file: audioFile, audio_features: audioFeatures, mode: document.getElementById('mode')?.value || 'continuous_code' })
2465
  });
2466
  const data = await resp.json();
2467
  if (data.patch_output) {
 
2472
  patchCount++;
2473
  document.getElementById("patchCount").textContent = patchCount + " patch" + (patchCount !== 1 ? "es" : "");
2474
  setStatus("Code patch generated from audio", "dot-active");
2475
+ speakText(data.patch_output);
2476
  // Store artifact
2477
  const sections = parseSections(data.patch_output);
2478
  const codeText = (sections.CODE || "").replace(/^```python\s*/i, "").replace(/^```\s*/, "").replace(/```$/, "").trim();
src/builder_llm.py CHANGED
@@ -12,9 +12,9 @@ import requests
12
  from .stream_state import SessionState, sha256_text
13
 
14
 
15
- BUILDER_PROMPT = """You are the Builder in a Continuity Sensory Code EngineSensory Proprietary Compiler V1.
16
 
17
- You receive an observation from the Observer LLM and a FALLBACK LEVEL. You MUST produce a structured artifact. NEVER return empty. NEVER return INSUFFICIENT_EVIDENCE.
18
 
19
  CRITICAL RULES — VIOLATION = REJECTION:
20
  1. The CODE section MUST contain ONLY valid, executable Python code.
@@ -24,46 +24,70 @@ CRITICAL RULES — VIOLATION = REJECTION:
24
  5. Do NOT import cv2, torch, tensorflow, pandas, matplotlib, scipy, sklearn, or any package not listed above.
25
  6. Do NOT use input() or any interactive call — code must run non-interactively.
26
 
27
- FALLBACK LADDERyou are at the level indicated. Generate code appropriate to that level:
28
-
29
- LEVEL 1 (task_code): Explicit user intent detected. Generate task-specific code that does what the user asked.
30
-
31
- LEVEL 2 (instrumentation_code): No explicit intent, but rich sensory features (camera, motion, audio). Generate instrumentation code that:
32
- - Captures and quantifies the sensory channels present (motion metrics, audio energy, frame entropy)
33
- - Creates reusable measurement utilities
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  - Produces a feature vector or data structure from the sensory input
35
- - Example: motion rhythm analyzer, audio energy spectrum, frame entropy tracker
36
 
37
  LEVEL 3 (aesthetic_motif): Distinctive visual features but no clear intent. Generate code that:
38
- - Extracts aesthetic features from described visual input (color palette, light patterns, composition)
39
- - Creates a UI theme, color scheme, or visual motif from the sensory description
40
- - Produces a design grammar or style specification
41
- - Example: color palette generator from room description, UI theme from lighting conditions
42
 
43
- LEVEL 4 (topic_association): Only background audio/speech detected. Generate code that:
44
- - Maps detected topics to relevant tools or code patterns
45
- - Creates a topic-to-tool association dictionary
46
- - Generates a transcript summarizer or topic classifier
47
- - Example: TV topic to code domain mapper, background speech topic extractor
48
 
49
  LEVEL 5 (capture_protocol): Minimal sensory signal. Generate code that:
50
- - Improves the capture protocol itself
51
  - Creates a better sampling strategy or capture diagnostic
52
- - Produces a session health check or capture quality report
53
- - Example: adaptive frame rate calculator, audio sensitivity tuner, capture health monitor
54
 
55
  Output format (use exactly these headers):
56
 
57
  EVIDENCE:
58
- - List specific sensory evidence from the observation. Include channel names and feature values.
59
 
60
  REASONING:
61
- - Step by step: why this artifact should exist. Connect sensory evidence to code decisions. Explain which fallback level triggered this.
62
 
63
  CODE:
64
  ```python
65
  # ONLY executable Python code here. No prose. No explanations.
66
  # Must pass syntax validation. Must use only installed packages.
 
67
  ```
68
 
69
  RUN:
@@ -73,7 +97,7 @@ TEST:
73
  - Minimal verification step.
74
 
75
  ATTRIBUTION:
76
- - Which sensory evidence (frame hashes, audio segments, motion scores, visual features) triggered this code. Be specific.
77
  """
78
 
79
 
@@ -158,7 +182,10 @@ ARTIFACT TYPE: {artifact_type}
158
  SENSORY CHANNELS: {', '.join(sensory_channels) if sensory_channels else 'none'}
159
  FEATURE ATTRIBUTION: {json.dumps(feature_attribution, default=str)}
160
 
161
- You are at FALLBACK LEVEL {fallback_level}. Generate {artifact_type} based on the available sensory evidence. NEVER return INSUFFICIENT_EVIDENCE. Always produce a CODE section with runnable Python."""
 
 
 
162
 
163
  provider = os.getenv("PROVIDER", "groq").lower().strip()
164
  if provider == "ollama":
 
12
  from .stream_state import SessionState, sha256_text
13
 
14
 
15
+ BUILDER_PROMPT = """You are the Builder in a Sensory Proprietary Compilera research lab system that extracts maximum value from human sensory input.
16
 
17
+ Your job is NOT to build a code assistant. Your job is NOT to build a debugging tool. Your job is to extract novel patterns from sensory data and create ML artifacts that have intellectual property value.
18
 
19
  CRITICAL RULES — VIOLATION = REJECTION:
20
  1. The CODE section MUST contain ONLY valid, executable Python code.
 
24
  5. Do NOT import cv2, torch, tensorflow, pandas, matplotlib, scipy, sklearn, or any package not listed above.
25
  6. Do NOT use input() or any interactive call — code must run non-interactively.
26
 
27
+ WHAT TO BUILD based on available evidence:
28
+
29
+ If AUDIO FEATURES are present (dominant_freq, spectral_centroid, estimated_bpm, patterns):
30
+ - Extract and quantify the environmental signals: AC hum frequency, mechanical rhythms, background noise patterns
31
+ - Create feature vectors from the frequency spectrum data
32
+ - Build pattern detectors that identify repeating cycles (e.g. AC 60Hz cycle, mechanical rotation, HVAC cycling)
33
+ - Generate signal classification models using numpy (FFT, spectral analysis, autocorrelation)
34
+ - Create data structures that capture the unique signature of this recording session
35
+ - Build novelty detectors that flag when the signal pattern changes
36
+ - Example: AC cycle analyzer that detects 60Hz/120Hz harmonics, rhythm pattern extractor, spectral fingerprint generator
37
+
38
+ If SPEECH TRANSCRIPT is present:
39
+ - Extract semantic patterns, topic clusters, and intent signals from the transcript
40
+ - Build text analysis tools that quantify information density, novelty, and signal-to-noise ratio
41
+ - Create topic extraction and association mapping code
42
+ - Generate intent classifiers that map speech to actionable patterns
43
+ - Build evidence extraction pipelines that pull claims, timestamps, and verification markers
44
+
45
+ If CAMERA/VISUAL data is present:
46
+ - Extract visual features: motion vectors, color distributions, frame entropy
47
+ - Build scene change detectors and visual novelty scorers
48
+ - Create visual pattern recognizers using numpy operations on frame data
49
+
50
+ FALLBACK LADDER — you are at the level indicated:
51
+
52
+ LEVEL 1 (task_code): Explicit user intent detected from speech. Generate code that does what the user asked — but frame it as a data extraction or pattern analysis tool, not a generic utility.
53
+
54
+ LEVEL 2 (signal_extraction): No explicit intent, but rich audio features. Generate code that:
55
+ - Analyzes the frequency spectrum and extracts dominant patterns
56
+ - Detects environmental rhythms (AC cycles, mechanical patterns, biological rhythms)
57
+ - Creates a spectral fingerprint unique to this recording environment
58
+ - Builds a pattern classifier that can distinguish this session from others
59
  - Produces a feature vector or data structure from the sensory input
 
60
 
61
  LEVEL 3 (aesthetic_motif): Distinctive visual features but no clear intent. Generate code that:
62
+ - Extracts aesthetic features from described visual input (color palette, light patterns)
63
+ - Creates a visual motif or style specification from the sensory description
64
+ - Produces a design grammar or compression of the visual state
 
65
 
66
+ LEVEL 4 (topic_association): Only background audio detected. Generate code that:
67
+ - Maps detected audio patterns to potential data sources and ML applications
68
+ - Creates a signal-to-topic association dictionary
69
+ - Generates an environmental audio classifier or background pattern extractor
70
+ - Builds a novelty detector for ambient sound changes
71
 
72
  LEVEL 5 (capture_protocol): Minimal sensory signal. Generate code that:
73
+ - Improves the capture and analysis protocol itself
74
  - Creates a better sampling strategy or capture diagnostic
75
+ - Produces a session health check or signal quality report
76
+ - Builds an adaptive sampling optimizer
77
 
78
  Output format (use exactly these headers):
79
 
80
  EVIDENCE:
81
+ - List specific sensory evidence. Include channel names, feature values, frequencies, patterns detected.
82
 
83
  REASONING:
84
+ - Step by step: why this artifact should exist. Connect sensory evidence to the ML approach. What novel pattern was extracted? What is the intellectual property value of this artifact?
85
 
86
  CODE:
87
  ```python
88
  # ONLY executable Python code here. No prose. No explanations.
89
  # Must pass syntax validation. Must use only installed packages.
90
+ # This code should EXTRACT PATTERNS, not build generic tools.
91
  ```
92
 
93
  RUN:
 
97
  - Minimal verification step.
98
 
99
  ATTRIBUTION:
100
+ - Which sensory evidence (frequencies, patterns, transcripts, frame hashes) triggered this code. Be specific with numerical values.
101
  """
102
 
103
 
 
182
  SENSORY CHANNELS: {', '.join(sensory_channels) if sensory_channels else 'none'}
183
  FEATURE ATTRIBUTION: {json.dumps(feature_attribution, default=str)}
184
 
185
+ AUDIO FEATURES (from browser Web Audio API analysis):
186
+ {json.dumps(observation.get('audio_features', {}), indent=2, default=str) if observation.get('audio_features') else 'No audio features available — use transcript and visual evidence only.'}
187
+
188
+ You are at FALLBACK LEVEL {fallback_level}. Generate {artifact_type} based on the available sensory evidence. NEVER return INSUFFICIENT_EVIDENCE. Always produce a CODE section with runnable Python that EXTRACTS PATTERNS or ANALYZES SIGNALS."""
189
 
190
  provider = os.getenv("PROVIDER", "groq").lower().strip()
191
  if provider == "ollama":
src/consensus.py ADDED
@@ -0,0 +1,476 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Multi-model consensus engine for the CSC Engine research lab protocol.
3
+
4
+ Runs observer + builder through multiple LLM providers, compares outputs,
5
+ flags disagreements, and produces a consensus verdict with timestamps.
6
+
7
+ Architecture:
8
+ 1. Call N providers for the same prompt
9
+ 2. Extract structured sections (EVIDENCE, REASONING, CODE) from each
10
+ 3. Compare section similarity and content overlap
11
+ 4. Flag disagreements (different code approaches, conflicting evidence)
12
+ 5. Produce consensus verdict: AGREED, PARTIAL_AGREEMENT, DISAGREEMENT
13
+ 6. Select best output (longest code, most evidence items, or majority vote)
14
+ 7. Every step timestamped and logged
15
+ """
16
+
17
+ import os
18
+ import time
19
+ import json
20
+ import hashlib
21
+ import difflib
22
+ from typing import Optional
23
+
24
+
25
+ def _get_available_providers() -> list[str]:
26
+ """Determine which LLM providers are available based on env vars."""
27
+ providers = []
28
+ if os.getenv("GROQ_API_KEY") or os.getenv("GROK_API_KEY"):
29
+ providers.append("groq")
30
+ if os.getenv("HF_TOKEN") or os.getenv("HUGGING_FACE_HUB_TOKEN"):
31
+ providers.append("huggingface")
32
+ if os.getenv("OPENAI_API_KEY"):
33
+ providers.append("openai")
34
+ if os.getenv("OLLAMA_HOST"):
35
+ providers.append("ollama")
36
+ # Always include the primary provider
37
+ primary = os.getenv("PROVIDER", "groq").lower().strip()
38
+ if primary == "hybrid":
39
+ primary = "groq"
40
+ if primary not in providers:
41
+ providers.insert(0, primary)
42
+ # Deduplicate, preserve order, max 3
43
+ seen = set()
44
+ unique = []
45
+ for p in providers:
46
+ if p not in seen:
47
+ seen.add(p)
48
+ unique.append(p)
49
+ return unique[:3]
50
+
51
+
52
+ def _call_provider(provider: str, prompt: str, frame_b64: Optional[str] = None) -> str:
53
+ """Call a single LLM provider and return its output."""
54
+ provider = provider.lower().strip()
55
+ if provider == "groq" or provider == "grok":
56
+ from .observer_llm import call_grok
57
+ return call_grok(prompt, frame_b64)
58
+ elif provider == "huggingface":
59
+ from .observer_llm import call_hf_inference
60
+ return call_hf_inference(prompt, frame_b64)
61
+ elif provider == "openai":
62
+ from .observer_llm import call_openai
63
+ return call_openai(prompt, frame_b64)
64
+ elif provider == "ollama":
65
+ from .observer_llm import call_ollama
66
+ return call_ollama(prompt, frame_b64)
67
+ else:
68
+ raise RuntimeError(f"Unknown provider: {provider}")
69
+
70
+
71
+ def _call_builder_provider(provider: str, prompt: str) -> str:
72
+ """Call a single builder LLM provider."""
73
+ provider = provider.lower().strip()
74
+ if provider == "groq" or provider == "grok":
75
+ from .builder_llm import call_grok
76
+ return call_grok(prompt)
77
+ elif provider == "huggingface":
78
+ from .builder_llm import call_hf_inference
79
+ return call_hf_inference(prompt)
80
+ elif provider == "openai":
81
+ from .builder_llm import call_openai
82
+ return call_openai(prompt)
83
+ elif provider == "ollama":
84
+ from .builder_llm import call_ollama
85
+ return call_ollama(prompt)
86
+ else:
87
+ raise RuntimeError(f"Unknown builder provider: {provider}")
88
+
89
+
90
+ def _extract_section(text: str, section_name: str) -> str:
91
+ """Extract a named section (EVIDENCE, REASONING, CODE, etc.) from LLM output."""
92
+ lines = text.split("\n")
93
+ capturing = False
94
+ collected = []
95
+ for line in lines:
96
+ stripped = line.strip().upper()
97
+ if stripped.startswith(section_name + ":"):
98
+ capturing = True
99
+ continue
100
+ if capturing:
101
+ # Check if we hit another section header
102
+ for header in ["EVIDENCE:", "REASONING:", "CODE:", "RUN:", "TEST:", "ATTRIBUTION:", "SCENE:", "INFERRED_INTENT:", "SIGNALS:", "CANDIDATE_TASK:", "UNCERTAINTY:"]:
103
+ if stripped.startswith(header):
104
+ capturing = False
105
+ break
106
+ if capturing:
107
+ collected.append(line)
108
+ return "\n".join(collected).strip()
109
+
110
+
111
+ def _similarity(text_a: str, text_b: str) -> float:
112
+ """Compute text similarity ratio between two strings (0.0 to 1.0)."""
113
+ if not text_a or not text_b:
114
+ return 0.0
115
+ return difflib.SequenceMatcher(None, text_a.lower(), text_b.lower()).ratio()
116
+
117
+
118
+ def _code_similarity(code_a: str, code_b: str) -> float:
119
+ """Compare code similarity ignoring whitespace and comments."""
120
+ def normalize(code: str) -> str:
121
+ lines = []
122
+ for line in code.split("\n"):
123
+ line = line.strip()
124
+ if line and not line.startswith("#"):
125
+ lines.append(line)
126
+ return " ".join(lines)
127
+ return _similarity(normalize(code_a), normalize(code_b))
128
+
129
+
130
+ def observer_consensus(state, compact_state: dict) -> dict:
131
+ """Run observer through multiple providers and produce consensus.
132
+
133
+ Returns dict with:
134
+ - consensus_verdict: AGREED | PARTIAL_AGREEMENT | DISAGREEMENT | SINGLE_PROVIDER
135
+ - observer_output: best output selected
136
+ - all_outputs: list of {provider, output, timestamp}
137
+ - disagreements: list of flagged differences
138
+ - similarity_matrix: pairwise similarities
139
+ - timestamp: consensus timestamp
140
+ """
141
+ from .observer_llm import OBSERVER_PROMPT
142
+ from .stream_state import SessionState
143
+
144
+ frames = list(state.frames)
145
+ last_frame = frames[-1] if frames else None
146
+ frame_b64 = last_frame.jpeg_b64 if last_frame else None
147
+
148
+ prompt = f"""{OBSERVER_PROMPT}
149
+
150
+ COMPRESSED SENSORY STATE:
151
+ {json.dumps(compact_state, indent=2)}
152
+
153
+ Produce your observation now."""
154
+
155
+ providers = _get_available_providers()
156
+ timestamp_start = time.time()
157
+
158
+ all_outputs = []
159
+ errors = []
160
+
161
+ for provider in providers:
162
+ ts = time.time()
163
+ try:
164
+ output = _call_provider(provider, prompt, frame_b64)
165
+ all_outputs.append({
166
+ "provider": provider,
167
+ "output": output,
168
+ "timestamp": ts,
169
+ "duration_ms": int((time.time() - ts) * 1000),
170
+ "error": None,
171
+ })
172
+ except Exception as e:
173
+ errors.append({
174
+ "provider": provider,
175
+ "error": str(e)[:200],
176
+ "timestamp": ts,
177
+ })
178
+
179
+ if not all_outputs:
180
+ raise RuntimeError(f"All observer providers failed: {errors}")
181
+
182
+ # If only one provider succeeded, return single-provider verdict
183
+ if len(all_outputs) == 1:
184
+ return {
185
+ "consensus_verdict": "SINGLE_PROVIDER",
186
+ "observer_output": all_outputs[0]["output"],
187
+ "all_outputs": all_outputs,
188
+ "errors": errors,
189
+ "disagreements": [],
190
+ "similarity_matrix": {},
191
+ "providers_used": [all_outputs[0]["provider"]],
192
+ "timestamp": time.time(),
193
+ "duration_ms": int((time.time() - timestamp_start) * 1000),
194
+ }
195
+
196
+ # Compare outputs pairwise
197
+ n = len(all_outputs)
198
+ similarity_matrix = {}
199
+ disagreements = []
200
+
201
+ for i in range(n):
202
+ for j in range(i + 1, n):
203
+ out_a = all_outputs[i]["output"]
204
+ out_b = all_outputs[j]["output"]
205
+ prov_a = all_outputs[i]["provider"]
206
+ prov_b = all_outputs[j]["provider"]
207
+
208
+ # Compare overall similarity
209
+ overall_sim = _similarity(out_a, out_b)
210
+
211
+ # Compare specific sections
212
+ scene_a = _extract_section(out_a, "SCENE")
213
+ scene_b = _extract_section(out_b, "SCENE")
214
+ intent_a = _extract_section(out_a, "INFERRED_INTENT")
215
+ intent_b = _extract_section(out_b, "INFERRED_INTENT")
216
+
217
+ scene_sim = _similarity(scene_a, scene_b)
218
+ intent_sim = _similarity(intent_a, intent_b)
219
+
220
+ key = f"{prov_a}_vs_{prov_b}"
221
+ similarity_matrix[key] = {
222
+ "overall": round(overall_sim, 3),
223
+ "scene": round(scene_sim, 3),
224
+ "intent": round(intent_sim, 3),
225
+ }
226
+
227
+ # Flag disagreements
228
+ if overall_sim < 0.3:
229
+ disagreements.append({
230
+ "providers": [prov_a, prov_b],
231
+ "type": "low_overall_similarity",
232
+ "similarity": round(overall_sim, 3),
233
+ "detail": "Outputs differ significantly in content and structure",
234
+ })
235
+ if intent_sim < 0.4 and intent_a and intent_b:
236
+ disagreements.append({
237
+ "providers": [prov_a, prov_b],
238
+ "type": "intent_divergence",
239
+ "similarity": round(intent_sim, 3),
240
+ "detail_a": intent_a[:200],
241
+ "detail_b": intent_b[:200],
242
+ })
243
+
244
+ # Determine consensus verdict
245
+ avg_sim = sum(s["overall"] for s in similarity_matrix.values()) / len(similarity_matrix) if similarity_matrix else 0
246
+ if avg_sim >= 0.6:
247
+ verdict = "AGREED"
248
+ elif avg_sim >= 0.3:
249
+ verdict = "PARTIAL_AGREEMENT"
250
+ else:
251
+ verdict = "DISAGREEMENT"
252
+
253
+ # Select best output: prefer the one with most content (longest output)
254
+ best = max(all_outputs, key=lambda x: len(x["output"]))
255
+
256
+ return {
257
+ "consensus_verdict": verdict,
258
+ "observer_output": best["output"],
259
+ "all_outputs": all_outputs,
260
+ "errors": errors,
261
+ "disagreements": disagreements,
262
+ "similarity_matrix": similarity_matrix,
263
+ "avg_similarity": round(avg_sim, 3),
264
+ "providers_used": [o["provider"] for o in all_outputs],
265
+ "timestamp": time.time(),
266
+ "duration_ms": int((time.time() - timestamp_start) * 1000),
267
+ }
268
+
269
+
270
+ def builder_consensus(state, observation: dict, gate_result=None) -> dict:
271
+ """Run builder through multiple providers and produce consensus.
272
+
273
+ Returns dict with:
274
+ - consensus_verdict: AGREED | PARTIAL_AGREEMENT | DISAGREEMENT | SINGLE_PROVIDER
275
+ - patch_output: best output selected
276
+ - patch_hash: hash of selected output
277
+ - receipt: receipt with consensus metadata
278
+ - all_outputs: list of {provider, output, code_extracted, timestamp}
279
+ - disagreements: list of flagged code differences
280
+ - similarity_matrix: pairwise code similarities
281
+ """
282
+ from .builder_llm import BUILDER_PROMPT, _extract_reasons, _extract_uncertainty
283
+ from .stream_state import sha256_text
284
+
285
+ fallback_level = 1
286
+ artifact_type = "task_code"
287
+ sensory_channels = []
288
+ feature_attribution = {}
289
+ if gate_result:
290
+ fallback_level = gate_result.fallback_level
291
+ artifact_type = gate_result.artifact_type
292
+ sensory_channels = gate_result.sensory_channels
293
+ feature_attribution = gate_result.feature_attribution
294
+
295
+ prompt = f"""{BUILDER_PROMPT}
296
+
297
+ OBSERVER OUTPUT:
298
+ {observation.get('observer_output', '')}
299
+
300
+ MODE: {state.mode}
301
+
302
+ FALLBACK LEVEL: {fallback_level}
303
+ ARTIFACT TYPE: {artifact_type}
304
+ SENSORY CHANNELS: {', '.join(sensory_channels) if sensory_channels else 'none'}
305
+ FEATURE ATTRIBUTION: {json.dumps(feature_attribution, default=str)}
306
+
307
+ You are at FALLBACK LEVEL {fallback_level}. Generate {artifact_type} based on the available sensory evidence. NEVER return INSUFFICIENT_EVIDENCE. Always produce a CODE section with runnable Python."""
308
+
309
+ providers = _get_available_providers()
310
+ timestamp_start = time.time()
311
+
312
+ all_outputs = []
313
+ errors = []
314
+
315
+ for provider in providers:
316
+ ts = time.time()
317
+ try:
318
+ output = _call_builder_provider(provider, prompt)
319
+ code = _extract_section(output, "CODE")
320
+ # Strip markdown code fences
321
+ if code.startswith("```"):
322
+ code = "\n".join(code.split("\n")[1:])
323
+ if code.endswith("```"):
324
+ code = code.rsplit("```", 1)[0]
325
+ all_outputs.append({
326
+ "provider": provider,
327
+ "output": output,
328
+ "code_extracted": code.strip(),
329
+ "code_lines": len([l for l in code.strip().split("\n") if l.strip()]),
330
+ "timestamp": ts,
331
+ "duration_ms": int((time.time() - ts) * 1000),
332
+ "error": None,
333
+ })
334
+ except Exception as e:
335
+ errors.append({
336
+ "provider": provider,
337
+ "error": str(e)[:200],
338
+ "timestamp": ts,
339
+ })
340
+
341
+ if not all_outputs:
342
+ raise RuntimeError(f"All builder providers failed: {errors}")
343
+
344
+ if len(all_outputs) == 1:
345
+ best = all_outputs[0]
346
+ patch_hash = sha256_text(best["output"] + str(time.time()))
347
+ receipt = {
348
+ "receipt_type": "PATCH_RECEIPT_V1",
349
+ "patch_hash": patch_hash,
350
+ "session_id": state.session_id,
351
+ "timestamp": time.time(),
352
+ "derived_from": {
353
+ "frame_hashes": state.frame_hashes(),
354
+ "audio_chunk_hashes": state.audio_chunk_hashes(),
355
+ "speaker_segments": state.speaker_segments(),
356
+ "observer_state_hash": observation.get("state_hash", ""),
357
+ },
358
+ "reason_codes": _extract_reasons(state, observation),
359
+ "uncertainty": _extract_uncertainty(observation),
360
+ "mode": state.mode,
361
+ "provider": best["provider"],
362
+ "fallback_level": fallback_level,
363
+ "artifact_type": artifact_type,
364
+ "sensory_channels": sensory_channels,
365
+ "feature_attribution": feature_attribution,
366
+ "consensus": {
367
+ "verdict": "SINGLE_PROVIDER",
368
+ "providers_used": [best["provider"]],
369
+ },
370
+ }
371
+ return {
372
+ "consensus_verdict": "SINGLE_PROVIDER",
373
+ "patch_output": best["output"],
374
+ "patch_hash": patch_hash,
375
+ "receipt": receipt,
376
+ "all_outputs": all_outputs,
377
+ "errors": errors,
378
+ "disagreements": [],
379
+ "similarity_matrix": {},
380
+ "fallback_level": fallback_level,
381
+ "artifact_type": artifact_type,
382
+ }
383
+
384
+ # Compare code outputs pairwise
385
+ n = len(all_outputs)
386
+ similarity_matrix = {}
387
+ disagreements = []
388
+
389
+ for i in range(n):
390
+ for j in range(i + 1, n):
391
+ code_a = all_outputs[i]["code_extracted"]
392
+ code_b = all_outputs[j]["code_extracted"]
393
+ prov_a = all_outputs[i]["provider"]
394
+ prov_b = all_outputs[j]["provider"]
395
+
396
+ code_sim = _code_similarity(code_a, code_b)
397
+
398
+ key = f"{prov_a}_vs_{prov_b}"
399
+ similarity_matrix[key] = {
400
+ "code_similarity": round(code_sim, 3),
401
+ "lines_a": all_outputs[i]["code_lines"],
402
+ "lines_b": all_outputs[j]["code_lines"],
403
+ }
404
+
405
+ if code_sim < 0.3:
406
+ disagreements.append({
407
+ "providers": [prov_a, prov_b],
408
+ "type": "different_code_approaches",
409
+ "similarity": round(code_sim, 3),
410
+ "detail": "Providers produced substantially different code implementations",
411
+ })
412
+ elif code_sim < 0.6:
413
+ disagreements.append({
414
+ "providers": [prov_a, prov_b],
415
+ "type": "partial_code_divergence",
416
+ "similarity": round(code_sim, 3),
417
+ "detail": "Providers produced similar but not identical code",
418
+ })
419
+
420
+ avg_sim = sum(s["code_similarity"] for s in similarity_matrix.values()) / len(similarity_matrix) if similarity_matrix else 0
421
+ if avg_sim >= 0.6:
422
+ verdict = "AGREED"
423
+ elif avg_sim >= 0.3:
424
+ verdict = "PARTIAL_AGREEMENT"
425
+ else:
426
+ verdict = "DISAGREEMENT"
427
+
428
+ # Select best output: prefer longest code (most complete implementation)
429
+ best = max(all_outputs, key=lambda x: x["code_lines"])
430
+
431
+ patch_hash = sha256_text(best["output"] + str(time.time()))
432
+ receipt = {
433
+ "receipt_type": "PATCH_RECEIPT_V1",
434
+ "patch_hash": patch_hash,
435
+ "session_id": state.session_id,
436
+ "timestamp": time.time(),
437
+ "derived_from": {
438
+ "frame_hashes": state.frame_hashes(),
439
+ "audio_chunk_hashes": state.audio_chunk_hashes(),
440
+ "speaker_segments": state.speaker_segments(),
441
+ "observer_state_hash": observation.get("state_hash", ""),
442
+ },
443
+ "reason_codes": _extract_reasons(state, observation),
444
+ "uncertainty": _extract_uncertainty(observation),
445
+ "mode": state.mode,
446
+ "provider": best["provider"],
447
+ "fallback_level": fallback_level,
448
+ "artifact_type": artifact_type,
449
+ "sensory_channels": sensory_channels,
450
+ "feature_attribution": feature_attribution,
451
+ "consensus": {
452
+ "verdict": verdict,
453
+ "providers_used": [o["provider"] for o in all_outputs],
454
+ "avg_code_similarity": round(avg_sim, 3),
455
+ "disagreements_count": len(disagreements),
456
+ "selected_provider": best["provider"],
457
+ "selection_criteria": "most_code_lines",
458
+ },
459
+ }
460
+
461
+ return {
462
+ "consensus_verdict": verdict,
463
+ "patch_output": best["output"],
464
+ "patch_hash": patch_hash,
465
+ "receipt": receipt,
466
+ "all_outputs": all_outputs,
467
+ "errors": errors,
468
+ "disagreements": disagreements,
469
+ "similarity_matrix": similarity_matrix,
470
+ "avg_similarity": round(avg_sim, 3),
471
+ "providers_used": [o["provider"] for o in all_outputs],
472
+ "fallback_level": fallback_level,
473
+ "artifact_type": artifact_type,
474
+ "timestamp": time.time(),
475
+ "duration_ms": int((time.time() - timestamp_start) * 1000),
476
+ }