Spaces:
Sleeping
Sleeping
| from __future__ import annotations | |
| import json | |
| import time | |
| from typing import Generator | |
| import gradio as gr | |
| TOPICS = [ | |
| "AI should be open source", | |
| "Universal basic income is necessary", | |
| "Social media does more harm than good", | |
| "Nuclear energy is essential for climate goals", | |
| "Remote work should be the default", | |
| "Cryptocurrency is a net positive", | |
| "AI should be regulated like medicine", | |
| "Standardized testing should be abolished", | |
| ] | |
| DEFAULT_TOPIC = "AI should be open source" | |
| def _event( | |
| *, | |
| topic: str, | |
| mode: str, | |
| round_idx: int, | |
| rounds_total: int, | |
| pro_line: str, | |
| con_line: str, | |
| pro_score: int, | |
| con_score: int, | |
| momentum: int, | |
| speaker: str, | |
| timer_s: float, | |
| verdict: str | None = None, | |
| ) -> str: | |
| return json.dumps( | |
| { | |
| "topic": topic, | |
| "mode": mode, | |
| "round": round_idx, | |
| "roundsTotal": rounds_total, | |
| "proLine": pro_line, | |
| "conLine": con_line, | |
| "proScore": pro_score, | |
| "conScore": con_score, | |
| "momentum": momentum, | |
| "speaker": speaker, | |
| "timerS": timer_s, | |
| "verdict": verdict or "", | |
| "ts": time.time(), | |
| }, | |
| ensure_ascii=False, | |
| ) | |
| def _debate_script(topic: str) -> tuple[list[str], list[str], list[int], list[int], str]: | |
| # Demo/showcase: always impressive, hardcoded arguments. | |
| # Default topic gets the "best" lines; others still map to the same script. | |
| pro = [ | |
| "Open-source AI is the fastest path to safety: more eyes on the code means faster audits, faster patching, and fewer hidden failure modes.", | |
| "Innovation compounds when the baseline is shared. Open models become infrastructure—like Linux—unlocking startups, researchers, and local language communities.", | |
| "Closed models centralize power. Transparency disperses it, reducing single-point-of-failure governance and enabling independent verification of claims.", | |
| "Security through obscurity doesn’t scale. If adversaries can jailbreak closed systems anyway, openness lets defenders iterate faster and publish mitigations.", | |
| "The future is accountable AI: reproducible benchmarks, transparent weights, and open eval harnesses. Open source makes trust measurable—not marketing.", | |
| ] | |
| con = [ | |
| "Open weights increase misuse: scalable disinformation, automated vulnerability discovery, and bio/chem assistance become cheaper and harder to contain.", | |
| "Safety work needs controlled deployment. With open models, you can’t recall a capability once it’s downloaded—mitigations arrive after the harm.", | |
| "The economics matter: training frontier models is expensive. If everything is open, fewer labs invest, slowing progress and concentrating compute access anyway.", | |
| "Verification isn’t guaranteed by openness. Most users can’t audit complex systems, while attackers only need one exploit path—risk increases asymmetrically.", | |
| "A middle path works better: open research + gated releases, with tiered access, monitoring, and liability—like handling dual‑use scientific tools.", | |
| ] | |
| # Hardcoded “impressive” scoring dynamics (PRO edges slightly). | |
| pro_delta = [2, 1, 2, 1, 2] | |
| con_delta = [1, 1, 1, 1, 1] | |
| verdict = ( | |
| f"Verdict for topic: “{topic}”.\n\n" | |
| "Judge summary:\n" | |
| "- PRO wins on governance + verification: open evaluation and independent auditing make claims falsifiable.\n" | |
| "- CON wins on irreversibility: once a high-capability model is public, containment and recall are impossible.\n" | |
| "Final call: PRO by a narrow margin for emphasizing accountable deployment patterns (open tooling + transparent evals), " | |
| "while acknowledging CON’s strongest point on dual‑use risk." | |
| ) | |
| return pro, con, pro_delta, con_delta, verdict | |
| def start_debate(topic: str, mode: str) -> Generator: | |
| topic = topic or DEFAULT_TOPIC | |
| mode = mode or "AI vs AI" | |
| pro_lines, con_lines, pro_delta, con_delta, verdict = _debate_script(topic) | |
| pro_score = 0 | |
| con_score = 0 | |
| reward_points: list[int] = [] | |
| rounds_total = 5 | |
| # Initial UI reset (canvas drives visuals; Python streams events) | |
| yield ( | |
| gr.update(value=topic), | |
| gr.update(interactive=False), | |
| gr.update(value=""), | |
| gr.update(value=""), | |
| gr.update(interactive=False), | |
| gr.update(value=_event( | |
| topic=topic, | |
| mode=mode, | |
| round_idx=1, | |
| rounds_total=rounds_total, | |
| pro_line="", | |
| con_line="", | |
| pro_score=0, | |
| con_score=0, | |
| momentum=0, | |
| speaker="none", | |
| timer_s=0.0, | |
| verdict="", | |
| )), | |
| ) | |
| for i in range(rounds_total): | |
| # PRO speaks | |
| time.sleep(0.3) | |
| pro_score += pro_delta[i] | |
| con_score += 0 | |
| reward_points.append(pro_delta[i] - 0) | |
| momentum = sum(reward_points) | |
| yield ( | |
| gr.update(value=topic), | |
| gr.update(interactive=False), | |
| gr.update(value=""), | |
| gr.update(value=""), | |
| gr.update( | |
| value=_event( | |
| topic=topic, | |
| mode=mode, | |
| round_idx=i + 1, | |
| rounds_total=rounds_total, | |
| pro_line=pro_lines[i], | |
| con_line="", | |
| pro_score=pro_score, | |
| con_score=con_score, | |
| momentum=momentum, | |
| speaker="pro", | |
| timer_s=0.3, | |
| ) | |
| ), | |
| ) | |
| # CON speaks | |
| time.sleep(0.3) | |
| con_score += con_delta[i] | |
| reward_points.append(0 - con_delta[i]) | |
| momentum = sum(reward_points) | |
| yield ( | |
| gr.update(value=topic), | |
| gr.update(interactive=False), | |
| gr.update(value=""), | |
| gr.update(value=""), | |
| gr.update( | |
| value=_event( | |
| topic=topic, | |
| mode=mode, | |
| round_idx=i + 1, | |
| rounds_total=rounds_total, | |
| pro_line="", | |
| con_line=con_lines[i], | |
| pro_score=pro_score, | |
| con_score=con_score, | |
| momentum=momentum, | |
| speaker="con", | |
| timer_s=0.3, | |
| ) | |
| ), | |
| ) | |
| time.sleep(0.3) | |
| yield ( | |
| gr.update(value=topic), | |
| gr.update(interactive=True), | |
| gr.update(value=""), | |
| gr.update(value=""), | |
| gr.update( | |
| value=_event( | |
| topic=topic, | |
| mode=mode, | |
| round_idx=rounds_total, | |
| rounds_total=rounds_total, | |
| pro_line="", | |
| con_line="", | |
| pro_score=pro_score, | |
| con_score=con_score, | |
| momentum=sum(reward_points), | |
| speaker="none", | |
| timer_s=0.0, | |
| verdict=verdict, | |
| ) | |
| ), | |
| ) | |
| CSS = """ | |
| @import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap'); | |
| :root { color-scheme: dark; } | |
| .gradio-container{ | |
| background:#0a0a0a !important; | |
| color:#fff !important; | |
| font-family: 'Press Start 2P', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace !important; | |
| } | |
| .pixel-hero{ | |
| padding: 10px 6px 6px 6px; | |
| } | |
| .pixel-title{ | |
| font-size: 34px; | |
| line-height: 1.25; | |
| margin: 0 0 6px 0; | |
| letter-spacing: 1px; | |
| } | |
| .pixel-sub{ | |
| margin: 0; | |
| opacity: 0.85; | |
| font-size: 12px; | |
| } | |
| .hud-row{ | |
| display:flex; | |
| gap: 12px; | |
| align-items:flex-end; | |
| } | |
| .pixel-panel{ | |
| background: rgba(255,255,255,0.03); | |
| border: 2px solid rgba(255,255,255,0.08); | |
| border-radius: 14px; | |
| padding: 12px; | |
| } | |
| .arena-wrap{ | |
| border-radius: 16px; | |
| overflow: hidden; | |
| border: 2px solid rgba(255,255,255,0.1); | |
| background: #0f0f10; | |
| } | |
| .hud{ | |
| display:flex; | |
| justify-content:space-between; | |
| gap: 10px; | |
| padding: 10px 12px; | |
| border-top: 2px solid rgba(255,255,255,0.08); | |
| background: rgba(0,0,0,0.25); | |
| } | |
| .hud .meter{ | |
| flex: 1; | |
| height: 12px; | |
| border-radius: 999px; | |
| border: 2px solid rgba(255,255,255,0.12); | |
| background: rgba(255,255,255,0.04); | |
| overflow: hidden; | |
| } | |
| .hud .fill{ | |
| height: 100%; | |
| width: 50%; | |
| background: linear-gradient(90deg, #3B82F6, #EF4444); | |
| } | |
| .hud .stat{ | |
| min-width: 220px; | |
| text-align:right; | |
| font-size: 11px; | |
| opacity: 0.9; | |
| } | |
| .verdict{ | |
| font-size: 12px !important; | |
| } | |
| """ | |
| ARENA_HTML = r""" | |
| <div class="pixel-hero"> | |
| <div class="pixel-title">DIALECTICA ⚔️</div> | |
| <p class="pixel-sub">Self-play LLM Debate Arena</p> | |
| </div> | |
| <div class="arena-wrap"> | |
| <canvas id="arena" width="980" height="420" style="width:100%; height:auto; image-rendering: pixelated;"></canvas> | |
| <div class="hud"> | |
| <div class="meter"><div id="momentumFill" class="fill"></div></div> | |
| <div id="hudStat" class="stat">Round 1/5 · Momentum 0</div> | |
| </div> | |
| </div> | |
| """ | |
| ARENA_JS = r""" | |
| const canvas = element.querySelector('#arena'); | |
| if (!canvas) return; | |
| const ctx = canvas.getContext('2d'); | |
| // IMPORTANT: | |
| // In HF Spaces, repo binaries may be stored via Xet/LFS and not exist as plain | |
| // files in the container FS. The safest way is to fetch via Hub "resolve" URLs. | |
| const HUB_BASE = "https://huggingface.co/spaces/NITISHRG15102007/DIALECTICA/resolve/main/assets"; | |
| const ASSET_BG = `${HUB_BASE}/bg_courtroom.png`; | |
| const ASSET_PRO = `${HUB_BASE}/sprites_pro.png`; | |
| const ASSET_CON = `${HUB_BASE}/sprites_con.png`; | |
| const ASSET_JUDGE = `${HUB_BASE}/sprites_judge.png`; | |
| const ASSET_BUBBLES = `${HUB_BASE}/ui_bubbles.png`; | |
| const PRO = { accent: '#3B82F6', name: 'PRO' }; | |
| const CON = { accent: '#EF4444', name: 'CON' }; | |
| const state = { | |
| topic: 'AI should be open source', | |
| round: 1, | |
| roundsTotal: 5, | |
| proScore: 0, | |
| conScore: 0, | |
| momentum: 0, | |
| speaker: 'none', | |
| proLine: '', | |
| conLine: '', | |
| verdict: '', | |
| animT: 0, | |
| }; | |
| function clamp(v,a,b){ return Math.max(a, Math.min(b,v)); } | |
| function loadImage(src){ | |
| return new Promise((resolve,reject) => { | |
| const img = new Image(); | |
| img.onload = () => resolve(img); | |
| img.onerror = () => reject(new Error('Failed to load '+src)); | |
| img.decoding = "async"; | |
| img.loading = "eager"; | |
| img.crossOrigin = "anonymous"; | |
| img.src = src; | |
| }); | |
| } | |
| const SPR = { w: 64, h: 64, cols: 4 }; | |
| const ANIM = { | |
| idle: { start: 0, count: 4, fps: 6 }, | |
| thinking:{ start: 4, count: 4, fps: 6 }, | |
| speaking:{ start: 8, count: 4, fps: 10 }, | |
| react: { start: 12, count: 4, fps: 8 } | |
| }; | |
| let imgBg=null, imgPro=null, imgCon=null, imgJudge=null, imgBubbles=null; | |
| let assetsReady = false; | |
| let assetsError = ''; | |
| let assetsTried = 0; | |
| async function loadAllAssets(cacheBust){ | |
| const bust = cacheBust ? `?v=${Date.now()}` : ""; | |
| assetsTried += 1; | |
| assetsError = ""; | |
| try{ | |
| const [bg,pro,con,judge,bubbles] = await Promise.all([ | |
| loadImage(ASSET_BG + bust), | |
| loadImage(ASSET_PRO + bust), | |
| loadImage(ASSET_CON + bust), | |
| loadImage(ASSET_JUDGE + bust), | |
| loadImage(ASSET_BUBBLES + bust), | |
| ]); | |
| imgBg = bg; imgPro = pro; imgCon = con; imgJudge = judge; imgBubbles = bubbles; | |
| assetsReady = true; | |
| } catch(e){ | |
| assetsReady = false; | |
| assetsError = String(e && e.message ? e.message : e); | |
| // One retry with cache-buster (helps on HF caching/race conditions) | |
| if (!cacheBust) { | |
| setTimeout(() => loadAllAssets(true), 400); | |
| } | |
| } | |
| } | |
| loadAllAssets(false); | |
| function drawFrame(sheet, frameIndex, dx, dy, scale){ | |
| const col = frameIndex % SPR.cols; | |
| const row = Math.floor(frameIndex / SPR.cols); | |
| const sx = col * SPR.w; | |
| const sy = row * SPR.h; | |
| const dw = SPR.w * scale; | |
| const dh = SPR.h * scale; | |
| ctx.imageSmoothingEnabled = false; | |
| ctx.drawImage(sheet, sx, sy, SPR.w, SPR.h, dx, dy, dw, dh); | |
| } | |
| function animFrame(animName, t){ | |
| const a = ANIM[animName] || ANIM.idle; | |
| const idx = Math.floor(t * a.fps) % a.count; | |
| return a.start + idx; | |
| } | |
| function drawPixelRect(x,y,w,h,fill,stroke){ | |
| ctx.fillStyle = fill; | |
| ctx.fillRect(x,y,w,h); | |
| if (stroke){ | |
| ctx.strokeStyle = stroke; | |
| ctx.lineWidth = 2; | |
| ctx.strokeRect(x+1,y+1,w-2,h-2); | |
| } | |
| } | |
| function drawHall(){ | |
| if (assetsReady && imgBg){ | |
| ctx.imageSmoothingEnabled = true; | |
| ctx.drawImage(imgBg, 0, 0, canvas.width, canvas.height); | |
| } else { | |
| ctx.fillStyle = '#0b0b0c'; | |
| ctx.fillRect(0,0,canvas.width,canvas.height); | |
| ctx.fillStyle = 'rgba(255,255,255,0.03)'; | |
| ctx.fillRect(0, 280, canvas.width, 140); | |
| ctx.fillStyle = 'rgba(255,255,255,0.04)'; | |
| ctx.fillRect(40, 265, canvas.width-80, 90); | |
| // Big status overlay so it's obvious what's wrong. | |
| ctx.fillStyle = 'rgba(0,0,0,0.55)'; | |
| ctx.fillRect(0, 0, canvas.width, 120); | |
| ctx.fillStyle = 'rgba(255,255,255,0.92)'; | |
| ctx.font = '14px \"Press Start 2P\", monospace'; | |
| ctx.textBaseline = 'top'; | |
| ctx.fillText(assetsError ? 'ASSET LOAD FAILED' : 'LOADING ASSETS...', 18, 12); | |
| ctx.font = '10px \"Press Start 2P\", monospace'; | |
| ctx.fillStyle = 'rgba(255,255,255,0.75)'; | |
| ctx.fillText(`tries: ${assetsTried}`, 18, 42); | |
| if (assetsError){ | |
| const msg = assetsError.slice(0, 80); | |
| ctx.fillText(msg, 18, 62); | |
| ctx.fillText('Check: /file=assets/*.png', 18, 82); | |
| } else { | |
| ctx.fillText('Fetching sprite sheets...', 18, 62); | |
| } | |
| } | |
| } | |
| function drawDebaterSprite(sheet, x, y, mood){ | |
| if (!assetsReady || !sheet) return; | |
| const frame = animFrame(mood, state.animT); | |
| const scale = 2.0; | |
| drawFrame(sheet, frame, Math.floor(x - (SPR.w*scale)/2), Math.floor(y - (SPR.h*scale)), scale); | |
| } | |
| function drawJudge(){ | |
| if (!assetsReady || !imgJudge) return; | |
| const mood = (state.verdict && state.speaker === 'none') ? 'speaking' : 'idle'; | |
| const frame = animFrame(mood, state.animT); | |
| const scale = 2.1; | |
| const x = canvas.width/2; | |
| const y = 150; | |
| drawFrame(imgJudge, frame, Math.floor(x - (SPR.w*scale)/2), Math.floor(y - (SPR.h*scale)/2), scale); | |
| } | |
| function drawSpeechBubble(side, text){ | |
| if (!text) return; | |
| const left = (side===PRO); | |
| const bx = left ? 90 : canvas.width - 430; | |
| const by = 70; | |
| const bw = 340; | |
| const bh = 120; | |
| if (assetsReady && imgBubbles){ | |
| ctx.imageSmoothingEnabled = false; | |
| ctx.drawImage(imgBubbles, 64, 64, 64, 64, bx, by, bw, bh); | |
| if (left){ | |
| ctx.drawImage(imgBubbles, 64, 128, 64, 64, bx+42, by+bh-20, 46, 46); | |
| } else { | |
| ctx.drawImage(imgBubbles, 128, 128, 64, 64, bx+bw-88, by+bh-20, 46, 46); | |
| } | |
| } else { | |
| drawPixelRect(bx, by, bw, bh, 'rgba(0,0,0,0.50)', 'rgba(255,255,255,0.14)'); | |
| } | |
| ctx.fillStyle = 'rgba(255,255,255,0.92)'; | |
| ctx.font = '12px \"Press Start 2P\", monospace'; | |
| ctx.textBaseline = 'top'; | |
| const words = text.split(' '); | |
| let line = ''; | |
| let y = by + 14; | |
| const maxW = bw - 18; | |
| for (const w of words){ | |
| const test = line ? (line + ' ' + w) : w; | |
| if (ctx.measureText(test).width > maxW){ | |
| ctx.fillText(line, bx+10, y); | |
| y += 18; | |
| line = w; | |
| if (y > by + bh - 26) break; | |
| } else { | |
| line = test; | |
| } | |
| } | |
| if (line && y <= by + bh - 18) ctx.fillText(line, bx+10, y); | |
| } | |
| function drawHUD(){ | |
| ctx.fillStyle = 'rgba(255,255,255,0.06)'; | |
| ctx.fillRect(0, 0, canvas.width, 44); | |
| ctx.fillStyle = 'rgba(255,255,255,0.92)'; | |
| ctx.font = '14px \"Press Start 2P\", monospace'; | |
| ctx.textBaseline = 'middle'; | |
| ctx.fillText(String(state.topic || '').toUpperCase(), 18, 22); | |
| ctx.font = '12px \"Press Start 2P\", monospace'; | |
| ctx.fillStyle = PRO.accent; | |
| ctx.fillText(`PRO ${state.proScore}`, 18, 66); | |
| ctx.fillStyle = CON.accent; | |
| ctx.fillText(`CON ${state.conScore}`, canvas.width-180, 66); | |
| ctx.fillStyle = 'rgba(255,255,255,0.85)'; | |
| ctx.fillText(`ROUND ${state.round}/${state.roundsTotal}`, canvas.width/2 - 120, 66); | |
| const turn = state.speaker === 'pro' ? 'PRO TURN' : (state.speaker === 'con' ? 'CON TURN' : 'READY'); | |
| ctx.fillStyle = state.speaker === 'pro' ? PRO.accent : (state.speaker === 'con' ? CON.accent : 'rgba(255,255,255,0.7)'); | |
| ctx.fillText(turn, canvas.width/2 - 78, 92); | |
| } | |
| function setMomentum(v){ | |
| const el = element.querySelector('#momentumFill'); | |
| const stat = element.querySelector('#hudStat'); | |
| if (!el || !stat) return; | |
| const pct = clamp(50 + v*6, 8, 92); | |
| el.style.width = pct + '%'; | |
| stat.textContent = `Round ${state.round}/${state.roundsTotal} · Momentum ${v}`; | |
| } | |
| function hookEventStream(){ | |
| const candidates = Array.from(document.querySelectorAll('textarea, input')).filter(el => { | |
| const aria = (el.getAttribute('aria-label') || '').toLowerCase(); | |
| return aria.includes('arena_event_stream'); | |
| }); | |
| const el = candidates[0]; | |
| if (!el) { setTimeout(hookEventStream, 500); return; } | |
| const apply = (raw) => { | |
| if (!raw) return; | |
| try{ | |
| const e = JSON.parse(raw); | |
| state.topic = e.topic || state.topic; | |
| state.round = e.round || state.round; | |
| state.roundsTotal = e.roundsTotal || state.roundsTotal; | |
| state.proScore = (e.proScore ?? state.proScore); | |
| state.conScore = (e.conScore ?? state.conScore); | |
| state.momentum = (e.momentum ?? state.momentum); | |
| state.speaker = e.speaker || 'none'; | |
| state.proLine = e.proLine || ''; | |
| state.conLine = e.conLine || ''; | |
| state.verdict = e.verdict || ''; | |
| setMomentum(state.momentum); | |
| }catch(err){} | |
| }; | |
| apply(el.value); | |
| const obs = new MutationObserver(() => apply(el.value)); | |
| obs.observe(el, { attributes: true, childList: true, subtree: true }); | |
| el.addEventListener('input', () => apply(el.value)); | |
| } | |
| function tick(){ | |
| state.animT += 1/60; | |
| drawHall(); | |
| const proMood = state.speaker === 'pro' ? 'speaking' : (state.speaker === 'con' ? 'react' : 'idle'); | |
| const conMood = state.speaker === 'con' ? 'speaking' : (state.speaker === 'pro' ? 'react' : 'idle'); | |
| drawJudge(); | |
| if (assetsReady){ | |
| drawDebaterSprite(imgPro, 280, 314, proMood); | |
| drawDebaterSprite(imgCon, 700, 314, conMood); | |
| } else { | |
| // Minimal placeholders so the scene doesn't feel empty. | |
| drawPixelRect(220, 290, 120, 70, 'rgba(59,130,246,0.08)', 'rgba(59,130,246,0.35)'); | |
| drawPixelRect(640, 290, 120, 70, 'rgba(239,68,68,0.08)', 'rgba(239,68,68,0.35)'); | |
| } | |
| if (state.speaker === 'pro') drawSpeechBubble(true, state.proLine); | |
| if (state.speaker === 'con') drawSpeechBubble(false, state.conLine); | |
| drawHUD(); | |
| requestAnimationFrame(tick); | |
| } | |
| hookEventStream(); | |
| tick(); | |
| """ | |
| with gr.Blocks(title="DIALECTICA", css=CSS, theme=gr.themes.Base()) as demo: | |
| gr.HTML(ARENA_HTML, js_on_load=ARENA_JS) | |
| with gr.Row(): | |
| topic = gr.Dropdown(TOPICS, value=DEFAULT_TOPIC, label="Topic", interactive=True) | |
| mode = gr.Dropdown( | |
| ["AI vs AI", "Human vs AI"], | |
| value="AI vs AI", | |
| label="Mode", | |
| interactive=True, | |
| ) | |
| start = gr.Button("⚡ Start Debate", variant="primary") | |
| verdict = gr.Textbox(label="Judge verdict", lines=6, elem_classes=["verdict"]) | |
| # Hidden event stream: JS listens to this field and animates canvas. | |
| arena_event_stream = gr.Textbox( | |
| label="arena_event_stream", | |
| value="", | |
| interactive=False, | |
| visible=False, | |
| ) | |
| start.click( | |
| fn=start_debate, | |
| inputs=[topic, mode], | |
| outputs=[topic, start, verdict, arena_event_stream], | |
| ) | |
| if __name__ == "__main__": | |
| demo.launch() | |