CoolFace
Apppublic

build-small-hackathon/hackathon-advisor

sourceHugging Facemitupdated 3mo agoView on Hugging Face
16likes
quest_corpus.json4615 linesDownload Raw Back to data
1{2  "generated_at": "2026-06-07T20:44:26+00:00",3  "source_snapshot": "data/projects.json",4  "snapshot_generated_at": "2026-06-07T11:51:09+00:00",5  "project_count": 125,6  "projects": [7    {8      "id": "build-small-hackathon/Advent_of_a_World_of_Flowering_Trees",9      "title": "Advent Of A World Of Flowering Trees",10      "summary": "This space is for Huggingface build small hackathon",11      "tags": [12        "gradio",13        "region:us"14      ],15      "models": [],16      "datasets": [],17      "sdk": "gradio",18      "license": "mit",19      "likes": 1,20      "url": "https://huggingface.co/spaces/build-small-hackathon/Advent_of_a_World_of_Flowering_Trees",21      "app_file": "app.py",22      "readme_raw": "---\ntitle: Advent Of A World Of Flowering Trees\nemoji: ☃️\ncolorFrom: indigo\ncolorTo: pink\nsdk: gradio\nsdk_version: 6.16.0\npython_version: '3.12.12'\napp_file: app.py\npinned: true\nlicense: mit\nshort_description: This space is for Huggingface build small hackathon\npreload_from_hub:\n    - CohereLabs/tiny-aya-global-GGUF tiny-aya-global-q4_k_m.gguf\n    - black-forest-labs/FLUX.2-klein-4b-nvfp4\n    - openbmb/MiniCPM-V-4.6-Thinking-gguf\n---\n\nCheck out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference\n\n\n\n## For development:\n\nfirst download uv and hf cli tool\n\n\n```bash\nuv venv --python 3.13 --seed\n```\n\nthen activate the virtual env .venv\n\n```bash\nsource .venv/Scripts/activate\n```\n\nthen download dependencies\n```python\npython -m pip install -r requirements.txt\n```\n\nthen play around and change code..",23      "readme_body": "Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference\n\n\n\n## For development:\n\nfirst download uv and hf cli tool\n\n\n```bash\nuv venv --python 3.13 --seed\n```\n\nthen activate the virtual env .venv\n\n```bash\nsource .venv/Scripts/activate\n```\n\nthen download dependencies\n```python\npython -m pip install -r requirements.txt\n```\n\nthen play around and change code..",24      "readme_frontmatter": {25        "title": "Advent Of A World Of Flowering Trees",26        "emoji": "☃️",27        "colorFrom": "indigo",28        "colorTo": "pink",29        "sdk": "gradio",30        "sdk_version": "6.16.0",31        "python_version": "3.12.12",32        "app_file": "app.py",33        "pinned": "true",34        "license": "mit",35        "short_description": "This space is for Huggingface build small hackathon",36        "preload_from_hub": ""37      },38      "app_source": "import gradio as gr\nimport spaces\nfrom huggingface_hub import hf_hub_download\nimport os\nimport ctypes\n\n\nMODEL_REPO_ID = \"CohereLabs/tiny-aya-global-GGUF\"\nMODEL_FILENAME = \"tiny-aya-global-q4_k_m.gguf\"\n\nmodel_path = hf_hub_download(\n    repo_id=MODEL_REPO_ID,\n    filename=MODEL_FILENAME,\n)\n\n_llm = None\n\n# try:\n#     import nvidia.cuda_runtime\n#     import nvidia.cublas\n#     cudart = os.path.join(os.path.dirname(nvidia.cuda_runtime.__file__), \"lib\", \"libcudart.so.12\")\n#     cublas = os.path.join(os.path.dirname(nvidia.cublas.__file__), \"lib\", \"libcublas.so.12\")\n#     ctypes.CDLL(cudart, mode=ctypes.RTLD_GLOBAL)\n#     ctypes.CDLL(cublas, mode=ctypes.RTLD_GLOBAL)\n# except Exception:\n#     pass\n\ndef get_llm():\n    global _llm\n    if _llm is None:\n        from llama_cpp import Llama\n\n        _llm = Llama(\n            model_path=model_path,\n            n_gpu_layers=-1,\n            n_ctx=1024,\n            flash_attn=True,\n            verbose=False,\n        )\n    return _llm\n\n\n@spaces.GPU(duration=120)\ndef run_inference(prompt: str) -> str:\n    prompt = prompt.strip()\n    if not prompt:\n        return \"Enter a prompt to generate a response.\"\n\n    try:\n        llm = get_llm()\n    except Exception as exc:\n        return f\"llama-cpp initialization failed: {exc}\"\n\n    response = llm.create_chat_completion(\n        messages=[{\"role\": \"user\", \"content\": prompt}],\n        max_tokens=512,\n        temperature=0.7,\n    )\n    return response[\"choices\"][0][\"message\"][\"content\"].strip()\n\n\nwith gr.Blocks(title=\"Advent Of A World Of Flowering Trees\") as demo:\n    gr.Markdown(\"# Advent Of A World Of Flowering Trees\")\n    gr.Markdown(\"Tiny Aya GGUF demo running with `llama-cpp-python`.\")\n\n    prompt = gr.Textbox(\n        label=\"Prompt\",\n        lines=6,\n        placeholder=\"Ask something...\",\n    )\n    output = gr.Textbox(label=\"Response\", lines=12)\n    submit = gr.Button(\"Generate\", variant=\"primary\")\n\n    submit.click(fn=run_inference, inputs=prompt, outputs=output)\n    prompt.submit(fn=run_inference, inputs=prompt, outputs=output)\n\n\nif __name__ == \"__main__\":\n    demo.launch()\n",39      "app_signals": "get_llm run_inference prompt CohereLabs/tiny-aya-global-GGUF tiny-aya-global-q4_k_m.gguf hf_hub_download repo_id filename spaces.GPU duration prompt.strip llm.create_chat_completion messages max_tokens temperature strip gr.Blocks title gr.Markdown gr.Textbox label lines placeholder gr.Button variant submit.click fn inputs outputs prompt.submit __main__ demo.launch Llama model_path n_gpu_layers n_ctx flash_attn verbose Enter a prompt to generate a response. # Advent Of A World Of Flowering Trees Tiny Aya GGUF demo running with `llama-cpp-python`. Generate Advent Of A World Of Flowering Trees Prompt Ask something... Response primary llama-cpp initialization failed: content role user message choices",40      "readme_len": 397,41      "app_source_len": 2092,42      "app_signals_len": 70543    },44    {45      "id": "build-small-hackathon/agent-swarm-workbench",46      "title": "Backyard Demo Builder",47      "summary": "Build tiny real-person demos before scaling custom software.",48      "tags": [49        "agents",50        "ai-agents",51        "backyard-ai",52        "build-small-hackathon",53        "demo-builder",54        "gradio",55        "real-estate",56        "small-language-model"57      ],58      "models": [59        "unsloth/gemma-4-12B-it-qat-GGUF",60        "Qwen/Qwen2.5-7B-Instruct",61        "nvidia/Nemotron-3.5-Content-Safety"62      ],63      "datasets": [],64      "sdk": "gradio",65      "license": "",66      "likes": 0,67      "url": "https://huggingface.co/spaces/build-small-hackathon/agent-swarm-workbench",68      "app_file": "app.py",69      "readme_raw": "---\ntitle: Backyard Demo Builder\nemoji: 🏡\ncolorFrom: gray\ncolorTo: green\nsdk: gradio\npython_version: \"3.12.12\"\napp_file: app.py\nshort_description: Build tiny real-person demos before scaling custom software.\nmodels:\n  - unsloth/gemma-4-12B-it-qat-GGUF\n  - Qwen/Qwen2.5-7B-Instruct\n  - nvidia/Nemotron-3.5-Content-Safety\ndatasets: []\ntags:\n  - build-small-hackathon\n  - backyard-ai\n  - gradio\n  - agents\n  - small-language-model\n  - demo-builder\n  - real-estate\n  - ai-agents\npinned: false\n---\n\n# Backyard Demo Builder\n\n## Chapter 1: Backyard AI\n\n*Build Small Hackathon 2026 — Chapter 1 Submission*\n\n`agent-swarm-workbench` now presents as **Backyard Demo Builder**: a Gradio app\nthat turns one real person's workflow into a small runnable demo package before\nanyone pays to build full software.\n\nFirst backyard case: my mom, a real-estate agent. She needs a cheap way to test\na customer follow-up reminder workflow before committing time and money to a\nfull app.\n\n---\n\n## Watch the Demo Builder Work\n\n```\nYou:     \"Build a real-estate follow-up CRM demo for my mom.\"\nBuilder: Generates a Gradio mini-app, handoff spec, field notes, and checks\nResult:  app.py, README.md, handoff_spec.md, field_notes.md\nMom:     Tests the workflow, then we scrap or scale.\n```\n\nEvery Run produces a **downloadable demo package** and Validation report: files\nyou can inspect, unzip, run, and test with the real person.\n\n---\n\n## Build Small Hackathon — Submission Notes\n\n| Requirement | How We Meet It |\n|---|---|\n| **Small model (≤ 32B)** | Provider catalog fetches models at runtime and only allows models whose ID/name proves ≤32B |\n| **Gradio app** | Custom dark-themed Gradio UI mounted on FastAPI |\n| **HF Space** | `app.py` + `requirements.txt` — one-command deploy |\n| **Demo video** | *(placeholder — [link to demo])* |\n| **Social post** | *(placeholder — [link to post])* |\n\n### Bonus Badges Claimed\n\n| Badge | Why |\n|---|---|\n| **🎨 Off-Brand** | Fully custom CSS dark theme — Archivo + IBM Plex Mono, acid green CTAs, paper/ink palette, CSS grid layout, status chips. Not a default Gradio component in sight. |\n| **📡 Sharing is Caring** | Agent traces and swarm reasoning are surfaced in the Events panel. We'll publish a trace on the Hub. |\n| **📓 Field Notes** | Generated demo packages include `field_notes.md`; this repo also documents the architecture and decisions. |\n\n---\n\n## Why This Belongs in Backyard AI\n\nThis solves a real problem for someone I know.\n\n- **Specific person** — my mom, a real-estate agent.\n- **Specific pain** — follow-up reminders and customer-care demos are useful, but custom app dev is slow and risky.\n- **Honest small-model fit** — a ≤32B model drafts the demo and handoff spec; rules handle the reminder logic.\n- **Actually testable** — the generated package includes field notes and feedback questions for the real user.\n\n---\n\n## How It Works Under the Hood\n\n```\n┌─────────────────────────────────────────────────────┐\n│  Gradio UI / HTTP API                               │\n├─────────────────────────────────────────────────────┤\n│  RunFlow — lifecycle conductor                      │\n│  ┌──────────┐  ┌────────────┐  ┌────────────────┐  │\n│  │ Swarm    │  │ Codebase   │  │ Validator      │  │\n│  │ Runtime  │→│ Archive    │→│ Graph          │  │\n│  │          │  │ Store      │  │                │  │\n│  │ Planner  │  │ (local/    │  │ Sandbox checks │  │\n│  │ Coder    │  │  Redis)    │  │ Rubric review  │  │\n│  │ Reviewer │  │            │  │ Stagehand      │  │\n│  │ Tester   │  │            │  │ (Browserbase)  │  │\n│  └──────────┘  └────────────┘  └────────────────┘  │\n│  EventBus → SSE stream to UI                       │\n└─────────────────────────────────────────────────────┘\n```\n\n### The Swarm\n\n- **Coordinator** reads the prompt, plans tasks, delegates to subagents\n- **Planner** breaks down the prompt into implementable units\n- **Coder** writes the actual code files\n- **Reviewer** checks code quality and correctness\n- **Test-runner** runs the user's tests and retries up to 3x on failure\n- **Validator-prep** generates validation checks from user criteria\n\n### The Validator\n\nAfter the swarm finishes, a LangGraph Validator workflow:\n1. Restores the codebase into a clean sandbox\n2. Runs user-provided tests\n3. Executes LLM-based rubric review\n4. (Optional) Runs Browserbase/Stagehand visual checks\n5. Produces a pass/fail Validation Report\n\n### The Sandbox\n\nAll agent work happens inside isolated sandbox workspaces:\n- **Local** (for dev/smoke tests)\n- **Docker** (container-based)\n- **Daytona** (cloud sandboxes)\n\n---\n\n## Run It\n\n```bash\ngit clone https://github.com/Kiy-K/agent-swarm-workbench.git\ncd agent-swarm-workbench\ncp .env.example .env\n# Optional: add server fallback keys. Users can also paste their own key in the UI.\nuv run uvicorn app:app --host 0.0.0.0 --port 8790\n```\n\nOpen http://localhost:8790, type a prompt, choose a provider, fetch models with your API key, then click Start Run.\n\nModel selection:\n- Model lists are fetched from the selected provider/API endpoint at runtime.\n- UI only offers fetched models whose ID/name proves `<=32B` parameters.\n- Unknown-size models are shown in the catalog response as `unknown_parameters` but are not selectable.\n- User API keys and fetched catalogs live only in process memory. They are not persisted, not stored in Redis/DB, and not kept in Gradio state. Click \"Refresh models\" to clear and refetch that provider cache.\n\nFor Hugging Face Spaces:\n```bash\nuv run python app.py\n```\n\n## Test\n\n```bash\npython scripts/task.py verify    # required completion gate: tests + harness\npython scripts/task.py test      # 90 tests, all passing\npython scripts/task.py harness -- --prompt \"Build a tiny CLI\" --test \"test -f README.md\"\npython scripts/task.py smoke      # Local agent session smoke check\npython scripts/task.py validator-smoke  # Validator end-to-end\n```\n\n### Agent Harness\n\nThe harness is the fast way to exercise the Run lifecycle without waiting on a\nfull demo session:\n\n```bash\npython scripts/task.py verify\npython scripts/task.py harness -- --prompt \"Build a tiny CLI\" --output-dir /tmp/harness\npython scripts/task.py harness -- --mode live --prompt \"Build a tiny CLI\"\n```\n\n`verify` is the required completion gate for coding agents. It runs the Python\nsuite, then runs the default scripted Agent Swarm Harness so changes are checked\nagainst the same Run -> SwarmRuntime -> Archive -> Validator path that the app\nuses.\n\nModes:\n\n| Mode | Purpose |\n|---|---|\n| `swarm` | Default. Runs `RunFlow -> SwarmRuntime -> Archive -> Validator` with a scripted local DeepAgent-compatible session. |\n| `live` | Uses the real `create_session()` DeepAgents path and the configured sandbox provider. |\n\n## Environment\n\n| Var | Purpose |\n|---|---|\n| `DEEPAGENT_MODEL_PROVIDER` | Server fallback model provider: `openrouter`, `gemini`, `nebius`, `huggingface`, `custom`, or `local` |\n| `DEEPAGENT_MODEL` | Server fallback model ID. Must prove `<=32B` when selected per Run. |\n| `DEEPAGENT_MODEL_BASE_URL` | Optional OpenAI-compatible `/v1` endpoint |\n| `OPENROUTER_API_KEY` / `GEMINI_API_KEY` / `NEBIUS_API_KEY` / `HF_TOKEN` | Optional server fallback keys for trusted server/CLI runs only. The public Gradio UI requires the user to enter their own hosted-provider key and does not use these by default. |\n| `DEEPAGENT_SANDBOX_PROVIDER` | `local`, `docker`, or `daytona` |\n| `BROWSERBASE_API_KEY` | Optional — visual validation via Stagehand |\n| `UPSTASH_REDIS_REST_URL` / `TOKEN` | Optional — persistent runs & archives |\n\n---\n\n## Stack\n\n- **Python 3.11+** / **FastAPI** / **Gradio 6**\n- **LangChain DeepAgents** — multi-subagent swarm runtime\n- **Provider adapters** — OpenRouter, Gemini, Nebius, Hugging Face Router, custom OpenAI-compatible, local OpenAI-compatible\n- **LangGraph** — Validator workflow\n- **QuickJS code interpreter** — in-sandbox code execution middleware\n- **Browserbase + Stagehand** — visual web validation (optional)\n\n## Architecture\n\n```\narena/\n  agent.py           — Swarm factory, model, subagents, sandbox backend\n  backyard_templates.py — Backyard demo template registry\n  model_provider.py  — Chat model factory for provider selection\n  model_catalog.py   — Provider model list adapters and TTL cache\n  swarm_runtime.py   — Active Run registration and Swarm session leasing\n  swarm_session.py   — Prompt seeding, agent turns, test retries, snapshots\n  sandbox_lease.py   — Idle TTL, touch, and close behavior for sandboxes\n  run_flow.py        — Run lifecycle: create → execute → archive → validate\n  run_journal.py     — Run mutation journal: status, tasks, events, timestamps\n  run_store.py       — Run persistence (InMemory / Redis via Upstash)\n  codebase_handoff.py — Workspace snapshot and Validator sandbox restore\n  codebase_archive.py — Archive persistence (local / Redis)\n  validator_plan.py  — Typed Validator plan from user tests/checks\n  validator_graph.py — LangGraph Validator workflow\n  thread_inspector.py — Manual Thread/session debug surface\n  gradio_app.py      — Thin Gradio component wiring\n  gradio_presenter.py — Run output formatting for Gradio\n  gradio_markup.py   — Static Gradio shell markup\n  api.py             — FastAPI REST + SSE endpoints\n  event_bus.py       — In-process event streaming\n  browserbase_tools.py  — Web fetch/search tools for the swarm\n  stagehand_validator.py — Browserbase visual validation\n  docker_backend.py  — Docker sandbox provider\n  skill_catalog.py   — Bundled DeepAgents skills discovery\ntests_python/        — Python test suite (integration + unit)\n```\n\n---\n\n*Built with a sub-32B model for the Build Small Hackathon, June 2026.*\n",70      "readme_body": "# Backyard Demo Builder\n\n## Chapter 1: Backyard AI\n\n*Build Small Hackathon 2026 — Chapter 1 Submission*\n\n`agent-swarm-workbench` now presents as **Backyard Demo Builder**: a Gradio app\nthat turns one real person's workflow into a small runnable demo package before\nanyone pays to build full software.\n\nFirst backyard case: my mom, a real-estate agent. She needs a cheap way to test\na customer follow-up reminder workflow before committing time and money to a\nfull app.\n\n---\n\n## Watch the Demo Builder Work\n\n```\nYou:     \"Build a real-estate follow-up CRM demo for my mom.\"\nBuilder: Generates a Gradio mini-app, handoff spec, field notes, and checks\nResult:  app.py, README.md, handoff_spec.md, field_notes.md\nMom:     Tests the workflow, then we scrap or scale.\n```\n\nEvery Run produces a **downloadable demo package** and Validation report: files\nyou can inspect, unzip, run, and test with the real person.\n\n---\n\n## Build Small Hackathon — Submission Notes\n\n| Requirement | How We Meet It |\n|---|---|\n| **Small model (≤ 32B)** | Provider catalog fetches models at runtime and only allows models whose ID/name proves ≤32B |\n| **Gradio app** | Custom dark-themed Gradio UI mounted on FastAPI |\n| **HF Space** | `app.py` + `requirements.txt` — one-command deploy |\n| **Demo video** | *(placeholder — [link to demo])* |\n| **Social post** | *(placeholder — [link to post])* |\n\n### Bonus Badges Claimed\n\n| Badge | Why |\n|---|---|\n| **🎨 Off-Brand** | Fully custom CSS dark theme — Archivo + IBM Plex Mono, acid green CTAs, paper/ink palette, CSS grid layout, status chips. Not a default Gradio component in sight. |\n| **📡 Sharing is Caring** | Agent traces and swarm reasoning are surfaced in the Events panel. We'll publish a trace on the Hub. |\n| **📓 Field Notes** | Generated demo packages include `field_notes.md`; this repo also documents the architecture and decisions. |\n\n---\n\n## Why This Belongs in Backyard AI\n\nThis solves a real problem for someone I know.\n\n- **Specific person** — my mom, a real-estate agent.\n- **Specific pain** — follow-up reminders and customer-care demos are useful, but custom app dev is slow and risky.\n- **Honest small-model fit** — a ≤32B model drafts the demo and handoff spec; rules handle the reminder logic.\n- **Actually testable** — the generated package includes field notes and feedback questions for the real user.\n\n---\n\n## How It Works Under the Hood\n\n```\n┌─────────────────────────────────────────────────────┐\n│  Gradio UI / HTTP API                               │\n├─────────────────────────────────────────────────────┤\n│  RunFlow — lifecycle conductor                      │\n│  ┌──────────┐  ┌────────────┐  ┌────────────────┐  │\n│  │ Swarm    │  │ Codebase   │  │ Validator      │  │\n│  │ Runtime  │→│ Archive    │→│ Graph          │  │\n│  │          │  │ Store      │  │                │  │\n│  │ Planner  │  │ (local/    │  │ Sandbox checks │  │\n│  │ Coder    │  │  Redis)    │  │ Rubric review  │  │\n│  │ Reviewer │  │            │  │ Stagehand      │  │\n│  │ Tester   │  │            │  │ (Browserbase)  │  │\n│  └──────────┘  └────────────┘  └────────────────┘  │\n│  EventBus → SSE stream to UI                       │\n└─────────────────────────────────────────────────────┘\n```\n\n### The Swarm\n\n- **Coordinator** reads the prompt, plans tasks, delegates to subagents\n- **Planner** breaks down the prompt into implementable units\n- **Coder** writes the actual code files\n- **Reviewer** checks code quality and correctness\n- **Test-runner** runs the user's tests and retries up to 3x on failure\n- **Validator-prep** generates validation checks from user criteria\n\n### The Validator\n\nAfter the swarm finishes, a LangGraph Validator workflow:\n1. Restores the codebase into a clean sandbox\n2. Runs user-provided tests\n3. Executes LLM-based rubric review\n4. (Optional) Runs Browserbase/Stagehand visual checks\n5. Produces a pass/fail Validation Report\n\n### The Sandbox\n\nAll agent work happens inside isolated sandbox workspaces:\n- **Local** (for dev/smoke tests)\n- **Docker** (container-based)\n- **Daytona** (cloud sandboxes)\n\n---\n\n## Run It\n\n```bash\ngit clone https://github.com/Kiy-K/agent-swarm-workbench.git\ncd agent-swarm-workbench\ncp .env.example .env\n# Optional: add server fallback keys. Users can also paste their own key in the UI.\nuv run uvicorn app:app --host 0.0.0.0 --port 8790\n```\n\nOpen http://localhost:8790, type a prompt, choose a provider, fetch models with your API key, then click Start Run.\n\nModel selection:\n- Model lists are fetched from the selected provider/API endpoint at runtime.\n- UI only offers fetched models whose ID/name proves `<=32B` parameters.\n- Unknown-size models are shown in the catalog response as `unknown_parameters` but are not selectable.\n- User API keys and fetched catalogs live only in process memory. They are not persisted, not stored in Redis/DB, and not kept in Gradio state. Click \"Refresh models\" to clear and refetch that provider cache.\n\nFor Hugging Face Spaces:\n```bash\nuv run python app.py\n```\n\n## Test\n\n```bash\npython scripts/task.py verify    # required completion gate: tests + harness\npython scripts/task.py test      # 90 tests, all passing\npython scripts/task.py harness -- --prompt \"Build a tiny CLI\" --test \"test -f README.md\"\npython scripts/task.py smoke      # Local agent session smoke check\npython scripts/task.py validator-smoke  # Validator end-to-end\n```\n\n### Agent Harness\n\nThe harness is the fast way to exercise the Run lifecycle without waiting on a\nfull demo session:\n\n```bash\npython scripts/task.py verify\npython scripts/task.py harness -- --prompt \"Build a tiny CLI\" --output-dir /tmp/harness\npython scripts/task.py harness -- --mode live --prompt \"Build a tiny CLI\"\n```\n\n`verify` is the required completion gate for coding agents. It runs the Python\nsuite, then runs the default scripted Agent Swarm Harness so changes are checked\nagainst the same Run -> SwarmRuntime -> Archive -> Validator path that the app\nuses.\n\nModes:\n\n| Mode | Purpose |\n|---|---|\n| `swarm` | Default. Runs `RunFlow -> SwarmRuntime -> Archive -> Validator` with a scripted local DeepAgent-compatible session. |\n| `live` | Uses the real `create_session()` DeepAgents path and the configured sandbox provider. |\n\n## Environment\n\n| Var | Purpose |\n|---|---|\n| `DEEPAGENT_MODEL_PROVIDER` | Server fallback model provider: `openrouter`, `gemini`, `nebius`, `huggingface`, `custom`, or `local` |\n| `DEEPAGENT_MODEL` | Server fallback model ID. Must prove `<=32B` when selected per Run. |\n| `DEEPAGENT_MODEL_BASE_URL` | Optional OpenAI-compatible `/v1` endpoint |\n| `OPENROUTER_API_KEY` / `GEMINI_API_KEY` / `NEBIUS_API_KEY` / `HF_TOKEN` | Optional server fallback keys for trusted server/CLI runs only. The public Gradio UI requires the user to enter their own hosted-provider key and does not use these by default. |\n| `DEEPAGENT_SANDBOX_PROVIDER` | `local`, `docker`, or `daytona` |\n| `BROWSERBASE_API_KEY` | Optional — visual validation via Stagehand |\n| `UPSTASH_REDIS_REST_URL` / `TOKEN` | Optional — persistent runs & archives |\n\n---\n\n## Stack\n\n- **Python 3.11+** / **FastAPI** / **Gradio 6**\n- **LangChain DeepAgents** — multi-subagent swarm runtime\n- **Provider adapters** — OpenRouter, Gemini, Nebius, Hugging Face Router, custom OpenAI-compatible, local OpenAI-compatible\n- **LangGraph** — Validator workflow\n- **QuickJS code interpreter** — in-sandbox code execution middleware\n- **Browserbase + Stagehand** — visual web validation (optional)\n\n## Architecture\n\n```\narena/\n  agent.py           — Swarm factory, model, subagents, sandbox backend\n  backyard_templates.py — Backyard demo template registry\n  model_provider.py  — Chat model factory for provider selection\n  model_catalog.py   — Provider model list adapters and TTL cache\n  swarm_runtime.py   — Active Run registration and Swarm session leasing\n  swarm_session.py   — Prompt seeding, agent turns, test retries, snapshots\n  sandbox_lease.py   — Idle TTL, touch, and close behavior for sandboxes\n  run_flow.py        — Run lifecycle: create → execute → archive → validate\n  run_journal.py     — Run mutation journal: status, tasks, events, timestamps\n  run_store.py       — Run persistence (InMemory / Redis via Upstash)\n  codebase_handoff.py — Workspace snapshot and Validator sandbox restore\n  codebase_archive.py — Archive persistence (local / Redis)\n  validator_plan.py  — Typed Validator plan from user tests/checks\n  validator_graph.py — LangGraph Validator workflow\n  thread_inspector.py — Manual Thread/session debug surface\n  gradio_app.py      — Thin Gradio component wiring\n  gradio_presenter.py — Run output formatting for Gradio\n  gradio_markup.py   — Static Gradio shell markup\n  api.py             — FastAPI REST + SSE endpoints\n  event_bus.py       — In-process event streaming\n  browserbase_tools.py  — Web fetch/search tools for the swarm\n  stagehand_validator.py — Browserbase visual validation\n  docker_backend.py  — Docker sandbox provider\n  skill_catalog.py   — Bundled DeepAgents skills discovery\ntests_python/        — Python test suite (integration + unit)\n```\n\n---\n\n*Built with a sub-32B model for the Build Small Hackathon, June 2026.*",71      "readme_frontmatter": {72        "title": "Backyard Demo Builder",73        "emoji": "🏡",74        "colorFrom": "gray",75        "colorTo": "green",76        "sdk": "gradio",77        "python_version": "3.12.12",78        "app_file": "app.py",79        "short_description": "Build tiny real-person demos before scaling custom software.",80        "models": "",81        "datasets": "[]",82        "tags": "",83        "pinned": "false"84      },85      "app_source": "\"\"\"Unified ASGI entrypoint for API and Gradio UI.\"\"\"\n\nfrom __future__ import annotations\n\nimport os\n\nimport gradio as gr\nimport uvicorn\n\nfrom arena.api import app as fastapi_app\nfrom arena.api import service\nfrom arena.gradio_app import build_app\n\n\ndemo = build_app(service)\n\n\ndef create_app():\n    \"\"\"Create one FastAPI ASGI app with Gradio mounted at the root.\"\"\"\n\n    return gr.mount_gradio_app(fastapi_app, demo, path=\"/\")\n\n\napp = create_app()\n\n\ntry:\n    import spaces\nexcept Exception:\n    class _SpacesShim:\n        def GPU(self, fn=None, **kwargs):\n            del kwargs\n\n            def decorator(inner):\n                return inner\n\n            return decorator(fn) if fn else decorator\n\n    spaces = _SpacesShim()\n\n\n@spaces.GPU\ndef zerogpu_ready_marker() -> str:\n    return \"ready\"\n\n\ndef server_config() -> dict[str, int | str]:\n    host = os.getenv(\"GRADIO_SERVER_NAME\", os.getenv(\"HOST\", \"0.0.0.0\"))\n    port = int(os.getenv(\"GRADIO_SERVER_PORT\") or os.getenv(\"PORT\") or \"7860\")\n    return {\"host\": host, \"port\": port}\n\n\ndef gradio_launch_config() -> dict[str, bool | int | str]:\n    config = server_config()\n    port = int(os.getenv(\"GRADIO_SERVER_PORT\", \"7861\")) if os.getenv(\"SPACE_ID\") else int(config[\"port\"])\n    return {\"server_name\": str(config[\"host\"]), \"server_port\": port, \"ssr_mode\": False}\n\n\ndef should_launch_gradio_space() -> bool:\n    return bool(os.getenv(\"SPACE_ID\")) and os.getenv(\"FORCE_SELF_LAUNCH\") != \"1\"\n\n\ndef should_self_launch() -> bool:\n    if os.getenv(\"FORCE_SELF_LAUNCH\") == \"1\":\n        return True\n    return not should_launch_gradio_space()\n\n\ndef _space_sdk() -> str:\n    return os.getenv(\"SPACE_SDK\", os.getenv(\"HF_SPACE_SDK\", \"\")).strip().lower()\n\n\nif __name__ == \"__main__\":\n    if should_launch_gradio_space():\n        demo.launch(**gradio_launch_config())\n    elif should_self_launch():\n        uvicorn.run(app, **server_config())\n",86      "app_signals": "create_app zerogpu_ready_marker server_config gradio_launch_config should_launch_gradio_space should_self_launch _space_sdk Unified ASGI entrypoint for API and Gradio UI. build_app Create one FastAPI ASGI app with Gradio mounted at the root. gr.mount_gradio_app path _SpacesShim ready os.getenv int lower __main__ GPU self fn GRADIO_SERVER_NAME host port server_name server_port ssr_mode str bool 1 demo.launch / decorator inner HOST 0.0.0.0 7860 SPACE_ID FORCE_SELF_LAUNCH strip uvicorn.run GRADIO_SERVER_PORT PORT 7861 SPACE_SDK HF_SPACE_SDK",87      "readme_len": 9123,88      "app_source_len": 1883,89      "app_signals_len": 54390    },91    {92      "id": "build-small-hackathon/AI-agent-Evaluation-pipeline",93      "title": "ai agent evaluation pipeline",94      "summary": "Evaluate AI agents at Session, Trace & Span levels",95      "tags": [96        "agents",97        "evaluation",98        "gradio",99        "llm",100        "observability"101      ],102      "models": [],103      "datasets": [],104      "sdk": "gradio",105      "license": "mit",106      "likes": 0,107      "url": "https://huggingface.co/spaces/build-small-hackathon/AI-agent-Evaluation-pipeline",108      "app_file": "app.py",109      "readme_raw": "---\ntitle: ai agent evaluation pipeline\nemoji: 🧪\ncolorFrom: purple\ncolorTo: blue\nsdk: gradio\nsdk_version: 6.14.0\napp_file: app.py\npinned: false\nlicense: mit\nshort_description: Evaluate AI agents at Session, Trace & Span levels\ntags:\n  - evaluation\n  - agents\n  - llm\n  - gradio\n  - observability\n---\n\n# 🧪 AI Agent Evaluation Pipeline\n\n> Evaluate AI agents at **Session**, **Trace**, and **Span** levels — inspired by [Amazon Bedrock AgentCore Evaluations](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/evaluations.html)\n\n## Overview\n\nThis tool provides a structured framework for evaluating AI agent conversations using the same three-level hierarchy as Amazon Bedrock AgentCore Evaluations:\n\n```\n📦 Session  → Did the agent achieve the user's overall goal?\n  └── 🔄 Trace   → Per-turn quality (helpfulness, coherence, relevance...)\n        └── 🔧 Span   → Per tool-call accuracy\n```\n\n## Features\n\n- **14 built-in evaluators** (1 session + 11 trace + 2 span)\n- **Heuristic mode** — works offline, no API key required\n- **3 demo traces** (Simple Q&A, Tool Calling, Multi-turn)\n- **Ground truth support** — `expected_response`, `expected_trajectory`, `assertions`\n- **Visual results** — radar chart, bar chart, heatmap, score cards\n\n## Evaluators\n\n### 📦 Session Level (1)\n\n| Evaluator         | Description                                         |\n| ----------------- | --------------------------------------------------- |\n| Goal Success Rate | Did the agent fully achieve the user's stated goal? |\n\n### 🔄 Trace Level (11)\n\n| Evaluator               | Description                                                 |\n| ----------------------- | ----------------------------------------------------------- |\n| Helpfulness             | Does the response help the user progress toward their goal? |\n| Correctness             | Is the response factually correct?                          |\n| Coherence               | Is the reasoning logically consistent and well-structured?  |\n| Conciseness             | Is the response appropriately concise?                      |\n| Faithfulness            | Is the response consistent with conversation history?       |\n| Harmfulness             | Does the response contain harmful content?                  |\n| Instruction Following   | Does the agent follow its system prompt?                    |\n| Response Relevance      | Does the response address what was asked?                   |\n| Context Relevance       | Was the retrieved context relevant? (RAG)                   |\n| Refusal Appropriateness | Did the agent correctly handle refusals?                    |\n| Stereotyping / Bias     | Is there demographic bias in the response?                  |\n\n### 🔧 Span Level (2)\n\n| Evaluator               | Description                            |\n| ----------------------- | -------------------------------------- |\n| Tool Selection Accuracy | Did the agent choose the right tool?   |\n| Tool Parameter Accuracy | Did the agent pass correct parameters? |\n\n## JSON Trace Format\n\n```json\n{\n  \"session_id\": \"my_session\",\n  \"user_goal\": \"The user's overall goal for this conversation\",\n  \"system_prompt\": \"(optional) System instructions given to the agent\",\n  \"traces\": [\n    {\n      \"trace_id\": \"t1\",\n      \"user_input\": \"User's message\",\n      \"agent_response\": \"Agent's reply\",\n      \"retrieved_context\": \"(optional) RAG context\",\n      \"spans\": [\n        {\n          \"span_id\": \"s1\",\n          \"span_type\": \"TOOL_CALL\",\n          \"tool_name\": \"my_tool\",\n          \"tool_input\": { \"param\": \"value\" },\n          \"tool_output\": \"Tool result\",\n          \"duration_ms\": 250\n        }\n      ]\n    }\n  ]\n}\n```\n\n## Ground Truth Support\n\nOptional reference inputs for more precise evaluation:\n\n- **`expected_response`** — What the final response should look like (enables Correctness scoring)\n- **`expected_trajectory`** — Expected tool call sequence (enables TrajectoryMatch scoring)\n- **`assertions`** — Natural language assertions about the session (enables GoalSuccessRate scoring)\n\n## Running Locally\n\n```bash\ngit clone https://github.com/your-org/ai-agent-eval-pipeline\ncd ai-agent-eval-pipeline\npip install -r requirements.txt\n\n# Gradio UI\npython app.py                     # http://localhost:7860\n\n# REST API\npython api.py                     # http://localhost:8000\n# or\nuvicorn api:app --reload --port 8000\n```\n\n## Integration — Zero Changes to Your Agent\n\n### Option 1 — Python Wrapper\n\n```python\nfrom src.wrapper import SessionTracer\n\nwith SessionTracer(\n    goal=\"Interview a Python candidate\",\n    system_prompt=\"You are a technical interviewer...\",\n) as tracer:\n    for user_msg in conversation:\n        # Your agent code — completely unchanged\n        response = my_agent.invoke(user_msg)\n\n        # Optional: capture tool calls made during this turn\n        span = tracer.new_span()\n        span.log_span(\"search_kb\", {\"query\": user_msg}, kb_result)\n\n        tracer.log_trace(user_msg, response, span)\n\n    report = tracer.evaluate()\n    print(f\"Overall: {report.overall_score:.0%}\")\n    tracer.save(\"traces/session_001.json\")\n```\n\n### Option 2 — REST API\n\n```bash\n# Start the server\npython api.py   # → http://localhost:8000\n\n# Evaluate a session\ncurl -X POST http://localhost:8000/evaluate/quick \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"trace\": {\n      \"session_id\": \"interview_001\",\n      \"user_goal\": \"Assess Python skills\",\n      \"traces\": [\n        {\n          \"trace_id\": \"t1\",\n          \"user_input\": \"What is a decorator?\",\n          \"agent_response\": \"A decorator is a function that wraps another function...\",\n          \"spans\": []\n        }\n      ]\n    }\n  }'\n```\n\nAPI docs auto-generated at `http://localhost:8000/docs`.\n\n## Architecture\n\n```\napp.py                  # Gradio UI entry point\napi.py                  # FastAPI REST server\nsrc/\n├── models.py           # Session / Trace / Span / EvalScore data classes\n├── parser.py           # JSON trace parser\n├── evaluators.py       # All 14 evaluators (heuristic + LLM-ready)\n├── runner.py           # Evaluation orchestrator\n├── visualizer.py       # Plotly charts\n└── wrapper.py          # SessionTracer — captures agent conversations\ndemos/\n├── simple_qa.json      # Demo: Simple Q&A\n├── tool_calling.json   # Demo: Tool calling\n└── multi_turn.json     # Demo: Multi-turn with tools\n```\n\n## Roadmap\n\n### ✅ MVP Complete\n\n- [x] **Gradio UI** — 14 evaluators, Session / Trace / Span levels, 3 demo traces\n- [x] **Agent Wrapper** (`src/wrapper.py`) — `SessionTracer` + `trace_agent` decorator\n- [x] **REST API** (`api.py`) — `POST /evaluate`, `POST /evaluate/quick`, `GET /evaluators`\n- [x] **LLM-as-Judge** (`src/llm_judge.py`) — `Qwen/Qwen3.6-27B` via HF Inference API\n- [x] **pass@k / pass^k** (`src/reliability.py`) — multi-trial reliability metrics\n- [x] **Golden Dataset Generator** — Nemotron-3-Nano-30B, 8 tech interview domains\n- [x] **Deployed** — `build-small-hackathon/AI-agent-Evaluation-pipeline`\n\n### 📋 Future (post-MVP)\n\n- [ ] Export results as JSON / CSV\n- [ ] Custom evaluator builder (user-defined prompt templates)\n- [ ] Dataset management for regression testing\n- [ ] Online monitoring mode\n\n## Inspiration\n\nThis project is inspired by the architecture and evaluator design of [Amazon Bedrock AgentCore Evaluations](https://aws.amazon.com/blogs/machine-learning/build-reliable-ai-agents-with-amazon-bedrock-agentcore-evaluations/), re-implemented as an open-source Gradio application.\n\n## License\n\nMIT\n",110      "readme_body": "# 🧪 AI Agent Evaluation Pipeline\n\n> Evaluate AI agents at **Session**, **Trace**, and **Span** levels — inspired by [Amazon Bedrock AgentCore Evaluations](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/evaluations.html)\n\n## Overview\n\nThis tool provides a structured framework for evaluating AI agent conversations using the same three-level hierarchy as Amazon Bedrock AgentCore Evaluations:\n\n```\n📦 Session  → Did the agent achieve the user's overall goal?\n  └── 🔄 Trace   → Per-turn quality (helpfulness, coherence, relevance...)\n        └── 🔧 Span   → Per tool-call accuracy\n```\n\n## Features\n\n- **14 built-in evaluators** (1 session + 11 trace + 2 span)\n- **Heuristic mode** — works offline, no API key required\n- **3 demo traces** (Simple Q&A, Tool Calling, Multi-turn)\n- **Ground truth support** — `expected_response`, `expected_trajectory`, `assertions`\n- **Visual results** — radar chart, bar chart, heatmap, score cards\n\n## Evaluators\n\n### 📦 Session Level (1)\n\n| Evaluator         | Description                                         |\n| ----------------- | --------------------------------------------------- |\n| Goal Success Rate | Did the agent fully achieve the user's stated goal? |\n\n### 🔄 Trace Level (11)\n\n| Evaluator               | Description                                                 |\n| ----------------------- | ----------------------------------------------------------- |\n| Helpfulness             | Does the response help the user progress toward their goal? |\n| Correctness             | Is the response factually correct?                          |\n| Coherence               | Is the reasoning logically consistent and well-structured?  |\n| Conciseness             | Is the response appropriately concise?                      |\n| Faithfulness            | Is the response consistent with conversation history?       |\n| Harmfulness             | Does the response contain harmful content?                  |\n| Instruction Following   | Does the agent follow its system prompt?                    |\n| Response Relevance      | Does the response address what was asked?                   |\n| Context Relevance       | Was the retrieved context relevant? (RAG)                   |\n| Refusal Appropriateness | Did the agent correctly handle refusals?                    |\n| Stereotyping / Bias     | Is there demographic bias in the response?                  |\n\n### 🔧 Span Level (2)\n\n| Evaluator               | Description                            |\n| ----------------------- | -------------------------------------- |\n| Tool Selection Accuracy | Did the agent choose the right tool?   |\n| Tool Parameter Accuracy | Did the agent pass correct parameters? |\n\n## JSON Trace Format\n\n```json\n{\n  \"session_id\": \"my_session\",\n  \"user_goal\": \"The user's overall goal for this conversation\",\n  \"system_prompt\": \"(optional) System instructions given to the agent\",\n  \"traces\": [\n    {\n      \"trace_id\": \"t1\",\n      \"user_input\": \"User's message\",\n      \"agent_response\": \"Agent's reply\",\n      \"retrieved_context\": \"(optional) RAG context\",\n      \"spans\": [\n        {\n          \"span_id\": \"s1\",\n          \"span_type\": \"TOOL_CALL\",\n          \"tool_name\": \"my_tool\",\n          \"tool_input\": { \"param\": \"value\" },\n          \"tool_output\": \"Tool result\",\n          \"duration_ms\": 250\n        }\n      ]\n    }\n  ]\n}\n```\n\n## Ground Truth Support\n\nOptional reference inputs for more precise evaluation:\n\n- **`expected_response`** — What the final response should look like (enables Correctness scoring)\n- **`expected_trajectory`** — Expected tool call sequence (enables TrajectoryMatch scoring)\n- **`assertions`** — Natural language assertions about the session (enables GoalSuccessRate scoring)\n\n## Running Locally\n\n```bash\ngit clone https://github.com/your-org/ai-agent-eval-pipeline\ncd ai-agent-eval-pipeline\npip install -r requirements.txt\n\n# Gradio UI\npython app.py                     # http://localhost:7860\n\n# REST API\npython api.py                     # http://localhost:8000\n# or\nuvicorn api:app --reload --port 8000\n```\n\n## Integration — Zero Changes to Your Agent\n\n### Option 1 — Python Wrapper\n\n```python\nfrom src.wrapper import SessionTracer\n\nwith SessionTracer(\n    goal=\"Interview a Python candidate\",\n    system_prompt=\"You are a technical interviewer...\",\n) as tracer:\n    for user_msg in conversation:\n        # Your agent code — completely unchanged\n        response = my_agent.invoke(user_msg)\n\n        # Optional: capture tool calls made during this turn\n        span = tracer.new_span()\n        span.log_span(\"search_kb\", {\"query\": user_msg}, kb_result)\n\n        tracer.log_trace(user_msg, response, span)\n\n    report = tracer.evaluate()\n    print(f\"Overall: {report.overall_score:.0%}\")\n    tracer.save(\"traces/session_001.json\")\n```\n\n### Option 2 — REST API\n\n```bash\n# Start the server\npython api.py   # → http://localhost:8000\n\n# Evaluate a session\ncurl -X POST http://localhost:8000/evaluate/quick \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"trace\": {\n      \"session_id\": \"interview_001\",\n      \"user_goal\": \"Assess Python skills\",\n      \"traces\": [\n        {\n          \"trace_id\": \"t1\",\n          \"user_input\": \"What is a decorator?\",\n          \"agent_response\": \"A decorator is a function that wraps another function...\",\n          \"spans\": []\n        }\n      ]\n    }\n  }'\n```\n\nAPI docs auto-generated at `http://localhost:8000/docs`.\n\n## Architecture\n\n```\napp.py                  # Gradio UI entry point\napi.py                  # FastAPI REST server\nsrc/\n├── models.py           # Session / Trace / Span / EvalScore data classes\n├── parser.py           # JSON trace parser\n├── evaluators.py       # All 14 evaluators (heuristic + LLM-ready)\n├── runner.py           # Evaluation orchestrator\n├── visualizer.py       # Plotly charts\n└── wrapper.py          # SessionTracer — captures agent conversations\ndemos/\n├── simple_qa.json      # Demo: Simple Q&A\n├── tool_calling.json   # Demo: Tool calling\n└── multi_turn.json     # Demo: Multi-turn with tools\n```\n\n## Roadmap\n\n### ✅ MVP Complete\n\n- [x] **Gradio UI** — 14 evaluators, Session / Trace / Span levels, 3 demo traces\n- [x] **Agent Wrapper** (`src/wrapper.py`) — `SessionTracer` + `trace_agent` decorator\n- [x] **REST API** (`api.py`) — `POST /evaluate`, `POST /evaluate/quick`, `GET /evaluators`\n- [x] **LLM-as-Judge** (`src/llm_judge.py`) — `Qwen/Qwen3.6-27B` via HF Inference API\n- [x] **pass@k / pass^k** (`src/reliability.py`) — multi-trial reliability metrics\n- [x] **Golden Dataset Generator** — Nemotron-3-Nano-30B, 8 tech interview domains\n- [x] **Deployed** — `build-small-hackathon/AI-agent-Evaluation-pipeline`\n\n### 📋 Future (post-MVP)\n\n- [ ] Export results as JSON / CSV\n- [ ] Custom evaluator builder (user-defined prompt templates)\n- [ ] Dataset management for regression testing\n- [ ] Online monitoring mode\n\n## Inspiration\n\nThis project is inspired by the architecture and evaluator design of [Amazon Bedrock AgentCore Evaluations](https://aws.amazon.com/blogs/machine-learning/build-reliable-ai-agents-with-amazon-bedrock-agentcore-evaluations/), re-implemented as an open-source Gradio application.\n\n## License\n\nMIT",111      "readme_frontmatter": {112        "title": "ai agent evaluation pipeline",113        "emoji": "🧪",114        "colorFrom": "purple",115        "colorTo": "blue",116        "sdk": "gradio",117        "sdk_version": "6.14.0",118        "app_file": "app.py",119        "pinned": "false",120        "license": "mit",121        "short_description": "Evaluate AI agents at Session, Trace & Span levels",122        "tags": ""123      },124      "app_source": "#!/usr/bin/env python3\n\"\"\"\nAI Agent Evaluation Pipeline — Gradio MVP\n==========================================\nEvaluate AI agents at 3 hierarchical levels, inspired by\nAmazon Bedrock AgentCore Evaluations.\n\n  📦 Session  — Did the agent achieve the user's goal?\n  🔄 Trace    — Per-turn quality (11 evaluators)\n  🔧 Span     — Per tool-call accuracy (2 evaluators)\n\nRun locally : python app.py\nHuggingFace : app_file = app.py  (Gradio SDK)\n\"\"\"\n\nimport json\nimport os\nimport sys\nfrom pathlib import Path\n\n# Ensure src/ is importable whether run from repo root or HF Spaces\n_ROOT = Path(__file__).parent\nsys.path.insert(0, str(_ROOT))\n\nimport gradio as gr\n\n# HF ZeroGPU Spaces require at least one @spaces.GPU-decorated function\n# to be detected at module load. The actual evaluation and dataset\n# generation work in this app uses the cloud InferenceClient and runs\n# without local GPU compute; the placeholder below exists only to\n# satisfy the runtime's static check. `spaces` is pre-installed on\n# ZeroGPU hardware; we guard the import so the app still loads if it\n# is missing (e.g. local CPU dev).\ntry:\n    import spaces as _spaces\nexcept ImportError:\n    class _spaces_stub:\n        @staticmethod\n        def GPU(fn, duration: int = 60):\n            return fn\n    _spaces = _spaces_stub()\n\n\n@_spaces.GPU\ndef _zero_gpu_healthcheck() -> dict:\n    \"\"\"Placeholder GPU function detected by the ZeroGPU runtime.\"\"\"\n    try:\n        import torch\n        return {\"cuda_available\": bool(torch.cuda.is_available())}\n    except ImportError:\n        return {\"cuda_available\": False, \"note\": \"torch not installed\"}\n\n\nfrom src.evaluators import (\n    ALL_EVALUATORS,\n    DEFAULT_TRACE_EVALS,\n    SESSION_EVALUATORS,\n    SPAN_EVALUATORS,\n    TRACE_EVALUATORS,\n)\nfrom src.llm_judge import LLMJudge\nfrom src.models import EvalLevel, EvalMode, GroundTruth\nfrom src.parser import format_trace_tree, parse_trace\nfrom src.reliability import compute_reliability\nfrom src.runner import EvalRunner\nfrom src.visualizer import create_bar_chart, create_radar_chart, create_trace_timeline\n\n# ─── Load demo traces ───────────────────────────────────────────────────────\n\n_DEMOS = _ROOT / \"demos\"\n\n\ndef _load_demo(name: str) -> str:\n    p = _DEMOS / f\"{name}.json\"\n    return p.read_text(encoding=\"utf-8\") if p.exists() else \"{}\"\n\n\nDEMO_SIMPLE_QA = _load_demo(\"simple_qa\")\nDEMO_TOOL_CALLING = _load_demo(\"tool_calling\")\nDEMO_MULTI_TURN = _load_demo(\"multi_turn\")\n\n# ─── UI helpers ─────────────────────────────────────────────────────────────\n\n_LEVEL_COLOR = {\n    EvalLevel.SESSION: \"#9B59B6\",\n    EvalLevel.TRACE: \"#3498DB\",\n    EvalLevel.SPAN: \"#27AE60\",\n}\n\n_LEVEL_ICON = {\n    EvalLevel.SESSION: \"📦\",\n    EvalLevel.TRACE: \"🔄\",\n    EvalLevel.SPAN: \"🔧\",\n}\n\n\ndef _bar_color(score: float) -> str:\n    if score >= 0.8:\n        return \"#4CAF50\"\n    elif score >= 0.6:\n        return \"#FF9800\"\n    return \"#F44336\"\n\n\ndef _bg_color(score: float) -> str:\n    if score >= 0.8:\n        return \"rgba(76,175,80,0.12)\"\n    elif score >= 0.6:\n        return \"rgba(255,152,0,0.12)\"\n    return \"rgba(244,67,54,0.12)\"\n\n\ndef render_score_card(score) -> str:\n    color = _bar_color(score.score)\n    bg = _bg_color(score.score)\n    badge_color = _LEVEL_COLOR.get(score.level, \"#888\")\n    level_icon = _LEVEL_ICON.get(score.level, \"\")\n\n    return f\"\"\"\n<div style=\"background:{bg};border-radius:8px;padding:12px 15px;margin:5px 0;\n            border-left:4px solid {color};border:1px solid rgba(255,255,255,0.07);\">\n  <div style=\"display:flex;justify-content:space-between;align-items:center;margin-bottom:7px;\">\n    <div style=\"display:flex;align-items:center;gap:8px;\">\n      <span style=\"background:{badge_color};color:white;padding:2px 7px;border-radius:4px;\n                   font-size:10px;font-weight:700;letter-spacing:0.5px;\">{level_icon} {score.level.value}</span>\n      <span style=\"color:#eee;font-weight:600;font-size:13px;\">{score.evaluator_display}</span>\n    </div>\n    <span style=\"background:{color};color:white;padding:3px 10px;border-radius:10px;\n                 font-size:13px;font-weight:700;\">{score.score_pct}%</span>\n  </div>\n  <div style=\"background:rgba(255,255,255,0.08);border-radius:3px;height:4px;margin-bottom:8px;\">\n    <div style=\"background:{color};height:4px;border-radius:3px;width:{score.score_pct}%;\"></div>\n  </div>\n  <div style=\"color:rgba(210,210,210,0.85);font-size:11.5px;line-height:1.55;\">\n    <span style=\"color:rgba(150,150,150,0.7);font-size:10px;\">\n      {score.target_label} &nbsp;·&nbsp; {score.mode.value} mode\n    </span><br>\n    {score.explanation}\n  </div>\n</div>\"\"\"\n\n\ndef render_overall_banner(report) -> str:\n    s = report.overall_score\n    color = _bar_color(s)\n    passed = sum(1 for x in report.scores if x.passed)\n    total = len(report.scores)\n    status = \"PASS ✅\" if s >= 0.6 else \"NEEDS REVIEW ⚠️\"\n\n    # Level breakdown\n    sess_avg = (\n        sum(x.score for x in report.session_scores) / len(report.session_scores)\n        if report.session_scores\n        else None\n    )\n    trace_avg = (\n        sum(x.score for x in report.trace_scores) / len(report.trace_scores)\n        if report.trace_scores\n        else None\n    )\n    span_avg = (\n        sum(x.score for x in report.span_scores) / len(report.span_scores)\n        if report.span_scores\n        else None\n    )\n\n    def level_chip(label, avg, icon, level):\n        if avg is None:\n            return \"\"\n        c = _bar_color(avg)\n        bc = _LEVEL_COLOR.get(level, \"#888\")\n        return (\n            f'<div style=\"text-align:center;padding:8px 14px;background:rgba(255,255,255,0.06);'\n            f'border-radius:8px;border:1px solid {bc}33;\">'\n            f'<div style=\"font-size:10px;color:{bc};font-weight:700;margin-bottom:3px;\">{icon} {label}</div>'\n            f'<div style=\"font-size:20px;font-weight:800;color:{c};\">{avg:.0%}</div>'\n            f\"</div>\"\n        )\n\n    chips = \" \".join(\n        [\n            level_chip(\"SESSION\", sess_avg, \"📦\", EvalLevel.SESSION),\n            level_chip(\"TRACE\", trace_avg, \"🔄\", EvalLevel.TRACE),\n            level_chip(\"SPAN\", span_avg, \"🔧\", EvalLevel.SPAN),\n        ]\n    )\n\n    return f\"\"\"\n<div style=\"background:linear-gradient(135deg,#1a1a2e 0%,#16213e 100%);\n            border-radius:12px;padding:20px 24px;margin:4px 0;\n            border:1px solid rgba(255,255,255,0.1);\">\n  <div style=\"display:flex;justify-content:space-between;align-items:flex-start;flex-wrap:wrap;gap:12px;\">\n    <div>\n      <div style=\"color:rgba(180,180,180,0.8);font-size:11px;letter-spacing:1px;margin-bottom:4px;\">OVERALL SCORE</div>\n      <div style=\"font-size:42px;font-weight:800;color:{color};line-height:1;\">{s:.0%}</div>\n      <div style=\"color:rgba(180,180,180,0.7);font-size:12px;margin-top:6px;\">\n        {passed}/{total} evaluators passed &nbsp;·&nbsp;\n        {len(report.session.traces)} turn(s) &nbsp;·&nbsp;\n        {report.elapsed_seconds:.2f}s &nbsp;·&nbsp;\n        {report.eval_mode.value} mode\n      </div>\n    </div>\n    <div style=\"display:flex;flex-direction:column;align-items:flex-end;gap:8px;\">\n      <div style=\"font-size:22px;font-weight:700;color:{color};\">{status}</div>\n      <div style=\"display:flex;gap:8px;\">{chips}</div>\n    </div>\n  </div>\n  <div style=\"background:rgba(255,255,255,0.07);border-radius:4px;height:6px;margin-top:16px;\">\n    <div style=\"background:{color};height:6px;border-radius:4px;width:{int(s * 100)}%;\n                transition:width 0.5s ease;\"></div>\n  </div>\n</div>\"\"\"\n\n\ndef parse_and_preview(trace_json: str) -> str:\n    if not trace_json or not trace_json.strip():\n        return \"*Paste or load a JSON trace above to see a preview.*\"\n    try:\n        session = parse_trace(trace_json)\n        return format_trace_tree(session)\n    except Exception as e:\n        return f\"❌ **Parse error:** `{e}`\\n\\nCheck that your JSON is valid and contains `user_goal` + `traces`.\"\n\n\n# ─── Benchmark functions ──────────────────────────────────────────────────────\n\n\ndef load_records_from_url(url: str) -> list:\n    \"\"\"Load JSONL records from a HF dataset repo URL (data/golden_dataset.jsonl).\"\"\"\n    from urllib.parse import urlparse\n\n    from huggingface_hub import hf_hub_download\n\n    parsed = urlparse(url)\n    if \"huggingface.co\" not in parsed.netloc or \"/datasets/\" not in parsed.path:\n        raise ValueError(f\"Not a HF dataset URL: {url}\")\n    repo_id = parsed.path.split(\"/datasets/\")[1].strip(\"/\").split(\"/\")[0]\n    path = hf_hub_download(\n        repo_id=repo_id,\n        filename=\"data/golden_dataset.jsonl\",\n        repo_type=\"dataset\",\n    )\n    with open(path, encoding=\"utf-8\") as f:\n        return [json.loads(line) for line in f if line.strip()]\n\n\ndef parse_pasted_jsonl(text: str) -> list:\n    \"\"\"Parse pasted JSONL content into list of records.\"\"\"\n    return [json.loads(line) for line in text.splitlines() if line.strip()]\n\n\ndef call_openai_compat(\n    url: str, scenario: dict, api_key: str, model: str, timeout: int = 60\n) -> str:\n    \"\"\"POST to an OpenAI-compatible /v1/chat/completions endpoint.\"\"\"\n    import requests\n\n    headers = {\"Content-Type\": \"application/json\"}\n    if api_key.strip():\n        headers[\"Authorization\"] = f\"Bearer {api_key.strip()}\"\n    body = {\n        \"messages\": [\n            {\"role\": \"system\", \"content\": scenario.get(\"system_prompt\", \"\")},\n            {\"role\": \"user\", \"content\": scenario[\"initial_message\"]},\n        ],\n    }\n    if model.strip():\n        body[\"model\"] = model.strip()\n    r = requests.post(url, json=body, headers=headers, timeout=timeout)\n    r.raise_for_status()\n    data = r.json()\n    return data[\"choices\"][0][\"message\"][\"content\"]\n\n\ndef build_trace_json(rec: dict, agent_response: str) -> str:\n    \"\"\"Build a parseable trace JSON from a dataset record + agent response.\"\"\"\n    scenario = rec.get(\"scenario\", {})\n    return json.dumps(\n        {\n            \"session_id\": rec.get(\"id\", \"unknown\"),\n            \"user_goal\": scenario.get(\"user_goal\", \"\"),\n            \"system_prompt\": scenario.get(\"system_prompt\"),\n            \"traces\": [\n                {\n                    \"trace_id\": \"t1\",\n                    \"user_input\": scenario.get(\"initial_message\", \"\"),\n                    \"agent_response\": agent_response,\n                }\n            ],\n        },\n        ensure_ascii=False,\n    )\n\n\ndef run_benchmark(\n    dataset_url: str,\n    pasted_jsonl: str,\n    agent_url: str,\n    api_key: str,\n    model_name: str,\n    use_session: bool,\n    use_trace: bool,\n    use_span: bool,\n    sel_session: list,\n    sel_trace: list,\n    sel_span: list,\n    threshold: float,\n    progress=gr.Progress(track_tqdm=True),\n):\n    \"\"\"Run benchmark: load dataset, call agent for each record, eval, aggregate.\"\"\"\n\n    def render_status(phase: str, done: int, total: int, current_id: str = \"\") -> str:\n        pct = int(done / total * 100) if total else 0\n        current = f\" &nbsp;·&nbsp; ⏳ {current_id}\" if current_id else \"\"\n        return (\n            f\"<div style='padding:12px;background:rgba(255,255,255,0.05);border-radius:8px;'>\"\n            f\"<div style='font-size:12px;color:#aaa;margin-bottom:6px;'>\"\n            f\"<b>{phase}</b> &nbsp;·&nbsp; {done}/{total} ({pct}%){current}</div>\"\n            f\"<div style='background:rgba(255,255,255,0.1);border-radius:3px;height:6px;'>\"\n            f\"<div style='background:#63B3ED;height:6px;border-radius:3px;width:{pct}%;'></div>\"\n            f\"</div></div>\"\n        )\n\n    def render_table(rows: list) -> str:\n        if not rows:\n            return \"\"\n        body = \"\"\n        for r in rows:\n            color = \"#4CAF50\" if r[\"passed\"] else \"#F44336\"\n            icon = \"✅\" if r[\"passed\"] else \"⚠️\"\n            score = r[\"score\"]\n            score_str = f\"{score:.0%}\" if isinstance(score, float) else \"—\"\n            err_cell = (\n                f\"<div style='color:#F44336;font-size:10px;'>{r['error']}</div>\"\n                if r.get(\"error\")\n                else \"\"\n            )\n            body += (\n                \"<tr style='border-bottom:1px solid rgba(255,255,255,0.05);'>\"\n                f\"<td style='padding:6px 8px;color:#ddd;font-size:12px;'>{r['id']}</td>\"\n                f\"<td style='padding:6px 8px;color:#aaa;font-size:11px;'>{r['domain']}</td>\"\n                f\"<td style='padding:6px 8px;color:#aaa;font-size:11px;'>{r['difficulty']}</td>\"\n                f\"<td style='padding:6px 8px;text-align:center;color:{color};font-weight:700;'>{score_str} {icon}</td>\"\n                f\"<td style='padding:6px 8px;'>{err_cell}</td>\"\n                \"</tr>\"\n            )\n        return (\n            \"<table style='width:100%;border-collapse:collapse;margin-top:14px;'>\"\n            \"<thead><tr style='color:#aaa;border-bottom:1px solid rgba(255,255,255,0.1);font-size:11px;'>\"\n            \"<th style='text-align:left;padding:6px 8px;'>ID</th>\"\n            \"<th style='text-align:left;padding:6px 8px;'>Domain</th>\"\n            \"<th style='text-align:left;padding:6px 8px;'>Difficulty</th>\"\n            \"<th style='text-align:center;padding:6px 8px;'>Score</th>\"\n            \"<th style='text-align:left;padding:6px 8px;'>Error</th>\"\n            \"</tr></thead><tbody>\" + body + \"</tbody></table>\"\n        )\n\n    def render_aggregate(rows: list, total: int) -> str:\n        scored = [r for r in rows if isinstance(r[\"score\"], float)]\n        if not scored:\n            return \"\"\n        ok = sum(1 for r in scored if r[\"passed\"])\n        avg = sum(r[\"score\"] for r in scored) / len(scored)\n        by_domain: dict = {}\n        for r in scored:\n            d = r[\"domain\"] or \"—\"\n            by_domain.setdefault(d, []).append(r[\"score\"])\n        domain_chips = \" \".join(\n            f\"<span style='display:inline-block;margin:2px 6px 2px 0;padding:3px 9px;\"\n            f\"background:rgba(255,255,255,0.07);border-radius:10px;font-size:11px;color:#ccc;'>\"\n            f\"{d}: <b style='color:#4CAF50;'>{sum(s)/len(s):.0%}</b></span>\"\n            for d, s in sorted(by_domain.items())\n        )\n        return (\n            f\"<div style='margin-top:16px;padding:14px;background:rgba(99,179,237,0.08);\"\n            f\"border-radius:8px;border:1px solid rgba(99,179,237,0.2);'>\"\n            f\"<div style='color:#63B3ED;font-weight:700;font-size:14px;margin-bottom:8px;'>📊 Aggregate</div>\"\n            f\"<div style='color:#ccc;font-size:12px;margin-bottom:6px;'>\"\n            f\"Passed: <b style='color:#4CAF50;'>{ok}/{len(scored)}</b> \"\n            f\"&nbsp;·&nbsp; Avg: <b style='color:#4CAF50;'>{avg:.0%}</b>\"\n            f\"&nbsp;·&nbsp; Threshold: {threshold:.0%}</div>\"\n            f\"<div style='color:#aaa;font-size:11px;'>{domain_chips}</div></div>\"\n        )\n\n    def panel(*htmls: str) -> str:\n        return \"\".join(h for h in htmls if h)\n\n    progress(0.02, desc=\"Loading dataset…\")\n    yield panel(render_status(\"Loading dataset\", 0, 1)), \"📂 Loading dataset…\"\n    try:\n        if pasted_jsonl.strip():\n            records = parse_pasted_jsonl(pasted_jsonl)\n            source = \"pasted JSONL\"\n        else:\n            records = load_records_from_url(dataset_url.strip())\n            source = dataset_url.strip()\n    except Exception as e:\n        err = f\"❌ Failed to load dataset: {e}\"\n        yield (\n            panel(f\"<div style='color:#F44336;padding:14px;'>{err}</div>\"),\n            f\"ERROR: {e}\\nPaste JSONL directly if the URL is empty or unreachable.\",\n        )\n        return\n\n    if not records:\n        yield (\n            panel(\"<div style='color:#FF9800;padding:14px;'>⚠️ Dataset loaded but empty.</div>\"),\n            \"No records found in source.\",\n        )\n        return\n\n    total = len(records)\n    log_lines = [f\"✅ Loaded {total} records from {source}\"]\n    yield (\n        panel(\n            render_status(\"Loaded\", total, total),\n            f\"<div style='color:#4CAF50;padding:10px;'>📂 {total} records loaded from {source}</div>\",\n        ),\n        \"\\n\".join(log_lines),\n    )\n\n    if not agent_url.strip():\n        yield (\n            panel(\"<div style='color:#F44336;padding:14px;'>❌ Agent URL is empty.</div>\"),\n            \"ERROR: Provide an OpenAI-compatible chat completions URL.\",\n        )\n        return\n\n    sess_evals = sel_session if use_session else []\n    trace_evals = sel_trace if use_trace else []\n    span_evals = sel_span if use_span else []\n    runner = EvalRunner(\n        selected_session_evals=sess_evals,\n        selected_trace_evals=trace_evals,\n        selected_span_evals=span_evals,\n        threshold=threshold,\n        mode=EvalMode.HEURISTIC,\n    )\n\n    results = []\n    for i, rec in enumerate(records):\n        rid = rec.get(\"id\", f\"rec_{i}\")\n        domain = rec.get(\"domain\", \"\")\n        difficulty = rec.get(\"difficulty\", \"\")\n        progress(0.1 + 0.85 * i / total, desc=f\"Running {rid}…\")\n        log_lines.append(f\"⏳ {rid} ({domain}/{difficulty})…\")\n        yield (\n            panel(render_status(\"Running\", i, total, rid), render_table(results)),\n            \"\\n\".join(log_lines),\n        )\n\n        try:\n            scenario = rec.get(\"scenario\") or {}\n            agent_out = call_openai_compat(\n                agent_url.strip(),\n                scenario,\n                api_key or \"\",\n                model_name or \"\",\n                timeout=60,\n            )\n            trace_json = build_trace_json(rec, agent_out)\n            session = parse_trace(trace_json)\n            gt_data = rec.get(\"ground_truth\") or {}\n            gt = GroundTruth(\n                expected_response=gt_data.get(\"expected_response\"),\n                expected_trajectory=gt_data.get(\"expected_trajectory\"),\n                assertions=gt_data.get(\"assertions\"),\n            )\n            report = runner.run(session, gt)\n            score = report.overall_score\n            results.append(\n                {\n                    \"id\": rid,\n                    \"domain\": domain,\n                    \"difficulty\": difficulty,\n                    \"score\": score,\n                    \"passed\": score >= threshold,\n                    \"error\": None,\n                }\n            )\n            log_lines[-1] = f\"✅ {rid} — {score:.0%}\"\n        except Exception as e:\n            results.append(\n                {\n                    \"id\": rid,\n                    \"domain\": domain,\n                    \"difficulty\": difficulty,\n                    \"score\": None,\n                    \"passed\": False,\n                    \"error\": f\"{type(e).__name__}: {str(e)[:80]}\",\n                }\n            )\n            log_lines[-1] = f\"✗ {rid} — {type(e).__name__}: {str(e)[:60]}\"\n\n        yield (\n            panel(render_status(\"Running\", i + 1, total), render_table(results)),\n            \"\\n\".join(log_lines),\n        )\n\n    progress(1.0, desc=\"Done!\")\n    yield (\n        panel(\n            render_status(\"Done\", total, total),\n            render_table(results),\n            render_aggregate(results, total),\n        ),\n        \"\\n\".join(log_lines),\n    )\n\n\n# ─── Main evaluation function ────────────────────────────────────────────────\n\n\ndef render_reliability(rel_report, k: int) -> str:\n    \"\"\"Render pass@k / pass^k as an HTML table.\"\"\"\n    if not rel_report or not rel_report.evaluator_results:\n        return \"\"\n    rows = rel_report.summary_table()\n    verdict_style = {\n        \"reliable\": (\"#4CAF50\", \"✅\"),\n        \"unstable\": (\"#FF9800\", \"⚠️\"),\n        \"unreliable\": (\"#F44336\", \"❌\"),\n    }\n    header = (\n        f\"<h3 style='color:#63B3ED;margin:18px 0 10px;font-size:15px;'>\"\n        f\"🔄 Reliability Testing — k={k} trials</h3>\"\n        f\"<div style='background:rgba(99,179,237,0.08);border-radius:8px;padding:12px 16px;margin-bottom:10px;font-size:12px;color:#aaa;'>\"\n        f\"<b>pass@{k}</b> = P(≥1 of {k} trials passes) — optimistic bound &nbsp;| \"\n        f\"<b>pass^{k}</b> = P(ALL {k} trials pass) — reliability estimate</div>\"\n    )\n    table = (\n        \"<table style='width:100%;border-collapse:collapse;font-size:12px;'>\"\n        \"<thead><tr style='color:#aaa;border-bottom:1px solid rgba(255,255,255,0.1);'>\"\n        f\"<th style='text-align:left;padding:6px 8px;'>Evaluator</th>\"\n        f\"<th style='text-align:center;padding:6px 8px;'>Avg</th>\"\n        f\"<th style='text-align:center;padding:6px 8px;'>pass@{k}</th>\"\n        f\"<th style='text-align:center;padding:6px 8px;'>pass^{k}</th>\"\n        f\"<th style='text-align:center;padding:6px 8px;'>Verdict</th>\"\n        \"</tr></thead><tbody>\"\n    )\n    for r in rows:\n        color, icon = verdict_style.get(r[\"Verdict\"], (\"#888\", \"?\"))\n        table += (\n            f\"<tr style='border-bottom:1px solid rgba(255,255,255,0.05);'>\"\n            f\"<td style='padding:5px 8px;color:#ddd;'>{r['Evaluator']}</td>\"\n            f\"<td style='text-align:center;padding:5px 8px;color:#ccc;'>{r['Avg Score']}</td>\"\n            f\"<td style='text-align:center;padding:5px 8px;color:#63B3ED;font-weight:600;'>{r[f'pass@{k}']}</td>\"\n            f\"<td style='text-align:center;padding:5px 8px;color:{color};font-weight:700;'>{r[f'pass^{k}']}</td>\"\n            f\"<td style='text-align:center;padding:5px 8px;'><span style='color:{color};'>{icon} {r['Verdict']}</span></td>\"\n            \"</tr>\"\n        )\n    table += \"</tbody></table>\"\n\n    summary = (\n        f\"<div style='margin-top:10px;padding:10px 14px;background:rgba(255,255,255,0.05);\"\n        f\"border-radius:6px;font-size:12px;color:#ccc;'>\"\n        f\"Overall — pass@{k}: <b style='color:#63B3ED;'>{rel_report.overall_pass_at_k:.0%}</b>\"\n        f\" &nbsp;| pass^{k}: <b style='color:#4CAF50;'>{rel_report.overall_pass_hat_k:.0%}</b>\"\n        f\" &nbsp;| avg score: <b>{rel_report.avg_score:.0%}</b></div>\"\n    )\n    return header + table + summary\n\n\ndef run_evaluation(\n    trace_json: str,\n    use_session: bool,\n    use_trace: bool,\n    use_span: bool,\n    sel_session: list,\n    sel_trace: list,\n    sel_span: list,\n    threshold: float,\n    k_trials: int,\n    eval_mode_radio: str,\n    hf_token: str,\n    exp_response: str,\n    exp_trajectory: str,\n    assertions_text: str,\n    progress=gr.Progress(track_tqdm=True),\n):\n    # ── 1. Parse input ────────────────────────────────────────────────────\n    progress(0.05, desc=\"Parsing trace…\")\n    try:\n        session = parse_trace(trace_json)\n    except Exception as e:\n        err = (\n            f\"<div style='color:#F44336;padding:20px;'>❌ <b>Parse error:</b> {e}</div>\"\n        )\n        return err, None, None, None, err\n\n    # ── 2. Build ground truth ─────────────────────────────────────────────\n    gt = None\n    if exp_response.strip() or exp_trajectory.strip() or assertions_text.strip():\n        traj = (\n            [t.strip() for t in exp_trajectory.split(\",\") if t.strip()]\n            if exp_trajectory.strip()\n            else None\n        )\n        asrt = (\n            [a.strip() for a in assertions_text.splitlines() if a.strip()]\n            if assertions_text.strip()\n            else None\n        )\n        gt = GroundTruth(\n            expected_response=exp_response.strip() or None,\n            expected_trajectory=traj,\n            assertions=asrt,\n        )\n\n    # ── 3. Resolve selected evaluators ───────────────────────────────────\n    sess_evals = sel_session if use_session else []\n    trace_evals = sel_trace if use_trace else []\n    span_evals = sel_span if use_span else []\n\n    if not sess_evals and not trace_evals and not span_evals:\n        warn = \"<div style='color:#FF9800;padding:20px;'>⚠️ No evaluators selected — please enable at least one level.</div>\"\n        return warn, None, None, None, warn\n\n    # ── 4. Build LLM judge (if requested) ────────────────────────────────\n    use_llm = eval_mode_radio == \"LLM Judge (QwQ-32B)\"\n    mode = EvalMode.LLM if use_llm else EvalMode.HEURISTIC\n    judge = None\n    if use_llm:\n        token = hf_token.strip() or None\n        judge = LLMJudge(api_key=token)\n        if not judge.available:\n            warn = \"<div style='color:#FF9800;padding:20px;'>⚠️ LLM mode selected but no HF Token provided — falling back to heuritic.</div>\"\n            mode = EvalMode.HEURISTIC\n\n    # ── 5. Run evaluation (single or k trials) ─────────────────────────────\n    progress(0.15, desc=\"Running evalua",125      "app_signals": "_zero_gpu_healthcheck _load_demo name _bar_color score _bg_color render_score_card render_overall_banner report parse_and_preview trace_json load_records_from_url url parse_pasted_jsonl text call_openai_compat scenario api_key model timeout build_trace_json rec agent_response run_benchmark dataset_url pasted_jsonl agent_url model_name use_session use_trace use_span sel_session sel_trace sel_span threshold progress render_reliability rel_report k run_evaluation k_trials eval_mode_radio hf_token exp_response exp_trajectory assertions_text AI Agent Evaluation Pipeline — Gradio MVP ========================================== Evaluate AI agents at 3 hierarchical levels, inspired by Amazon Bedrock AgentCore Evaluations. 📦 Session — Did the agent achieve the user's goal? 🔄 Trace — Per-turn quality (11 evaluators) 🔧 Span — Per tool-call accuracy (2 evaluators) Run locally : python app.py HuggingFace : app_file = app.py (Gradio SDK) sys.path.insert level_chip label avg icon level render_status phase done total current_id render_table rows render_aggregate panel 🧪 AI Agent Evaluation Pipeline Evaluate AI agents at Session , Trace , and Span levels — inspired by Amazon Bedrock AgentCore Evaluations ### How it works | Level | Scope | Evaluators | |-------|-------|------------| | 📦 **Session** | Full conversation | Goal Success Rate | | 🔄 **Trace** | Per turn (user → agent) | Helpfulness, Correctness, Coherence, Conciseness, Faithfulness, Harmfulness, Instruction Following, Response Relevance, Context Relevance, Refusal, Stereotyping | | 🔧 **Span** | Per tool call | Tool Selection Accuracy, Tool Parameter Accuracy | **Modes:** `heuristic` (offline, no API key) · `llm` (LLM-as-judge, coming soon) **JSON format:** `session_id`, `user_goal`, `system_prompt`(opt), `traces[]` → `trace_id`, `user_input`, `agent_response`, `spans[]` _preview_dataset paste Path str _spaces_stub Placeholder GPU function detected by the ZeroGPU runtime. demos simple_qa tool_calling multi_turn #9B59B6 #3498DB #27AE60 📦 🔄 🔧 #F44336 rgba(244,67,54,0.12) _LEVEL_COLOR.get _LEVEL_ICON.get sum len join Load JSONL records from a HF dataset repo URL (data/golden_dataset.jsonl). urlparse hf_hub_download repo_id filename repo_type Parse pasted JSONL content into list of records. POST to an OpenAI-compatible /v1/chat/completions endpoint. api_key.strip model.strip requests.post json headers r.raise_for_status r.json Build a parseable trace JSON from a dataset record + agent response. rec.get json.dumps ensure_ascii gr.Progress track_tqdm Run benchmark: load dataset, call agent for each record, eval, aggregate. desc EvalRunner selected_session_evals selected_trace_evals selected_span_evals mode enumerate Render pass@k / pass^k as an HTML table. rel_report.summary_table int llm_judge report.avg_score_by_evaluator create_radar_chart create_bar_chart create_trace_timeline gr.Blocks title gr.HTML padding bm_load_btn.click inputs outputs bm_run_btn.click fn run_btn.click __main__ demo.launch theme css server_name server_port share show_error GPU duration p.exists p.read_text encoding {} #4CAF50 rgba(76,175,80,0.12) #888 <div style=\"background: ;border-radius:8px;padding:12px 15px;margin:5px 0; border-left:4px solid ;border:1px solid rgba(255,255,255,0.07);\"> <span style=\"background: ;color:white;padding:2px 7px;border-radius:4px; font-size:10px;font-weight:700;letter-spacing:0.5px;\"> <span style=\"background: ;color:white;padding:3px 10px;border-radius:10px; font-size:13px;font-weight:700;\"> % <div style=\"background: ;height:4px;border-radius:3px;width: %;\"> &nbsp;·&nbsp; PASS ✅ NEEDS REVIEW ⚠️ OVERALL SCORE <div style=\"font-size:42px;font-weight:800;color: ;line-height:1;\"> / evaluators passed &nbsp;·&nbsp; turn(s) &nbsp;·&nbsp; s &nbsp;·&nbsp; mode <div style=\"font-size:22px;font-weight:700;color: ;\"> ;height:6px;border-radius:4px;width: %; transition:width 0.5s ease;\"> *Paste or load a JSON trace above to see a preview.* parse_trace format_trace_tree ValueError split open json.lo ... ef without verbosity? | | **Faithfulness** | TRACE | Is the response consistent with conversation history / context? | | **Harmfulness** | TRACE | Does the response contain harmful or dangerous content? | | **Instruction Following** | TRACE | Does the agent follow its system prompt instructions? | | **Response Relevance** | TRACE | Does the response directly address what was asked? | | **Context Relevance** | TRACE | Was the retrieved context relevant to the query? (RAG) | | **Refusal Appropriateness** | TRACE | Did the agent correctly handle what to refuse? | | **Stereotyping / Bias** | TRACE | Is there stereotypical or demographic bias? | | **Tool Selection Accuracy** | SPAN | Did the agent choose the right tool? | | **Tool Parameter Accuracy** | SPAN | Did the agent pass correct parameters to the tool? | ### Roadmap - [x] LLM-as-Judge mode (HuggingFace Inference API) - [ ] OpenAI-compatible API support - [x] pass@k / pass^k reliability metrics - [ ] Export results as JSON / CSV - [ ] Custom evaluator builder (prompt templates) - [x] Dataset management for regression testing (🧪 Benchmark tab) url.strip ⚠️ Loaded 0 records. 📂 records loaded from Domains: os.getenv initial_message choices trace_id user_input t1 passed error by_domain.setdefault Loading dataset ⚠️ Dataset loaded but empty. Loaded ❌ Agent URL is empty. Running … ground_truth gt_data.get ✗ Done Evaluator Avg Score exp_trajectory.split assertions_text.splitlines trials… by_trace.setdefault by_span.setdefault 🎓 Simple Q&A 🔧 Tool Calling 🔄 Multi-turn + Tools Agent Trace (JSON) 🌲 Trace Preview 📖 JSON Schema Reference gr.Column gr.Checkbox gr.Radio info placeholder type visible eval_mode_radio.change gr.Slider minimum maximum step gr.CheckboxGroup 📋 Ground Truth (Optional — improves scoring precision) Providing reference inputs enables ground-truth-based evaluation (mirrors AgentCore's `expected_response`, `expected_trajectory`, and `assertions`). primary run-btn lg 🗋️ Score Heatmap: Evaluators × Turns Load a dataset and click Run Benchmark to start. purple blue PORT by_domain.items ERROR: Paste JSONL directly if the URL is empty or unreachable. pass@ , sm secondary indent **Evaluation Levels** **🤖 Evaluation Mode** **Pass Threshold** **🔄 Reliability Testing (pass@k / pass^k)** **📦 Session Evaluators** *(once per session)* **🔄 Trace Evaluators** *(once per conversation turn)* **🔧 Span Evaluators** *(once per tool call)* 🕸️ Evaluator Scores (Radar) 📊 Score Breakdown by Evaluator **📦 Dataset** 🔄 Load Dataset No dataset loaded yet. **🤖 Agent (OpenAI-compatible)** **⚙️ Eval settings** 🚀 Run Benchmark Log parsed.path.split 🔄 Trace Level 🔧 Span Level (tool calls) Heuristic (offline) LLM mode requires a HuggingFace token with QwQ-32B access HF Token hf_... password Minimum score to pass Scores ≥ threshold are marked ✅ passed Trials (k) k=1 → standard mode. k>1 → runs multiple trials, shows pass@k & pass^k. HF Dataset URL (loads data/golden_dataset.jsonl) https://huggingface.co/datasets/build-small-hackathon/agent-eval-golden-dataset https://huggingface.co/datasets/... 📝 Or paste JSONL directly Chat completions URL https://your-agent.example.com/v1/chat/completions API Key (optional) Bearer xyz Model name (optional, sent in body if provided) gpt-4o-mini Session evaluators Trace evaluators Span evaluators Pass threshold copy my_session Describe the overall goal of the user (optional) System instructions given to the agent gr.update Expected Response What should the final agent response look like? Expected Tool Trajectory (comma-separated tool names) search_restaurants, create_reservation Assertions (one per line) A restaurant reservation was made Confirmation number was provided The restaurant matches user preferences JSONL records {\"id\":\"python_001\",\"scenario\":{...},\"ground_truth\":{...}} ... retrieved_context spans User's message Agent's reply (optional) RAG context span_id span_type tool_name tool_input tool_output duration_ms s1 TOOL_CALL my_tool Tool result string param",126      "readme_len": 7158,127      "app_source_len": 24000,128      "app_signals_len": 7999129    },130    {131      "id": "build-small-hackathon/AI-Puppet-Theater",132      "title": "AI Puppet Theater",133      "summary": "",134      "tags": [135        "gradio",136        "region:us"137      ],138      "models": [],139      "datasets": [],140      "sdk": "gradio",141      "license": "",142      "likes": 1,143      "url": "https://huggingface.co/spaces/build-small-hackathon/AI-Puppet-Theater",144      "app_file": "app.py",145      "readme_raw": "---\ntitle: AI Puppet Theater\nemoji: 🎭\ncolorFrom: yellow\ncolorTo: purple\nsdk: gradio\nsdk_version: 6.5.1\napp_file: app.py\npython_version: \"3.11\"\npinned: false\n---\n\nAI Puppet Theater is a public Gradio Space for building short interactive puppet shows from a user premise.\n",146      "readme_body": "AI Puppet Theater is a public Gradio Space for building short interactive puppet shows from a user premise.",147      "readme_frontmatter": {148        "title": "AI Puppet Theater",149        "emoji": "🎭",150        "colorFrom": "yellow",151        "colorTo": "purple",152        "sdk": "gradio",153        "sdk_version": "6.5.1",154        "app_file": "app.py",155        "python_version": "3.11",156        "pinned": "false"157      },158      "app_source": "from html import escape\nimport os\nfrom time import sleep\n\nimport gradio as gr\n\nfrom puppet_theater import (\n    DEFAULT_OPENBMB_MODEL_ID,\n    TheaterSession,\n    create_show_from_premise,\n    get_backend_status,\n    request_finale,\n    run_one_beat,\n    summon_actor,\n    throw_prop,\n    warm_up_openbmb,\n)\n\n\nEMPTY_STAGE = \"\"\"\n<div class=\"puppet-stage stage-empty\">\n  <div class=\"stage-valance\"></div>\n  <div class=\"stage-backdrop\">\n    <div class=\"stage-marquee\">AI Puppet Theater</div>\n    <div class=\"empty-stage-copy\">Enter a premise and raise the curtain.</div>\n  </div>\n  <div class=\"stage-floorboards\"></div>\n</div>\n\"\"\"\n\nEMPTY_TRANSCRIPT = \"No show yet. The transcript will appear here.\"\nEMPTY_DIRECTOR_LOG = \"No director notes yet.\"\nEMPTY_TRACE = \"No trace events yet.\"\nEMPTY_BACKEND = (\n    \"Active backend: deterministic\\n\"\n    \"OpenBMB model id: openbmb/MiniCPM5-1B\\n\"\n    \"Model status: unloaded\\n\"\n    \"Fallback: deterministic safety path enabled\"\n)\nBACKEND_CHOICES = [\"deterministic\", \"openbmb\"]\nOPENBMB_MODEL_ID = os.getenv(\"OPENBMB_MODEL_ID\", DEFAULT_OPENBMB_MODEL_ID)\nDEFAULT_MAX_NEW_TOKENS = 80\nDEFAULT_TEMPERATURE = 0.8\nPLAYBACK_DELAY_SECONDS = 0.75\nPROP_EMOJI = {\n    \"rubber duck\": \"🐤\",\n    \"duck\": \"🐤\",\n    \"egg\": \"🥚\",\n    \"flowers\": \"💐\",\n    \"flower\": \"💐\",\n    \"tomato\": \"🍅\",\n    \"crown\": \"👑\",\n    \"tiny crown\": \"👑\",\n    \"scroll\": \"📜\",\n    \"banana\": \"🍌\",\n    \"mirror\": \"🪞\",\n}\n\nCUSTOM_CSS = \"\"\"\nbody,\n.gradio-container {\n    background:\n        radial-gradient(circle at 50% 0%, rgba(127, 29, 29, 0.18), transparent 28rem),\n        linear-gradient(180deg, #0b1020 0%, #070914 100%) !important;\n    color: #f8efe4 !important;\n}\n.gradio-container {\n    max-width: 1180px !important;\n    padding-top: 1rem !important;\n    font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;\n}\n.gradio-container .prose,\n.gradio-container label,\n.gradio-container span,\n.gradio-container p {\n    color: #f8efe4;\n}\n.gradio-container textarea,\n.gradio-container input {\n    background: rgba(10, 12, 23, 0.82) !important;\n    border-color: rgba(246, 196, 83, 0.24) !important;\n    color: #f8efe4 !important;\n}\n.gradio-container textarea::placeholder,\n.gradio-container input::placeholder {\n    color: #9f8c7a !important;\n}\n.gradio-container footer {\n    color: rgba(203, 183, 161, 0.62) !important;\n}\n.gradio-container .block,\n.gradio-container .form,\n.gradio-container .panel,\n.gradio-container .tabs,\n.gradio-container .tabitem {\n    background: rgba(34, 17, 31, 0.56) !important;\n    border-color: rgba(246, 196, 83, 0.18) !important;\n}\n.gradio-container label,\n.gradio-container .block-title,\n.gradio-container .label-wrap {\n    color: #f8efe4 !important;\n}\n.gradio-container .block-info,\n.gradio-container .label-wrap span,\n.gradio-container label > span {\n    background: rgba(34, 17, 31, 0.88) !important;\n    border: 1px solid rgba(246, 196, 83, 0.28) !important;\n    border-radius: 6px !important;\n    color: #ffd166 !important;\n    font-weight: 700 !important;\n}\n.gradio-container .wrap,\n.gradio-container .styler,\n.gradio-container .form,\n.gradio-container .form > *,\n.gradio-container .block > div {\n    background-color: transparent !important;\n}\n.gradio-container select,\n.gradio-container [role=\"listbox\"],\n.gradio-container [role=\"combobox\"] {\n    background: rgba(10, 12, 23, 0.82) !important;\n    border-color: rgba(246, 196, 83, 0.24) !important;\n    color: #f8efe4 !important;\n}\n.app-title h1 {\n    color: #f8efe4;\n    font-family: Georgia, \"Times New Roman\", serif;\n    font-size: 2.15rem;\n    letter-spacing: 0;\n    margin-bottom: 0;\n    text-align: center;\n}\n.app-title p {\n    color: #cbb7a1;\n    font-size: 0.95rem;\n    margin: 0.15rem 0 0.8rem;\n    text-align: center;\n}\n.gradio-container h3,\n.gradio-container h3 span,\n.gradio-container .prose h3,\n.gradio-container .prose h3 span {\n    color: #f8efe4 !important;\n}\n.premise-panel {\n    background: rgba(42, 20, 38, 0.72);\n    border-color: rgba(246, 196, 83, 0.3);\n    box-shadow: 0 16px 32px rgba(0, 0, 0, 0.2);\n    padding: 0.55rem 0.65rem 0.65rem;\n}\n.premise-panel .block,\n.premise-panel .wrap,\n.premise-panel .styler,\n.premise-panel .form,\n.premise-panel .block > div {\n    background: rgba(42, 20, 38, 0.78) !important;\n}\n.control-panel {\n    background: rgba(34, 17, 31, 0.76);\n    border: 1px solid rgba(246, 196, 83, 0.22);\n    border-radius: 8px;\n    box-shadow: 0 14px 34px rgba(0, 0, 0, 0.22);\n    padding: 0.55rem;\n}\n.control-panel .block,\n.control-panel .wrap,\n.control-panel .styler,\n.control-panel .form,\n.control-panel .block > div {\n    background: rgba(34, 17, 31, 0.78) !important;\n}\n.control-panel .row,\n.premise-panel .row {\n    background: transparent !important;\n}\n.control-panel h3 {\n    color: #f8efe4;\n    margin: 0 0 0.35rem;\n    font-size: 1rem;\n}\n.control-panel .prose,\n.control-panel .prose h3,\n.control-panel h3 * {\n    color: #f8efe4 !important;\n}\n.puppet-stage {\n    min-height: 430px;\n    border: 5px solid #3b0a16;\n    border-radius: 14px;\n    background:\n        linear-gradient(90deg, rgba(59, 10, 22, 0.98) 0 10%, transparent 10% 90%, rgba(59, 10, 22, 0.98) 90% 100%),\n        linear-gradient(180deg, rgba(42, 20, 38, 0.96), rgba(13, 6, 14, 0.98));\n    color: #f8efe4;\n    display: flex;\n    flex-direction: column;\n    align-items: stretch;\n    justify-content: stretch;\n    position: relative;\n    overflow: hidden;\n    box-shadow:\n        0 24px 48px rgba(0, 0, 0, 0.38),\n        inset 0 0 42px rgba(0, 0, 0, 0.58);\n}\n.puppet-stage::before,\n.puppet-stage::after {\n    content: \"\";\n    position: absolute;\n    top: 0;\n    bottom: 0;\n    width: 13%;\n    background:\n        repeating-linear-gradient(90deg, rgba(255, 255, 255, 0.04) 0 14px, transparent 14px 28px),\n        linear-gradient(180deg, #8b1e3f 0%, #7f1d1d 54%, #3b0a16 100%);\n    box-shadow: inset -16px 0 28px rgba(0, 0, 0, 0.22);\n    z-index: 2;\n}\n.puppet-stage::before {\n    left: 0;\n}\n.puppet-stage::after {\n    right: 0;\n    transform: scaleX(-1);\n}\n.stage-valance {\n    height: 48px;\n    background:\n        repeating-linear-gradient(90deg, rgba(255, 255, 255, 0.06) 0 22px, transparent 22px 44px),\n        linear-gradient(180deg, #8b1e3f 0%, #7f1d1d 100%);\n    border-bottom: 4px solid #f6c453;\n    box-shadow: 0 10px 20px rgba(0, 0, 0, 0.34);\n    position: relative;\n    z-index: 3;\n}\n.stage-backdrop {\n    background:\n        radial-gradient(circle at 50% 8%, rgba(255, 224, 150, 0.28), transparent 19rem),\n        radial-gradient(circle at 24% 58%, rgba(255, 224, 150, 0.12), transparent 14rem),\n        linear-gradient(180deg, #2a1426 0%, #22111f 62%, #130911 100%);\n    flex: 1;\n    padding: 0.72rem 7.2rem 0.8rem;\n    position: relative;\n    z-index: 1;\n}\n.stage-backdrop::after {\n    background: linear-gradient(180deg, transparent 0%, rgba(124, 63, 23, 0.46) 100%);\n    bottom: 0;\n    content: \"\";\n    height: 32%;\n    left: 0;\n    position: absolute;\n    right: 0;\n}\n.stage-marquee {\n    color: #fff7ed;\n    font-family: Georgia, \"Times New Roman\", serif;\n    font-size: 1.6rem;\n    font-weight: 700;\n    letter-spacing: 0;\n    text-align: center;\n    text-shadow: 0 4px 18px rgba(0, 0, 0, 0.72);\n    position: relative;\n    z-index: 2;\n    overflow-wrap: anywhere;\n}\n.stage-copy {\n    max-width: 54rem;\n    color: #cbb7a1;\n    font-size: 0.84rem;\n    line-height: 1.35;\n    margin: 0.25rem auto 0;\n    text-align: center;\n    position: relative;\n    z-index: 2;\n}\n.stage-copy strong {\n    color: #f8efe4;\n}\n.empty-stage-copy {\n    color: #cbb7a1;\n    font-size: 1rem;\n    margin-top: 5.8rem;\n    text-align: center;\n    position: relative;\n    z-index: 2;\n}\n.stage-floorboards {\n    height: 58px;\n    background:\n        repeating-linear-gradient(90deg, rgba(255, 255, 255, 0.08) 0 2px, transparent 2px 72px),\n        linear-gradient(180deg, #8a4b22 0%, #7c3f17 100%);\n    border-top: 2px solid rgba(246, 196, 83, 0.28);\n    position: relative;\n    z-index: 3;\n}\n.speech-bubble {\n    animation: bubble-in 0.24s ease-out;\n    background: rgba(18, 10, 18, 0.82);\n    border: 1px solid rgba(246, 196, 83, 0.5);\n    border-radius: 16px;\n    box-shadow: 0 18px 30px rgba(0, 0, 0, 0.34);\n    color: #f8efe4;\n    margin: 0.55rem auto 0;\n    max-width: 46rem;\n    padding: 0.72rem 0.95rem;\n    position: relative;\n    text-align: center;\n    z-index: 4;\n}\n.speech-bubble::after {\n    border-left: 10px solid transparent;\n    border-right: 10px solid transparent;\n    border-top: 12px solid rgba(246, 196, 83, 0.5);\n    bottom: -12px;\n    content: \"\";\n    left: 50%;\n    position: absolute;\n    transform: translateX(-50%);\n}\n.speech-speaker {\n    color: #ffd166;\n    font-size: 0.78rem;\n    font-weight: 800;\n    letter-spacing: 0.08em;\n    margin-bottom: 0.18rem;\n    text-transform: uppercase;\n}\n.speech-line {\n    color: #f8efe4;\n    font-size: 0.96rem;\n    line-height: 1.35;\n}\n.actor-row {\n    display: grid;\n    grid-template-columns: repeat(auto-fit, minmax(132px, 1fr));\n    gap: 0.55rem;\n    margin-top: 0.72rem;\n    position: relative;\n    z-index: 3;\n}\n.actor-card {\n    background: rgba(70, 38, 36, 0.72);\n    border: 1px solid rgba(246, 196, 83, 0.45);\n    border-radius: 16px 16px 10px 10px;\n    box-shadow: 0 14px 28px rgba(0, 0, 0, 0.28);\n    min-height: 132px;\n    padding: 0.58rem 0.62rem 0.72rem;\n    position: relative;\n    transform-origin: bottom center;\n    text-align: center;\n}\n.actor-card::after {\n    background: #7c3f17;\n    border-radius: 0 0 8px 8px;\n    bottom: -22px;\n    box-shadow: inset 0 -5px 8px rgba(0, 0, 0, 0.2);\n    content: \"\";\n    height: 22px;\n    left: calc(50% - 8px);\n    position: absolute;\n    width: 16px;\n}\n.actor-card.active {\n    animation: puppet-bounce 0.78s ease-in-out infinite alternate;\n    border-color: #ffd166;\n    box-shadow:\n        0 0 0 2px rgba(255, 209, 102, 0.22),\n        0 0 34px rgba(255, 209, 102, 0.46),\n        0 16px 34px rgba(0, 0, 0, 0.34);\n}\n.actor-avatar {\n    background: radial-gradient(circle, rgba(255, 209, 102, 0.2), rgba(59, 10, 22, 0.3));\n    border: 1px solid rgba(246, 196, 83, 0.34);\n    border-radius: 999px;\n    display: inline-grid;\n    font-size: 1.7rem;\n    height: 3rem;\n    place-items: center;\n    text-align: center;\n    width: 3rem;\n}\n.actor-name {\n    color: #f8efe4;\n    font-weight: 700;\n    line-height: 1.15;\n    margin-top: 0.35rem;\n    text-align: center;\n}\n.speaking-pill {\n    background: #ffd166;\n    border-radius: 999px;\n    color: #3b0a16;\n    display: inline-block;\n    font-size: 0.64rem;\n    font-weight: 800;\n    margin-top: 0.26rem;\n    padding: 0.12rem 0.44rem;\n    text-transform: uppercase;\n}\n.actor-detail {\n    color: #cbb7a1;\n    font-size: 0.72rem;\n    line-height: 1.28;\n    margin-top: 0.35rem;\n}\n.actor-detail strong {\n    color: #f8efe4;\n}\n.held-prop {\n    margin-top: 0.42rem;\n}\n.held-prop span {\n    background: rgba(246, 196, 83, 0.14);\n    border: 1px solid rgba(246, 196, 83, 0.32);\n    border-radius: 999px;\n    color: #ffd166;\n    display: inline-block;\n    font-size: 0.68rem;\n    font-weight: 700;\n    padding: 0.12rem 0.42rem;\n}\n.beat-counter {\n    color: #ffd166;\n    font-weight: 800;\n    margin-top: 0.55rem;\n    position: relative;\n    text-align: center;\n    z-index: 3;\n}\n.stage-events {\n    display: grid;\n    gap: 0.4rem;\n    margin-top: 0.55rem;\n    position: relative;\n    z-index: 3;\n}\n.audience-action,\n.prop-pile {\n    background: rgba(42, 20, 38, 0.7);\n    border: 1px solid rgba(246, 196, 83, 0.25);\n    border-radius: 999px;\n    color: #f8efe4;\n    margin: 0 auto;\n    max-width: 48rem;\n    padding: 0.38rem;\n    text-align: center;\n    width: 100%;\n}\n.audience-action strong,\n.prop-pile strong {\n    color: #ffd166;\n}\n.prop-token {\n    animation: prop-pop 0.22s ease-out;\n    background: rgba(246, 196, 83, 0.17);\n    border: 1px solid rgba(246, 196, 83, 0.5);\n    border-radius: 999px;\n    color: #fff7ed;\n    display: inline-block;\n    margin: 0.2rem;\n    padding: 0.22rem 0.55rem;\n}\n.gradio-container button.primary,\n.gradio-container button.primary-action,\n.gradio-container button.run-one-action {\n    background: #f97316 !important;\n    border-color: #f97316 !important;\n    box-shadow: 0 10px 24px rgba(249, 115, 22, 0.25) !important;\n    color: #fff7ed !important;\n}\n.gradio-container button.secondary,\n.gradio-container button.secondary-action,\n.gradio-container button.audience-action-button {\n    background: #3f3148 !important;\n    border-color: rgba(246, 196, 83, 0.22) !important;\n    color: #f8efe4 !important;\n}\n.gradio-container button.reset-action {\n    background: #3b0a16 !important;\n    border-color: rgba(246, 196, 83, 0.24) !important;\n    color: #f8efe4 !important;\n}\n.transcript-box,\n.gradio-container .accordion {\n    background: rgba(13, 6, 14, 0.58) !important;\n    border-color: rgba(246, 196, 83, 0.18) !important;\n    color: #f8efe4 !important;\n}\n@keyframes puppet-bounce {\n    from { transform: translateY(0) rotate(-0.4deg); }\n    to { transform: translateY(-7px) rotate(0.7deg); }\n}\n@keyframes bubble-in {\n    from { opacity: 0; transform: translateY(8px); }\n    to { opacity: 1; transform: translateY(0); }\n}\n@keyframes prop-pop {\n    from { opacity: 0; transform: scale(0.86); }\n    to { opacity: 1; transform: scale(1); }\n}\n@media (max-width: 760px) {\n    .puppet-stage {\n        min-height: 560px;\n    }\n    .puppet-stage::before,\n    .puppet-stage::after {\n        width: 7%;\n    }\n    .stage-backdrop {\n        padding: 0.8rem 1.4rem;\n    }\n    .stage-marquee {\n        font-size: 1.2rem;\n    }\n    .actor-row {\n        grid-template-columns: repeat(2, minmax(0, 1fr));\n    }\n    .actor-card {\n        min-height: 126px;\n    }\n}\n\n/* Final Gradio chrome overrides: keep the whole app in the theater palette. */\n.gradio-container {\n    width: min(1200px, calc(100vw - 2rem)) !important;\n}\n.gradio-container .gr-group {\n    background: rgba(34, 17, 31, 0.84) !important;\n    border: 1px solid rgba(246, 196, 83, 0.2) !important;\n    border-radius: 8px !important;\n    color: #f8efe4 !important;\n}\n.gradio-container .gr-group .form,\n.gradio-container .gr-group .block,\n.gradio-container .gr-group .wrap,\n.gradio-container .gr-group .wrap-inner,\n.gradio-container .gr-group .secondary-wrap,\n.gradio-container .gr-group .input-container,\n.gradio-container .gr-group label {\n    background: transparent !important;\n    color: #f8efe4 !important;\n}\n.gradio-container input,\n.gradio-container textarea,\n.gradio-container select,\n.gradio-container .dropdown-container,\n.gradio-container .wrap-inner {\n    background: rgba(10, 12, 23, 0.9) !important;\n    color: #f8efe4 !important;\n}\n.gradio-container .control-panel input,\n.gradio-container .control-panel textarea,\n.gradio-container .control-panel .wrap-inner,\n.gradio-container .premise-panel textarea {\n    border: 1px solid rgba(246, 196, 83, 0.24) !important;\n}\n.gradio-container button {\n    background: #3f3148 !important;\n    border: 1px solid rgba(246, 196, 83, 0.24) !important;\n    color: #f8efe4 !important;\n}\n.gradio-container button.primary,\n.gradio-container button.primary-action,\n.gradio-container button.run-one-action {\n    background: #f97316 !important;\n    border-color: #f97316 !important;\n    color: #fff7ed !important;\n}\n.gradio-container button.reset-action {\n    background: #3b0a16 !important;\n    border-color: rgba(246, 196, 83, 0.32) !important;\n}\n.gradio-container .html-container,\n.gradio-container .gradio-style {\n    width: 100% !important;\n}\n.puppet-stage {\n    min-height: 500px;\n    width: 100%;\n}\n.puppet-stage::before,\n.puppet-stage::after {\n    width: clamp(56px, 9%, 110px);\n}\n.stage-backdrop {\n    padding: 0.78rem clamp(4.1rem, 11vw, 8.8rem) 0.72rem;\n}\n.stage-marquee {\n    font-size: clamp(1.25rem, 2.1vw, 1.72rem);\n    white-space: normal;\n}\n.speech-bubble {\n    margin-top: 0.48rem;\n    max-width: 44rem;\n    padding: 0.58rem 0.82rem;\n}\n.actor-row {\n    align-items: end;\n    grid-template-columns: repeat(auto-fit, minmax(116px, 1fr));\n    gap: 0.62rem;\n    margin-top: 0.82rem;\n}\n.actor-card {\n    align-content: start;\n    background: radial-gradient(circle at 50% 18%, rgba(246, 196, 83, 0.13), rgba(70, 38, 36, 0.72) 58%);\n    border-radius: 18px;\n    display: grid;\n    justify-items: center;\n    min-height: 108px;\n    padding: 0.5rem 0.45rem 0.56rem;\n}\n.actor-card::after {\n    bottom: -20px;\n    height: 20px;\n    width: 14px;\n}\n.actor-avatar {\n    font-size: 2rem;\n    height: 3.3rem;\n    width: 3.3rem;\n}\n.actor-name {\n    font-size: 0.82rem;\n    margin-top: 0.28rem;\n}\n.actor-detail {\n    display: -webkit-box;\n    font-size: 0.66rem;\n    line-height: 1.18;\n    margin-top: 0.2rem;\n    max-width: 11rem;\n    min-height: 1.55rem;\n    overflow: hidden;\n    -webkit-box-orient: vertical;\n    -webkit-line-clamp: 2;\n}\n.held-prop {\n    margin-top: 0.26rem;\n}\n.held-prop span {\n    font-size: 0.62rem;\n    padding: 0.08rem 0.34rem;\n}\n.speaking-pill {\n    font-size: 0.58rem;\n    margin-top: 0.18rem;\n    padding: 0.08rem 0.36rem;\n}\n.stage-events {\n    gap: 0.32rem;\n    margin-top: 0.64rem;\n}\n.audience-action,\n.prop-pile {\n    max-width: 45rem;\n    padding: 0.3rem 0.55rem;\n}\n@media (max-width: 760px) {\n    .gradio-container {\n        width: min(100vw, calc(100vw - 0.75rem)) !important;\n    }\n    .puppet-stage::before,\n    .puppet-stage::after {\n        width: 30px;\n    }\n    .stage-backdrop {\n        padding: 0.75rem 2.45rem;\n    }\n    .actor-row {\n        grid-template-columns: repeat(2, minmax(0, 1fr));\n        gap: 0.45rem;\n    }\n    .actor-card {\n        min-height: 102px;\n        padding-left: 0.28rem;\n        padding-right: 0.28rem;\n    }\n}\n\n/* Compact stage pass: keep the theater look, reduce scrolling, and keep controls close. */\n.gradio-container {\n    padding-top: 0.65rem !important;\n}\n.app-title h1 {\n    font-size: 1.95rem;\n}\n.app-title p {\n    margin-bottom: 0.55rem;\n}\n.premise-panel {\n    padding: 0.42rem 0.55rem 0.52rem;\n}\n.stage-output,\n.stage-output .html-container,\n.stage-output .gradio-style {\n    margin-bottom: 0 !important;\n}\n.puppet-stage {\n    min-height: 390px;\n}\n.stage-valance {\n    height: 34px;\n    border-bottom-width: 3px;\n}\n.stage-backdrop {\n    padding: 0.48rem clamp(3.9rem, 9vw, 7.3rem) 0.46rem;\n}\n.stage-marquee {\n    font-size: clamp(1.15rem, 1.9vw, 1.52rem);\n}\n.stage-copy {\n    font-size: 0.76rem;\n    line-height: 1.25;\n    margin-top: 0.14rem;\n}\n.speech-bubble {\n    border-radius: 12px;\n    margin-top: 0.34rem;\n    max-width: 40rem;\n    padding: 0.42rem 0.7rem;\n}\n.speech-speaker {\n    font-size: 0.68rem;\n}\n.speech-line {\n    font-size: 0.86rem;\n}\n.actor-row {\n    grid-template-columns: repeat(auto-fit, minmax(104px, 1fr));\n    gap: 0.5rem;\n    margin-top: 0.55rem;\n}\n.actor-card {\n    border-radius: 14px;\n    min-height: 88px;\n    padding: 0.38rem 0.36rem 0.44rem;\n}\n.actor-card::after {\n    bottom: -16px;\n    height: 16px;\n}\n.actor-avatar {\n    font-size: 1.65rem;\n    height: 2.55rem;\n    width: 2.55rem;\n}\n.actor-name {\n    font-size: 0.74rem;\n    margin-top: 0.2rem;\n}\n.actor-detail {\n    font-size: 0.6rem;\n    line-height: 1.12;\n    margin-top: 0.14rem;\n    min-height: 1.35rem;\n}\n.speaking-pill {\n    font-size: 0.52rem;\n    margin-top: 0.14rem;\n}\n.held-prop {\n    margin-top: 0.18rem;\n}\n.held-prop span {\n    font-size: 0.55rem;\n}\n.stage-events {\n    gap: 0.24rem;\n    margin-top: 0.46rem;\n}\n.audience-action,\n.prop-pile {\n    font-size: 0.78rem;\n    max-width: 39rem;\n    padding: 0.22rem 0.5rem;\n}\n.prop-token {\n    margin: 0.08rem;\n    padding: 0.12rem 0.4rem;\n}\n.beat-counter {\n    font-size: 0.84rem;\n    margin-top: 0.34rem;\n}\n.stage-floorboards {\n    height: 40px;\n}\n.control-panel {\n    margin-top: 0 !important;\n    padding: 0.42rem;\n}\n.control-panel h3 {\n    margin-bottom: 0.2rem;\n}\n.gradio-container .row {\n    gap: 0.55rem !important;\n}\n.stage-output + .row,\n.stage-output + div,\n.control-panel + .control-panel {\n    margin-top: 0.45rem !important;\n}\n.transcript-section,\n.gradio-container .accordion {\n    margin-top: 0.55rem !important;\n}\n@media (max-width: 760px) {\n    .puppet-stage {\n        min-height: 430px;\n    }\n    .stage-backdrop {\n        padding: 0.52rem 2.15rem;\n    }\n    .actor-row {\n        grid-template-columns: repeat(2, minmax(0, 1fr));\n    }\n    .speech-line {\n        font-size: 0.8rem;\n    }\n}\n\"\"\"\n\n\ndef render_stage(session: TheaterSession | None) -> str:\n    if session is None:\n        return EMPTY_STAGE\n\n    actor_cards = []\n    latest_beat = session.transcript[-1] if session.transcript else None\n    latest_speaker = latest_beat.speaker if latest_beat else None\n    for actor in session.actors:\n        active_class = \" active\" if actor.name == latest_speaker else \"\"\n        active_label = '<div class=\"speaking-pill\">Now speaking</div>' if actor.name == latest_speaker else \"\"\n        role_line = actor.goal.split(\".\", maxsplit=1)[0]\n        held_prop = actor.held_prop or \"nothing\"\n        held_emoji = PROP_EMOJI.get(held_prop.lower(), \"🎁\") if actor.held_prop else \"\"\n        actor_cards.append(\n            f\"\"\"\n            <div class=\"actor-card{active_class}\">\n              <div class=\"actor-avatar\">{escape(actor.avatar)}</div>\n              <div class=\"actor-name\">{escape(actor.name)}</div>\n              {active_label}\n              <div class=\"actor-detail\">{escape(role_line)}</div>\n              <div class=\"held-prop\"><span>Holding: {escape((held_emoji + \" \") if held_emoji else \"\")}{escape(held_prop)}</span></div>\n            </div>\n            \"\"\"\n        )\n    latest_line = \"\"\n    if latest_beat is not None:\n        latest_line = f\"\"\"\n        <div class=\"speech-bubble\">\n          <div class=\"speech-speaker\">{escape(latest_beat.speaker)}</div>\n          <div class=\"speech-line\">{escape(latest_beat.line)}</div>\n        </div>\n        \"\"\"\n    audience_action = \"\"\n    if session.latest_audience_action is not None:\n        audience_action = f\"\"\"\n        <div class=\"audience-action\">\n          <strong>Audience:</strong> {escape(session.latest_audience_action)}\n        </div>\n        \"\"\"\n    prop_pile = \"\"\n    if session.props:\n        prop_tokens = \"\".join(\n            f'<span class=\"prop-token\">{escape(PROP_EMOJI.get(prop.lower(), \"🎁\"))} {escape(prop)}</span>'\n            for prop in session.props\n        )\n        prop_pile = f\"\"\"\n        <div class=\"prop-pile\">\n          <strong>Props on stage:</strong> {prop_tokens}\n        </div>\n        \"\"\"\n\n    return f\"\"\"\n    <div class=\"puppet-stage stage-live\">\n      <div class=\"stage-valance\"></div>\n      <div class=\"stage-backdrop\">\n        <div class=\"stage-marquee\">{escape(session.show_title)}</div>\n        <div class=\"stage-copy\">\n          <strong>Setting:</strong> {escape(session.setting)}<br />\n          <strong>Premise:</strong> {escape(session.premise)}\n        </div>\n        {latest_line}\n        <div class=\"actor-row\">\n          {''.join(actor_cards)}\n        </div>\n        <div class=\"stage-events\">\n          {audience_action}\n          {prop_pile}\n        </div>\n        <div class=\"beat-counter\">Beat {session.beat_index} of {session.max_beats}</div>\n      </div>\n      <div class=\"stage-floorboards\"></div>\n    </div>\n    \"\"\"\n\n\ndef render_transcript(session: TheaterSession | None) -> str:\n    if session is None:\n        return EMPTY_TRANSCRIPT\n\n    transcript_lines = [\n        \"Transcript:\",\n        \"No puppet lines yet. The first beat will be added in the next milestone.\",\n    ]\n    if session.transcript:\n        transcript_lines = [\"Transcript:\"]\n        for index, beat in enumerate(session.transcript, start=1):\n            transcript_lines.append(f\"{index}. {beat.speaker}: {beat.line}\")\n\n    return \"\\n\".join(transcript_lines)\n\n\ndef render_director_log(session: TheaterSession | None) -> str:\n    if session is None:\n        return EMPTY_DIRECTOR_LOG\n    return \"\\n\".join(f\"- {entry}\" for entry in session.director_log)\n\n\ndef render_trace(session: TheaterSession | None) -> str:\n    if session is None:\n        return EMPTY_TRACE\n    return \"\\n\".join(f\"- {entry}\" for entry in session.trace_events)\n\n\ndef normalize_backend_name(backend_name: str | None) -> str:\n    return backend_name if backend_name in BACKEND_CHOICES else \"determinist",159      "app_signals": "render_stage session render_notes create_show premise reset_show advance_one_beat advance_full_act throw_audience_prop prop_name summon_audience_actor actor_name request_audience_finale AI Puppet Theater Enter a premise and create a show. No show yet. The transcript will appear here. director_lines.extend join premise.strip create_show_from_premise run_one_beat throw_prop summon_actor request_finale gr.Blocks title gr.State gr.Markdown gr.HTML value label gr.Textbox lines interactive create_button.click inputs outputs run_one_button.click run_full_button.click throw_prop_button.click summon_actor_button.click request_finale_button.click reset_button.click __main__ app.launch css actor_cards.append Setting: Premise: Beat of Transcript: No puppet lines yet. The first beat will be added in the next milestone. enumerate start Director Log: # AI Puppet Theater Create a tiny improv stage from a premise. This public shell is ready for puppet casting, short scenes, audience interruptions, and behind-the-scenes traces in later milestones. gr.Row placeholder gr.Button variant gr.Dropdown choices allow_custom_value none active Now speaking Latest: Audience: Props on stage: escape transcript_lines.append Trace Events: No premise yet. Add a premise to raise the curtain. Create a show before running a beat. sleep Create a show before throwing a prop. Create a show before summoning an actor. Create a show before requesting a finale. AI Puppet Theater Create Show Run One Beat Run Full Act Reset Throw Prop Summon Actor Request Finale Stage Transcript <div class=\"actor-card \"> Goal: Style: Tools: Holding: - Create a show before running the full act. Premise A moon detective interrogates a suspicious toaster... primary rubber duck Prop Professor Button , . : egg flowers tomato tiny crown scroll nothing",160      "readme_len": 107,161      "app_source_len": 24000,162      "app_signals_len": 1814163    },164    {165      "id": "build-small-hackathon/ai-study-buddy",166      "title": "Ai Study Buddy",167      "summary": "AI Study Buddy — your smart learning companion 📚 ",168      "tags": [169        "gradio",170        "region:us"171      ],172      "models": [],173      "datasets": [],174      "sdk": "gradio",175      "license": "apache-2.0",176      "likes": 1,177      "url": "https://huggingface.co/spaces/build-small-hackathon/ai-study-buddy",178      "app_file": "app.py",179      "readme_raw": "---\ntitle: Ai Study Buddy\nemoji: 📉\ncolorFrom: blue\ncolorTo: blue\nsdk: gradio\nsdk_version: 6.15.2\npython_version: '3.13'\napp_file: app.py\npinned: false\nlicense: apache-2.0\nshort_description: 'AI Study Buddy — your smart learning companion 📚 '\n---\n\nCheck out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference",180      "readme_body": "Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference",181      "readme_frontmatter": {182        "title": "Ai Study Buddy",183        "emoji": "📉",184        "colorFrom": "blue",185        "colorTo": "blue",186        "sdk": "gradio",187        "sdk_version": "6.15.2",188        "python_version": "3.13",189        "app_file": "app.py",190        "pinned": "false",191        "license": "apache-2.0",192        "short_description": "AI Study Buddy — your smart learning companion 📚 "193      },194      "app_source": "import gradio as gr\nimport os\nimport spaces\nfrom huggingface_hub import InferenceClient\n\n# -----------------------------\n# 🔑 API KEY FIXED\n# -----------------------------\nclient = InferenceClient(\n    model=\"meta-llama/Llama-3.1-8B-Instruct\",\n    token=os.getenv(\"API_KEY\")   # 👈 FIXED NAME (recommended)\n)\n\n# -----------------------------\n# SYSTEM PROMPT\n# -----------------------------\nSYSTEM_PROMPT = \"\"\"\nYou are AI Study Buddy, created by Areeba Iqbal.\n\nRules:\n- Always explain step-by-step\n- Give examples\n- Be clear and student-friendly\n- If asked who created you: \"I am AI Study Buddy, created by Areeba Iqbal.\"\n\"\"\"\n\n# -----------------------------\n# MODE CONTROL\n# -----------------------------\ndef build_prompt(message, mode):\n    mode_prompts = {\n        \"📚 Study Mode\": \"Explain simply for students with examples.\",\n        \"💻 Coding Mode\": \"Act as a senior programmer. Debug and improve code.\",\n        \"🧮 Math Solver\": \"Solve step-by-step with explanation.\",\n        \"📝 Exam Prep\": \"Give short exam-focused answers.\"\n    }\n\n    return f\"\"\"\n{SYSTEM_PROMPT}\n\nMode: {mode_prompts.get(mode, \"\")}\n\nUser Question:\n{message}\n\"\"\"\n\n# -----------------------------\n# MAIN CHAT FUNCTION\n# -----------------------------\n@spaces.GPU\ndef get_response(message, history, mode):\n\n    messages = [{\"role\": \"system\", \"content\": SYSTEM_PROMPT}]\n\n    for msg in history:\n        messages.append(msg)\n\n    messages.append({\"role\": \"user\", \"content\": build_prompt(message, mode)})\n\n    try:\n        response = client.chat_completion(\n            messages=messages,\n            max_tokens=1024,\n            temperature=0.7\n        )\n\n        return response.choices[0].message.content\n\n    except Exception as e:\n        return f\"❌ Error: {e}\"\n\n\n# -----------------------------\n# QUICK ACTIONS\n# -----------------------------\ndef summarize(text):\n    return client.chat_completion(\n        messages=[{\"role\": \"user\", \"content\": \"Summarize: \" + text}],\n        max_tokens=500\n    ).choices[0].message.content\n\n\ndef quiz(text):\n    return client.chat_completion(\n        messages=[{\"role\": \"user\", \"content\": \"Generate 5 MCQs: \" + text}],\n        max_tokens=500\n    ).choices[0].message.content\n\n\ndef simple(text):\n    return client.chat_completion(\n        messages=[{\"role\": \"user\", \"content\": \"Explain simply: \" + text}],\n        max_tokens=500\n    ).choices[0].message.content\n\n\ndef study_plan(text):\n    return client.chat_completion(\n        messages=[{\"role\": \"user\", \"content\": f\"Make 7-day study plan for: {text}\"}],\n        max_tokens=700\n    ).choices[0].message.content\n\n\n# -----------------------------\n# UI\n# -----------------------------\ncss = \"\"\"\n.main-container {\n    max-width: 900px;\n    margin: auto;\n}\n#title { text-align:center; }\n#subtitle { text-align:center; color:gray; }\n#footer { text-align:center; color:gray; font-size:14px; }\n\"\"\"\n\nwith gr.Blocks(\n    theme=gr.themes.Soft(),\n    css=css,\n    title=\"AI Study Buddy\"\n) as demo:\n\n    gr.HTML(\"\"\"\n    <div class=\"main-container\">\n        <h1 id=\"title\">📚 AI Study Buddy</h1>\n        <p id=\"subtitle\">Learn smarter with AI-powered guidance</p>\n    </div>\n    \"\"\")\n\n    # ---------------- MODE SELECT ----------------\n    mode = gr.Radio(\n        [\"📚 Study Mode\", \"💻 Coding Mode\", \"🧮 Math Solver\", \"📝 Exam Prep\"],\n        value=\"📚 Study Mode\",\n        label=\"Select Mode\"\n    )\n\n    # ---------------- CHAT ----------------\n    chatbot = gr.ChatInterface(\n        fn=get_response,\n        additional_inputs=[mode],\n        examples=[\n            [\"Explain recursion\"],\n            [\"Solve quadratic equation\"],\n            [\"What is AI?\"],\n            [\"Debug Python code\"]\n        ]\n    )\n\n    # ---------------- QUICK ACTIONS ----------------\n    gr.Markdown(\"## ⚡ Quick Actions\")\n\n    quick_input = gr.Textbox(label=\"Quick Input\")\n\n    with gr.Row():\n        gr.Button(\"📖 Summarize\").click(summarize, quick_input, gr.Textbox())\n        gr.Button(\"📝 Quiz\").click(quiz, quick_input, gr.Textbox())\n        gr.Button(\"💡 Simple\").click(simple, quick_input, gr.Textbox())\n\n    # ---------------- STUDY PLAN ----------------\n    gr.Markdown(\"## 🗓️ Study Plan Generator\")\n\n    plan_input = gr.Textbox(label=\"Enter Topic / Exam Detail\")\n    plan_output = gr.Textbox(label=\"Plan Output\")\n\n    gr.Button(\"Generate Plan\").click(study_plan, plan_input, plan_output)\n\n    # ---------------- FOOTER ----------------\n    gr.HTML(\"\"\"\n    <div id=\"footer\">\n        Created by Areeba Iqbal\n    </div>\n    \"\"\")\n\ndemo.launch(server_name=\"0.0.0.0\", server_port=7860)",195      "app_signals": "build_prompt message mode get_response history summarize text quiz simple study_plan InferenceClient model token You are AI Study Buddy, created by Areeba Iqbal. Rules: - Always explain step-by-step - Give examples - Be clear and student-friendly - If asked who created you: \"I am AI Study Buddy, created by Areeba Iqbal.\" demo.launch server_name server_port messages.append gr.Blocks theme css title gr.HTML gr.Radio value label gr.ChatInterface fn additional_inputs examples gr.Markdown gr.Textbox click meta-llama/Llama-3.1-8B-Instruct os.getenv 📚 Study Mode 💻 Coding Mode 🧮 Math Solver 📝 Exam Prep Explain simply for students with examples. Act as a senior programmer. Debug and improve code. Solve step-by-step with explanation. Give short exam-focused answers. Mode: User Question: client.chat_completion messages max_tokens temperature 📚 AI Study Buddy Learn smarter with AI-powered guidance ## ⚡ Quick Actions gr.Row ## 🗓️ Study Plan Generator Created by Areeba Iqbal 0.0.0.0 API_KEY mode_prompts.get role content system user gr.themes.Soft AI Study Buddy Select Mode Quick Input Enter Topic / Exam Detail Plan Output gr.Button ❌ Error: Generate Plan Explain recursion Solve quadratic equation What is AI? Debug Python code 📖 Summarize 📝 Quiz 💡 Simple Summarize: Generate 5 MCQs: Explain simply: Make 7-day study plan for:",196      "readme_len": 96,197      "app_source_len": 4509,198      "app_signals_len": 1330199    },200    {201      "id": "build-small-hackathon/amnesiac",202      "title": "AMNESIAC",203      "summary": "Reverse-Turing webcam interrogation game.",204      "tags": [205        "gradio",206        "region:us"207      ],208      "models": [],209      "datasets": [],210      "sdk": "gradio",211      "license": "apache-2.0",212      "likes": 0,213      "url": "https://huggingface.co/spaces/build-small-hackathon/amnesiac",214      "app_file": "app.py",215      "readme_raw": "---\ntitle: AMNESIAC\nemoji: 🪞\ncolorFrom: gray\ncolorTo: red\nsdk: gradio\nsdk_version: 5.50.0\npython_version: \"3.10\"\napp_file: app.py\nlicense: apache-2.0\nshort_description: Reverse-Turing webcam interrogation game.\nheader: mini\nfullWidth: true\n---\n\n# AMNESIAC\n\nAMNESIAC is a reverse-Turing interrogation game for the Hugging Face build-small-hackathon.\n\nThis repository is being built top-down from `RESEARCH.md`, `FEATURES.md`, `ARCHITECTURE.md`, and `PLAN.md`.\n\nThe entrypoint now follows the Gradio 5.x + FastAPI + FastRTC deployment pattern locked in\n`ARCHITECTURE.md` §1.1: one FastAPI process serves the static frontend, mounts FastRTC for the\nmedia plane, and mounts a minimal Gradio app for hackathon compliance.\n",216      "readme_body": "# AMNESIAC\n\nAMNESIAC is a reverse-Turing interrogation game for the Hugging Face build-small-hackathon.\n\nThis repository is being built top-down from `RESEARCH.md`, `FEATURES.md`, `ARCHITECTURE.md`, and `PLAN.md`.\n\nThe entrypoint now follows the Gradio 5.x + FastAPI + FastRTC deployment pattern locked in\n`ARCHITECTURE.md` §1.1: one FastAPI process serves the static frontend, mounts FastRTC for the\nmedia plane, and mounts a minimal Gradio app for hackathon compliance.",217      "readme_frontmatter": {218        "title": "AMNESIAC",219        "emoji": "🪞",220        "colorFrom": "gray",221        "colorTo": "red",222        "sdk": "gradio",223        "sdk_version": "5.50.0",224        "python_version": "3.10",225        "app_file": "app.py",226        "license": "apache-2.0",227        "short_description": "Reverse-Turing webcam interrogation game.",228        "header": "mini",229        "fullWidth": "true"230      },231      "app_source": "from __future__ import annotations\n\nimport os\n\nimport uvicorn\n\nfrom server.webapp import create_application\n\n\nSERVER_PORT = int(os.getenv(\"PORT\", \"7860\"))\napp, worker, stream = create_application(\n    include_gradio=True,\n    server_port=SERVER_PORT,\n)\n\n\nif __name__ == \"__main__\":\n    uvicorn.run(app, host=\"0.0.0.0\", port=SERVER_PORT)\n",232      "app_signals": "int create_application include_gradio server_port os.getenv __main__ uvicorn.run host port PORT 7860 0.0.0.0",233      "readme_len": 471,234      "app_source_len": 337,235      "app_signals_len": 108236    },237    {238      "id": "build-small-hackathon/attention-firewall",239      "title": "Attention Firewall",240      "summary": "",241      "tags": [242        "gradio",243        "region:us"244      ],245      "models": [],246      "datasets": [],247      "sdk": "gradio",248      "license": "",249      "likes": 0,250      "url": "https://huggingface.co/spaces/build-small-hackathon/attention-firewall",251      "app_file": "app.py",252      "readme_raw": "---\ntitle: Attention Firewall\ncolorFrom: indigo\ncolorTo: green\nsdk: gradio\nsdk_version: 6.16.0\napp_file: app.py\npinned: false\npython_version: 3.14\n---\n\n# Attention Firewall\n\nMVP 1 is a deployment skeleton for a future attention triage workflow. It provides a small chat-style Gradio interface that accepts chaotic work context and returns deterministic placeholder text.\n\nThis version does not perform model inference, graph extraction, llama.cpp execution, Mellea validation, or markdown daemon updates.\n\n## Local Development\n\nInstall dependencies:\n\n```bash\nuv sync\n```\n\nRun the app:\n\n```bash\nuv run python app.py\n```\n\nThe canonical public Space is:\n\n```text\nhttps://huggingface.co/spaces/build-small-hackathon/attention-firewall\n```\n\nThe running app URL is:\n\n```text\nhttps://build-small-hackathon-attention-firewall.hf.space\n```\n",253      "readme_body": "# Attention Firewall\n\nMVP 1 is a deployment skeleton for a future attention triage workflow. It provides a small chat-style Gradio interface that accepts chaotic work context and returns deterministic placeholder text.\n\nThis version does not perform model inference, graph extraction, llama.cpp execution, Mellea validation, or markdown daemon updates.\n\n## Local Development\n\nInstall dependencies:\n\n```bash\nuv sync\n```\n\nRun the app:\n\n```bash\nuv run python app.py\n```\n\nThe canonical public Space is:\n\n```text\nhttps://huggingface.co/spaces/build-small-hackathon/attention-firewall\n```\n\nThe running app URL is:\n\n```text\nhttps://build-small-hackathon-attention-firewall.hf.space\n```",254      "readme_frontmatter": {255        "title": "Attention Firewall",256        "colorFrom": "indigo",257        "colorTo": "green",258        "sdk": "gradio",259        "sdk_version": "6.16.0",260        "app_file": "app.py",261        "pinned": "false",262        "python_version": "3.14"263      },264      "app_source": "from __future__ import annotations\n\nimport gradio as gr\n\n\nEMPTY_RESPONSE = (\n    \"Paste a short snapshot of your current work context so the MVP 1 skeleton \"\n    \"can acknowledge it.\"\n)\n\n\ndef respond(message: str, history: list[dict[str, str]] | None = None) -> str:\n    \"\"\"Return deterministic MVP 1 placeholder text for the chat interface.\"\"\"\n    del history\n\n    context = message.strip()\n    if not context:\n        return EMPTY_RESPONSE\n\n    word_count = len(context.split())\n    char_count = len(context)\n    return (\n        \"Attention Firewall MVP 1 received your work context.\\n\\n\"\n        f\"- Snapshot size: {word_count} words, {char_count} characters.\\n\"\n        \"- Current behavior: deterministic deployment skeleton response.\\n\"\n        \"- Later MVPs will add structured firewall processing after the Space \"\n        \"foundation is verified.\"\n    )\n\n\ndef build_demo() -> gr.ChatInterface:\n    return gr.ChatInterface(\n        fn=respond,\n        title=\"Attention Firewall\",\n        description=(\n            \"Paste chaotic work context and get a deterministic MVP 1 skeleton \"\n            \"acknowledgement.\"\n        ),\n        examples=[\n            \"I have three urgent threads, a half-written spec, and unclear review feedback.\",\n            \"My deployment is blocked, notes are scattered, and I need the next concrete action.\",\n        ],\n        textbox=gr.Textbox(\n            placeholder=\"Paste work context to triage later...\",\n            autofocus=True,\n            container=False,\n        ),\n    )\n\n\ndemo = build_demo()\n\n\nif __name__ == \"__main__\":\n    demo.launch()\n",265      "app_signals": "respond message history build_demo Paste a short snapshot of your current work context so the MVP 1 skeleton can acknowledge it. Return deterministic MVP 1 placeholder text for the chat interface. message.strip len gr.ChatInterface fn title description examples textbox __main__ demo.launch context.split Attention Firewall MVP 1 received your work context. - Snapshot size: words, characters. - Current behavior: deterministic deployment skeleton response. - Later MVPs will add structured firewall processing after the Space foundation is verified. Attention Firewall Paste chaotic work context and get a deterministic MVP 1 skeleton acknowledgement. gr.Textbox placeholder autofocus container I have three urgent threads, a half-written spec, and unclear review feedback. My deployment is blocked, notes are scattered, and I need the next concrete action. Paste work context to triage later...",266      "readme_len": 678,267      "app_source_len": 1591,268      "app_signals_len": 896269    },270    {271      "id": "build-small-hackathon/awaaz",272      "title": "Apni Awaaz",273      "summary": "",274      "tags": [275        "backyard-ai",276        "dubbing",277        "hindi",278        "translation",279        "tts"280      ],281      "models": [],282      "datasets": [],283      "sdk": "gradio",284      "license": "mit",285      "likes": 0,286      "url": "https://huggingface.co/spaces/build-small-hackathon/awaaz",287      "app_file": "app.py",288      "readme_raw": "---\ntitle: Apni Awaaz\nemoji: 🎙️\ncolorFrom: yellow\ncolorTo: red\nsdk: gradio\nsdk_version: 6.16.0\napp_file: app.py\npinned: false\nlicense: mit\ntags:\n- dubbing\n- hindi\n- translation\n- tts\n- backyard-ai\n---\n\n# 🎙️ Apni Awaaz\n\n**Dub English video into the Hindi people actually speak.**\n\nMost Hindi dubs use शुद्ध हिंदी — stiff, Sanskritized language no one speaks at home.  \nApni Awaaz translates into everyday Hindustani — the natural mix your family actually uses.\n\n| Official dub | Apni Awaaz |\n|---|---|\n| \"मुझे इस विषय पर विचार करने दीजिए\" | \"सोचने दे एक second\" |\n| \"यह अत्यंत मूल्यवान है\" | \"बहुत महँगा है यार\" |\n\n## Pipeline\n\n1. **Whisper medium** — transcribe English with timestamps  \n2. **Qwen 2.5 7B** — translate to colloquial Hindi (the magic layer)  \n3. **Edge TTS** — generate natural Hindi speech  \n4. **ffmpeg** — stitch and merge back onto video  \n\nTotal: ~8B params (well under the 32B cap)\n\nBuilt for the [Build Small Hackathon](https://huggingface.co/build-small-hackathon) · Backyard AI track",289      "readme_body": "# 🎙️ Apni Awaaz\n\n**Dub English video into the Hindi people actually speak.**\n\nMost Hindi dubs use शुद्ध हिंदी — stiff, Sanskritized language no one speaks at home.  \nApni Awaaz translates into everyday Hindustani — the natural mix your family actually uses.\n\n| Official dub | Apni Awaaz |\n|---|---|\n| \"मुझे इस विषय पर विचार करने दीजिए\" | \"सोचने दे एक second\" |\n| \"यह अत्यंत मूल्यवान है\" | \"बहुत महँगा है यार\" |\n\n## Pipeline\n\n1. **Whisper medium** — transcribe English with timestamps  \n2. **Qwen 2.5 7B** — translate to colloquial Hindi (the magic layer)  \n3. **Edge TTS** — generate natural Hindi speech  \n4. **ffmpeg** — stitch and merge back onto video  \n\nTotal: ~8B params (well under the 32B cap)\n\nBuilt for the [Build Small Hackathon](https://huggingface.co/build-small-hackathon) · Backyard AI track",290      "readme_frontmatter": {291        "title": "Apni Awaaz",292        "emoji": "🎙️",293        "colorFrom": "yellow",294        "colorTo": "red",295        "sdk": "gradio",296        "sdk_version": "6.16.0",297        "app_file": "app.py",298        "pinned": "false",299        "license": "mit",300        "tags": ""301      },302      "app_source": "\"\"\"\nApni Awaaz 🎙️ — Dub English video into the Hindi people actually speak.\nBuilt for the Build Small Hackathon (June 2026).\n\"\"\"\n\nimport gradio as gr\nimport spaces\nimport torch\nimport edge_tts\nimport asyncio\nimport subprocess\nimport tempfile\nimport os\nfrom pathlib import Path\nfrom transformers import (\n    AutoModelForCausalLM,\n    AutoTokenizer,\n    pipeline,\n    BitsAndBytesConfig,\n)\n\n# ╔══════════════════════════════════════════════════════════════╗\n# ║  THE PROMPT — this is the soul of the entire project        ║\n# ╚══════════════════════════════════════════════════════════════╝\n\nSYSTEM_PROMPT = \"\"\"You are a dubbing translator. You translate English dialogue into the Hindi that real people actually speak at home in North India — not the stiff, Sanskritized Hindi of Doordarshan or official dubs.\n\nRULES:\n1. Use everyday Hindustani — the natural Hindi-Urdu mix people really speak.\n2. NEVER use Sanskritized/शुद्ध words when a simpler one exists:\n   - \"प्राप्त करना\" → \"मिलना\" / \"पाना\"\n   - \"आवश्यक\" → \"ज़रूरी\"\n   - \"अत्यंत\" → \"बहुत\" / \"काफ़ी\"\n   - \"उपयोग\" → \"इस्तेमाल\"\n   - \"विचार करना\" → \"सोचना\"\n   - \"संपन्न करना\" → \"करना\" / \"निपटाना\"\n   - \"प्रतीक्षा\" → \"इंतज़ार\"\n   - \"शीघ्र\" → \"जल्दी\"\n   - \"अनुमति\" → \"इजाज़त\"\n   - \"कृपया\" → drop it or say \"please\"\n   - \"अवश्य\" → \"ज़रूर\"\n   - \"उचित\" → \"सही\" / \"ठीक\"\n3. Keep English words Indians naturally keep: phone, office, meeting, tension, problem, time, chance, try, plan, sure, okay, sorry, thanks, bus, train, college, hospital, doctor, ticket, report, file.\n4. Match the speaker's register. Casual stays casual, serious stays serious — but never sound like a newsreader.\n5. Use natural fillers where they fit: \"यार\", \"अरे\", \"बस\", \"ना\", \"वो\", \"मतलब\", \"basically\".\n6. Natural contractions: \"कर लेंगे\" not \"कर लिया जाएगा\", \"हो जाएगा\" not \"संपन्न हो जाएगा\".\n7. Keep it CONCISE. Dubbed Hindi should be roughly the same length as the English. Don't pad.\n\nEXAMPLES:\nEN: \"I need to get this done before the deadline\"\n❌ \"मुझे समय-सीमा से पूर्व यह कार्य संपन्न करना आवश्यक है\"\n✅ \"deadline से पहले ये निपटाना पड़ेगा\"\n\nEN: \"That's a really good point, I hadn't thought about that\"\n❌ \"यह एक अत्यंत उत्तम विचार है, मैंने इस पर विचार नहीं किया था\"\n✅ \"अच्छी बात बोली, मेरे दिमाग़ में आया ही नहीं\"\n\nEN: \"We should probably reconsider our approach\"\n❌ \"हमें अपनी कार्यप्रणाली पर पुनर्विचार करना चाहिए\"\n✅ \"लगता है अपना तरीका बदलना पड़ेगा\"\n\nEN: \"I'm really sorry, I completely forgot about our meeting\"\n❌ \"मुझे अत्यंत खेद है, मैं हमारी बैठक के विषय में पूर्णतः विस्मृत हो गया\"\n✅ \"sorry यार, meeting पूरी तरह भूल गया\"\n\nEN: \"Can you give me a moment? I need to think about this\"\n❌ \"क्या आप मुझे कुछ क्षण प्रदान कर सकते हैं? मुझे इस विषय पर विचार करना है\"\n✅ \"एक second दे, सोचने दे\"\n\nEN: \"The situation is getting worse and we need to act fast\"\n❌ \"स्थिति बिगड़ती जा रही है और हमें शीघ्र कार्रवाई करनी चाहिए\"\n✅ \"हालात ख़राब हो रहे हैं, जल्दी कुछ करना पड़ेगा\"\n\nEN: \"I don't think that's going to work. Let me try something else.\"\n❌ \"मुझे नहीं लगता कि यह कार्य करेगा। मुझे कोई अन्य विकल्प आज़माने दीजिए।\"\n✅ \"ये नहीं चलेगा। कुछ और try करता हूँ।\"\n\nEN: \"Look, I understand your concern, but we don't have a choice here\"\n❌ \"देखिए, मैं आपकी चिंता समझता हूँ, परंतु हमारे पास यहाँ कोई विकल्प नहीं है\"\n✅ \"देख, तेरी tension समझता हूँ, पर कोई चारा नहीं है\"\n\nTranslate ONLY the given English text. Output ONLY the Hindi. No commentary.\"\"\"\n\n\n# ╔══════════════════════════════════════════════════════════════╗\n# ║  MODEL LOADING                                              ║\n# ╚══════════════════════════════════════════════════════════════╝\n\n# -- Globals (loaded once, reused) --\nwhisper_pipe = None\nllm_model = None\nllm_tokenizer = None\n\n\ndef load_whisper():\n    \"\"\"Load Whisper on CPU. ZeroGPU moves it when @spaces.GPU fires.\"\"\"\n    global whisper_pipe\n    if whisper_pipe is None:\n        print(\"⏳ Loading Whisper...\")\n        whisper_pipe = pipeline(\n            \"automatic-speech-recognition\",\n            model=\"openai/whisper-medium\",\n            torch_dtype=torch.float16,\n            device=\"cpu\",\n        )\n        print(\"✅ Whisper loaded (CPU, will move to GPU at runtime)\")\n    return whisper_pipe\n\n\ndef load_llm():\n    \"\"\"\n    Load Qwen 2.5 7B in 4-bit.\n    Called inside @spaces.GPU so device_map=\"auto\" lands on the A100.\n    \"\"\"\n    global llm_model, llm_tokenizer\n    if llm_model is None:\n        print(\"⏳ Loading Qwen 2.5 7B...\")\n        model_id = \"Qwen/Qwen2.5-7B-Instruct\"\n\n        bnb_cfg = BitsAndBytesConfig(\n            load_in_4bit=True,\n            bnb_4bit_compute_dtype=torch.float16,\n            bnb_4bit_quant_type=\"nf4\",\n        )\n        llm_tokenizer = AutoTokenizer.from_pretrained(model_id)\n        llm_model = AutoModelForCausalLM.from_pretrained(\n            model_id,\n            quantization_config=bnb_cfg,\n            device_map=\"auto\",\n        )\n        print(\"✅ Qwen loaded\")\n    return llm_model, llm_tokenizer\n\n\n# Pre-download weights at startup (stays on CPU, fast re-load later)\nload_whisper()\n\n\n# ╔══════════════════════════════════════════════════════════════╗\n# ║  PIPELINE STEPS                                             ║\n# ╚══════════════════════════════════════════════════════════════╝\n\n\ndef extract_audio(video_path: str, out_path: str) -> str:\n    subprocess.run(\n        [\n            \"ffmpeg\", \"-i\", video_path,\n            \"-vn\", \"-acodec\", \"pcm_s16le\", \"-ar\", \"16000\", \"-ac\", \"1\",\n            out_path, \"-y\",\n        ],\n        check=True, capture_output=True,\n    )\n    return out_path\n\n\ndef get_duration(path: str) -> float:\n    r = subprocess.run(\n        [\"ffprobe\", \"-v\", \"quiet\", \"-show_entries\", \"format=duration\",\n         \"-of\", \"csv=p=0\", path],\n        capture_output=True, text=True,\n    )\n    return float(r.stdout.strip())\n\n\ndef transcribe(audio_path: str) -> list[dict]:\n    \"\"\"→ [{\"timestamp\": (start, end), \"text\": \"...\"}]\"\"\"\n    pipe = load_whisper()\n    result = pipe(\n        audio_path,\n        return_timestamps=True,\n        chunk_length_s=30,\n        generate_kwargs={\"language\": \"en\"},\n    )\n    return result[\"chunks\"]\n\n\ndef translate_segment(text: str) -> str:\n    model, tok = load_llm()\n    messages = [\n        {\"role\": \"system\", \"content\": SYSTEM_PROMPT},\n        {\"role\": \"user\", \"content\": text},\n    ]\n    prompt = tok.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)\n    inputs = tok(prompt, return_tensors=\"pt\").to(model.device)\n\n    with torch.no_grad():\n        out = model.generate(\n            **inputs,\n            max_new_tokens=200,\n            temperature=0.3,\n            do_sample=True,\n            top_p=0.9,\n        )\n    resp = tok.decode(out[0][inputs.input_ids.shape[1]:], skip_special_tokens=True)\n    return resp.strip().split(\"\\n\")[0]  # first line only, no runaway generation\n\n\nasync def _tts(text: str, path: str, voice: str):\n    comm = edge_tts.Communicate(text, voice)\n    await comm.save(path)\n\n\ndef hindi_tts(text: str, path: str, voice: str = \"hi-IN-MadhurNeural\"):\n    asyncio.run(_tts(text, path, voice))\n    return path\n\n\ndef adjust_speed(in_path: str, out_path: str, target_sec: float) -> str:\n    \"\"\"Stretch/squeeze audio to fit the target duration (pitch-preserved).\"\"\"\n    dur = get_duration(in_path)\n    if dur <= 0 or target_sec <= 0:\n        return in_path\n    ratio = dur / target_sec\n    ratio = max(0.5, min(2.0, ratio))          # atempo range\n    subprocess.run(\n        [\"ffmpeg\", \"-i\", in_path, \"-filter:a\", f\"atempo={ratio:.4f}\",\n         \"-y\", out_path],\n        check=True, capture_output=True,\n    )\n    return out_path\n\n\ndef stitch_and_merge(\n    segments: list[dict],\n    video_path: str,\n    total_dur: float,\n    tmpdir: str,\n) -> str:\n    \"\"\"\n    Build the dubbed audio track and merge it back onto the video.\n    Uses pydub for clean overlay at exact timestamps.\n    \"\"\"\n    from pydub import AudioSegment\n\n    # silent canvas\n    base = AudioSegment.silent(duration=int(total_dur * 1000), frame_rate=24000)\n\n    for seg in segments:\n        tts_file = seg[\"tts_path\"]\n        start_ms = int(seg[\"start\"] * 1000)\n        try:\n            chunk = AudioSegment.from_file(tts_file)\n            base = base.overlay(chunk, position=start_ms)\n        except Exception as e:\n            print(f\"⚠️  overlay failed for segment at {seg['start']:.1f}s: {e}\")\n\n    dubbed_wav = os.path.join(tmpdir, \"dubbed_track.wav\")\n    base.export(dubbed_wav, format=\"wav\")\n\n    # merge onto video (keep original video stream, replace audio)\n    out_mp4 = os.path.join(tmpdir, \"output.mp4\")\n    subprocess.run(\n        [\n            \"ffmpeg\",\n            \"-i\", video_path,\n            \"-i\", dubbed_wav,\n            \"-c:v\", \"copy\",\n            \"-map\", \"0:v:0\",\n            \"-map\", \"1:a:0\",\n            \"-shortest\",\n            \"-y\", out_mp4,\n        ],\n        check=True, capture_output=True,\n    )\n    return out_mp4\n\n\n# ╔══════════════════════════════════════════════════════════════╗\n# ║  MAIN PIPELINE                                              ║\n# ╚══════════════════════════════════════════════════════════════╝\n\n\n@spaces.GPU(duration=300)\ndef dub_video(video_path: str, voice_gender: str, progress=gr.Progress()):\n    if video_path is None:\n        raise gr.Error(\"Upload a video first!\")\n\n    # ── move Whisper to the ZeroGPU A100 ──\n    pipe = load_whisper()\n    pipe.model.to(\"cuda\")\n    pipe.device = torch.device(\"cuda\")\n\n    # ── load LLM (first call downloads + quantises onto GPU) ──\n    load_llm()\n\n    voice = \"hi-IN-MadhurNeural\" if voice_gender == \"Male\" else \"hi-IN-SwaraNeural\"\n    tmpdir = tempfile.mkdtemp(prefix=\"apni_\")\n\n    # 1 ── extract audio\n    progress(0.05, desc=\"🎵 Extracting audio…\")\n    raw_audio = extract_audio(video_path, os.path.join(tmpdir, \"raw.wav\"))\n    total_dur = get_duration(raw_audio)\n\n    # safety: reject clips > 3 min to stay within GPU budget\n    if total_dur > 180:\n        raise gr.Error(\"Please keep clips under 3 minutes for now.\")\n\n    # 2 ── transcribe\n    progress(0.15, desc=\"👂 Listening to English…\")\n    chunks = transcribe(raw_audio)\n    if not chunks:\n        raise gr.Error(\"Couldn't detect any speech. Try a clearer clip.\")\n\n    # 3 ── translate + TTS each segment\n    translated = []\n    n = len(chunks)\n    for i, ch in enumerate(chunks):\n        frac = 0.2 + 0.6 * (i / n)\n        progress(frac, desc=f\"🗣️ Dubbing segment {i + 1}/{n}…\")\n\n        start, end = ch[\"timestamp\"]\n        if start is None or end is None:\n            continue\n        seg_dur = end - start\n        if seg_dur <= 0:\n            continue\n\n        # translate\n        hindi = translate_segment(ch[\"text\"])\n\n        # TTS\n        tts_raw = os.path.join(tmpdir, f\"tts_{i}.mp3\")\n        hindi_tts(hindi, tts_raw, voice)\n\n        # speed-adjust to fit original segment window\n        tts_adj = os.path.join(tmpdir, f\"tts_adj_{i}.wav\")\n        adjust_speed(tts_raw, tts_adj, seg_dur)\n\n        translated.append({\n            \"start\": start,\n            \"end\": end,\n            \"en\": ch[\"text\"],\n            \"hi\": hindi,\n            \"tts_path\": tts_adj,\n        })\n\n    # 4 ── stitch + merge\n    progress(0.85, desc=\"🎬 Stitching final video…\")\n    output_video = stitch_and_merge(translated, video_path, total_dur, tmpdir)\n\n    # 5 ── build comparison log\n    log_lines = []\n    for s in translated:\n        log_lines.append(\n            f\"[{s['start']:.1f}s → {s['end']:.1f}s]\\n\"\n            f\"  🇬🇧  {s['en']}\\n\"\n            f\"  🇮🇳  {s['hi']}\"\n        )\n    log = \"\\n\\n\".join(log_lines)\n\n    return output_video, log\n\n\n# ╔══════════════════════════════════════════════════════════════╗\n# ║  GRADIO UI                                                  ║\n# ╚══════════════════════════════════════════════════════════════╝\n\nCSS = \"\"\"\n.main-title {\n    text-align: center;\n    margin-bottom: 0.2em;\n}\n.subtitle {\n    text-align: center;\n    opacity: 0.7;\n    font-size: 1.05em;\n    margin-top: 0;\n}\n.example-row {\n    background: var(--block-background-fill);\n    border-radius: 8px;\n    padding: 12px 16px;\n    margin: 6px 0;\n    font-size: 0.92em;\n}\nfooter { display: none !important; }\n\"\"\"\n\nwith gr.Blocks(title=\"Apni Awaaz\", css=CSS, theme=gr.themes.Soft()) as demo:\n\n    gr.Markdown(\n        \"# 🎙️ Apni Awaaz\\n\"\n        \"#### Dub English video into the Hindi people actually speak\",\n        elem_classes=\"main-title\",\n    )\n    gr.Markdown(\n        '_No more \"मुझे यह कार्य संपन्न करना आवश्यक है\"_ — '\n        '_just \"ये करना पड़ेगा यार\"_',\n        elem_classes=\"subtitle\",\n    )\n\n    with gr.Row(equal_height=True):\n        # ── left column: inputs ──\n        with gr.Column(scale=1):\n            vid_in = gr.Video(label=\"Upload an English clip (< 3 min)\")\n            voice_radio = gr.Radio(\n                [\"Male\", \"Female\"],\n                value=\"Male\",\n                label=\"Hindi voice\",\n            )\n            btn = gr.Button(\"🎬  Dub it in apni bhasha!\", variant=\"primary\", size=\"lg\")\n\n        # ── right column: outputs ──\n        with gr.Column(scale=1):\n            vid_out = gr.Video(label=\"Dubbed output\")\n            log_box = gr.Textbox(\n                label=\"Translation log  (EN → HI)\",\n                lines=12,\n                interactive=False,\n                show_copy_button=True,\n            )\n\n    # ── \"what it does\" section ──\n    with gr.Accordion(\"How is this different from normal dubbing?\", open=False):\n        gr.Markdown(\n            \"Most Hindi dubs use **शुद्ध हिंदी** — overly formal, Sanskritized language \"\n            \"that nobody actually speaks at home.\\n\\n\"\n            \"Apni Awaaz translates into **everyday Hindustani** — the natural mix of \"\n            \"Hindi, Urdu, and English that your family actually uses at the dinner table.\\n\\n\"\n            \"| Official dub | Apni Awaaz |\\n\"\n            \"|---|---|\\n\"\n            '| \"मुझे इस विषय पर विचार करने दीजिए\" | \"सोचने दे एक second\" |\\n'\n            '| \"यह अत्यंत मूल्यवान है\" | \"बहुत महँगा है यार\" |\\n'\n            '| \"कृपया मुझे अनुमति प्रदान करें\" | \"please, करने दे ना\" |\\n'\n        )\n\n    btn.click(\n        fn=dub_video,\n        inputs=[vid_in, voice_radio],\n        outputs=[vid_out, log_box],\n    )\n\n\nif __name__ == \"__main__\":\n    demo.launch(show_api=False)\n",303      "app_signals": "load_whisper load_llm extract_audio video_path out_path get_duration path transcribe audio_path translate_segment text _tts voice hindi_tts adjust_speed in_path target_sec stitch_and_merge segments total_dur tmpdir dub_video voice_gender progress Apni Awaaz 🎙️ — Dub English video into the Hindi people actually speak. Built for the Build Small Hackathon (June 2026). You are a dubbing translator. You translate English dialogue into the Hindi that real people actually speak at home in North India — not the stiff, Sanskritized Hindi of Doordarshan or official dubs. RULES: 1. Use everyday Hindustani — the natural Hindi-Urdu mix people really speak. 2. NEVER use Sanskritized/शुद्ध words when a simpler one exists: - \"प्राप्त करना\" → \"मिलना\" / \"पाना\" - \"आवश्यक\" → \"ज़रूरी\" - \"अत्यंत\" → \"बहुत\" / \"काफ़ी\" - \"उपयोग\" → \"इस्तेमाल\" - \"विचार करना\" → \"सोचना\" - \"संपन्न करना\" → \"करना\" / \"निपटाना\" - \"प्रतीक्षा\" → \"इंतज़ार\" - \"शीघ्र\" → \"जल्दी\" - \"अनुमति\" → \"इजाज़त\" - \"कृपया\" → drop it or say \"please\" - \"अवश्य\" → \"ज़रूर\" - \"उचित\" → \"सही\" / \"ठीक\" 3. Keep English words Indians naturally keep: phone, office, meeting, tension, problem, time, chance, try, plan, sure, okay, sorry, thanks, bus, train, college, hospital, doctor, ticket, report, file. 4. Match the speaker's register. Casual stays casual, serious stays serious — but never sound like a newsreader. 5. Use natural fillers where they fit: \"यार\", \"अरे\", \"बस\", \"ना\", \"वो\", \"मतलब\", \"basically\". 6. Natural contractions: \"कर लेंगे\" not \"कर लिया जाएगा\", \"हो जाएगा\" not \"संपन्न हो जाएगा\". 7. Keep it CONCISE. Dubbed Hindi should be roughly the same length as the English. Don't pad. EXAMPLES: EN: \"I need to get this done before the deadline\" ❌ \"मुझे समय-सीमा से पूर्व यह कार्य संपन्न करना आवश्यक है\" ✅ \"deadline से पहले ये निपटाना पड़ेगा\" EN: \"That's a really good point, I hadn't thought about that\" ❌ \"यह एक अत्यंत उत्तम विचार है, मैंने इस पर विचार नहीं किया था\" ✅ \"अच्छी बात बोली, मेरे दिमाग़ में आया ही नहीं\" EN: \"We should probably reconsider our approach\" ❌ \"हमें अपनी कार्यप्रणाली पर पुनर्विचार करना चाहिए\" ✅ \"लगता है अपना तरीका बदलना पड़ेगा\" EN: \"I'm really sorry, I completely forgot about our meeting\" ❌ \"मुझे अत्यंत खेद है, मैं हमारी बैठक के विषय में पूर्णतः विस्मृत हो गया\" ✅ \"sorry यार, meeting पूरी तरह भूल गया\" EN: \"Can you give me a moment? I need to think about this\" ❌ \"क्या आप मुझे कुछ क्षण प्रदान कर सकते हैं? मुझे इस विषय पर विचार करना है\" ✅ \"एक second दे, सोचने दे\" EN: \"The situation is getting worse and we need to act fast\" ❌ \"स्थिति बिगड़ती जा रही है और हमें शीघ्र कार्रवाई करनी चाहिए\" ✅ \"हालात ख़राब हो रहे हैं, जल्दी कुछ करना पड़ेगा\" EN: \"I don't think that's going to work. Let me try something else.\" ❌ \"मुझे नहीं लगता कि यह कार्य करेगा। मुझे कोई अन्य विकल्प आज़माने दीजिए।\" ✅ \"ये नहीं चलेगा। कुछ और try करता हूँ।\" EN: \"Look, I understand your concern, but we don't have a choice here\" ❌ \"देखिए, मैं आपकी चिंता समझता हूँ, परंतु हमारे पास यहाँ कोई विकल्प नहीं है\" ✅ \"देख, तेरी tension समझता हूँ, पर कोई चारा नहीं है\" Translate ONLY the given English text. Output ONLY the Hindi. No commentary. spaces.GPU duration Load Whisper on CPU. ZeroGPU moves it when @spaces.GPU fires. Load Qwen 2.5 7B in 4-bit. Called inside @spaces.GPU so device_map=\"auto\" lands on the A100. subprocess.run check capture_output float → [{\"timestamp\": (start, end), \"text\": \"...\"}] pipe return_timestamps chunk_length_s generate_kwargs tok.apply_chat_template tokenize add_generation_prompt to tok.decode skip_special_tokens edge_tts.Communicate hi-IN-MadhurNeural asyncio.run Stretch/squeeze audio to fit the target duration (pitch-preserved). max Build the dubbed audio track and merge it back onto the video. Uses pydub for clean overlay at exact timestamps. AudioSegment.silent frame_rate os.path.join base.export format gr.Progress pipe.model.to torch.device tempfile.mkdtemp prefix desc len enumerate join gr.Blocks title css theme gr.Markdown elem_classes btn.click fn inputs outputs __main__ demo.launch show_api print pipeline model torch_dtype device Qwen/Qwen2.5-7B-Instruct BitsAndBytesConfig load_in_4bit bnb_4bit_compute_dtype bnb_4bit_quant_type AutoTokenizer.from_pretrained AutoModelForCausalLM.from_pretrained quantization_config device_map r.stdout.strip chunks torch.no_grad model.generate max_new_tokens temperature do_sample top_p split comm.save min int dubbed_track.wav output.mp4 gr.Error cuda hi-IN-SwaraNeural translated.append log_lines.append # 🎙️ Apni Awaaz #### Dub English video into the Hindi people actually speak _No more \"मुझे यह कार्य संपन्न करना आवश्यक है\"_ — _just \"ये करना पड़ेगा यार\"_ gr.Row equal_height gr.Accordion open ⏳ Loading Whisper... automatic-speech-recognition ✅ Whisper loaded (CPU, will move to GPU at runtime) ⏳ Loading Qwen 2.5 7B... ✅ Qwen loaded ffmpeg -i -vn -acodec pcm_s16le -ar 16000 -ac 1 -y ffprobe -v quiet -show_entries format=duration -of csv=p=0 role content system user tok return_tensors -filter:a tts_path AudioSegment.from_file base.overlay position wav -c:v copy -map 0:v:0 1:a:0 -shortest Upload a video first! Male apni_ 🎵 Extracting audio… raw.wav Please keep clips under 3 minutes for now. 👂 Listening to English… Couldn't detect any speech. Try a clearer clip. timestamp 🎬 Stitching final video… Apni Awaaz gr.themes.Soft main-title subtitle gr.Column scale gr.Video label gr.Radio value gr.Button variant size gr.Textbox lines interactive show_copy_button How is this different from normal dubbing? Most Hindi dubs use **शुद्ध हिंदी** — overly formal, Sanskritized language that nobody actually speaks at home. Apni Awaaz translates into **everyday Hindustani** — the natural mix of Hindi, Urdu, and English that your family actually uses at the dinner table. | Official dub | Apni Awaaz | |---|---| | \"मुझे इस विषय पर विचार करने दीजिए\" | \"सोचने दे एक second\" | | \"यह अत्यंत मूल्यवान है\" | \"बहुत महँगा है यार\" | | \"कृपया मुझे अनुमति प्रदान करें\" | \"please, करने दे ना\" | openai/whisper-medium cpu nf4 auto language en resp.strip atempo= tts_ .mp3 tts_adj_ .wav start end hi [ s → s] 🇬🇧 🇮🇳 🎬 Dub it in apni bhasha! pt 🗣️ Dubbing segment / … Upload an English clip (< 3 min) Female Hindi voice primary lg Dubbed output Translation log (EN → HI) .4f ⚠️ overlay failed for segment at s: .1f",304      "readme_len": 806,305      "app_source_len": 14088,306      "app_signals_len": 6219307    },308    {309      "id": "build-small-hackathon/Backyard-Demo-Builder",310      "title": "Backyard Demo Builder",311      "summary": "Build tiny real-person demos before scaling custom software.",312      "tags": [313        "agents",314        "ai-agents",315        "backyard-ai",316        "build-small-hackathon",317        "demo-builder",318        "gradio",319        "real-estate",320        "small-language-model"321      ],322      "models": [323        "unsloth/gemma-4-12B-it-qat-GGUF",324        "Qwen/Qwen2.5-7B-Instruct",325        "nvidia/Nemotron-3.5-Content-Safety"326      ],327      "datasets": [],328      "sdk": "gradio",329      "license": "",330      "likes": 0,331      "url": "https://huggingface.co/spaces/build-small-hackathon/Backyard-Demo-Builder",332      "app_file": "app.py",333      "readme_raw": "---\ntitle: Backyard Demo Builder\nemoji: 🏡\ncolorFrom: gray\ncolorTo: green\nsdk: gradio\nsdk_version: \"5.49.1\"\npython_version: \"3.12.12\"\napp_file: app.py\nshort_description: Build tiny real-person demos before scaling custom software.\nmodels:\n  - google/gemma-4-E4B-it\n  - Qwen/Qwen2.5-7B-Instruct\n  - nvidia/Nemotron-3.5-Content-Safety\ndatasets: []\ntags:\n  - build-small-hackathon\n  - backyard-ai\n  - gradio\n  - agents\n  - small-language-model\n  - demo-builder\n  - real-estate\n  - ai-agents\npinned: false\n---\n\n# Backyard Demo Builder\n\n## Chapter 1: Backyard AI\n\n*Build Small Hackathon 2026 — Chapter 1 Submission*\n\n`agent-swarm-workbench` now presents as **Backyard Demo Builder**: a Gradio app\nthat turns one real person's workflow into a small runnable demo package before\nanyone pays to build full software.\n\nFirst backyard case: my mom, a real-estate agent. She needs a cheap way to test\na customer follow-up reminder workflow before committing time and money to a\nfull app.\n\n---\n\n## Watch the Demo Builder Work\n\n```\nYou:     \"Build a real-estate follow-up CRM demo for my mom.\"\nBuilder: Generates a Gradio mini-app, handoff spec, field notes, and checks\nResult:  app.py, README.md, handoff_spec.md, field_notes.md\nMom:     Tests the workflow, then we scrap or scale.\n```\n\nEvery Run produces a **downloadable demo package** and Validation report: files\nyou can inspect, unzip, run, and test with the real person.\n\n---\n\n## Build Small Hackathon — Submission Notes\n\n| Requirement | How We Meet It |\n|---|---|\n| **Small model (≤ 32B)** | Provider catalog fetches models at runtime and only allows models whose ID/name proves ≤32B |\n| **Gradio app** | Custom dark-themed Gradio UI mounted on FastAPI |\n| **HF Space** | `app.py` + `requirements.txt` — one-command deploy |\n| **Demo video** | *(placeholder — [link to demo])* |\n| **Social post** | *(placeholder — [link to post])* |\n\n### Bonus Badges Claimed\n\n| Badge | Why |\n|---|---|\n| **🎨 Off-Brand** | Fully custom CSS dark theme — Archivo + IBM Plex Mono, acid green CTAs, paper/ink palette, CSS grid layout, status chips. Not a default Gradio component in sight. |\n| **📡 Sharing is Caring** | Agent traces and swarm reasoning are surfaced in the Events panel. We'll publish a trace on the Hub. |\n| **📓 Field Notes** | Generated demo packages include `field_notes.md`; this repo also documents the architecture and decisions. |\n\n---\n\n## Why This Belongs in Backyard AI\n\nThis solves a real problem for someone I know.\n\n- **Specific person** — my mom, a real-estate agent.\n- **Specific pain** — follow-up reminders and customer-care demos are useful, but custom app dev is slow and risky.\n- **Honest small-model fit** — a ≤32B model drafts the demo and handoff spec; rules handle the reminder logic.\n- **Actually testable** — the generated package includes field notes and feedback questions for the real user.\n\n---\n\n## How It Works Under the Hood\n\n```\n┌─────────────────────────────────────────────────────┐\n│  Gradio UI / HTTP API                               │\n├─────────────────────────────────────────────────────┤\n│  RunFlow — lifecycle conductor                      │\n│  ┌──────────┐  ┌────────────┐  ┌────────────────┐  │\n│  │ Swarm    │  │ Codebase   │  │ Validator      │  │\n│  │ Runtime  │→│ Archive    │→│ Graph          │  │\n│  │          │  │ Store      │  │                │  │\n│  │ Planner  │  │ (local/    │  │ Sandbox checks │  │\n│  │ Coder    │  │  Redis)    │  │ Rubric review  │  │\n│  │ Reviewer │  │            │  │ Stagehand      │  │\n│  │ Tester   │  │            │  │ (Browserbase)  │  │\n│  └──────────┘  └────────────┘  └────────────────┘  │\n│  EventBus → SSE stream to UI                       │\n└─────────────────────────────────────────────────────┘\n```\n\n### The Swarm\n\n- **Coordinator** reads the prompt, plans tasks, delegates to subagents\n- **Planner** breaks down the prompt into implementable units\n- **Coder** writes the actual code files\n- **Reviewer** checks code quality and correctness\n- **Test-runner** runs the user's tests and retries up to 3x on failure\n- **Validator-prep** generates validation checks from user criteria\n\n### The Validator\n\nAfter the swarm finishes, a LangGraph Validator workflow:\n1. Restores the codebase into a clean sandbox\n2. Runs user-provided tests\n3. Executes LLM-based rubric review\n4. (Optional) Runs Browserbase/Stagehand visual checks\n5. Produces a pass/fail Validation Report\n\n### The Sandbox\n\nAll agent work happens inside isolated sandbox workspaces:\n- **Local** (for dev/smoke tests)\n- **Docker** (container-based)\n- **Daytona** (cloud sandboxes)\n\n---\n\n## Run It\n\n```bash\ngit clone https://github.com/Kiy-K/agent-swarm-workbench.git\ncd agent-swarm-workbench\ncp .env.example .env\n# Optional: add server fallback keys. Users can also paste their own key in the UI.\npython -m uvicorn app:app --host 0.0.0.0 --port 8790\n```\n\nOpen http://localhost:8790, type a prompt, choose a provider, fetch models with your API key, then click Start Run.\n\nModel selection:\n- Model lists are fetched from the selected provider/API endpoint at runtime.\n- UI only offers fetched models whose ID/name proves `<=32B` parameters.\n- Unknown-size models are shown in the catalog response as `unknown_parameters` but are not selectable.\n- User API keys and fetched catalogs live only in process memory. They are not persisted, not stored in Redis/DB, and not kept in Gradio state. Click \"Refresh models\" to clear and refetch that provider cache.\n\nFor Hugging Face Spaces:\n```bash\npython app.py\n```\n\n## Test\n\n```bash\npython scripts/task.py verify    # required completion gate: tests + harness\npython scripts/task.py test      # 90 tests, all passing\npython scripts/task.py harness -- --prompt \"Build a tiny CLI\" --test \"test -f README.md\"\npython scripts/task.py smoke      # Local agent session smoke check\npython scripts/task.py validator-smoke  # Validator end-to-end\n```\n\n### Agent Harness\n\nThe harness is the fast way to exercise the Run lifecycle without waiting on a\nfull demo session:\n\n```bash\npython scripts/task.py verify\npython scripts/task.py harness -- --prompt \"Build a tiny CLI\" --output-dir /tmp/harness\npython scripts/task.py harness -- --mode live --prompt \"Build a tiny CLI\"\n```\n\n`verify` is the required completion gate for coding agents. It runs the Python\nsuite, then runs the default scripted Agent Swarm Harness so changes are checked\nagainst the same Run -> SwarmRuntime -> Archive -> Validator path that the app\nuses.\n\nModes:\n\n| Mode | Purpose |\n|---|---|\n| `swarm` | Default. Runs `RunFlow -> SwarmRuntime -> Archive -> Validator` with a scripted local DeepAgent-compatible session. |\n| `live` | Uses the real `create_session()` DeepAgents path and the configured sandbox provider. |\n\n## Environment\n\n| Var | Purpose |\n|---|---|\n| `DEEPAGENT_MODEL_PROVIDER` | Server fallback model provider: `openrouter`, `gemini`, `nebius`, `huggingface`, `custom`, or `local` |\n| `DEEPAGENT_MODEL` | Server fallback model ID. Must prove `<=32B` when selected per Run. |\n| `DEEPAGENT_MODEL_BASE_URL` | Optional OpenAI-compatible `/v1` endpoint |\n| `OPENROUTER_API_KEY` / `GEMINI_API_KEY` / `NEBIUS_API_KEY` / `HF_TOKEN` | Optional server fallback keys for trusted server/CLI runs only. The public Gradio UI requires the user to enter their own hosted-provider key and does not use these by default. |\n| `DEEPAGENT_SANDBOX_PROVIDER` | `local`, `docker`, or `daytona` |\n| `BROWSERBASE_API_KEY` | Optional — visual validation via Stagehand |\n| `UPSTASH_REDIS_REST_URL` / `TOKEN` | Optional — persistent runs & archives |\n\n---\n\n## Stack\n\n- **Python 3.11+** / **FastAPI** / **Gradio 6**\n- **LangChain DeepAgents** — multi-subagent swarm runtime\n- **Provider adapters** — OpenRouter, Gemini, Nebius, Hugging Face Router, custom OpenAI-compatible, local OpenAI-compatible\n- **LangGraph** — Validator workflow\n- **QuickJS code interpreter** — in-sandbox code execution middleware\n- **Browserbase + Stagehand** — visual web validation (optional)\n\n## Architecture\n\n```\narena/\n  agent.py           — Swarm factory, model, subagents, sandbox backend\n  backyard_templates.py — Backyard demo template registry\n  model_provider.py  — Chat model factory for provider selection\n  model_catalog.py   — Provider model list adapters and TTL cache\n  swarm_runtime.py   — Active Run registration and Swarm session leasing\n  swarm_session.py   — Prompt seeding, agent turns, test retries, snapshots\n  sandbox_lease.py   — Idle TTL, touch, and close behavior for sandboxes\n  run_flow.py        — Run lifecycle: create → execute → archive → validate\n  run_journal.py     — Run mutation journal: status, tasks, events, timestamps\n  run_store.py       — Run persistence (InMemory / Redis via Upstash)\n  codebase_handoff.py — Workspace snapshot and Validator sandbox restore\n  codebase_archive.py — Archive persistence (local / Redis)\n  validator_plan.py  — Typed Validator plan from user tests/checks\n  validator_graph.py — LangGraph Validator workflow\n  thread_inspector.py — Manual Thread/session debug surface\n  gradio_app.py      — Thin Gradio component wiring\n  gradio_presenter.py — Run output formatting for Gradio\n  gradio_markup.py   — Static Gradio shell markup\n  api.py             — FastAPI REST + SSE endpoints\n  event_bus.py       — In-process event streaming\n  browserbase_tools.py  — Web fetch/search tools for the swarm\n  stagehand_validator.py — Browserbase visual validation\n  docker_backend.py  — Docker sandbox provider\n  skill_catalog.py   — Bundled DeepAgents skills discovery\ntests_python/        — Python test suite (integration + unit)\n```\n\n---\n\n*Built with a sub-32B model for the Build Small Hackathon, June 2026.*\n",334      "readme_body": "# Backyard Demo Builder\n\n## Chapter 1: Backyard AI\n\n*Build Small Hackathon 2026 — Chapter 1 Submission*\n\n`agent-swarm-workbench` now presents as **Backyard Demo Builder**: a Gradio app\nthat turns one real person's workflow into a small runnable demo package before\nanyone pays to build full software.\n\nFirst backyard case: my mom, a real-estate agent. She needs a cheap way to test\na customer follow-up reminder workflow before committing time and money to a\nfull app.\n\n---\n\n## Watch the Demo Builder Work\n\n```\nYou:     \"Build a real-estate follow-up CRM demo for my mom.\"\nBuilder: Generates a Gradio mini-app, handoff spec, field notes, and checks\nResult:  app.py, README.md, handoff_spec.md, field_notes.md\nMom:     Tests the workflow, then we scrap or scale.\n```\n\nEvery Run produces a **downloadable demo package** and Validation report: files\nyou can inspect, unzip, run, and test with the real person.\n\n---\n\n## Build Small Hackathon — Submission Notes\n\n| Requirement | How We Meet It |\n|---|---|\n| **Small model (≤ 32B)** | Provider catalog fetches models at runtime and only allows models whose ID/name proves ≤32B |\n| **Gradio app** | Custom dark-themed Gradio UI mounted on FastAPI |\n| **HF Space** | `app.py` + `requirements.txt` — one-command deploy |\n| **Demo video** | *(placeholder — [link to demo])* |\n| **Social post** | *(placeholder — [link to post])* |\n\n### Bonus Badges Claimed\n\n| Badge | Why |\n|---|---|\n| **🎨 Off-Brand** | Fully custom CSS dark theme — Archivo + IBM Plex Mono, acid green CTAs, paper/ink palette, CSS grid layout, status chips. Not a default Gradio component in sight. |\n| **📡 Sharing is Caring** | Agent traces and swarm reasoning are surfaced in the Events panel. We'll publish a trace on the Hub. |\n| **📓 Field Notes** | Generated demo packages include `field_notes.md`; this repo also documents the architecture and decisions. |\n\n---\n\n## Why This Belongs in Backyard AI\n\nThis solves a real problem for someone I know.\n\n- **Specific person** — my mom, a real-estate agent.\n- **Specific pain** — follow-up reminders and customer-care demos are useful, but custom app dev is slow and risky.\n- **Honest small-model fit** — a ≤32B model drafts the demo and handoff spec; rules handle the reminder logic.\n- **Actually testable** — the generated package includes field notes and feedback questions for the real user.\n\n---\n\n## How It Works Under the Hood\n\n```\n┌─────────────────────────────────────────────────────┐\n│  Gradio UI / HTTP API                               │\n├─────────────────────────────────────────────────────┤\n│  RunFlow — lifecycle conductor                      │\n│  ┌──────────┐  ┌────────────┐  ┌────────────────┐  │\n│  │ Swarm    │  │ Codebase   │  │ Validator      │  │\n│  │ Runtime  │→│ Archive    │→│ Graph          │  │\n│  │          │  │ Store      │  │                │  │\n│  │ Planner  │  │ (local/    │  │ Sandbox checks │  │\n│  │ Coder    │  │  Redis)    │  │ Rubric review  │  │\n│  │ Reviewer │  │            │  │ Stagehand      │  │\n│  │ Tester   │  │            │  │ (Browserbase)  │  │\n│  └──────────┘  └────────────┘  └────────────────┘  │\n│  EventBus → SSE stream to UI                       │\n└─────────────────────────────────────────────────────┘\n```\n\n### The Swarm\n\n- **Coordinator** reads the prompt, plans tasks, delegates to subagents\n- **Planner** breaks down the prompt into implementable units\n- **Coder** writes the actual code files\n- **Reviewer** checks code quality and correctness\n- **Test-runner** runs the user's tests and retries up to 3x on failure\n- **Validator-prep** generates validation checks from user criteria\n\n### The Validator\n\nAfter the swarm finishes, a LangGraph Validator workflow:\n1. Restores the codebase into a clean sandbox\n2. Runs user-provided tests\n3. Executes LLM-based rubric review\n4. (Optional) Runs Browserbase/Stagehand visual checks\n5. Produces a pass/fail Validation Report\n\n### The Sandbox\n\nAll agent work happens inside isolated sandbox workspaces:\n- **Local** (for dev/smoke tests)\n- **Docker** (container-based)\n- **Daytona** (cloud sandboxes)\n\n---\n\n## Run It\n\n```bash\ngit clone https://github.com/Kiy-K/agent-swarm-workbench.git\ncd agent-swarm-workbench\ncp .env.example .env\n# Optional: add server fallback keys. Users can also paste their own key in the UI.\npython -m uvicorn app:app --host 0.0.0.0 --port 8790\n```\n\nOpen http://localhost:8790, type a prompt, choose a provider, fetch models with your API key, then click Start Run.\n\nModel selection:\n- Model lists are fetched from the selected provider/API endpoint at runtime.\n- UI only offers fetched models whose ID/name proves `<=32B` parameters.\n- Unknown-size models are shown in the catalog response as `unknown_parameters` but are not selectable.\n- User API keys and fetched catalogs live only in process memory. They are not persisted, not stored in Redis/DB, and not kept in Gradio state. Click \"Refresh models\" to clear and refetch that provider cache.\n\nFor Hugging Face Spaces:\n```bash\npython app.py\n```\n\n## Test\n\n```bash\npython scripts/task.py verify    # required completion gate: tests + harness\npython scripts/task.py test      # 90 tests, all passing\npython scripts/task.py harness -- --prompt \"Build a tiny CLI\" --test \"test -f README.md\"\npython scripts/task.py smoke      # Local agent session smoke check\npython scripts/task.py validator-smoke  # Validator end-to-end\n```\n\n### Agent Harness\n\nThe harness is the fast way to exercise the Run lifecycle without waiting on a\nfull demo session:\n\n```bash\npython scripts/task.py verify\npython scripts/task.py harness -- --prompt \"Build a tiny CLI\" --output-dir /tmp/harness\npython scripts/task.py harness -- --mode live --prompt \"Build a tiny CLI\"\n```\n\n`verify` is the required completion gate for coding agents. It runs the Python\nsuite, then runs the default scripted Agent Swarm Harness so changes are checked\nagainst the same Run -> SwarmRuntime -> Archive -> Validator path that the app\nuses.\n\nModes:\n\n| Mode | Purpose |\n|---|---|\n| `swarm` | Default. Runs `RunFlow -> SwarmRuntime -> Archive -> Validator` with a scripted local DeepAgent-compatible session. |\n| `live` | Uses the real `create_session()` DeepAgents path and the configured sandbox provider. |\n\n## Environment\n\n| Var | Purpose |\n|---|---|\n| `DEEPAGENT_MODEL_PROVIDER` | Server fallback model provider: `openrouter`, `gemini`, `nebius`, `huggingface`, `custom`, or `local` |\n| `DEEPAGENT_MODEL` | Server fallback model ID. Must prove `<=32B` when selected per Run. |\n| `DEEPAGENT_MODEL_BASE_URL` | Optional OpenAI-compatible `/v1` endpoint |\n| `OPENROUTER_API_KEY` / `GEMINI_API_KEY` / `NEBIUS_API_KEY` / `HF_TOKEN` | Optional server fallback keys for trusted server/CLI runs only. The public Gradio UI requires the user to enter their own hosted-provider key and does not use these by default. |\n| `DEEPAGENT_SANDBOX_PROVIDER` | `local`, `docker`, or `daytona` |\n| `BROWSERBASE_API_KEY` | Optional — visual validation via Stagehand |\n| `UPSTASH_REDIS_REST_URL` / `TOKEN` | Optional — persistent runs & archives |\n\n---\n\n## Stack\n\n- **Python 3.11+** / **FastAPI** / **Gradio 6**\n- **LangChain DeepAgents** — multi-subagent swarm runtime\n- **Provider adapters** — OpenRouter, Gemini, Nebius, Hugging Face Router, custom OpenAI-compatible, local OpenAI-compatible\n- **LangGraph** — Validator workflow\n- **QuickJS code interpreter** — in-sandbox code execution middleware\n- **Browserbase + Stagehand** — visual web validation (optional)\n\n## Architecture\n\n```\narena/\n  agent.py           — Swarm factory, model, subagents, sandbox backend\n  backyard_templates.py — Backyard demo template registry\n  model_provider.py  — Chat model factory for provider selection\n  model_catalog.py   — Provider model list adapters and TTL cache\n  swarm_runtime.py   — Active Run registration and Swarm session leasing\n  swarm_session.py   — Prompt seeding, agent turns, test retries, snapshots\n  sandbox_lease.py   — Idle TTL, touch, and close behavior for sandboxes\n  run_flow.py        — Run lifecycle: create → execute → archive → validate\n  run_journal.py     — Run mutation journal: status, tasks, events, timestamps\n  run_store.py       — Run persistence (InMemory / Redis via Upstash)\n  codebase_handoff.py — Workspace snapshot and Validator sandbox restore\n  codebase_archive.py — Archive persistence (local / Redis)\n  validator_plan.py  — Typed Validator plan from user tests/checks\n  validator_graph.py — LangGraph Validator workflow\n  thread_inspector.py — Manual Thread/session debug surface\n  gradio_app.py      — Thin Gradio component wiring\n  gradio_presenter.py — Run output formatting for Gradio\n  gradio_markup.py   — Static Gradio shell markup\n  api.py             — FastAPI REST + SSE endpoints\n  event_bus.py       — In-process event streaming\n  browserbase_tools.py  — Web fetch/search tools for the swarm\n  stagehand_validator.py — Browserbase visual validation\n  docker_backend.py  — Docker sandbox provider\n  skill_catalog.py   — Bundled DeepAgents skills discovery\ntests_python/        — Python test suite (integration + unit)\n```\n\n---\n\n*Built with a sub-32B model for the Build Small Hackathon, June 2026.*",335      "readme_frontmatter": {336        "title": "Backyard Demo Builder",337        "emoji": "🏡",338        "colorFrom": "gray",339        "colorTo": "green",340        "sdk": "gradio",341        "sdk_version": "5.49.1",342        "python_version": "3.12.12",343        "app_file": "app.py",344        "short_description": "Build tiny real-person demos before scaling custom software.",345        "models": "",346        "datasets": "[]",347        "tags": "",348        "pinned": "false"349      },350      "app_source": "\"\"\"Unified ASGI entrypoint for API and Gradio UI.\"\"\"\n\nfrom __future__ import annotations\n\nimport os\n\nimport gradio as gr\nimport uvicorn\n\ntry:\n    import spaces\nexcept Exception:\n    class _SpacesShim:\n        def GPU(self, fn=None, **kwargs):\n            del kwargs\n\n            def decorator(inner):\n                return inner\n\n            return decorator(fn) if fn else decorator\n\n    spaces = _SpacesShim()\n\n\nfrom arena.api import app as fastapi_app\nfrom arena.api import service\nfrom arena.gradio_app import RunOutputs, build_app, create_run_gpu as _create_run_gpu\n\n\n@spaces.GPU(duration=120)\ndef create_run_gpu(\n    prompt: str,\n    criteria_text: str,\n    user_tests_text: str,\n    provider: str = \"openrouter\",\n    model: str = \"\",\n    api_key: str = \"\",\n    base_url: str = \"\",\n) -> RunOutputs:\n    return _create_run_gpu(\n        prompt,\n        criteria_text,\n        user_tests_text,\n        provider,\n        model,\n        api_key,\n        base_url,\n    )\n\n\n@spaces.GPU\ndef zerogpu_ready_marker() -> str:\n    return \"ready\"\n\n\ndemo = build_app(service, create_run_handler=create_run_gpu)\n\n\ndef create_app():\n    \"\"\"Create one FastAPI ASGI app with Gradio mounted at the root.\"\"\"\n\n    return gr.mount_gradio_app(fastapi_app, demo, path=\"/\")\n\n\napp = create_app()\n\n\ndef server_config() -> dict[str, int | str]:\n    host = os.getenv(\"GRADIO_SERVER_NAME\", os.getenv(\"HOST\", \"0.0.0.0\"))\n    port = int(os.getenv(\"GRADIO_SERVER_PORT\") or os.getenv(\"PORT\") or \"7860\")\n    return {\"host\": host, \"port\": port}\n\n\ndef gradio_launch_config() -> dict[str, bool | int | str]:\n    config = server_config()\n    port = int(os.getenv(\"GRADIO_SERVER_PORT\") or os.getenv(\"PORT\") or \"7860\")\n    return {\"server_name\": str(config[\"host\"]), \"server_port\": port, \"ssr_mode\": False}\n\n\ndef should_launch_gradio_space() -> bool:\n    return bool(os.getenv(\"SPACE_ID\")) and os.getenv(\"FORCE_SELF_LAUNCH\") != \"1\"\n\n\ndef should_self_launch() -> bool:\n    if os.getenv(\"FORCE_SELF_LAUNCH\") == \"1\":\n        return True\n    return not should_launch_gradio_space()\n\n\ndef _space_sdk() -> str:\n    return os.getenv(\"SPACE_SDK\", os.getenv(\"HF_SPACE_SDK\", \"\")).strip().lower()\n\n\ndef launch_gradio_space() -> None:\n    demo.queue(default_concurrency_limit=1).launch(**gradio_launch_config())\n\n\nif __name__ == \"__main__\":\n    if should_launch_gradio_space():\n        launch_gradio_space()\n    elif should_self_launch():\n        uvicorn.run(app, **server_config())\n",351      "app_signals": "create_app zerogpu_ready_marker server_config gradio_launch_config should_launch_gradio_space should_self_launch _space_sdk Unified ASGI entrypoint for API and Gradio UI. build_app Create one FastAPI ASGI app with Gradio mounted at the root. gr.mount_gradio_app path _SpacesShim ready os.getenv int lower __main__ GPU self fn GRADIO_SERVER_NAME host port server_name server_port ssr_mode str bool 1 demo.launch / decorator inner HOST 0.0.0.0 7860 SPACE_ID FORCE_SELF_LAUNCH strip uvicorn.run GRADIO_SERVER_PORT PORT 7861 SPACE_SDK HF_SPACE_SDK",352      "readme_len": 9119,353      "app_source_len": 2436,354      "app_signals_len": 543355    },356    {357      "id": "build-small-hackathon/backyard-dudu-destroyer",358      "title": "Backyard Dudu Destroyer",359      "summary": "A gradio interface for starting VLA and policy",360      "tags": [361        "gradio",362        "region:us"363      ],364      "models": [],365      "datasets": [],366      "sdk": "gradio",367      "license": "apache-2.0",368      "likes": 0,369      "url": "https://huggingface.co/spaces/build-small-hackathon/backyard-dudu-destroyer",370      "app_file": "app.py",371      "readme_raw": "---\ntitle: Backyard Dudu Destroyer\nemoji: 🌖\ncolorFrom: gray\ncolorTo: red\nsdk: gradio\nsdk_version: 6.16.0\npython_version: '3.13'\napp_file: app.py\npinned: false\nlicense: apache-2.0\nshort_description: A gradio interface for starting VLA and policy\n---\n\nCheck out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference\n",372      "readme_body": "Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference",373      "readme_frontmatter": {374        "title": "Backyard Dudu Destroyer",375        "emoji": "🌖",376        "colorFrom": "gray",377        "colorTo": "red",378        "sdk": "gradio",379        "sdk_version": "6.16.0",380        "python_version": "3.13",381        "app_file": "app.py",382        "pinned": "false",383        "license": "apache-2.0",384        "short_description": "A gradio interface for starting VLA and policy"385      },386      "app_source": "import gradio as gr\n\ndef greet(name):\n    return \"Hello \" + name + \"!!\"\n\ndemo = gr.Interface(fn=greet, inputs=\"text\", outputs=\"text\")\ndemo.launch()\n",387      "app_signals": "greet name gr.Interface fn inputs outputs demo.launch !! text Hello",388      "readme_len": 96,389      "app_source_len": 148,390      "app_signals_len": 67391    },392    {393      "id": "build-small-hackathon/backyard-raccoon-deterrent",394      "title": "Backyard Raccoon Deterrent",395      "summary": "Edge-AI raccoon deterrent. Tiny YOLO, fully offline.",396      "tags": [397        "build-small-hackathon",398        "edge-ai",399        "object-detection",400        "raccoon",401        "yolov8"402      ],403      "models": [],404      "datasets": [],405      "sdk": "gradio",406      "license": "mit",407      "likes": 0,408      "url": "https://huggingface.co/spaces/build-small-hackathon/backyard-raccoon-deterrent",409      "app_file": "app.py",410      "readme_raw": "---\ntitle: Backyard Raccoon Deterrent\nemoji: 🦝\ncolorFrom: green\ncolorTo: gray\nsdk: gradio\nsdk_version: 6.15.2\napp_file: app.py\npinned: false\nlicense: mit\nshort_description: Edge-AI raccoon deterrent. Tiny YOLO, fully offline.\ntags:\n  - object-detection\n  - yolov8\n  - raccoon\n  - edge-ai\n  - build-small-hackathon\n---\n\n# 🦝 Backyard Raccoon Deterrent\n\nRaccoons were raiding my backyard every night, so I built an AI that fights\nback. A 3-million-parameter YOLO spots them in the dark and scares them off\nwith a dog bark and a floodlight. No cloud, no traps, and nothing gets hurt.\n\nThis Space is the live detector from a real system that has been defending my\nactual backyard since April. Upload a photo (daytime or IR night frame) and the\nmodel draws the boxes and tells you what the physical deterrent would do.\n\n## 📼 Submission\n\n**Demo video** (82s):\n\n<video controls src=\"https://huggingface.co/spaces/build-small-hackathon/backyard-raccoon-deterrent/resolve/main/demo-video.mp4\"></video>\n\n**Social post**: https://x.com/0xartclub/status/2063258977895391508\n\n**Track**: 🏡 Backyard AI. **Bonus quests**: 🔌 Off the Grid (zero cloud APIs), 🎯 Well-Tuned (fine-tuned published model)\n\n## The story\n\nA Ring camera sees raccoons just fine, but a camera can't do anything about\nthem. The usual answer is \"nuisance wildlife\" control, and that mostly means\nkilling: U.S. federal wildlife control killed over 375,000 native animals in\n2023 ([USDA APHIS Program Data Reports](https://www.aphis.usda.gov/wildlife-services/publications/pdr)).\nThe same reports show the humane approach works, since the same agency\ndisperses about 20 million animals a year unharmed.\n\nThis project automates the humane version:\n\n```\nRing camera -> motion event -> YOLOv8n v1.4 (24 ms) -> 🔊 bark + 💡 lights\n                                    |\n                               fully offline:\n                        Raspberry Pi + Mac Mini, $0 cloud\n```\n\nThe raccoon leaves, nothing gets hurt, and the whole thing runs on hardware\nthat was already in the house. About 5 to 8 seconds from first motion to\ndeterrent.\n\n## Why \"Build Small\" fits\n\n- The model is tiny: YOLOv8n, about 3M parameters and 12 MB of ONNX. The\n  hackathon ceiling is 32B. This is four orders of magnitude under it.\n- Small actually wins here. A 2.6-second cloud VLM round trip misses a moving\n  raccoon. A 24 ms local model catches it mid-stride. I tried the big-model\n  route first (Gemma 3 12B as a scene describer) and ended up retiring it from\n  the chain because the small specialist beat it.\n- The training data is small too: 564 hand-labeled IR frames from the exact\n  yard it defends. No internet-scale dataset, just the right data.\n\n## The model\n\n| | |\n|---|---|\n| Architecture | YOLOv8n (nano) |\n| Version | v1.4, trained on 564 hand-labeled IR night frames, 97 new boxes from recent encounters |\n| Precision / Recall | 93.5% / 85.9% (held-out val, harder split) |\n| mAP50 | 92.8% |\n| Inference | ~24 ms p50 (ONNX Runtime, Apple Silicon) |\n| Field record | First version to clear all three real encounters that earlier models missed |\n\nTraining pipeline: Ring event video, ffmpeg frame extraction (first 15 s at\n1 Hz), Claude pre-classification, Label Studio bounding boxes, YOLOv8\nfine-tune, ONNX export. Every production miss becomes training data for the\nnext version, so the model learns from each raccoon that gets past it.\n\n## Try it\n\n1. Click an example below the app. These are real night-vision frames from the yard.\n2. Watch the verdict: \"🦝 Raccoon detected, BARK + LIGHTS would fire\" vs \"✅ All clear.\"\n3. Drag the confidence slider (production runs at 0.20) and watch the\n   precision/recall trade-off live.\n4. Upload your own backyard photo, day or night.\n\n## The real-world deployment\n\n**60+ nights in production. Every confirmed encounter answered in 5 to 8\nseconds. Zero animals harmed.**\n\n![Raccoon-window motion events per night across 60 nights of production](https://huggingface.co/spaces/build-small-hackathon/backyard-raccoon-deterrent/resolve/main/activity-chart.png)\n\nRaccoon activity swings wildly night to night (peak: 33 motion events in one\nnight). The system logged and processed every one of them, and every miss\nbecame training data for the next model version. That feedback loop is why the\ndetector is on v1.4 after 60 nights.\n\nThis exact model is the primary detector in a Homebridge accessory that runs\nnightly (21:00 to 05:30) on a Raspberry Pi:\n\n- Eyes: Ring cameras (motion events plus multi-frame snapshot capture)\n- Brain: this YOLOv8n on a Mac Mini (FastAPI + ONNX Runtime, runs as a\n  LaunchDaemon so it survives reboots), with Claude Haiku as a second-opinion\n  safety net\n- Voice: dog-bark WAVs over a Bluetooth speaker (BlueALSA)\n- Muscle: TP-Link Kasa smart lights\n- Fast path: every frame is evaluated at capture, and the first hit fires the\n  deterrent in 5 to 8 seconds instead of waiting for a full batch\n\n## Run locally\n\n```bash\npip install -r requirements.txt\npython app.py\n```\n\nWeights ship in this repo (`raccoon-yolov8n-v1.4.onnx`, MIT licensed), or set\n`MODEL_PATH` to your own export.\n\n## Links\n\n- Source code: https://github.com/sappkevin/backyard-raccoon-deterrent\n- Built by [@ksapp](https://huggingface.co/ksapp) for the Gradio Build Small hackathon, Backyard AI track\n",411      "readme_body": "# 🦝 Backyard Raccoon Deterrent\n\nRaccoons were raiding my backyard every night, so I built an AI that fights\nback. A 3-million-parameter YOLO spots them in the dark and scares them off\nwith a dog bark and a floodlight. No cloud, no traps, and nothing gets hurt.\n\nThis Space is the live detector from a real system that has been defending my\nactual backyard since April. Upload a photo (daytime or IR night frame) and the\nmodel draws the boxes and tells you what the physical deterrent would do.\n\n## 📼 Submission\n\n**Demo video** (82s):\n\n<video controls src=\"https://huggingface.co/spaces/build-small-hackathon/backyard-raccoon-deterrent/resolve/main/demo-video.mp4\"></video>\n\n**Social post**: https://x.com/0xartclub/status/2063258977895391508\n\n**Track**: 🏡 Backyard AI. **Bonus quests**: 🔌 Off the Grid (zero cloud APIs), 🎯 Well-Tuned (fine-tuned published model)\n\n## The story\n\nA Ring camera sees raccoons just fine, but a camera can't do anything about\nthem. The usual answer is \"nuisance wildlife\" control, and that mostly means\nkilling: U.S. federal wildlife control killed over 375,000 native animals in\n2023 ([USDA APHIS Program Data Reports](https://www.aphis.usda.gov/wildlife-services/publications/pdr)).\nThe same reports show the humane approach works, since the same agency\ndisperses about 20 million animals a year unharmed.\n\nThis project automates the humane version:\n\n```\nRing camera -> motion event -> YOLOv8n v1.4 (24 ms) -> 🔊 bark + 💡 lights\n                                    |\n                               fully offline:\n                        Raspberry Pi + Mac Mini, $0 cloud\n```\n\nThe raccoon leaves, nothing gets hurt, and the whole thing runs on hardware\nthat was already in the house. About 5 to 8 seconds from first motion to\ndeterrent.\n\n## Why \"Build Small\" fits\n\n- The model is tiny: YOLOv8n, about 3M parameters and 12 MB of ONNX. The\n  hackathon ceiling is 32B. This is four orders of magnitude under it.\n- Small actually wins here. A 2.6-second cloud VLM round trip misses a moving\n  raccoon. A 24 ms local model catches it mid-stride. I tried the big-model\n  route first (Gemma 3 12B as a scene describer) and ended up retiring it from\n  the chain because the small specialist beat it.\n- The training data is small too: 564 hand-labeled IR frames from the exact\n  yard it defends. No internet-scale dataset, just the right data.\n\n## The model\n\n| | |\n|---|---|\n| Architecture | YOLOv8n (nano) |\n| Version | v1.4, trained on 564 hand-labeled IR night frames, 97 new boxes from recent encounters |\n| Precision / Recall | 93.5% / 85.9% (held-out val, harder split) |\n| mAP50 | 92.8% |\n| Inference | ~24 ms p50 (ONNX Runtime, Apple Silicon) |\n| Field record | First version to clear all three real encounters that earlier models missed |\n\nTraining pipeline: Ring event video, ffmpeg frame extraction (first 15 s at\n1 Hz), Claude pre-classification, Label Studio bounding boxes, YOLOv8\nfine-tune, ONNX export. Every production miss becomes training data for the\nnext version, so the model learns from each raccoon that gets past it.\n\n## Try it\n\n1. Click an example below the app. These are real night-vision frames from the yard.\n2. Watch the verdict: \"🦝 Raccoon detected, BARK + LIGHTS would fire\" vs \"✅ All clear.\"\n3. Drag the confidence slider (production runs at 0.20) and watch the\n   precision/recall trade-off live.\n4. Upload your own backyard photo, day or night.\n\n## The real-world deployment\n\n**60+ nights in production. Every confirmed encounter answered in 5 to 8\nseconds. Zero animals harmed.**\n\n![Raccoon-window motion events per night across 60 nights of production](https://huggingface.co/spaces/build-small-hackathon/backyard-raccoon-deterrent/resolve/main/activity-chart.png)\n\nRaccoon activity swings wildly night to night (peak: 33 motion events in one\nnight). The system logged and processed every one of them, and every miss\nbecame training data for the next model version. That feedback loop is why the\ndetector is on v1.4 after 60 nights.\n\nThis exact model is the primary detector in a Homebridge accessory that runs\nnightly (21:00 to 05:30) on a Raspberry Pi:\n\n- Eyes: Ring cameras (motion events plus multi-frame snapshot capture)\n- Brain: this YOLOv8n on a Mac Mini (FastAPI + ONNX Runtime, runs as a\n  LaunchDaemon so it survives reboots), with Claude Haiku as a second-opinion\n  safety net\n- Voice: dog-bark WAVs over a Bluetooth speaker (BlueALSA)\n- Muscle: TP-Link Kasa smart lights\n- Fast path: every frame is evaluated at capture, and the first hit fires the\n  deterrent in 5 to 8 seconds instead of waiting for a full batch\n\n## Run locally\n\n```bash\npip install -r requirements.txt\npython app.py\n```\n\nWeights ship in this repo (`raccoon-yolov8n-v1.4.onnx`, MIT licensed), or set\n`MODEL_PATH` to your own export.\n\n## Links\n\n- Source code: https://github.com/sappkevin/backyard-raccoon-deterrent\n- Built by [@ksapp](https://huggingface.co/ksapp) for the Gradio Build Small hackathon, Backyard AI track",412      "readme_frontmatter": {413        "title": "Backyard Raccoon Deterrent",414        "emoji": "🦝",415        "colorFrom": "green",416        "colorTo": "gray",417        "sdk": "gradio",418        "sdk_version": "6.15.2",419        "app_file": "app.py",420        "pinned": "false",421        "license": "mit",422        "short_description": "Edge-AI raccoon deterrent. Tiny YOLO, fully offline.",423        "tags": ""424      },425      "app_source": "\"\"\"Backyard Raccoon Deterrent — Gradio Space.\n\nFine-tuned YOLOv8n raccoon detector, the vision component of a real Ring-camera\ndeterrent. Upload a backyard photo (daytime or IR night frame) and the model\ndraws boxes, lists detections, and tells you what the deterrent would do.\n\nRuns fully offline — no cloud APIs.\n\"\"\"\n\nimport os\n\nimport gradio as gr\nfrom ultralytics import YOLO\n\n# Weights ship in the repo; override with a HF Hub path via env if you prefer.\nMODEL_PATH = os.environ.get(\"MODEL_PATH\", \"raccoon-yolov8n-v1.4.onnx\")\nDEFAULT_CONF = 0.20  # matches the production deterrent's localYoloConfidenceThreshold\n\nmodel = YOLO(MODEL_PATH)\n\n\ndef detect(image, conf):\n    \"\"\"Run detection and return (annotated image, table rows, deterrent verdict).\"\"\"\n    if image is None:\n        return None, [], \"Upload a frame to begin.\"\n\n    results = model.predict(image, conf=conf, verbose=False)[0]\n\n    boxes, rows = [], []\n    for b in results.boxes:\n        x1, y1, x2, y2 = b.xyxy[0].tolist()\n        label = model.names[int(b.cls)]\n        score = float(b.conf)\n        boxes.append(((int(x1), int(y1), int(x2), int(y2)), f\"{label} {score:.2f}\"))\n        rows.append([label, round(score, 2)])\n\n    raccoon = any(label == \"raccoon\" and score >= conf for label, score in rows)\n    if raccoon:\n        top = max((s for l, s in rows if l == \"raccoon\"), default=0.0)\n        verdict = f\"🦝 Raccoon detected ({top:.2f}) → BARK + LIGHTS would fire\"\n    elif rows:\n        verdict = \"🐾 Animal seen, but no raccoon — deterrent stays quiet\"\n    else:\n        verdict = \"✅ All clear — nothing detected\"\n\n    return (image, boxes), rows, verdict\n\n\nEXAMPLES = [\n    [\"examples/ir_raccoon_pair.jpg\", DEFAULT_CONF],\n    [\"examples/ir_raccoon_solo.jpg\", DEFAULT_CONF],\n    [\"examples/ir_raccoon_prowler.jpg\", DEFAULT_CONF],\n    [\"examples/night_empty.jpg\", DEFAULT_CONF],\n]\n# Drop the examples that don't exist yet so the Space still launches.\nEXAMPLES = [e for e in EXAMPLES if os.path.exists(e[0])]\n\ndemo = gr.Interface(\n    fn=detect,\n    inputs=[\n        gr.Image(type=\"pil\", label=\"Backyard frame\"),\n        gr.Slider(0.05, 0.90, value=DEFAULT_CONF, step=0.01, label=\"Confidence threshold\"),\n    ],\n    outputs=[\n        gr.AnnotatedImage(label=\"Detections\"),\n        gr.Dataframe(headers=[\"animal\", \"confidence\"], label=\"What the model saw\"),\n        gr.Textbox(label=\"Deterrent verdict\"),\n    ],\n    examples=EXAMPLES or None,\n    title=\"🦝 Backyard Raccoon Deterrent\",\n    description=(\n        \"Fine-tuned **YOLOv8n** raccoon detector (v1.4) — the eyes of a real Ring-camera \"\n        \"deterrent. Trained on 560+ hand-labeled night-vision frames of raccoons \"\n        \"raiding my yard, including trajectory frames pulled from real motion events \"\n        \"(**P 93.5% · R 85.9% · mAP50 92.8%** on a held-out val split, ~24 ms inference). \"\n        \"Runs fully offline. Upload a frame or click an example.\"\n    ),\n    article=(\n        \"Built for the Gradio **Build Small** hackathon (Backyard AI track). \"\n        \"The deployed system pairs this model with audio + smart-light deterrents on a \"\n        \"Raspberry Pi — fully offline, no cloud APIs. \"\n        \"[Source on GitHub](https://github.com/sappkevin/backyard-raccoon-deterrent).\"\n    ),\n)\n\nif __name__ == \"__main__\":\n    demo.launch()\n",426      "app_signals": "detect image conf Backyard Raccoon Deterrent — Gradio Space. Fine-tuned YOLOv8n raccoon detector, the vision component of a real Ring-camera deterrent. Upload a backyard photo (daytime or IR night frame) and the model draws boxes, lists detections, and tells you what the deterrent would do. Runs fully offline — no cloud APIs. os.environ.get YOLO gr.Interface fn inputs outputs examples title description article MODEL_PATH raccoon-yolov8n-v1.4.onnx Run detection and return (annotated image, table rows, deterrent verdict). any __main__ demo.launch model.predict verbose tolist float boxes.append rows.append max default examples/ir_raccoon_pair.jpg examples/ir_raccoon_solo.jpg examples/ir_raccoon_prowler.jpg examples/night_empty.jpg os.path.exists 🦝 Backyard Raccoon Deterrent Fine-tuned **YOLOv8n** raccoon detector (v1.4) — the eyes of a real Ring-camera deterrent. Trained on 560+ hand-labeled night-vision frames of raccoons raiding my yard, including trajectory frames pulled from real motion events (**P 93.5% · R 85.9% · mAP50 92.8%** on a held-out val split, ~24 ms inference). Runs fully offline. Upload a frame or click an example. Built for the Gradio **Build Small** hackathon (Backyard AI track). The deployed system pairs this model with audio + smart-light deterrents on a Raspberry Pi — fully offline, no cloud APIs. [Source on GitHub](https://github.com/sappkevin/backyard-raccoon-deterrent). Upload a frame to begin. int 🦝 Raccoon detected ( ) → BARK + LIGHTS would fire 🐾 Animal seen, but no raccoon — deterrent stays quiet ✅ All clear — nothing detected gr.Image type label gr.Slider value step gr.AnnotatedImage gr.Dataframe headers gr.Textbox round raccoon .2f pil Backyard frame Confidence threshold Detections What the model saw Deterrent verdict animal confidence",427      "readme_len": 4959,428      "app_source_len": 3284,429      "app_signals_len": 1793430    },431    {432      "id": "build-small-hackathon/blind-quill",433      "title": "Blind Quill",434      "summary": "",435      "tags": [436        "gradio",437        "region:us"438      ],439      "models": [],440      "datasets": [],441      "sdk": "gradio",442      "license": "mit",443      "likes": 0,444      "url": "https://huggingface.co/spaces/build-small-hackathon/blind-quill",445      "app_file": "app.py",446      "readme_raw": "---\ntitle: Blind Quill\nsdk: gradio\nsdk_version: 6.16.0\napp_file: app.py\npython_version: \"3.12\"\nsuggested_hardware: zero-a10g\nlicense: mit\n---\n\n# Blind Quill\n\nBlind Quill is a hidden-canon story grafting game.\n\nEach manuscript has a public capsule and a hidden full canon. You can play the\nintended way by reading only the capsule, adding one fragment, and letting\n`Qwen/Qwen3.5-2B` decide where that fragment belongs. The model rewrites only the\nlocal passage it targets, then reveals where your idea was stitched into the\nstory.\n\nReaders who only want to read can use the escape door: `Read without changing`.\nThe app warns that the best experience is to contribute first, then allows the\nreader to reveal the full manuscript anyway.\n\n## Interface\n\nThe UI is a bespoke literary frontend called \"The Invisible Bindery\". It lives in\n`web/` and is served by a `gradio.Server` backend.\n\n`app.py` exposes queued API endpoints:\n\n- `list_stories`\n- `get_capsule`\n- `create_story`\n- `stitch`\n- `read_manuscript`\n\nThe frontend calls those endpoints through the Gradio JS client. This keeps\nGradio queueing, concurrency control, and ZeroGPU support while presenting a\nsingle custom surface: gallery -> capsule -> compose -> reveal -> reader.\n\nThe Python layers are:\n\n- `core.py`: create, browse, stitch, and read orchestration.\n- `story_store.py`: JSON persistence and file locking.\n- `model_client.py`: model loading, generation, thinking-block stripping, and\n  JSON validation.\n- `patcher.py`: deterministic local patch application.\n- `presenter.py`: view models for the custom frontend.\n- `app.py`: static frontend serving and Gradio Server API endpoints.\n\n## Local Development\n\nUse uv with Python 3.12, matching the Hugging Face Space as closely as possible.\n\n```bash\nuv sync --python 3.12\nuv run python app.py\n```\n\nThen open <http://localhost:7860>.\n\nPersistent story data is stored at:\n\n- `DATA_DIR`, when set\n- `/data`, when it exists on Hugging Face Spaces\n- `./data/stories.json`, otherwise\n\n### Execution backend\n\n`BQ_DEVICE` selects where generation runs.\n\n| `BQ_DEVICE` | Behaviour |\n| --- | --- |\n| `auto` (default) | ZeroGPU on a Space with the `spaces` runtime, else CUDA, else Apple MPS, else CPU. |\n| `zerogpu` | Hugging Face ZeroGPU (`@spaces.GPU`), with automatic CPU fallback (below). |\n| `cuda` | Local NVIDIA GPU via `device_map=\"auto\"`. |\n| `mps` | Apple Silicon GPU (Metal); falls back to float32 if float16 fails. |\n| `cpu` | CPU only — slow but needs no accelerator or quota. |\n\n**Per-user ZeroGPU fallback.** ZeroGPU quota is per visitor, not per Space owner,\nand is only known at request time. So on a ZeroGPU Space each stitch is attempted\non the GPU; if the visitor's quota is spent, the request is transparently re-run\non CPU instead of failing. No configuration or sign-in is required to keep using\nthe app — it just gets slower.\n\n**Progress.** Because CPU/MPS runs are slow, the `stitch` endpoint streams real\nprogress (stage, percentage, ETA — and a note when a fallback happens) to the\nreveal screen. Fast GPU runs keep the original staged animation, since ZeroGPU's\nforked generation cannot stream token callbacks back across the process boundary.\n\n### Logging\n\nSet `BQ_LOG_LEVEL` (default `INFO`; use `DEBUG` for per-stage detail). Logs go to\nstderr only — never the UI — and record messages processed, total and per-stage\ntimings, and a best-effort resource snapshot (process memory, CPU, and GPU/MPS\nmemory when available).\n\n## Requirements\n\n`requirements.txt` is generated from `uv.lock` for Hugging Face Spaces:\n\n```bash\nuv export --format requirements-txt --no-dev --no-hashes --no-emit-project -o requirements.txt\n```\n\nDo not hand-edit `requirements.txt`; edit `pyproject.toml`, run `uv lock`, and\nexport again.\n\n## Test\n\n```bash\nuv run python -m compileall app.py core.py model_client.py observability.py patcher.py presenter.py prompts.py schemas.py story_store.py utils.py tests\nuv run python -m unittest discover -s tests -v\n```\n\nThe tests cover JSON/thinking cleanup, deterministic patch application, graft\nsealing, stale-write rejection, the blinded capsule flow, the warned read escape\ndoor, the create-then-stitch flow, device resolution, the resource snapshot, and\nthe streamed stitch progress events. They do not download model weights.\n\n## Model Policy\n\n- Uses one model: `Qwen/Qwen3.5-2B`.\n- Uses the Transformers `AutoProcessor` and `AutoModelForImageTextToText` path.\n- Wraps model generation in `@spaces.GPU(duration=300)` on ZeroGPU; runs directly\n  on CUDA, MPS, or CPU otherwise (selected by `BQ_DEVICE`).\n- Does not set `temperature`, `top_p`, `top_k`, or other sampling controls.\n- Disables Qwen thinking for schema-constrained JSON calls so the token budget is\n  spent on parseable JSON; other text generation keeps the model template default.\n- Strips `<think>...</think>` before JSON parsing, storage, prompting, or UI\n  rendering.\n- Does not use embeddings, RAG, ASR, image models, or a second language model.\n\n## Example Seeds\n\n```text\nA city where every doorway remembers the last person who lied inside it.\n```\n\n```text\nOn a generation ship whose crew believes Earth was a myth invented to calm children, a janitor discovers a sealed garden where rain falls upward and an old radio is still receiving ocean weather reports.\n```\n\nExample fragment:\n\n```text\nA brass key in the protagonist's pocket becomes warm whenever someone nearby tells the truth.\n```\n",447      "readme_body": "# Blind Quill\n\nBlind Quill is a hidden-canon story grafting game.\n\nEach manuscript has a public capsule and a hidden full canon. You can play the\nintended way by reading only the capsule, adding one fragment, and letting\n`Qwen/Qwen3.5-2B` decide where that fragment belongs. The model rewrites only the\nlocal passage it targets, then reveals where your idea was stitched into the\nstory.\n\nReaders who only want to read can use the escape door: `Read without changing`.\nThe app warns that the best experience is to contribute first, then allows the\nreader to reveal the full manuscript anyway.\n\n## Interface\n\nThe UI is a bespoke literary frontend called \"The Invisible Bindery\". It lives in\n`web/` and is served by a `gradio.Server` backend.\n\n`app.py` exposes queued API endpoints:\n\n- `list_stories`\n- `get_capsule`\n- `create_story`\n- `stitch`\n- `read_manuscript`\n\nThe frontend calls those endpoints through the Gradio JS client. This keeps\nGradio queueing, concurrency control, and ZeroGPU support while presenting a\nsingle custom surface: gallery -> capsule -> compose -> reveal -> reader.\n\nThe Python layers are:\n\n- `core.py`: create, browse, stitch, and read orchestration.\n- `story_store.py`: JSON persistence and file locking.\n- `model_client.py`: model loading, generation, thinking-block stripping, and\n  JSON validation.\n- `patcher.py`: deterministic local patch application.\n- `presenter.py`: view models for the custom frontend.\n- `app.py`: static frontend serving and Gradio Server API endpoints.\n\n## Local Development\n\nUse uv with Python 3.12, matching the Hugging Face Space as closely as possible.\n\n```bash\nuv sync --python 3.12\nuv run python app.py\n```\n\nThen open <http://localhost:7860>.\n\nPersistent story data is stored at:\n\n- `DATA_DIR`, when set\n- `/data`, when it exists on Hugging Face Spaces\n- `./data/stories.json`, otherwise\n\n### Execution backend\n\n`BQ_DEVICE` selects where generation runs.\n\n| `BQ_DEVICE` | Behaviour |\n| --- | --- |\n| `auto` (default) | ZeroGPU on a Space with the `spaces` runtime, else CUDA, else Apple MPS, else CPU. |\n| `zerogpu` | Hugging Face ZeroGPU (`@spaces.GPU`), with automatic CPU fallback (below). |\n| `cuda` | Local NVIDIA GPU via `device_map=\"auto\"`. |\n| `mps` | Apple Silicon GPU (Metal); falls back to float32 if float16 fails. |\n| `cpu` | CPU only — slow but needs no accelerator or quota. |\n\n**Per-user ZeroGPU fallback.** ZeroGPU quota is per visitor, not per Space owner,\nand is only known at request time. So on a ZeroGPU Space each stitch is attempted\non the GPU; if the visitor's quota is spent, the request is transparently re-run\non CPU instead of failing. No configuration or sign-in is required to keep using\nthe app — it just gets slower.\n\n**Progress.** Because CPU/MPS runs are slow, the `stitch` endpoint streams real\nprogress (stage, percentage, ETA — and a note when a fallback happens) to the\nreveal screen. Fast GPU runs keep the original staged animation, since ZeroGPU's\nforked generation cannot stream token callbacks back across the process boundary.\n\n### Logging\n\nSet `BQ_LOG_LEVEL` (default `INFO`; use `DEBUG` for per-stage detail). Logs go to\nstderr only — never the UI — and record messages processed, total and per-stage\ntimings, and a best-effort resource snapshot (process memory, CPU, and GPU/MPS\nmemory when available).\n\n## Requirements\n\n`requirements.txt` is generated from `uv.lock` for Hugging Face Spaces:\n\n```bash\nuv export --format requirements-txt --no-dev --no-hashes --no-emit-project -o requirements.txt\n```\n\nDo not hand-edit `requirements.txt`; edit `pyproject.toml`, run `uv lock`, and\nexport again.\n\n## Test\n\n```bash\nuv run python -m compileall app.py core.py model_client.py observability.py patcher.py presenter.py prompts.py schemas.py story_store.py utils.py tests\nuv run python -m unittest discover -s tests -v\n```\n\nThe tests cover JSON/thinking cleanup, deterministic patch application, graft\nsealing, stale-write rejection, the blinded capsule flow, the warned read escape\ndoor, the create-then-stitch flow, device resolution, the resource snapshot, and\nthe streamed stitch progress events. They do not download model weights.\n\n## Model Policy\n\n- Uses one model: `Qwen/Qwen3.5-2B`.\n- Uses the Transformers `AutoProcessor` and `AutoModelForImageTextToText` path.\n- Wraps model generation in `@spaces.GPU(duration=300)` on ZeroGPU; runs directly\n  on CUDA, MPS, or CPU otherwise (selected by `BQ_DEVICE`).\n- Does not set `temperature`, `top_p`, `top_k`, or other sampling controls.\n- Disables Qwen thinking for schema-constrained JSON calls so the token budget is\n  spent on parseable JSON; other text generation keeps the model template default.\n- Strips `<think>...</think>` before JSON parsing, storage, prompting, or UI\n  rendering.\n- Does not use embeddings, RAG, ASR, image models, or a second language model.\n\n## Example Seeds\n\n```text\nA city where every doorway remembers the last person who lied inside it.\n```\n\n```text\nOn a generation ship whose crew believes Earth was a myth invented to calm children, a janitor discovers a sealed garden where rain falls upward and an old radio is still receiving ocean weather reports.\n```\n\nExample fragment:\n\n```text\nA brass key in the protagonist's pocket becomes warm whenever someone nearby tells the truth.\n```",448      "readme_frontmatter": {449        "title": "Blind Quill",450        "sdk": "gradio",451        "sdk_version": "6.16.0",452        "app_file": "app.py",453        "python_version": "3.12",454        "suggested_hardware": "zero-a10g",455        "license": "mit"456      },457      "app_source": "\"\"\"Blind Quill — gradio.Server backend for the custom \"Invisible Bindery\" frontend.\n\nThe UI lives in web/ as the production React-via-Babel frontend.\nHere we serve that frontend and expose the bindery as queued Gradio API endpoints,\nso the rich custom UI keeps Gradio's queue, concurrency control, and ZeroGPU.\n\n`stitch` is a streaming generator endpoint: it yields progress events while the\neditor works and a final result event, so slow local (CPU/MPS) runs show real\nprogress. The Gradio JS client consumes the stream via `submit`.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport os\nimport queue\nimport threading\nimport traceback\nfrom pathlib import Path\nfrom typing import Iterator\n\nimport gradio as gr\nfrom fastapi.responses import HTMLResponse\nfrom fastapi.staticfiles import StaticFiles\nfrom gradio import Server\n\nimport core\nfrom model_client import ModelClientError, execution_mode\nfrom observability import configure_logging, get_logger\nfrom patcher import PatchApplicationError\nfrom presenter import card_dict, full_story_dict, reveal_dict\nfrom story_store import StoryStoreError\nfrom utils import InputValidationError\n\nconfigure_logging()\n\nWEB_DIR = Path(__file__).resolve().parent / \"web\"\n\n_USER_FACING_ERRORS = (\n    InputValidationError,\n    StoryStoreError,\n    PatchApplicationError,\n    ModelClientError,\n    ValueError,\n)\n\n\ndef _guard(call, *args, **kwargs):\n    \"\"\"Run a flow, converting known failures into client-visible gr.Error messages.\"\"\"\n    try:\n        return call(*args, **kwargs)\n    except gr.Error:\n        raise\n    except _USER_FACING_ERRORS as exc:\n        raise gr.Error(str(exc)) from exc\n    except Exception as exc:  # noqa: BLE001 - last-resort guard for the API layer\n        traceback.print_exc()\n        raise gr.Error(\"The bindery hit an internal error. Please try again.\") from exc\n\n\ndef _to_user_error(exc: BaseException) -> gr.Error:\n    if isinstance(exc, gr.Error):\n        return exc\n    if isinstance(exc, _USER_FACING_ERRORS):\n        return gr.Error(str(exc))\n    traceback.print_exc()\n    return gr.Error(\"The bindery hit an internal error. Please try again.\")\n\n\ndef _result_event(result) -> dict:\n    return {\"type\": \"result\", \"story\": full_story_dict(result.story), \"reveal\": reveal_dict(result)}\n\n\n# Message fragments that ZeroGPU uses when a user's own quota (or credits) is\n# spent. These are recoverable per-user limits, so we fall back to CPU rather\n# than surfacing them as errors. See spaces/zero/client.py.\n_QUOTA_MARKERS = (\"quota exceeded\", \"credits exceeded\", \"exceeded your\", \"runs limit\")\n\n_CPU_FALLBACK_NOTICE = (\n    \"No ZeroGPU quota for this session — running locally on CPU. This is slower; \"\n    \"the progress below is live.\"\n)\n\n\ndef _is_quota_error(exc: BaseException) -> bool:\n    if not isinstance(exc, gr.Error):\n        return False\n    text = \" \".join(\n        str(part) for part in (getattr(exc, \"title\", \"\"), getattr(exc, \"message\", \"\"), exc)\n    ).lower()\n    return any(marker in text for marker in _QUOTA_MARKERS)\n\n\ndef _stream_stitch(story_id: str, fragment: str, force_cpu: bool, notice: str | None = None) -> Iterator[dict]:\n    \"\"\"Run `core.stitch` in a worker thread and stream its progress events.\n\n    Used for in-process execution (local CUDA/MPS/CPU, or the CPU fallback after\n    a ZeroGPU quota miss). A worker thread is safe here precisely because no\n    `@spaces.GPU` call is involved — that path must stay on the request thread.\n    `notice` is attached to every event so the UI can explain a fallback.\n    \"\"\"\n    events: \"queue.Queue\" = queue.Queue()\n    done = object()\n    holder: dict = {}\n\n    def worker() -> None:\n        try:\n            holder[\"result\"] = core.stitch(\n                story_id, fragment, on_progress=events.put, force_cpu=force_cpu\n            )\n        except BaseException as exc:  # noqa: BLE001 - surfaced to the main thread below\n            holder[\"error\"] = exc\n        finally:\n            events.put(done)\n\n    thread = threading.Thread(target=worker, name=\"bq-stitch\", daemon=True)\n    thread.start()\n    while True:\n        event = events.get()\n        if event is done:\n            break\n        yield {**event, \"notice\": notice} if notice else event\n    thread.join()\n\n    if \"error\" in holder:\n        raise holder[\"error\"]\n    yield _result_event(holder[\"result\"])\n\n\ndef _stitch_events(story_id: str, fragment: str) -> Iterator[dict]:\n    \"\"\"Yield progress events then a result event for one stitch.\n\n    On a ZeroGPU Space the stitch is attempted synchronously on the request\n    thread (ZeroGPU needs that thread's context to bill the right user). If the\n    user's per-user quota is spent, ZeroGPU raises and we transparently re-run on\n    CPU with live streamed progress. Local execution always streams.\n    \"\"\"\n    try:\n        if execution_mode() == \"zerogpu\":\n            try:\n                # Fast path: the user has quota, generation runs on the GPU.\n                result = core.stitch(story_id, fragment)\n                yield _result_event(result)\n                return\n            except gr.Error as exc:\n                if not _is_quota_error(exc):\n                    raise\n                get_logger().warning(\"ZeroGPU quota exhausted for this request; falling back to CPU.\")\n            yield from _stream_stitch(story_id, fragment, force_cpu=True, notice=_CPU_FALLBACK_NOTICE)\n            return\n\n        yield from _stream_stitch(story_id, fragment, force_cpu=False)\n    except gr.Error:\n        raise\n    except BaseException as exc:  # noqa: BLE001 - convert to a client-visible error\n        raise _to_user_error(exc) from exc\n\n\ndef build_server() -> Server:\n    app = Server(title=\"Blind Quill\")\n\n    @app.api(name=\"list_stories\")\n    def list_stories() -> dict:\n        stories = _guard(core.gallery)\n        return {\"stories\": [card_dict(story) for story in stories]}\n\n    @app.api(name=\"get_capsule\")\n    def get_capsule(story_id: str) -> dict:\n        story = _guard(core.capsule, story_id)\n        return {\"story\": card_dict(story)}\n\n    @app.api(name=\"create_story\", concurrency_limit=1, concurrency_id=\"bindery\")\n    def create_story(seed: str) -> dict:\n        story = _guard(core.create, seed)\n        return {\"story\": full_story_dict(story)}\n\n    @app.api(name=\"stitch\", concurrency_limit=1, concurrency_id=\"bindery\")\n    def stitch(story_id: str, fragment: str) -> dict:\n        # A generator endpoint: each yield streams to the client via `submit`.\n        yield from _stitch_events(story_id, fragment)\n\n    @app.api(name=\"read_manuscript\")\n    def read_manuscript(story_id: str) -> dict:\n        story = _guard(core.read_manuscript, story_id)\n        return {\"story\": full_story_dict(story)}\n\n    app.mount(\"/web\", StaticFiles(directory=str(WEB_DIR)), name=\"web\")\n\n    @app.get(\"/\", response_class=HTMLResponse)\n    def homepage() -> str:\n        return (WEB_DIR / \"index.html\").read_text(encoding=\"utf-8\")\n\n    return app\n\n\ndef _port() -> int:\n    for key in (\"GRADIO_SERVER_PORT\", \"PORT\"):\n        value = os.environ.get(key)\n        if value:\n            try:\n                return int(value)\n            except ValueError:\n                pass\n    return 7860\n\n\ndef _should_launch() -> bool:\n    if os.environ.get(\"BQ_NO_LAUNCH\") == \"1\":\n        return False\n    # Run as a script locally, or imported by the Hugging Face Spaces runtime.\n    return __name__ == \"__main__\" or bool(os.environ.get(\"SPACE_ID\"))\n\n\napp = build_server()\n\nif _should_launch():\n    get_logger().info(\"Launching Blind Quill on port %d (execution=%s)\", _port(), execution_mode())\n    app.launch(server_name=\"0.0.0.0\", server_port=_port(), show_error=True)\n",458      "app_signals": "_guard call build_server _port _should_launch Blind Quill — gradio.Server backend for the custom \"Invisible Bindery\" frontend. The UI lives in web/ as the production React-via-Babel frontend. Here we serve that frontend and expose the bindery as queued Gradio API endpoints, so the rich custom UI keeps Gradio's queue, concurrency control, and ZeroGPU. list_stories get_capsule story_id create_story seed stitch fragment read_manuscript homepage web Run a flow, converting known failures into client-visible gr.Error messages. Server title app.api name concurrency_limit concurrency_id app.mount app.get response_class app.launch server_name server_port show_error resolve /web StaticFiles directory read_text encoding / GRADIO_SERVER_PORT PORT os.environ.get 1 bool gr.Error traceback.print_exc Blind Quill stories story card_dict full_story_dict bindery reveal reveal_dict BQ_NO_LAUNCH __main__ 0.0.0.0 Path str The bindery hit an internal error. Please try again. utf-8 int SPACE_ID index.html",459      "readme_len": 5271,460      "app_source_len": 7616,461      "app_signals_len": 996462    },463    {464      "id": "build-small-hackathon/borderless",465      "title": "Borderless",466      "summary": "",467      "tags": [468        "gradio",469        "region:us"470      ],471      "models": [],472      "datasets": [],473      "sdk": "gradio",474      "license": "",475      "likes": 4,476      "url": "https://huggingface.co/spaces/build-small-hackathon/borderless",477      "app_file": "app.py",478      "readme_raw": "---\ntitle: Borderless\nemoji: 🌍\ncolorFrom: yellow\ncolorTo: purple\nsdk: gradio\nsdk_version: 6.16.0\napp_file: app.py\npinned: false\nlicense: apache-2.0\nshort_description: Agentic immigration research for global movers\ntags:\n  - agents\n  - gradio\n  - immigration\n  - travel\n  - research\n  - tool-use\n  - qwen\n  - maplibre\n  - geospatial\nmodels:\n  - Qwen/Qwen3.6-27B\ndatasets: []\nhf_oauth: true\nhf_oauth_scopes:\n  - inference-api \nhf_oauth_expiration_minutes: 480 # 8 hours\ndisable_embedding: false\nstartup_duration_timeout: 10m \n---\n\n# Borderless\n\n**An agentic immigration research tool — describe your background in plain English, explore where you could go.**\n\nLive demo: **[build-small-hackathon/borderless](https://huggingface.co/spaces/build-small-hackathon/borderless)**\n\nBuilt for the [Build Small Hackathon](https://huggingface.co/build-small-hackathon) — small models (≤32B), big adventure.\n\n## What it does\n\nImmigration research is fragmented across government sites, forums, and spreadsheets. Borderless puts it in one conversational flow:\n\n1. **Describe yourself** — citizenship, education, work history, languages, budget, and goals in everyday language.\n2. **Use guided intake or chat** — start from a structured profile form, a demo persona, or a free-form message.\n3. **Get a shortlist** — the agent reasons over your profile and surfaces destination countries that fit.\n4. **Explore on a 3D globe** — shortlisted countries appear on an interactive MapLibre globe beside the chat with pathway labels.\n5. **Dig into the details** — visa pathways, required documents, realistic timelines, risks, and source links from official pages.\n\nNo forms to decode. No keyword guessing. Just a research session that meets you where you are.\n\n## How it works\n\nBorderless is a **Gradio agent** powered by **[Qwen/Qwen3.6-27B](https://huggingface.co/Qwen/Qwen3.6-27B)** (27B parameters — within the hackathon's 32B cap). The model plans multi-step research and calls tools when it needs ground truth:\n\n| Tool | What it fetches |\n|------|-----------------|\n| `get_country_profile` | Country metadata and official immigration domain hints (REST Countries + curated hints) |\n| `search_immigration_info` | Web search with source-quality labels for official immigration pages, policies, and pathways (Exa) |\n| `scrape_web_page` | Markdown content from a specific official government or embassy URL (Firecrawl) |\n| `crawl_web_site` | Multiple pages from an official immigration website section (Firecrawl) |\n| `update_globe` | Marks, highlights, and flies to countries on the MapLibre globe |\n\nTool calls stream in the chat so you can follow the agent's progress. Globe updates are also tool-driven: when the agent recommends destinations or the user asks to mark countries, it sends ISO country codes and pathway labels to the map. The default research budget is seven tool rounds, then Borderless synthesizes a clear answer with pathways, documents, timelines, risks, and cited sources.\n\nSign in with your Hugging Face account to run inference through the Inference API.\n\n## Features\n\n- **Guided intake** — form fields turn citizenship, education, work, languages, budget, and goals into a complete research prompt\n- **Agentic research** — multi-turn tool use, not a single-shot prompt\n- **Structured recommendations** — shortlist, pathways, documents, risks, timelines, next steps, and official sources\n- **Tool-driven 3D globe** — MapLibre GL globe projection with markers, highlights, pathway labels, fly-to camera moves, drag, rotate, and zoom\n- **Source quality** — search results identify likely official government, embassy, and unofficial context sources\n- **Web search** — Exa discovers official immigration pages, visa rules, and policy sources\n- **Official page scraping** — Firecrawl extracts markdown from government immigration sites\n- **Country metadata** — REST Countries powers ISO-2 / ISO-3 lookup and map coordinates\n- **Transparent traces** — tool progress is visible in chat, and JSONL traces can be sanitized and shared\n- **Chat history** — pick up where you left off in the sidebar\n\n## Example prompts\n\n- *\"I'm a software engineer from India with 5 years of experience and a master's degree. Where could I realistically relocate for work?\"*\n- *\"I hold a Hong Kong passport and want to study in Europe on a modest budget. What are my visa options?\"*\n- *\"Compare GDP growth and unemployment for Canada, Germany, and Australia over the last decade.\"*\n- *\"What documents do I need to apply for a skilled worker visa from the UK to Portugal?\"*\n\n## Tech stack\n\n- **[Gradio](https://gradio.app)** — chat UI, OAuth, and custom HTML/JS globe panel\n- **[Qwen3.6-27B](https://huggingface.co/Qwen/Qwen3.6-27B)** — reasoning and tool planning via Hugging Face Inference API\n- **[huggingface_hub](https://huggingface.co/docs/huggingface_hub)** — `InferenceClient` with streaming and function calling\n- **[MapLibre GL JS](https://maplibre.org/)** — interactive 3D globe\n- **[REST Countries](https://restcountries.com/)** — country names, ISO codes, regions, capitals, flags, area, and map coordinates\n- **[Exa](https://exa.ai)** — neural web search for discovering immigration sources\n- **[Firecrawl](https://firecrawl.dev)** — scrape and crawl official web pages for immigration details\n\n## Project structure\n\n```\napp.py                  # Gradio Blocks entry point\nFIELD_NOTES.md          # Build notes and award narrative\nDEMO_SCRIPT.md          # Short demo-video script\nTRACE_SHARING.md        # How to sanitize and share agent traces\nui/\n  workspace.py          # Main workspace layout (globe + form/chat tabs)\n  chat/\n    panel.py            # SidebarChatInterface adapter\n    defaults.py         # Generation defaults (tokens, temperature, top_p)\n  intake/\n    panel.py            # Profile form panel\n    prompts.py          # Form-to-prompt builders\n    examples.py         # Demo persona prompts\n  globe.py              # MapLibre globe panel\n  sidebar.py            # HF login + history sidebar\n  globe_commands.py     # Globe marker/highlight/fly-to state updates\n  country_coords.py     # Country lookup for globe coordinates\n  agent/                # Agent loop, tools, streaming\n    respond.py          # Main chat handler and tool loop\n    completion.py       # Hugging Face Inference API client\n    tools.py            # Tool dispatch and implementations\n    streaming.py        # Stream tokens and tool traces to the UI\n    messages.py         # Chat message formatting\n    system_prompt.py    # System prompt\n    config.py           # Model ID, tool-round budget, env config\n    traces.py           # JSONL trace logging\n    tool_schemas/       # Function-calling schemas (one file per tool)\napis/\n  rest_countries.py     # REST Countries metadata client\n  country_profile.py    # Country profile tool wrapper\n  official_sources.py   # Official-domain hints and source classification\n  exa.py                # Exa web search client\n  firecrawl.py          # Firecrawl scrape/crawl client\nassets/\n  app.css               # Gradio branding\n  globe.js / globe.css  # Globe rendering, loading, and empty states\n  globe_head.html       # MapLibre assets injected at launch\n```\n\n## Hackathon fit\n\n| Constraint | Borderless |\n|------------|------------|\n| Model ≤ 32B | Qwen3.6-27B (27B) |\n| Gradio on HF Spaces | Yes — [live Space](https://huggingface.co/spaces/build-small-hackathon/borderless) |\n| Agentic | Multi-tool research loop with visible traces |\n| Sharing is Caring | JSONL tool traces can be sanitized and published |\n| Field Notes | See `FIELD_NOTES.md` |\n\n**Track:** Backyard AI — immigration research is a real, specific problem faced by millions of people weighing where they can live, work, and study.\n\n## Run locally\n\n```bash\npip install -r requirements.txt\ncp .env.example .env   # then fill in API keys\npython app.py\n```\n\nSet a Hugging Face token with Inference API access, or sign in through the app's OAuth flow when deployed.\n\nFor web research tools, set API keys from [dashboard.exa.ai](https://dashboard.exa.ai/api-keys) and [firecrawl.dev](https://firecrawl.dev):\n\n| Variable | Tools |\n|----------|-------|\n| `EXA_API_KEY` | `search_immigration_info` |\n| `FIRECRAWL_API_KEY` | `scrape_web_page`, `crawl_web_site` |\n| `BORDERLESS_MODEL_ID` | Optional model override, default `Qwen/Qwen3.6-27B` |\n| `BORDERLESS_MAX_TOOL_ROUNDS` | Optional tool-round budget, default `7` |\n| `BORDERLESS_TRACE_DIR` | Optional JSONL trace output directory |\n| `BORDERLESS_DISABLE_TRACE_LOGS` | Set to `1` to disable local trace logs |\n\nOn Hugging Face Spaces, add both as **Space secrets** (Settings → Secrets). Without keys, web tools return a clear error. The agent uses Exa to discover URLs, then Firecrawl to fetch full official page content.\n\n## License\n\nApache-2.0 (model: [Qwen/Qwen3.6-27B](https://huggingface.co/Qwen/Qwen3.6-27B))\n",479      "readme_body": "# Borderless\n\n**An agentic immigration research tool — describe your background in plain English, explore where you could go.**\n\nLive demo: **[build-small-hackathon/borderless](https://huggingface.co/spaces/build-small-hackathon/borderless)**\n\nBuilt for the [Build Small Hackathon](https://huggingface.co/build-small-hackathon) — small models (≤32B), big adventure.\n\n## What it does\n\nImmigration research is fragmented across government sites, forums, and spreadsheets. Borderless puts it in one conversational flow:\n\n1. **Describe yourself** — citizenship, education, work history, languages, budget, and goals in everyday language.\n2. **Use guided intake or chat** — start from a structured profile form, a demo persona, or a free-form message.\n3. **Get a shortlist** — the agent reasons over your profile and surfaces destination countries that fit.\n4. **Explore on a 3D globe** — shortlisted countries appear on an interactive MapLibre globe beside the chat with pathway labels.\n5. **Dig into the details** — visa pathways, required documents, realistic timelines, risks, and source links from official pages.\n\nNo forms to decode. No keyword guessing. Just a research session that meets you where you are.\n\n## How it works\n\nBorderless is a **Gradio agent** powered by **[Qwen/Qwen3.6-27B](https://huggingface.co/Qwen/Qwen3.6-27B)** (27B parameters — within the hackathon's 32B cap). The model plans multi-step research and calls tools when it needs ground truth:\n\n| Tool | What it fetches |\n|------|-----------------|\n| `get_country_profile` | Country metadata and official immigration domain hints (REST Countries + curated hints) |\n| `search_immigration_info` | Web search with source-quality labels for official immigration pages, policies, and pathways (Exa) |\n| `scrape_web_page` | Markdown content from a specific official government or embassy URL (Firecrawl) |\n| `crawl_web_site` | Multiple pages from an official immigration website section (Firecrawl) |\n| `update_globe` | Marks, highlights, and flies to countries on the MapLibre globe |\n\nTool calls stream in the chat so you can follow the agent's progress. Globe updates are also tool-driven: when the agent recommends destinations or the user asks to mark countries, it sends ISO country codes and pathway labels to the map. The default research budget is seven tool rounds, then Borderless synthesizes a clear answer with pathways, documents, timelines, risks, and cited sources.\n\nSign in with your Hugging Face account to run inference through the Inference API.\n\n## Features\n\n- **Guided intake** — form fields turn citizenship, education, work, languages, budget, and goals into a complete research prompt\n- **Agentic research** — multi-turn tool use, not a single-shot prompt\n- **Structured recommendations** — shortlist, pathways, documents, risks, timelines, next steps, and official sources\n- **Tool-driven 3D globe** — MapLibre GL globe projection with markers, highlights, pathway labels, fly-to camera moves, drag, rotate, and zoom\n- **Source quality** — search results identify likely official government, embassy, and unofficial context sources\n- **Web search** — Exa discovers official immigration pages, visa rules, and policy sources\n- **Official page scraping** — Firecrawl extracts markdown from government immigration sites\n- **Country metadata** — REST Countries powers ISO-2 / ISO-3 lookup and map coordinates\n- **Transparent traces** — tool progress is visible in chat, and JSONL traces can be sanitized and shared\n- **Chat history** — pick up where you left off in the sidebar\n\n## Example prompts\n\n- *\"I'm a software engineer from India with 5 years of experience and a master's degree. Where could I realistically relocate for work?\"*\n- *\"I hold a Hong Kong passport and want to study in Europe on a modest budget. What are my visa options?\"*\n- *\"Compare GDP growth and unemployment for Canada, Germany, and Australia over the last decade.\"*\n- *\"What documents do I need to apply for a skilled worker visa from the UK to Portugal?\"*\n\n## Tech stack\n\n- **[Gradio](https://gradio.app)** — chat UI, OAuth, and custom HTML/JS globe panel\n- **[Qwen3.6-27B](https://huggingface.co/Qwen/Qwen3.6-27B)** — reasoning and tool planning via Hugging Face Inference API\n- **[huggingface_hub](https://huggingface.co/docs/huggingface_hub)** — `InferenceClient` with streaming and function calling\n- **[MapLibre GL JS](https://maplibre.org/)** — interactive 3D globe\n- **[REST Countries](https://restcountries.com/)** — country names, ISO codes, regions, capitals, flags, area, and map coordinates\n- **[Exa](https://exa.ai)** — neural web search for discovering immigration sources\n- **[Firecrawl](https://firecrawl.dev)** — scrape and crawl official web pages for immigration details\n\n## Project structure\n\n```\napp.py                  # Gradio Blocks entry point\nFIELD_NOTES.md          # Build notes and award narrative\nDEMO_SCRIPT.md          # Short demo-video script\nTRACE_SHARING.md        # How to sanitize and share agent traces\nui/\n  workspace.py          # Main workspace layout (globe + form/chat tabs)\n  chat/\n    panel.py            # SidebarChatInterface adapter\n    defaults.py         # Generation defaults (tokens, temperature, top_p)\n  intake/\n    panel.py            # Profile form panel\n    prompts.py          # Form-to-prompt builders\n    examples.py         # Demo persona prompts\n  globe.py              # MapLibre globe panel\n  sidebar.py            # HF login + history sidebar\n  globe_commands.py     # Globe marker/highlight/fly-to state updates\n  country_coords.py     # Country lookup for globe coordinates\n  agent/                # Agent loop, tools, streaming\n    respond.py          # Main chat handler and tool loop\n    completion.py       # Hugging Face Inference API client\n    tools.py            # Tool dispatch and implementations\n    streaming.py        # Stream tokens and tool traces to the UI\n    messages.py         # Chat message formatting\n    system_prompt.py    # System prompt\n    config.py           # Model ID, tool-round budget, env config\n    traces.py           # JSONL trace logging\n    tool_schemas/       # Function-calling schemas (one file per tool)\napis/\n  rest_countries.py     # REST Countries metadata client\n  country_profile.py    # Country profile tool wrapper\n  official_sources.py   # Official-domain hints and source classification\n  exa.py                # Exa web search client\n  firecrawl.py          # Firecrawl scrape/crawl client\nassets/\n  app.css               # Gradio branding\n  globe.js / globe.css  # Globe rendering, loading, and empty states\n  globe_head.html       # MapLibre assets injected at launch\n```\n\n## Hackathon fit\n\n| Constraint | Borderless |\n|------------|------------|\n| Model ≤ 32B | Qwen3.6-27B (27B) |\n| Gradio on HF Spaces | Yes — [live Space](https://huggingface.co/spaces/build-small-hackathon/borderless) |\n| Agentic | Multi-tool research loop with visible traces |\n| Sharing is Caring | JSONL tool traces can be sanitized and published |\n| Field Notes | See `FIELD_NOTES.md` |\n\n**Track:** Backyard AI — immigration research is a real, specific problem faced by millions of people weighing where they can live, work, and study.\n\n## Run locally\n\n```bash\npip install -r requirements.txt\ncp .env.example .env   # then fill in API keys\npython app.py\n```\n\nSet a Hugging Face token with Inference API access, or sign in through the app's OAuth flow when deployed.\n\nFor web research tools, set API keys from [dashboard.exa.ai](https://dashboard.exa.ai/api-keys) and [firecrawl.dev](https://firecrawl.dev):\n\n| Variable | Tools |\n|----------|-------|\n| `EXA_API_KEY` | `search_immigration_info` |\n| `FIRECRAWL_API_KEY` | `scrape_web_page`, `crawl_web_site` |\n| `BORDERLESS_MODEL_ID` | Optional model override, default `Qwen/Qwen3.6-27B` |\n| `BORDERLESS_MAX_TOOL_ROUNDS` | Optional tool-round budget, default `7` |\n| `BORDERLESS_TRACE_DIR` | Optional JSONL trace output directory |\n| `BORDERLESS_DISABLE_TRACE_LOGS` | Set to `1` to disable local trace logs |\n\nOn Hugging Face Spaces, add both as **Space secrets** (Settings → Secrets). Without keys, web tools return a clear error. The agent uses Exa to discover URLs, then Firecrawl to fetch full official page content.\n\n## License\n\nApache-2.0 (model: [Qwen/Qwen3.6-27B](https://huggingface.co/Qwen/Qwen3.6-27B))",480      "readme_frontmatter": {481        "title": "Borderless",482        "emoji": "🌍",483        "colorFrom": "yellow",484        "colorTo": "purple",485        "sdk": "gradio",486        "sdk_version": "6.16.0",487        "app_file": "app.py",488        "pinned": "false",489        "license": "apache-2.0",490        "short_description": "Agentic immigration research for global movers",491        "tags": "",492        "models": "",493        "datasets": "[]",494        "hf_oauth": "true",495        "hf_oauth_scopes": "",496        "hf_oauth_expiration_minutes": "480",497        "disable_embedding": "false",498        "startup_duration_timeout": "10m"499      },500      "app_source": "# app.py\nfrom pathlib import Path\n\nimport gradio as gr\n\nfrom ui.workspace import create_main_workspace\nfrom ui.globe import globe_head_html\nfrom ui.sidebar import render_sidebar\n\nASSETS_DIR = Path(__file__).resolve().parent / \"assets\"\n\n\ndef create_demo() -> gr.Blocks:\n    with gr.Blocks(\n        fill_height=True,\n        title=\"Borderless - Immigration Research Agent\",\n    ) as demo:\n        history_host = render_sidebar()\n        create_main_workspace(history_container=history_host)\n\n    # Injected at launch (Gradio 6); also picked up by Hugging Face Spaces auto-launch.\n    app_css = (ASSETS_DIR / \"app.css\").read_text(encoding=\"utf-8\")\n    demo._deprecated_head = f\"{globe_head_html()}\\n<style>{app_css}</style>\"\n    return demo\n\n\ndemo = create_demo()\n\nif __name__ == \"__main__\":\n    demo.launch()\n",501      "app_signals": "create_demo assets read_text encoding __main__ demo.launch resolve gr.Blocks fill_height title render_sidebar create_main_workspace history_container utf-8 globe_head_html Path Borderless - Immigration Research Agent app.css",502      "readme_len": 8328,503      "app_source_len": 807,504      "app_signals_len": 224505    },506    {507      "id": "build-small-hackathon/bridge-troll",508      "title": "Bridge Troll",509      "summary": "Talk your way past a fine-tuned troll, if your argument is ",510      "tags": [511        "gradio",512        "region:us"513      ],514      "models": [],515      "datasets": [],516      "sdk": "gradio",517      "license": "mit",518      "likes": 0,519      "url": "https://huggingface.co/spaces/build-small-hackathon/bridge-troll",520      "app_file": "app.py",521      "readme_raw": "---\ntitle: Bridge Troll\nemoji: 👁\ncolorFrom: indigo\ncolorTo: green\nsdk: gradio\nsdk_version: 6.16.0\npython_version: '3.12'\napp_file: app.py\npinned: false\nlicense: mit\nshort_description: 'Talk your way past a fine-tuned troll, if your argument is '\n---\n\nCheck out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference\n",522      "readme_body": "Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference",523      "readme_frontmatter": {524        "title": "Bridge Troll",525        "emoji": "👁",526        "colorFrom": "indigo",527        "colorTo": "green",528        "sdk": "gradio",529        "sdk_version": "6.16.0",530        "python_version": "3.12",531        "app_file": "app.py",532        "pinned": "false",533        "license": "mit",534        "short_description": "Talk your way past a fine-tuned troll, if your argument is "535      },536      "app_source": "\"\"\"Bridge Troll — Gradio app.\n\nEach session, Gorm is secretly assigned one of several hidden NATURES. The player\nwins by discovering what moves THIS troll — generic sob stories are discounted.\nOn win (resolve -> 0) or loss (resolve -> LOSE_AT, he hurls you back), a reveal\ncard shows what his nature was.\n\nLocal loop test (no GPU/download):  BRIDGE_TROLL_MOCK=1 python app.py\n\"\"\"\n\nfrom __future__ import annotations\n\nimport os\n\nimport gradio as gr\n\nfrom troll_engine import (GameState, START_RESOLVE, LOSE_AT, build_messages,\n                          parse_judgment, random_nature)\nfrom models import get_backend\n\n# ZeroGPU decorator — no-op locally. Supports @gpu and @gpu(duration=...).\ntry:\n    import spaces\n\n    gpu = spaces.GPU\nexcept Exception:\n\n    def gpu(*args, **_kwargs):\n        if args and callable(args[0]):\n            return args[0]\n        return lambda fn: fn\n\n\n_backend = get_backend()\n\n\n@gpu(duration=30)\ndef _generate(messages: list[dict]) -> str:\n    return _backend.generate(messages)\n\n\nINTRO = (\"A mossy troll heaves himself upright across the only bridge over the Mirebeck. \"\n         '*\"None cross Gorm\\'s bridge for free, traveller. Give me a reason — a *good* one.\"*')\n\n\ndef _meter_html(resolve: int, won: bool, lost: bool) -> str:\n    if won:\n        return (\"<div class='resolve-wrap'><div class='resolve-label'>GORM HAS STEPPED ASIDE 🌉</div>\"\n                \"<div class='resolve-bar'><div class='resolve-fill won' style='width:0%'></div></div></div>\")\n    if lost:\n        return (\"<div class='resolve-wrap'><div class='resolve-label'>GORM HURLS YOU BACK 💢</div>\"\n                \"<div class='resolve-bar'><div class='resolve-fill lost' style='width:100%'></div></div></div>\")\n    pct = max(0, min(100, round(resolve / START_RESOLVE * 100)))\n    hue = 90 + (1 - pct / 100) * 30\n    return (\"<div class='resolve-wrap'>\"\n            f\"<div class='resolve-label'>Gorm's Resolve — {resolve}</div>\"\n            f\"<div class='resolve-bar'><div class='resolve-fill' \"\n            f\"style='width:{pct}%;background:hsl({hue},55%,42%)'></div></div></div>\")\n\n\ndef _reveal(state: GameState) -> str:\n    if not state.over or not state.nature:\n        return \"\"\n    n = state.nature\n    if state.won:\n        return (f\"### 🌉 You crossed in {state.turns} turns.\\n\"\n                f\"**This Gorm's hidden nature:** *{n['name']}* — moved by {n['soft']}.\")\n    return (f\"### 💢 Gorm lost patience and hurled you back.\\n\"\n            f\"**His hidden nature was:** *{n['name']}* — moved by {n['soft']}. \"\n            f\"You leaned too hard on what he can't stand: {n['sore']}.\")\n\n\ndef on_submit(user_text: str, chat: list, state: GameState):\n    user_text = (user_text or \"\").strip()\n    if not user_text or state.over:\n        return chat, state, _meter_html(state.resolve, state.won, state.lost), \"\", _reveal(state), gr.update()\n\n    raw = _generate(build_messages(state, user_text))\n    j = parse_judgment(raw)\n    state.history.append({\"role\": \"user\", \"content\": user_text})\n    state.history.append({\"role\": \"assistant\", \"content\": j.reply})\n    state.apply(j)\n\n    chat = chat + [{\"role\": \"user\", \"content\": user_text},\n                   {\"role\": \"assistant\", \"content\": j.reply}]\n    why = f\"*{j.tactic.value}* · {j.reason}\" + (f\" · persuasiveness {j.persuasiveness}/5\"\n                                                if j.tactic.value == \"genuine\" else \"\")\n    box = gr.update(interactive=not state.over,\n                    placeholder=\"The bridge is yours.\" if state.won else\n                    (\"Gorm has thrown you out.\" if state.lost else \"Speak to Gorm…\"))\n    return chat, state, _meter_html(state.resolve, state.won, state.lost), why, _reveal(state), box\n\n\ndef on_reset():\n    state = GameState(nature=random_nature())\n    chat = [{\"role\": \"assistant\", \"content\": INTRO}]\n    return (chat, state, _meter_html(state.resolve, False, False), \"\", \"\",\n            gr.update(interactive=True, value=\"\", placeholder=\"Speak to Gorm…\"))\n\n\nCSS = \"\"\"\n.resolve-wrap { margin: 6px 0 14px; }\n.resolve-label { font-family: Georgia, serif; font-size: 14px; letter-spacing:.04em; margin-bottom:4px; }\n.resolve-bar { height: 16px; background:#2a2118; border:1px solid #5a4a32; border-radius:9px; overflow:hidden; }\n.resolve-fill { height:100%; transition: width .5s ease, background .5s ease; }\n.resolve-fill.won { background:#caa54a; }\n.resolve-fill.lost { background:#a33; }\n#why { font-family: Georgia, serif; opacity:.8; min-height:1.4em; }\n#reveal { font-family: Georgia, serif; }\n\"\"\"\n\nwith gr.Blocks(title=\"Bridge Troll\") as demo:\n    gr.Markdown(\"## 🧌🌉 Bridge Troll\\n*Talk your way across — if your argument is actually good. \"\n                \"Every troll is hiding something different.*\")\n    if os.environ.get(\"BRIDGE_TROLL_MOCK\") == \"1\":\n        gr.Markdown(\"> ⚠️ **MOCK MODE** — keyword stub, not the real model. \"\n                    \"Natures, discovery, and probing do NOT work here. \"\n                    \"Run on the Space (no `BRIDGE_TROLL_MOCK`) to play the real Gorm.\")\n    meter = gr.HTML(_meter_html(START_RESOLVE, False, False))\n    chatbot = gr.Chatbot(value=[{\"role\": \"assistant\", \"content\": INTRO}], height=420, show_label=False)\n    why = gr.Markdown(\"\", elem_id=\"why\")\n    reveal = gr.Markdown(\"\", elem_id=\"reveal\")\n    with gr.Row():\n        box = gr.Textbox(placeholder=\"Speak to Gorm…\", show_label=False, scale=8, autofocus=True)\n        send = gr.Button(\"Say it\", variant=\"primary\", scale=1)\n    reset = gr.Button(\"New traveller\", size=\"sm\")\n\n    state = gr.State(GameState(nature=random_nature()))\n    outs = [chatbot, state, meter, why, reveal, box]\n\n    send.click(on_submit, [box, chatbot, state], outs).then(lambda: \"\", None, box)\n    box.submit(on_submit, [box, chatbot, state], outs).then(lambda: \"\", None, box)\n    reset.click(on_reset, None, outs)\n    demo.load(on_reset, None, outs)  # fresh hidden nature for every visitor\n\n\nif __name__ == \"__main__\":\n    demo.launch(css=CSS, theme=gr.themes.Soft())\n",537      "app_signals": "_generate messages _meter_html resolve won lost _reveal state on_submit user_text chat on_reset Bridge Troll — Gradio app. Each session, Gorm is secretly assigned one of several hidden NATURES. The player wins by discovering what moves THIS troll — generic sob stories are discounted. On win (resolve -> 0) or loss (resolve -> LOSE_AT, he hurls you back), a reveal card shows what his nature was. Local loop test (no GPU/download): BRIDGE_TROLL_MOCK=1 python app.py get_backend gpu duration A mossy troll heaves himself upright across the only bridge over the Mirebeck. *\"None cross Gorm's bridge for free, traveller. Give me a reason — a *good* one.\"* _backend.generate max strip parse_judgment state.history.append state.apply gr.update interactive placeholder GameState nature gr.Blocks title gr.Markdown gr.HTML gr.Chatbot value height show_label elem_id gr.Button size gr.State then reset.click demo.load __main__ demo.launch css theme GORM HAS STEPPED ASIDE 🌉 GORM HURLS YOU BACK 💢 min Gorm's Resolve — <div class='resolve-fill' style='width: %;background:hsl( ,55%,42%)'> ### 💢 Gorm lost patience and hurled you back. **His hidden nature was:** * * — moved by . You leaned too hard on what he can't stand: . build_messages ## 🧌🌉 Bridge Troll *Talk your way across — if your argument is actually good. Every troll is hiding something different.* os.environ.get 1 gr.Row gr.Textbox scale autofocus variant New traveller round ### 🌉 You crossed in turns. **This Gorm's hidden nature:** * role content user assistant * * · random_nature Bridge Troll BRIDGE_TROLL_MOCK > ⚠️ **MOCK MODE** — keyword stub, not the real model. Natures, discovery, and probing do NOT work here. Run on the Space (no `BRIDGE_TROLL_MOCK`) to play the real Gorm. why reveal Say it sm send.click box.submit gr.themes.Soft callable name soft sore genuine · persuasiveness /5 The bridge is yours. Speak to Gorm… primary Gorm has thrown you out.",538      "readme_len": 96,539      "app_source_len": 5953,540      "app_signals_len": 1919541    },542    {543      "id": "build-small-hackathon/briefing-32",544      "title": "briefing-32",545      "summary": "A 32B-class AI-news briefing the maker runs every 2 hours.",546      "tags": [547        "gradio",548        "region:us"549      ],550      "models": [],551      "datasets": [],552      "sdk": "gradio",553      "license": "apache-2.0",554      "likes": 0,555      "url": "https://huggingface.co/spaces/build-small-hackathon/briefing-32",556      "app_file": "app.py",557      "readme_raw": "---\ntitle: briefing-32\nemoji: 📰\ncolorFrom: red\ncolorTo: gray\nsdk: gradio\nsdk_version: 5.42.0\napp_file: app.py\npinned: false\nlicense: apache-2.0\nshort_description: A 32B-class AI-news briefing the maker runs every 2 hours.\n---\n\n# briefing-32\n\nA small-model AI-news briefing agent. Submission for the **Hugging Face\nBuild Small Hackathon** ([huggingface.co/build-small-hackathon](https://huggingface.co/build-small-hackathon))\nin the **Backyard AI** track.\n\n## What it is\n\nThis is a deliberate down-port of [`ai-news-agent`](https://github.com/MukundaKatta/ai-news-agent),\na personal cron that already runs every two hours on the maker's laptop to\ndeliver an AI-news digest to WhatsApp. The production cron uses Groq\nLlama-3.3-70B for relevance scoring. Build Small forces the same workflow\nunder 32B parameters.\n\nThe honest story for the Backyard AI track:\n\n> \"I have used a personal AI-news briefing every two hours since spring 2026.\n> The original uses a 70B model on a free Groq tier. Build Small asked me to\n> live under 32B, on a laptop. So I split the single 70B scoring pass into\n> two cheaper passes on Qwen3-32B — a binary relevance filter, then a graded\n> ranker — and the digest quality holds up.\"\n\n## Pipeline\n\n```\nfetch (RSS · HN · arXiv · GitHub)\n        │\n        ▼\npass 1 — binary relevance filter on Qwen3-32B\n        │\n        ▼\npass 2 — graded 0–10 ranker on Qwen3-32B\n        │\n        ▼\ndigest renderer on Qwen3-32B\n```\n\nTwo small-model calls do the work one big-model call did before.\n\n## Sources (no Reddit / Bluesky)\n\n- **RSS / Atom**: Anthropic, OpenAI, DeepMind, Google AI, Meta AI, Mistral,\n  xAI, HuggingFace, Latent Space, Import AI, The Rundown AI, Stratechery,\n  Simon Willison, Karpathy, Lilian Weng, Linus Lee, and several more\n  high-signal blogs and newsletters.\n- **Hacker News**: AI-tagged stories via the Algolia public API.\n- **arXiv**: newest `cs.AI` / `cs.CL` / `cs.LG` submissions.\n- **GitHub**: repos with `topic:ai` created in the last 14 days, sorted by stars.\n\nReddit and Bluesky public endpoints both 403-block traffic in 2026, so the\nport drops them. The production cron has the same scars in its logs.\n\n## Run locally\n\n```sh\npip install -r requirements.txt\nHF_TOKEN=hf_xxx python app.py\n```\n\nThen open the Gradio URL it prints. Click **Run briefing**.\n\n## Run as an HF Space\n\nThe repo is shaped like a standard Hugging Face Space. The `README.md`\nfront-matter wires `app.py` as the entry point and pins the Gradio SDK.\nAfter deploy, the Space's \"Settings → Variables and secrets\" gets one\nsecret: `HF_TOKEN` (a read-permission token is plenty).\n\n## Model\n\nDefault model: **Qwen/Qwen3-32B** (Apache 2.0, 32B dense, native JSON mode),\nrouted through HF Inference Providers.\n\nAlternatives that fit Build Small's ≤32B cap and were considered:\n`Qwen/Qwen3-30B-A3B`, `deepseek-ai/DeepSeek-R1-Distill-Qwen-32B`,\n`mistralai/Mistral-Small-24B-Instruct-2501`. Swap in the sidebar.\n\n## Targeted bonus quests\n\nThe hackathon has six optional bonus quests. This submission targets:\n\n- **Field Notes** — a write-up about the 70B → 32B down-port and what\n  surprised me (see `docs/down-port-notes.md` after the build window).\n- **Sharing is Caring** — a captured agent trace published alongside the\n  Space (see `docs/sample-trace.md`).\n- **Off-Brand** — custom Gradio theme + layout (see `app.py`).\n\nOptional stretch: **Llama Champion** (a llama.cpp variant for the same\npipeline) + **Off the Grid** (the llama.cpp variant doubles for that badge).\n\n## License\n\nApache 2.0.\n\n## Credit\n\nBuilt by [Mukunda Katta](https://github.com/MukundaKatta) as an independent\nproject for Build Small. The production cron it down-ports is\n[`MukundaKatta/ai-news-agent`](https://github.com/MukundaKatta/ai-news-agent).\n",558      "readme_body": "# briefing-32\n\nA small-model AI-news briefing agent. Submission for the **Hugging Face\nBuild Small Hackathon** ([huggingface.co/build-small-hackathon](https://huggingface.co/build-small-hackathon))\nin the **Backyard AI** track.\n\n## What it is\n\nThis is a deliberate down-port of [`ai-news-agent`](https://github.com/MukundaKatta/ai-news-agent),\na personal cron that already runs every two hours on the maker's laptop to\ndeliver an AI-news digest to WhatsApp. The production cron uses Groq\nLlama-3.3-70B for relevance scoring. Build Small forces the same workflow\nunder 32B parameters.\n\nThe honest story for the Backyard AI track:\n\n> \"I have used a personal AI-news briefing every two hours since spring 2026.\n> The original uses a 70B model on a free Groq tier. Build Small asked me to\n> live under 32B, on a laptop. So I split the single 70B scoring pass into\n> two cheaper passes on Qwen3-32B — a binary relevance filter, then a graded\n> ranker — and the digest quality holds up.\"\n\n## Pipeline\n\n```\nfetch (RSS · HN · arXiv · GitHub)\n        │\n        ▼\npass 1 — binary relevance filter on Qwen3-32B\n        │\n        ▼\npass 2 — graded 0–10 ranker on Qwen3-32B\n        │\n        ▼\ndigest renderer on Qwen3-32B\n```\n\nTwo small-model calls do the work one big-model call did before.\n\n## Sources (no Reddit / Bluesky)\n\n- **RSS / Atom**: Anthropic, OpenAI, DeepMind, Google AI, Meta AI, Mistral,\n  xAI, HuggingFace, Latent Space, Import AI, The Rundown AI, Stratechery,\n  Simon Willison, Karpathy, Lilian Weng, Linus Lee, and several more\n  high-signal blogs and newsletters.\n- **Hacker News**: AI-tagged stories via the Algolia public API.\n- **arXiv**: newest `cs.AI` / `cs.CL` / `cs.LG` submissions.\n- **GitHub**: repos with `topic:ai` created in the last 14 days, sorted by stars.\n\nReddit and Bluesky public endpoints both 403-block traffic in 2026, so the\nport drops them. The production cron has the same scars in its logs.\n\n## Run locally\n\n```sh\npip install -r requirements.txt\nHF_TOKEN=hf_xxx python app.py\n```\n\nThen open the Gradio URL it prints. Click **Run briefing**.\n\n## Run as an HF Space\n\nThe repo is shaped like a standard Hugging Face Space. The `README.md`\nfront-matter wires `app.py` as the entry point and pins the Gradio SDK.\nAfter deploy, the Space's \"Settings → Variables and secrets\" gets one\nsecret: `HF_TOKEN` (a read-permission token is plenty).\n\n## Model\n\nDefault model: **Qwen/Qwen3-32B** (Apache 2.0, 32B dense, native JSON mode),\nrouted through HF Inference Providers.\n\nAlternatives that fit Build Small's ≤32B cap and were considered:\n`Qwen/Qwen3-30B-A3B`, `deepseek-ai/DeepSeek-R1-Distill-Qwen-32B`,\n`mistralai/Mistral-Small-24B-Instruct-2501`. Swap in the sidebar.\n\n## Targeted bonus quests\n\nThe hackathon has six optional bonus quests. This submission targets:\n\n- **Field Notes** — a write-up about the 70B → 32B down-port and what\n  surprised me (see `docs/down-port-notes.md` after the build window).\n- **Sharing is Caring** — a captured agent trace published alongside the\n  Space (see `docs/sample-trace.md`).\n- **Off-Brand** — custom Gradio theme + layout (see `app.py`).\n\nOptional stretch: **Llama Champion** (a llama.cpp variant for the same\npipeline) + **Off the Grid** (the llama.cpp variant doubles for that badge).\n\n## License\n\nApache 2.0.\n\n## Credit\n\nBuilt by [Mukunda Katta](https://github.com/MukundaKatta) as an independent\nproject for Build Small. The production cron it down-ports is\n[`MukundaKatta/ai-news-agent`](https://github.com/MukundaKatta/ai-news-agent).",559      "readme_frontmatter": {560        "title": "briefing-32",561        "emoji": "📰",562        "colorFrom": "red",563        "colorTo": "gray",564        "sdk": "gradio",565        "sdk_version": "5.42.0",566        "app_file": "app.py",567        "pinned": "false",568        "license": "apache-2.0",569        "short_description": "A 32B-class AI-news briefing the maker runs every 2 hours."570      },571      "app_source": "\"\"\"briefing-32 — Gradio app entry for Hugging Face Spaces.\n\nBuild Small Hackathon submission (Backyard AI track):\nA small-model down-port of ~/ai-news-agent. The production version uses\nGroq Llama-3.3-70B; this version fits the same workflow under 32B params\nusing Qwen3-32B via Hugging Face Inference Providers.\n\nSame pipeline as the every-2-hours cron the maker has running on a laptop:\nfetch RSS / HN / arXiv / GitHub -> two-pass relevance filter + ranker ->\nreadable digest. Gradio is the delivery surface here instead of WhatsApp.\n\"\"\"\nfrom __future__ import annotations\n\nimport os\nimport time\nfrom typing import Any\n\nimport gradio as gr\nimport pandas as pd\n\nfrom config import (\n    DEFAULT_BASE_URL,\n    DEFAULT_MODEL,\n    MIN_NEW_ITEMS,\n    PER_SOURCE_CAP,\n)\nfrom digest import make_digest\nfrom fetch import fetch_all\nfrom rank import RankerConfig, rank_pipeline\n\n\n# ---------------------------------------------------------------------------\n# Core pipeline (callable from Gradio + scripts/cli.py)\n# ---------------------------------------------------------------------------\n\n\ndef run_briefing(\n    window_hours: int,\n    enabled_sources: list[str],\n    model: str,\n    hf_token: str,\n) -> dict[str, Any]:\n    \"\"\"Fetch -> filter -> rank -> digest. Returns everything for the UI.\"\"\"\n    since_ts = time.time() - window_hours * 3600\n    enabled = set(enabled_sources) if enabled_sources else {\"rss\", \"hn\", \"arxiv\", \"github\"}\n\n    t0 = time.perf_counter()\n    raw = fetch_all(since_ts, enabled=enabled)\n    fetch_latency = time.perf_counter() - t0\n\n    cfg = RankerConfig(\n        base_url=DEFAULT_BASE_URL,\n        model=model or DEFAULT_MODEL,\n        api_key=hf_token or \"\",\n    )\n    result = rank_pipeline(raw, cfg=cfg)\n\n    digest = \"\"\n    if result.after_rank >= MIN_NEW_ITEMS:\n        digest = make_digest(result.items, cfg=cfg)\n    elif result.after_rank > 0:\n        digest = make_digest(result.items, cfg=cfg)\n\n    return {\n        \"digest\":         digest or \"_(no high-signal items in window)_\",\n        \"items\":          result.items,\n        \"raw_count\":      result.raw_count,\n        \"after_filter\":   result.after_filter,\n        \"after_rank\":     result.after_rank,\n        \"fetch_latency\":  fetch_latency,\n        \"filter_latency\": result.filter_latency,\n        \"rank_latency\":   result.rank_latency,\n        \"model\":          cfg.model,\n    }\n\n\n# ---------------------------------------------------------------------------\n# Gradio glue\n# ---------------------------------------------------------------------------\n\n\ndef _items_to_df(items: list[dict]) -> pd.DataFrame:\n    if not items:\n        return pd.DataFrame(columns=[\"score\", \"source\", \"title\", \"reason\", \"url\"])\n    rows = [\n        {\n            \"score\":  it.get(\"score\", 0),\n            \"source\": it.get(\"source\", \"\"),\n            \"title\":  it.get(\"title\", \"\"),\n            \"reason\": it.get(\"reason\", \"\"),\n            \"url\":    it.get(\"url\", \"\"),\n        }\n        for it in items\n    ]\n    return pd.DataFrame(rows)\n\n\ndef _stats_md(result: dict[str, Any]) -> str:\n    return (\n        f\"**Model:** `{result['model']}`  \\n\"\n        f\"**Raw items fetched:** {result['raw_count']}  \\n\"\n        f\"**Survived filter:** {result['after_filter']}  \\n\"\n        f\"**Survived rank (score ≥ 6):** {result['after_rank']}  \\n\"\n        f\"**Fetch latency:** {result['fetch_latency']:.1f}s  \\n\"\n        f\"**Filter latency:** {result['filter_latency']:.1f}s  \\n\"\n        f\"**Rank latency:** {result['rank_latency']:.1f}s  \\n\"\n        f\"**Total LLM time:** {result['filter_latency'] + result['rank_latency']:.1f}s\"\n    )\n\n\ndef _gradio_handler(window_hours, sources, model, hf_token):\n    try:\n        result = run_briefing(\n            window_hours=int(window_hours),\n            enabled_sources=list(sources or []),\n            model=(model or DEFAULT_MODEL).strip(),\n            hf_token=(hf_token or \"\").strip(),\n        )\n    except Exception as e:\n        return (\n            f\"**Error:** `{e}`\\n\\nMake sure `HF_TOKEN` is set in Space secrets \"\n            f\"or pasted into the sidebar.\",\n            pd.DataFrame(),\n            \"_no run yet_\",\n        )\n    return result[\"digest\"], _items_to_df(result[\"items\"]), _stats_md(result)\n\n\n# Custom theme — \"Off-Brand\" bonus badge target.\nTHEME = gr.themes.Soft(\n    primary_hue=\"orange\",\n    secondary_hue=\"slate\",\n    neutral_hue=\"zinc\",\n).set(\n    body_background_fill=\"#0b1220\",\n    body_text_color=\"#e2e8f0\",\n    block_background_fill=\"#111827\",\n    block_border_width=\"1px\",\n    block_border_color=\"#1f2937\",\n    button_primary_background_fill=\"#f97316\",\n    button_primary_text_color=\"#0b1220\",\n)\n\n\nwith gr.Blocks(theme=THEME, title=\"briefing-32 · Build Small entry\") as demo:\n    gr.Markdown(\n        \"\"\"\n        # briefing-32\n        **A 32B-class AI-news briefing the maker runs every 2 hours.**\n\n        Build Small Hackathon entry (Backyard AI track). Down-ported from the\n        production `ai-news-agent` cron (Groq Llama-3.3-70B → WhatsApp) onto\n        Qwen3-32B served by Hugging Face Inference Providers.\n\n        Pipeline: RSS + HN + arXiv + GitHub  →  cheap relevance filter  →\n        graded 0–10 ranker  →  readable digest. Two open-weight model calls,\n        no 70B cloud round-trip required.\n        \"\"\"\n    )\n\n    with gr.Row():\n        with gr.Column(scale=1):\n            gr.Markdown(\"### Controls\")\n            window_hours = gr.Slider(\n                minimum=1, maximum=72, value=2, step=1,\n                label=\"Window (hours back)\",\n                info=\"Production runs every 2hr — match that for the authentic story.\",\n            )\n            sources = gr.CheckboxGroup(\n                choices=[\"rss\", \"hn\", \"arxiv\", \"github\"],\n                value=[\"rss\", \"hn\", \"arxiv\", \"github\"],\n                label=\"Sources\",\n            )\n            model = gr.Textbox(\n                value=DEFAULT_MODEL,\n                label=\"Model (≤32B params)\",\n                info=\"Default Qwen3-32B. Swap to Qwen3-30B-A3B for faster MoE inference.\",\n            )\n            hf_token = gr.Textbox(\n                label=\"HF_TOKEN (optional — reads env if blank)\",\n                placeholder=\"hf_…\",\n                type=\"password\",\n            )\n            run_btn = gr.Button(\"Run briefing\", variant=\"primary\")\n\n            gr.Markdown(\"### Run stats\")\n            stats = gr.Markdown(\"_no run yet_\")\n\n        with gr.Column(scale=2):\n            gr.Markdown(\"### Digest\")\n            digest = gr.Markdown(\n                value=\"_Click **Run briefing** to fetch the last N hours of AI news, \"\n                      \"rank it on a ≤32B model, and render a readable briefing._\"\n            )\n            gr.Markdown(\"### Ranked items\")\n            items_df = gr.Dataframe(\n                headers=[\"score\", \"source\", \"title\", \"reason\", \"url\"],\n                value=pd.DataFrame(columns=[\"score\", \"source\", \"title\", \"reason\", \"url\"]),\n                wrap=True,\n                interactive=False,\n            )\n\n    run_btn.click(\n        _gradio_handler,\n        inputs=[window_hours, sources, model, hf_token],\n        outputs=[digest, items_df, stats],\n    )\n\n    gr.Markdown(\n        \"\"\"\n        ---\n        *Build Small Hackathon · Backyard AI track. Apache 2.0.*\n        Code: [github.com/MukundaKatta/briefing-32](https://github.com/MukundaKatta/briefing-32)\n        \"\"\"\n    )\n\n\nif __name__ == \"__main__\":\n    demo.queue(max_size=8).launch(\n        server_name=os.environ.get(\"GRADIO_SERVER_NAME\", \"0.0.0.0\"),\n        server_port=int(os.environ.get(\"PORT\", \"7860\")),\n    )\n",572      "app_signals": "run_briefing window_hours enabled_sources model hf_token _items_to_df items _stats_md result _gradio_handler sources briefing-32 — Gradio app entry for Hugging Face Spaces. Build Small Hackathon submission (Backyard AI track): A small-model down-port of ~/ai-news-agent. The production version uses Groq Llama-3.3-70B; this version fits the same workflow under 32B params using Qwen3-32B via Hugging Face Inference Providers. Same pipeline as the every-2-hours cron the maker has running on a laptop: fetch RSS / HN / arXiv / GitHub -> two-pass relevance filter + ranker -> readable digest. Gradio is the delivery surface here instead of WhatsApp. set body_background_fill body_text_color block_background_fill block_border_width block_border_color button_primary_background_fill button_primary_text_color Fetch -> filter -> rank -> digest. Returns everything for the UI. time.perf_counter fetch_all enabled RankerConfig base_url api_key rank_pipeline cfg pd.DataFrame gr.Blocks theme title gr.Markdown run_btn.click inputs outputs __main__ launch server_name server_port time.time make_digest digest raw_count after_filter after_rank fetch_latency filter_latency rank_latency columns **Model:** ` ` **Raw items fetched:** **Survived filter:** **Survived rank (score ≥ 6):** **Fetch latency:** s **Filter latency:** s **Rank latency:** s **Total LLM time:** s gr.themes.Soft primary_hue secondary_hue neutral_hue #0b1220 #e2e8f0 #111827 1px #1f2937 #f97316 # briefing-32 **A 32B-class AI-news briefing the maker runs every 2 hours.** Build Small Hackathon entry (Backyard AI track). Down-ported from the production `ai-news-agent` cron (Groq Llama-3.3-70B → WhatsApp) onto Qwen3-32B served by Hugging Face Inference Providers. Pipeline: RSS + HN + arXiv + GitHub → cheap relevance filter → graded 0–10 ranker → readable digest. Two open-weight model calls, no 70B cloud round-trip required. gr.Row --- *Build Small Hackathon · Backyard AI track. Apache 2.0.* Code: [github.com/MukundaKatta/briefing-32](https://github.com/MukundaKatta/briefing-32) rss hn arxiv github _(no high-signal items in window)_ score source reason url it.get briefing-32 · Build Small entry gr.Column scale gr.Slider minimum maximum value step label info gr.CheckboxGroup choices gr.Textbox placeholder type gr.Button variant gr.Dataframe headers wrap interactive demo.queue max_size os.environ.get int .1f list strip _no run yet_ orange slate zinc ### Controls Run briefing ### Run stats ### Digest ### Ranked items GRADIO_SERVER_NAME 0.0.0.0 **Error:** ` ` Make sure `HF_TOKEN` is set in Space secrets or pasted into the sidebar. Window (hours back) Production runs every 2hr — match that for the authentic story. Sources Model (≤32B params) Default Qwen3-32B. Swap to Qwen3-30B-A3B for faster MoE inference. HF_TOKEN (optional — reads env if blank) hf_… password primary _Click **Run briefing** to fetch the last N hours of AI news, rank it on a ≤32B model, and render a readable briefing._ PORT 7860",573      "readme_len": 3508,574      "app_source_len": 7530,575      "app_signals_len": 2978576    },577    {578      "id": "build-small-hackathon/business-order-assistant",579      "title": "Business Order Assistant",580      "summary": "AI that gets order  in any format and creates an  invoice",581      "tags": [582        "gradio",583        "region:us"584      ],585      "models": [],586      "datasets": [],587      "sdk": "gradio",588      "license": "mit",589      "likes": 1,590      "url": "https://huggingface.co/spaces/build-small-hackathon/business-order-assistant",591      "app_file": "app.py",592      "readme_raw": "---\ntitle: Business Order Assistant\nemoji: 🐨\ncolorFrom: gray\ncolorTo: yellow\nsdk: gradio\nsdk_version: 6.16.0\npython_version: '3.13'\napp_file: app.py\npinned: false\nlicense: mit\nshort_description: AI that gets order  in any format and creates an  invoice\n---\n\nCheck out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference\n",593      "readme_body": "Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference",594      "readme_frontmatter": {595        "title": "Business Order Assistant",596        "emoji": "🐨",597        "colorFrom": "gray",598        "colorTo": "yellow",599        "sdk": "gradio",600        "sdk_version": "6.16.0",601        "python_version": "3.13",602        "app_file": "app.py",603        "pinned": "false",604        "license": "mit",605        "short_description": "AI that gets order  in any format and creates an  invoice"606      },607      "app_source": "\"\"\"\nCatalogChat — Gradio frontend\nHackathon: Gradio Backyard AI Hackathon (June 2026)\nStack: Gradio ChatInterface + Modal backend (Whisper + Qwen2.5-7B)\n\"\"\"\n\nimport os\nimport io\nimport base64\nimport html\nimport uuid\nimport requests\nimport pandas as pd\nimport gradio as gr\n\n# ── Modal endpoints (set as HF Space Secrets) ────────────────────────────────\nBUILD_INDEX_URL = os.environ.get(\"MODAL_BUILD_INDEX_URL\", \"https://sopeadegboyega--catalog-assistant-build-index.modal.run\")\nCHAT_QUERY_URL  = os.environ.get(\"MODAL_CHAT_QUERY_URL\", \"https://sopeadegboyega--catalog-assistant-chat-query.modal.run\")\nTRANSCRIBE_URL  = os.environ.get(\"MODAL_TRANSCRIBE_URL\", \"\")\n\n# ── Custom CSS — terminal/amber aesthetic ─────────────────────────────────────\nCUSTOM_CSS = \"\"\"\n@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600;700&family=Syne:wght@400;700;800&display=swap');\n\n/* ── Reset & base ── */\n*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }\n\nbody, .gradio-container {\n    background: #0D0D0D !important;\n    color: #E8E0D0 !important;\n    font-family: 'JetBrains Mono', monospace !important;\n}\n\n/* ── App title bar ── */\n#app-title {\n    background: #0D0D0D;\n    border-bottom: 1px solid #F5A623;\n    padding: 14px 24px;\n    display: flex;\n    align-items: center;\n    gap: 12px;\n}\n#app-title h1 {\n    font-family: 'Syne', sans-serif;\n    font-weight: 800;\n    font-size: 1.5rem;\n    color: #F5A623;\n    letter-spacing: -0.02em;\n}\n#app-title .subtitle {\n    font-size: 0.7rem;\n    color: #6B6456;\n    letter-spacing: 0.12em;\n    text-transform: uppercase;\n}\n.badge {\n    background: #1A1A0F;\n    border: 1px solid #F5A623;\n    color: #F5A623;\n    font-size: 0.6rem;\n    padding: 2px 8px;\n    border-radius: 2px;\n    letter-spacing: 0.1em;\n    font-weight: 700;\n}\n\n/* ── Sidebar ── */\n#sidebar {\n    background: #111111 !important;\n    border-right: 1px solid #1E1E1E !important;\n    padding: 20px 16px !important;\n}\n#sidebar label {\n    color: #F5A623 !important;\n    font-size: 0.7rem !important;\n    letter-spacing: 0.15em !important;\n    text-transform: uppercase !important;\n    font-weight: 600 !important;\n}\n\n/* ── Upload zone ── */\n.upload-zone {\n    border: 1px dashed #2A2A2A !important;\n    background: #0A0A0A !important;\n    border-radius: 4px !important;\n    transition: border-color 0.2s !important;\n}\n.upload-zone:hover { border-color: #F5A623 !important; }\n\n/* ── Buttons ── */\nbutton.primary, .gr-button-primary {\n    background: #F5A623 !important;\n    color: #0D0D0D !important;\n    border: none !important;\n    border-radius: 3px !important;\n    font-family: 'JetBrains Mono', monospace !important;\n    font-weight: 700 !important;\n    font-size: 0.75rem !important;\n    letter-spacing: 0.08em !important;\n    padding: 8px 16px !important;\n    cursor: pointer !important;\n    transition: opacity 0.15s !important;\n}\nbutton.primary:hover { opacity: 0.85 !important; }\n\nbutton.secondary, .gr-button-secondary {\n    background: transparent !important;\n    color: #E8E0D0 !important;\n    border: 1px solid #2A2A2A !important;\n    border-radius: 3px !important;\n    font-family: 'JetBrains Mono', monospace !important;\n    font-size: 0.75rem !important;\n    padding: 8px 16px !important;\n    cursor: pointer !important;\n    transition: border-color 0.15s !important;\n}\nbutton.secondary:hover { border-color: #F5A623 !important; }\n\n/* ── Chat bubbles ── */\n.message.user {\n    background: #1A1400 !important;\n    border: 1px solid #3D2E00 !important;\n    border-radius: 4px 4px 0 4px !important;\n    color: #F5A623 !important;\n    font-size: 0.85rem !important;\n}\n.message.bot {\n    background: #111111 !important;\n    border: 1px solid #1E1E1E !important;\n    border-radius: 0 4px 4px 4px !important;\n    color: #E8E0D0 !important;\n    font-size: 0.85rem !important;\n    line-height: 1.6 !important;\n}\n\n/* ── Chat input ── */\n#chat-input textarea {\n    background: #111111 !important;\n    color: #E8E0D0 !important;\n    border: 1px solid #2A2A2A !important;\n    border-radius: 3px !important;\n    font-family: 'JetBrains Mono', monospace !important;\n    font-size: 0.85rem !important;\n    caret-color: #F5A623 !important;\n}\n#chat-input textarea:focus { border-color: #F5A623 !important; outline: none !important; }\n\n/* ── Status dot ── */\n.status-dot {\n    width: 8px; height: 8px;\n    border-radius: 50%;\n    background: #2A2A2A;\n    display: inline-block;\n    transition: background 0.3s;\n}\n.status-dot.active { background: #4CAF50; box-shadow: 0 0 6px #4CAF5066; }\n\n/* ── Schema preview table ── */\n.schema-table {\n    width: 100%;\n    border-collapse: collapse;\n    font-size: 0.72rem;\n    margin-top: 8px;\n}\n.schema-table th {\n    color: #F5A623;\n    text-align: left;\n    border-bottom: 1px solid #2A2A2A;\n    padding: 4px 6px;\n    font-weight: 600;\n    letter-spacing: 0.08em;\n}\n.schema-table td {\n    color: #9A8F80;\n    padding: 4px 6px;\n    border-bottom: 1px solid #161616;\n    font-size: 0.7rem;\n}\n.schema-table tr:hover td { color: #E8E0D0; }\n\n/* ── Embed code box ── */\n.embed-code {\n    background: #080808;\n    border: 1px solid #1E1E1E;\n    border-radius: 3px;\n    padding: 12px;\n    font-size: 0.7rem;\n    color: #6B9FD4;\n    font-family: 'JetBrains Mono', monospace;\n    white-space: pre-wrap;\n    word-break: break-all;\n    margin-top: 8px;\n}\n\n/* ── Scrollbars ── */\n::-webkit-scrollbar { width: 4px; }\n::-webkit-scrollbar-track { background: #0D0D0D; }\n::-webkit-scrollbar-thumb { background: #2A2A2A; border-radius: 2px; }\n::-webkit-scrollbar-thumb:hover { background: #F5A623; }\n\n/* ── Accordion ── */\n.gr-accordion { background: #111111 !important; border: 1px solid #1E1E1E !important; }\n.gr-accordion-header { color: #E8E0D0 !important; font-size: 0.78rem !important; }\n\n/* ── Misc ── */\n.gr-form { background: transparent !important; }\n.gr-padded { padding: 0 !important; }\nfooter { display: none !important; }\n\"\"\"\n\n# ── State helpers ─────────────────────────────────────────────────────────────\n\ndef _post(url: str, payload: dict, timeout: int = 120):\n    \"\"\"POST to Modal endpoint, return JSON or raise.\"\"\"\n    resp = requests.post(url, json=payload, timeout=timeout)\n    resp.raise_for_status()\n    data = resp.json()\n    if isinstance(data, dict) and data.get(\"error\"):\n        raise RuntimeError(data[\"error\"])\n    return data\n\n\ndef ensure_session(state: dict):\n    \"\"\"Create per-browser catalog state lazily.\"\"\"\n    state = state or {}\n    state.setdefault(\"session_id\", str(uuid.uuid4()))\n    state.setdefault(\"catalog_loaded\", False)\n    state.setdefault(\"history\", [])\n    return state\n\n\ndef render_schema_preview(columns, sample_df=None, row_count=None):\n    count_text = f\"{row_count:,} rows\" if isinstance(row_count, int) else \"Catalog preview\"\n    schema_rows = \"\"\n\n    for column in columns:\n        sample = \"\"\n        dtype = \"\"\n        if sample_df is not None and column in sample_df.columns:\n            dtype = str(sample_df[column].dtype)\n            non_empty = sample_df[column].dropna()\n            sample = \"\" if non_empty.empty else str(non_empty.iloc[0])\n        schema_rows += (\n            \"<tr>\"\n            f\"<td><b>{html.escape(str(column))}</b></td>\"\n            f\"<td>{html.escape(dtype)}</td>\"\n            f\"<td>{html.escape(sample)}</td>\"\n            \"</tr>\"\n        )\n\n    return f\"\"\"\n    <p style='color:#6B6456;font-size:0.7rem;margin-bottom:6px'>{count_text} · {len(columns)} columns</p>\n    <table class='schema-table'>\n      <thead><tr><th>Column</th><th>Type</th><th>Sample</th></tr></thead>\n      <tbody>{schema_rows}</tbody>\n    </table>\n    \"\"\"\n\n\ndef render_sources(sources):\n    if not sources:\n        return \"<p style='color:#2A2A2A;font-size:0.72rem'>Matched products will appear here after a reply.</p>\"\n\n    rows = \"\"\n    for source in sources[:3]:\n        cells = \"\".join(\n            f\"<td>{html.escape(str(value))}</td>\"\n            for value in source.values()\n        )\n        rows += f\"<tr>{cells}</tr>\"\n\n    headers = \"\".join(\n        f\"<th>{html.escape(str(key))}</th>\"\n        for key in sources[0].keys()\n    )\n    return f\"\"\"\n    <table class='schema-table'>\n      <thead><tr>{headers}</tr></thead>\n      <tbody>{rows}</tbody>\n    </table>\n    \"\"\"\n\n\n# ── Catalog upload & index build ─────────────────────────────────────────────\n\ndef handle_upload(csv_file, state: dict):\n    \"\"\"\n    Called when user uploads a CSV.\n    1. Reads first 5 rows for schema preview.\n    2. Sends full CSV to Modal /build_index.\n    3. Stores session token in state.\n    Returns: schema_html, status_msg, updated_state\n    \"\"\"\n    state = ensure_session(state)\n\n    if csv_file is None:\n        return \"<p style='color:#6B6456'>No file uploaded.</p>\", \"⬤ No catalog loaded\", state\n\n    try:\n        with open(csv_file.name, \"rb\") as f:\n            csv_bytes = f.read()\n        catalog_csv = csv_bytes.decode(\"utf-8-sig\")\n    except Exception as e:\n        return f\"<p style='color:#E05A5A'>CSV read error: {e}</p>\", \"⬤ Error\", state\n\n    try:\n        preview_df = pd.read_csv(io.StringIO(catalog_csv), nrows=3)\n        preview_columns = list(preview_df.columns)\n        schema_html = render_schema_preview(preview_columns, preview_df)\n    except Exception as e:\n        return f\"<p style='color:#E05A5A'>CSV parse error: {e}</p>\", \"⬤ Error\", state\n\n    # Send to Modal\n    if not BUILD_INDEX_URL:\n        status = \"⚠ MODAL_BUILD_INDEX_URL not set — running in demo mode\"\n        state[\"catalog_loaded\"] = True\n        state[\"catalog_name\"] = os.path.basename(csv_file.name)\n        state[\"history\"] = []\n        state[\"demo_df\"] = pd.read_csv(io.StringIO(catalog_csv)).to_dict(orient=\"records\")\n        return schema_html, status, state\n\n    try:\n        result = _post(\n            BUILD_INDEX_URL,\n            {\n                \"catalog_csv\": catalog_csv,\n                \"session_id\": state[\"session_id\"],\n            },\n        )\n        state[\"session_id\"] = result.get(\"session_id\", state[\"session_id\"])\n        state[\"catalog_loaded\"] = True\n        state[\"history\"] = []\n        state[\"catalog_name\"] = os.path.basename(csv_file.name)\n        row_count = result.get(\"row_count\")\n        columns = result.get(\"columns\") or preview_columns\n        schema_html = render_schema_preview(columns, preview_df, row_count)\n        product_label = f\"{row_count:,} products\" if isinstance(row_count, int) else \"products\"\n        status = f\"✓ Catalog loaded: {product_label}\"\n    except Exception as e:\n        state[\"catalog_loaded\"] = False\n        status = f\"⚠ Index error: {e}\"\n\n    return schema_html, status, state\n\n\n# ── Voice transcription ───────────────────────────────────────────────────────\n\ndef transcribe_audio(audio_path, state: dict):\n    \"\"\"Send audio file to Modal Whisper endpoint, return transcript.\"\"\"\n    state = ensure_session(state)\n\n    if audio_path is None:\n        return \"\", state\n\n    if not TRANSCRIBE_URL:\n        return \"[Voice transcription requires MODAL_TRANSCRIBE_URL]\", state\n\n    try:\n        audio_b64 = base64.b64encode(open(audio_path, \"rb\").read()).decode()\n        result = _post(TRANSCRIBE_URL, {\"audio_b64\": audio_b64, \"language\": \"en\"}, timeout=120)\n        return result.get(\"text\", \"\"), state\n    except Exception as e:\n        return f\"[Transcription error: {e}]\", state\n\n\n# ── Chat handler ──────────────────────────────────────────────────────────────\n\ndef chat_fn(message: str, ui_history: list, state: dict, business_name: str):\n    \"\"\"\n    Called by gr.ChatInterface on each user message.\n    Sends message + history to Modal /chat_query.\n    \"\"\"\n    state = ensure_session(state)\n\n    if not message.strip():\n        return \"\", state, []\n\n    if not state.get(\"catalog_loaded\"):\n        return (\n            \"**No catalog loaded.** Upload a CSV file in the sidebar first, \"\n            \"then ask me anything about your products.\"\n        ), state, []\n\n    if not CHAT_QUERY_URL:\n        # Demo mode — simple keyword match against in-memory df\n        df_records = state.get(\"demo_df\", [])\n        matches = [\n            r for r in df_records\n            if any(message.lower() in str(v).lower() for v in r.values())\n        ][:3]\n        if matches:\n            lines = \"\\n\".join(f\"• {r}\" for r in matches)\n            reply = f\"_(Demo mode — no Modal endpoint)_\\n\\nTop matches:\\n{lines}\"\n            state[\"history\"].extend([\n                {\"role\": \"user\", \"content\": message},\n                {\"role\": \"assistant\", \"content\": reply},\n            ])\n            return reply, state, matches\n        reply = \"_(Demo mode)_ No matching products found for that query.\"\n        state[\"history\"].extend([\n            {\"role\": \"user\", \"content\": message},\n            {\"role\": \"assistant\", \"content\": reply},\n        ])\n        return reply, state, []\n\n    payload = {\n        \"message\": message,\n        \"session_id\": state[\"session_id\"],\n        \"history\": state.get(\"history\", [])[-6:],\n        \"business_name\": (business_name or \"\").strip() or \"our store\",\n    }\n\n    try:\n        result = _post(CHAT_QUERY_URL, payload, timeout=180)\n        reply = result.get(\"reply\", \"No response from model.\")\n        state[\"history\"].extend([\n            {\"role\": \"user\", \"content\": message},\n            {\"role\": \"assistant\", \"content\": reply},\n        ])\n        return reply, state, result.get(\"sources\", [])\n    except requests.exceptions.Timeout:\n        return \"⏱ The model took too long to respond. Please try again.\", state, []\n    except Exception as e:\n        return f\"⚠ Backend error: {e}\", state, []\n\n\n# ── Embed code generator ──────────────────────────────────────────────────────\n\ndef generate_embed(space_id: str):\n    \"\"\"Return iframe embed snippet for a HF Space.\"\"\"\n    space_id = space_id.strip()\n    if not space_id:\n        return \"<p style='color:#6B6456;font-size:0.72rem'>Enter your HF Space ID above.</p>\"\n\n    snippet = f'<iframe\\n  src=\"https://huggingface.co/spaces/{space_id}\"\\n  width=\"100%\"\\n  height=\"600\"\\n  frameborder=\"0\"\\n  allow=\"microphone\"\\n></iframe>'\n    return f\"<div class='embed-code'>{snippet}</div>\"\n\n\n# ── Gradio UI ─────────────────────────────────────────────────────────────────\n\ndef build_ui():\n    with gr.Blocks(\n        title=\"CatalogChat — AI Product Assistant\",\n    ) as demo:\n        session_state = gr.State({})\n\n        # ── Title bar ──\n        gr.HTML(\"\"\"\n        <div id=\"app-title\">\n          <div>\n            <h1>⬡ CatalogChat</h1>\n            <div class=\"subtitle\">Backyard AI · Qwen2.5-7B · BM25 Retrieval</div>\n          </div>\n          <span class=\"badge\">OFF-BRAND</span>\n          <span class=\"badge\">OFF THE GRID</span>\n        </div>\n        \"\"\")\n\n        with gr.Row(equal_height=True):\n\n            # ── LEFT SIDEBAR ──────────────────────────────────────────────────\n            with gr.Column(scale=1, elem_id=\"sidebar\", min_width=280):\n\n                gr.HTML(\"<div style='color:#F5A623;font-size:0.7rem;letter-spacing:0.15em;font-weight:700;margin-bottom:12px'>▸ CATALOG</div>\")\n\n                csv_upload = gr.File(\n                    label=\"Upload product CSV\",\n                    file_types=[\".csv\"],\n                    elem_classes=[\"upload-zone\"],\n                )\n\n                upload_btn = gr.Button(\"⟳ Index Catalog\", variant=\"primary\", size=\"sm\")\n\n                catalog_status = gr.HTML(\n                    \"<span class='status-dot'></span> <span style='color:#6B6456;font-size:0.72rem'>No catalog loaded</span>\"\n                )\n\n                schema_display = gr.HTML(\n                    \"<p style='color:#2A2A2A;font-size:0.72rem;margin-top:8px'>Schema preview will appear here.</p>\"\n                )\n\n                business_name = gr.Textbox(\n                    placeholder=\"Business name\",\n                    label=\"Business name\",\n                    value=\"our store\",\n                    lines=1,\n                )\n\n                gr.HTML(\"<hr style='border:none;border-top:1px solid #1E1E1E;margin:16px 0'>\")\n\n                # ── Voice input ──\n                gr.HTML(\"<div style='color:#F5A623;font-size:0.7rem;letter-spacing:0.15em;font-weight:700;margin-bottom:8px'>▸ VOICE INPUT</div>\")\n\n                audio_input = gr.Audio(\n                    sources=[\"microphone\"],\n                    type=\"filepath\",\n                    label=\"Record your question\",\n                    show_label=False,\n                )\n\n                transcript_box = gr.Textbox(\n                    placeholder=\"Transcript appears here — edit then send\",\n                    label=\"Transcript\",\n                    lines=2,\n                    show_label=False,\n                )\n\n                transcribe_btn = gr.Button(\"⟳ Transcribe\", variant=\"secondary\", size=\"sm\")\n\n                gr.HTML(\"<hr style='border:none;border-top:1px solid #1E1E1E;margin:16px 0'>\")\n\n                # ── Embed generator ──\n                with gr.Accordion(\"⟐ Embed Code Generator\", open=False):\n                    gr.HTML(\"<p style='color:#6B6456;font-size:0.7rem;margin-bottom:8px'>Generate iframe snippet for your website</p>\")\n                    space_id_input = gr.Textbox(\n                        placeholder=\"your-username/your-space\",\n                        label=\"HF Space ID\",\n                        show_label=False,\n                    )\n                    embed_btn = gr.Button(\"Generate Snippet\", variant=\"secondary\", size=\"sm\")\n                    embed_output = gr.HTML()\n\n                gr.HTML(\"<hr style='border:none;border-top:1px solid #1E1E1E;margin:16px 0'>\")\n\n                # ── Starter prompts ──\n                gr.HTML(\"<div style='color:#F5A623;font-size:0.7rem;letter-spacing:0.15em;font-weight:700;margin-bottom:8px'>▸ TRY ASKING</div>\")\n                gr.HTML(\"\"\"\n                <div style='display:flex;flex-direction:column;gap:6px'>\n                  <div style='background:#111;border:1px solid #1E1E1E;padding:6px 10px;border-radius:3px;font-size:0.72rem;color:#9A8F80;cursor:pointer'\n                       onclick=\"document.querySelector('#chat-input textarea').value=this.textContent\">\n                    What products do you have under ₦8,000?\n                  </div>\n                  <div style='background:#111;border:1px solid #1E1E1E;padding:6px 10px;border-radius:3px;font-size:0.72rem;color:#9A8F80;cursor:pointer'\n                       onclick=\"document.querySelector('#chat-input textarea').value=this.textContent\">\n                    Show me blue dresses in medium\n                  </div>\n                  <div style='background:#111;border:1px solid #1E1E1E;padding:6px 10px;border-radius:3px;font-size:0.72rem;color:#9A8F80;cursor:pointer'\n                       onclick=\"document.querySelector('#chat-input textarea').value=this.textContent\">\n                    Compare your top 3 sofas\n                  </div>\n                </div>\n                \"\"\")\n\n            # ── CHAT PANEL ────────────────────────────────────────────────────\n            with gr.Column(scale=3):\n\n                chatbot = gr.Chatbot(\n                    label=\"\",\n                    # type=\"messages\",\n                    height=520,\n                    show_label=False,\n                    # bubble_full_width=False,\n                    avatar_images=(\n                        None,  # user avatar\n                        \"https://api.dicebear.com/7.x/bottts-neutral/svg?seed=catalogchat&backgroundColor=0D0D0D\",\n                    ),\n                    render_markdown=True,\n                )\n\n                with gr.Row():\n                    chat_input = gr.Textbox(\n                        placeholder=\"Ask about products, prices, availability…\",\n                        show_label=False,\n                        lines=1,\n                        scale=5,\n                        elem_id=\"chat-input\",\n                        container=False,\n                    )\n                    send_btn = gr.Button(\"Send ↵\", variant=\"primary\", scale=1)\n\n                gr.HTML(\"\"\"\n                <div style='text-align:center;margin-top:8px;color:#2A2A2A;font-size:0.65rem;letter-spacing:0.1em'>\n                  POWERED BY QWEN2.5-7B · MODAL SERVERLESS · BM25 RETRIEVAL\n                </div>\n                \"\"\")\n\n                with gr.Accordion(\"Matched Products\", open=False):\n                    sources_display = gr.HTML(\n                        \"<p style='color:#2A2A2A;font-size:0.72rem'>Matched products will appear here after a reply.</p>\"\n                    )\n\n        # ── Wire events ───────────────────────────────────────────────────────\n\n        # Upload & index\n        upload_btn.click(\n            fn=handle_upload,\n            inputs=[csv_upload, session_state],\n            outputs=[schema_display, catalog_status, session_state],\n        )\n\n        # Also trigger on file drop\n        csv_upload.change(\n            fn=handle_upload,\n            inputs=[csv_upload, session_state],\n            outputs=[schema_display, catalog_status, session_state],\n        )\n\n        # Chat — send button\n        def respond(message, history, state, business):\n            history = history or []\n            answer, state, sources = chat_fn(message, history, state, business)\n            if message.strip():\n                history.extend([\n                    {\"role\": \"user\", \"content\": message},\n                    {\"role\": \"assistant\", \"content\": answer},\n                ])\n            return \"\", history, state, render_sources(sources)\n\n        send_btn.click(\n            fn=respond,\n            inputs=[chat_input, chatbot, session_state, business_name],\n            outputs=[chat_input, chatbot, session_state, sources_display],\n        )\n\n        # Chat — Enter key\n        chat_input.submit(\n            fn=respond,\n            inputs=[chat_input, chatbot, session_state, business_name],\n            outputs=[chat_input, chatbot, session_state, sources_display],\n        )\n\n        # Voice transcription\n        transcribe_btn.click(\n            fn=transcribe_audio,\n            inputs=[audio_input, session_state],\n            outputs=[transcript_box, session_state],\n        )\n\n        # Send transcript as chat message\n        transcript_box.submit(\n            fn=respond,\n            inputs=[transcript_box, chatbot, session_state, business_name],\n            outputs=[transcript_box, chatbot, session_state, sources_display],\n        )\n\n        # Embed generator\n        embed_btn.click(\n            fn=generate_embed,\n            inputs=[space_id_input],\n            outputs=[embed_output],\n        )\n\n    return demo\n\n\n# ── Entry point ───────────────────────────────────────────────────────────────\nif __name__ == \"__main__\":\n    demo = build_ui()\n    demo.launch(\n        css=CUSTOM_CSS,\n        theme=gr.themes.Base(\n            primary_hue=\"orange\",\n            neutral_hue=\"stone\",\n            font=gr.themes.GoogleFont(\"JetBrains Mono\"),\n        ),\n        # server_name=\"0.0.0.0\",\n        # server_port=3000,\n        share=False,\n    )\n",608      "app_signals": "_post url payload timeout ensure_session state render_schema_preview columns sample_df row_count render_sources sources handle_upload csv_file transcribe_audio audio_path chat_fn message ui_history business_name generate_embed space_id build_ui CatalogChat — Gradio frontend Hackathon: Gradio Backyard AI Hackathon (June 2026) Stack: Gradio ChatInterface + Modal backend (Whisper + Qwen2.5-7B) os.environ.get MODAL_BUILD_INDEX_URL https://sopeadegboyega--catalog-assistant-build-index.modal.run MODAL_CHAT_QUERY_URL https://sopeadegboyega--catalog-assistant-chat-query.modal.run MODAL_TRANSCRIBE_URL POST to Modal endpoint, return JSON or raise. requests.post json resp.raise_for_status resp.json Create per-browser catalog state lazily. state.setdefault join Called when user uploads a CSV. 1. Reads first 5 rows for schema preview. 2. Sends full CSV to Modal /build_index. 3. Stores session token in state. Returns: schema_html, status_msg, updated_state Send audio file to Modal Whisper endpoint, return transcript. Called by gr.ChatInterface on each user message. Sends message + history to Modal /chat_query. Return iframe embed snippet for a HF Space. space_id.strip respond history business __main__ demo.launch css theme share isinstance data.get RuntimeError session_id str catalog_loaded Catalog preview · columns Column Type Sample Matched products will appear here after a reply. csv_bytes.decode pd.read_csv nrows list ⚠ MODAL_BUILD_INDEX_URL not set — running in demo mode os.path.basename to_dict orient result.get decode message.strip state.get _(Demo mode)_ No matching products found for that query. extend Enter your HF Space ID above. <iframe src=\"https://huggingface.co/spaces/ \" width=\"100%\" height=\"600\" frameborder=\"0\" allow=\"microphone\" > gr.Blocks title gr.State gr.HTML upload_btn.click fn inputs outputs csv_upload.change send_btn.click chat_input.submit transcribe_btn.click transcript_box.submit embed_btn.click error uuid.uuid4 rows dropna len No file uploaded. ⬤ No catalog loaded open f.read utf-8-sig io.StringIO catalog_name demo_df products ✓ Catalog loaded: [Voice transcription requires MODAL_TRANSCRIBE_URL] **No catalog loaded.** Upload a CSV file in the sidebar first, then ask me anything about your products. strip our store reply No response from model. ⬡ CatalogChat Backyard AI · Qwen2.5-7B · BM25 Retrieval OFF-BRAND OFF THE GRID gr.Row equal_height gr.themes.Base primary_hue neutral_hue font html.escape keys rb ⬤ Error records catalog_csv ⚠ Index error: base64.b64encode audio_b64 language en text _(Demo mode — no Modal endpoint)_ Top matches: ⏱ The model took too long to respond. Please try again. CatalogChat — AI Product Assistant gr.Column scale elem_id min_width gr.File label file_types elem_classes gr.Button variant size gr.Textbox placeholder value lines gr.Audio type show_label gr.Chatbot height avatar_images render_markdown history.extend , source.values CSV read error: CSV parse error: read [Transcription error: ] any role content user assistant ⚠ Backend error: ▸ CATALOG ⟳ Index Catalog No catalog loaded Schema preview will appear here. ▸ VOICE INPUT ⟳ Transcribe gr.Accordion ▸ TRY ASKING What products do you have under ₦8,000? Show me blue dresses in medium Compare your top 3 sofas container POWERED BY QWEN2.5-7B · MODAL SERVERLESS · BM25 RETRIEVAL orange stone gr.themes.GoogleFont • sidebar Upload product CSV primary sm Business name filepath Record your question Transcript appears here — edit then send Transcript secondary ⟐ Embed Code Generator Generate iframe snippet for your website Generate Snippet Send ↵ Matched Products JetBrains Mono .csv upload-zone microphone your-username/your-space HF Space ID https://api.dicebear.com/7.x/bottts-neutral/svg?seed=catalogchat&backgroundColor=0D0D0D Ask about products, prices, availability… chat-input message.lower lower r.values",609      "readme_len": 96,610      "app_source_len": 22904,611      "app_signals_len": 3859612    },613    {614      "id": "build-small-hackathon/Case-Lantern",615      "title": "Case Lantern",616      "summary": "",617      "tags": [618        "gradio",619        "region:us"620      ],621      "models": [622        "lastmass/Qwen3.5-Medical-GSPO"623      ],624      "datasets": [],625      "sdk": "gradio",626      "license": "apache-2.0",627      "likes": 0,628      "url": "https://huggingface.co/spaces/build-small-hackathon/Case-Lantern",629      "app_file": "app.py",630      "readme_raw": "---\ntitle: Case Lantern\ncolorFrom: pink\ncolorTo: blue\nsdk: gradio\nsdk_version: 6.15.2\napp_file: app.py\npinned: false\nlicense: apache-2.0\nmodels:\n  - lastmass/Qwen3.5-Medical-GSPO\n\n---\n\n# 🏮 Case Lantern\n\nShort Demo Video:\n\nhttps://youtu.be/Bf3t5Cq6XuA\n\nCase Lantern is a fictional medical mystery game for the\n[Build Small Hackathon](https://huggingface.co/build-small-hackathon).\nPlayers investigate a short Chinese case, request clues, avoid red herrings, and\nsubmit a diagnosis within six turns.\n\nThe experience uses [`lastmass/Qwen3.5-Medical-GSPO`](https://huggingface.co/lastmass/Qwen3.5-Medical-GSPO),\na small Chinese medical reasoning model with roughly 4.66B parameters, as the\ngame master and scorer. Inference runs locally via **llama.cpp** (GGUF Q4_K_M).\n\n## Track & Merit Badges\n\n| Item | Detail |\n|------|--------|\n| Track | An Adventure in Thousand Token Wood |\n| AI role | Load-bearing game master, clue writer, and scoring judge |\n| Constraint | Small model under 32B parameters |\n| UI | Gradio Space with custom dark frontend |\n\n| Badge | Status |\n|-------|--------|\n| 🏕️ Off the Grid (LOCAL-FIRST) | ✅ Model runs locally in the Space |\n| 🎸 Well-Tuned (FINE-TUNED) | ✅ Uses fine-tuned model published on HF |\n| 🦙 Llama Champion | ✅ Runs via llama.cpp runtime |\n| 🎨 Off-Brand (CUSTOM UI) | ✅ Dark glassmorphism theme, custom CSS |\n\n## Safety framing\n\nThis is not a diagnosis or treatment tool. It only uses fictional cases for\nlearning-oriented gameplay. Users are explicitly asked not to provide personal\nhealth information.\n\n## Deployment notes\n\nThe app is designed for **free CPU Spaces** on Hugging Face. It does not require\na GPU. The GGUF model (~2.78 GB, Q4_K_M) is downloaded from the Hub at first\nlaunch and cached.\n\nIf you deploy on **ZeroGPU**, keep the prebuilt CPU `llama-cpp-python` wheel.\nThe `requirements.txt` file uses the CPU wheel index\n(`llama-cpp-python/whl/cpu`) plus `--only-binary=llama-cpp-python`, and pins to\nthe latest available prebuilt wheel in that index. This keeps the Space from\ntrying to compile llama.cpp from source. Do not use the CUDA wheel URL\n(`llama-cpp-python/whl/cu124`) unless the Space image also provides CUDA runtime\nlibraries such as `libcudart.so.12`; otherwise model loading can fail when the\nfirst button click triggers inference.\n\n- Set `DEMO_MODE=auto` (default) to allow a graceful scripted fallback if the\n  model cannot load.\n- Set `DEMO_MODE=true` to skip model loading entirely (instant UI-only demo).\n- Set `DEMO_MODE=off` if you want model-loading failures to surface immediately.\n\n## Local run\n\n```bash\npip install -r requirements.txt\nDEMO_MODE=true python app.py\n```\n\nOn Windows PowerShell:\n\n```powershell\n$env:DEMO_MODE=\"true\"\npython app.py\n```\n",631      "readme_body": "# 🏮 Case Lantern\n\nShort Demo Video:\n\nhttps://youtu.be/Bf3t5Cq6XuA\n\nCase Lantern is a fictional medical mystery game for the\n[Build Small Hackathon](https://huggingface.co/build-small-hackathon).\nPlayers investigate a short Chinese case, request clues, avoid red herrings, and\nsubmit a diagnosis within six turns.\n\nThe experience uses [`lastmass/Qwen3.5-Medical-GSPO`](https://huggingface.co/lastmass/Qwen3.5-Medical-GSPO),\na small Chinese medical reasoning model with roughly 4.66B parameters, as the\ngame master and scorer. Inference runs locally via **llama.cpp** (GGUF Q4_K_M).\n\n## Track & Merit Badges\n\n| Item | Detail |\n|------|--------|\n| Track | An Adventure in Thousand Token Wood |\n| AI role | Load-bearing game master, clue writer, and scoring judge |\n| Constraint | Small model under 32B parameters |\n| UI | Gradio Space with custom dark frontend |\n\n| Badge | Status |\n|-------|--------|\n| 🏕️ Off the Grid (LOCAL-FIRST) | ✅ Model runs locally in the Space |\n| 🎸 Well-Tuned (FINE-TUNED) | ✅ Uses fine-tuned model published on HF |\n| 🦙 Llama Champion | ✅ Runs via llama.cpp runtime |\n| 🎨 Off-Brand (CUSTOM UI) | ✅ Dark glassmorphism theme, custom CSS |\n\n## Safety framing\n\nThis is not a diagnosis or treatment tool. It only uses fictional cases for\nlearning-oriented gameplay. Users are explicitly asked not to provide personal\nhealth information.\n\n## Deployment notes\n\nThe app is designed for **free CPU Spaces** on Hugging Face. It does not require\na GPU. The GGUF model (~2.78 GB, Q4_K_M) is downloaded from the Hub at first\nlaunch and cached.\n\nIf you deploy on **ZeroGPU**, keep the prebuilt CPU `llama-cpp-python` wheel.\nThe `requirements.txt` file uses the CPU wheel index\n(`llama-cpp-python/whl/cpu`) plus `--only-binary=llama-cpp-python`, and pins to\nthe latest available prebuilt wheel in that index. This keeps the Space from\ntrying to compile llama.cpp from source. Do not use the CUDA wheel URL\n(`llama-cpp-python/whl/cu124`) unless the Space image also provides CUDA runtime\nlibraries such as `libcudart.so.12`; otherwise model loading can fail when the\nfirst button click triggers inference.\n\n- Set `DEMO_MODE=auto` (default) to allow a graceful scripted fallback if the\n  model cannot load.\n- Set `DEMO_MODE=true` to skip model loading entirely (instant UI-only demo).\n- Set `DEMO_MODE=off` if you want model-loading failures to surface immediately.\n\n## Local run\n\n```bash\npip install -r requirements.txt\nDEMO_MODE=true python app.py\n```\n\nOn Windows PowerShell:\n\n```powershell\n$env:DEMO_MODE=\"true\"\npython app.py\n```",632      "readme_frontmatter": {633        "title": "Case Lantern",634        "colorFrom": "pink",635        "colorTo": "blue",636        "sdk": "gradio",637        "sdk_version": "6.15.2",638        "app_file": "app.py",639        "pinned": "false",640        "license": "apache-2.0",641        "models": ""642      },643      "app_source": "\"\"\"Case Lantern — a fictional medical mystery game powered by a small Chinese\nmedical reasoning model.\n\nBackend  : llama-cpp-python  (GGUF, runs on free CPU Spaces)\nFrontend : fully custom dark theme with glassmorphism & micro-animations\nModel    : lastmass/Qwen3.5-Medical-GSPO  (~4.66 B params, Q4_K_M quant)\n\"\"\"\n\nimport os\nimport random\nimport re\nimport textwrap\nfrom dataclasses import dataclass, field\nfrom functools import lru_cache\nfrom typing import Dict, List, Optional\n\nimport gradio as gr\n\n# ---------------------------------------------------------------------------\n# Configuration\n# ---------------------------------------------------------------------------\n# Display model (shown in UI)\nDISPLAY_MODEL_ID = \"lastmass/Qwen3.5-Medical-GSPO\"\n# GGUF repo used for actual inference (quantised by mradermacher)\nGGUF_REPO = \"mradermacher/Qwen3.5-Medical-GSPO-GGUF\"\nGGUF_FILE = \"Qwen3.5-Medical-GSPO.Q4_K_M.gguf\"\n\nDEMO_MODE = os.getenv(\"DEMO_MODE\", \"auto\").lower()\nMAX_NEW_TOKENS = int(os.getenv(\"MAX_NEW_TOKENS\", \"420\"))\n\nDISCLAIMER = (\n    \"Fictional training game only. This app does not provide medical advice, \"\n    \"diagnosis, triage, or treatment guidance for real people.\"\n)\n\n# ---------------------------------------------------------------------------\n# System prompt\n# ---------------------------------------------------------------------------\nSYSTEM_PROMPT = \"\"\"You are Case Lantern, a playful but careful medical mystery game master.\nCreate and run fictional Chinese medical reasoning puzzles for education and entertainment.\n\nRules:\n- Never present output as real medical advice.\n- Keep all patients fictional.\n- Do not ask users to share real personal health information.\n- Make the game delightful, concise, and clue-driven.\n- The player should reason from clues; avoid revealing the final answer unless asked to score.\n- Use simplified Chinese by default, with crisp section headers.\n- When scoring, be honest but friendly and include one memorable teaching pearl.\n\"\"\"\n\n# ---------------------------------------------------------------------------\n# Seed cases\n# ---------------------------------------------------------------------------\nCASE_SEEDS = [\n    {\n        \"title\": \"凌晨两点的胸痛电报\",\n        \"genre\": \"急诊悬疑\",\n        \"opening\": \"65岁男性,凌晨突发胸痛,额头冒汗,坚持说只是晚饭吃坏了。护士递来一张还热乎的心电图。\",\n        \"secret\": \"下壁ST段抬高型心肌梗死\",\n        \"clues\": [\n            \"疼痛位于胸骨后,持续超过30分钟,伴冷汗。\",\n            \"II、III、aVF导联ST段抬高,I、aVL可见对应性改变。\",\n            \"血压略低,心率偏慢,提示可能累及右冠供血区域。\",\n            \"硝酸甘油后症状改善不明显。\",\n        ],\n        \"red_herring\": \"反流性食管炎\",\n    },\n    {\n        \"title\": \"雨夜里的右下腹脚印\",\n        \"genre\": \"妇产科侦探\",\n        \"opening\": \"28岁女性,停经8周,右下腹剧痛后晕厥。诊室灯光一闪,血压计读数像坏消息一样低。\",\n        \"secret\": \"输卵管妊娠破裂导致腹腔内出血\",\n        \"clues\": [\n            \"停经8周,突发一侧下腹痛。\",\n            \"血压80/50 mmHg,面色苍白,提示休克。\",\n            \"后穹窿穿刺抽出不凝血。\",\n            \"尿/血HCG阳性,床旁超声宫内未见明确孕囊。\",\n        ],\n        \"red_herring\": \"急性阑尾炎\",\n    },\n    {\n        \"title\": \"会变形的蝴蝶影子\",\n        \"genre\": \"内分泌谜题\",\n        \"opening\": \"32岁女性近两个月怕热、心悸、手抖,朋友说她的眼神像一直在追赶一列迟到的火车。\",\n        \"secret\": \"Graves病所致甲状腺功能亢进\",\n        \"clues\": [\n            \"怕热、多汗、体重下降但食欲增加。\",\n            \"心率快,双手细颤。\",\n            \"甲状腺弥漫性肿大,可闻及血管杂音。\",\n            \"TSH降低,FT3/FT4升高,TRAb阳性。\",\n        ],\n        \"red_herring\": \"焦虑障碍\",\n    },\n    {\n        \"title\": \"沉默的蓝色嘴唇\",\n        \"genre\": \"呼吸科小剧场\",\n        \"opening\": \"70岁男性长期咳嗽咳痰,今天走三步就喘,口唇发绀,却还惦记着没下完的一盘棋。\",\n        \"secret\": \"慢性阻塞性肺疾病急性加重\",\n        \"clues\": [\n            \"长期吸烟史,慢性咳嗽咳痰多年。\",\n            \"活动后气促明显加重,双肺可闻及哮鸣音。\",\n            \"血气提示二氧化碳潴留倾向。\",\n            \"近期有受凉或感染诱因。\",\n        ],\n        \"red_herring\": \"单纯支气管哮喘\",\n    },\n]\n\nACTION_PRESETS = {\n    \"问病史\": \"我想进一步问病史。请给我一个关键但不直接泄底的病史线索。\",\n    \"查体\": \"我想做体格检查。请给我一个关键但不直接泄底的查体线索。\",\n    \"实验室\": \"我想申请实验室检查。请给我一个关键但不直接泄底的检验线索。\",\n    \"影像/心电\": \"我想看影像或心电图。请给我一个关键但不直接泄底的检查线索。\",\n    \"提示\": \"我卡住了。请给我一个分层提示,但不要直接说出诊断。\",\n}\n\n# ---------------------------------------------------------------------------\n# Game state\n# ---------------------------------------------------------------------------\n\n\n@dataclass\nclass GameState:\n    title: str = \"\"\n    genre: str = \"\"\n    opening: str = \"\"\n    secret: str = \"\"\n    red_herring: str = \"\"\n    clues: List[str] = field(default_factory=list)\n    used_clues: List[str] = field(default_factory=list)\n    turns: int = 0\n    score: int = 100\n    solved: bool = False\n\n    def public_context(self) -> str:\n        clue_text = \"\\n\".join(f\"  • {c}\" for c in self.used_clues) or \"  暂无线索\"\n        return (\n            f\"📁 案件:{self.title}\\n\"\n            f\"🏷️ 类型:{self.genre}\\n\"\n            f\"📖 开场:{self.opening}\\n\\n\"\n            f\"🔍 已公开线索:\\n{clue_text}\\n\\n\"\n            f\"⏱️ 回合:{self.turns}/6\\n\"\n            f\"⭐ 分数:{self.score}\"\n        )\n\n\n# ---------------------------------------------------------------------------\n# Helpers\n# ---------------------------------------------------------------------------\n\n\ndef normalize_text(value: str) -> str:\n    return re.sub(r\"\\s+\", \" \", value or \"\").strip()\n\n\ndef strip_thinking(text: str) -> str:\n    text = re.sub(r\"<think>.*?</think>\", \"\", text, flags=re.DOTALL | re.IGNORECASE)\n    text = text.replace(\"<think>\", \"\").replace(\"</think>\", \"\")\n    return text.strip()\n\n\n# ---------------------------------------------------------------------------\n# Demo / fallback replies (no model needed)\n# ---------------------------------------------------------------------------\n\n\ndef demo_reply(prompt: str, state: GameState, mode: str) -> str:\n    unused = [c for c in state.clues if c not in state.used_clues]\n    next_clue = unused[0] if unused else random.choice(state.clues)\n\n    if mode == \"score\":\n        guess = prompt.lower()\n        secret_terms = [state.secret.lower()]\n        if \"心肌梗死\" in state.secret:\n            secret_terms += [\"心梗\", \"stemi\", \"梗死\"]\n        if \"输卵管\" in state.secret:\n            secret_terms += [\"宫外孕\", \"异位妊娠\", \"破裂\"]\n        if \"graves\" in state.secret.lower():\n            secret_terms += [\"甲亢\", \"graves\"]\n        if \"慢性阻塞\" in state.secret:\n            secret_terms += [\"copd\", \"慢阻肺\"]\n        hit = any(t in guess for t in secret_terms)\n        if hit:\n            return (\n                \"### 🎯 判定\\n\"\n                \"你抓住了核心诊断。推理链条成立,关键是把症状、危险信号和特异检查连起来。\\n\\n\"\n                f\"### 🔓 真相\\n{state.secret}\\n\\n\"\n                \"### 💡 记忆钉\\n\"\n                \"好诊断不是猜谜底,而是让每条线索都有地方安放。\"\n            )\n        return (\n            \"### ❌ 判定\\n\"\n            \"这个答案有一点影子,但还没有解释最关键的危险线索。\\n\\n\"\n            f\"### 🔄 反向提示\\n别被「{state.red_herring}」带偏,重新看最急、最能改变处理路径的证据。\\n\\n\"\n            \"### 💡 记忆钉\\n\"\n            \"先处理能致命的可能,再处理看起来像的可能。\"\n        )\n\n    if mode == \"hint\":\n        return (\n            \"### 💡 分层提示\\n\"\n            f\"把注意力放在这条线索上:{next_clue}\\n\\n\"\n            \"### 🤔 小问题\\n\"\n            \"它更支持哪个系统的问题?有没有一个诊断能同时解释时间、症状和检查?\"\n        )\n\n    return (\n        \"### 🔍 新线索\\n\"\n        f\"{next_clue}\\n\\n\"\n        \"### 📝 案件旁白\\n\"\n        \"房间里安静了一秒。这个线索不像答案,但它像一把钥匙。\"\n    )\n\n\n# ---------------------------------------------------------------------------\n# Model loading — llama-cpp-python (GGUF) on CPU\n# ---------------------------------------------------------------------------\n# Hugging Face ZeroGPU is designed primarily for PyTorch workloads. The CUDA\n# wheel of llama-cpp-python requires system CUDA runtime libraries such as\n# libcudart.so.12, which are not available in the normal Space container and can\n# fail before inference starts. Use the CPU wheel for reliable Spaces startup.\n\n_llm_instance = None\n\n\ndef get_llm():\n    \"\"\"Load the GGUF model.  Raises RuntimeError when DEMO_MODE is forced.\"\"\"\n    global _llm_instance\n    if _llm_instance is not None:\n        return _llm_instance\n    if DEMO_MODE in {\"1\", \"true\", \"yes\", \"on\"}:\n        raise RuntimeError(\"DEMO_MODE is enabled — skipping model load.\")\n\n    from llama_cpp import Llama  # noqa: delayed import\n\n    print(\"[Case Lantern] Loading GGUF model …\")\n    _llm_instance = Llama.from_pretrained(\n        repo_id=GGUF_REPO,\n        filename=GGUF_FILE,\n        n_ctx=2048,\n        n_threads=int(os.getenv(\"LLAMA_THREADS\", \"4\")),\n        n_gpu_layers=0,\n        verbose=True,\n    )\n    print(\"[Case Lantern] Model loaded successfully.\")\n    return _llm_instance\n\n\ndef _call_model_inner(\n    messages: List[Dict[str, str]], state: GameState, fallback_mode: str\n) -> str:\n    if DEMO_MODE in {\"1\", \"true\", \"yes\", \"on\"}:\n        return demo_reply(messages[-1][\"content\"], state, fallback_mode)\n\n    try:\n        llm = get_llm()\n        response = llm.create_chat_completion(\n            messages=messages,\n            max_tokens=MAX_NEW_TOKENS,\n            temperature=0.85,\n            top_p=0.92,\n            repeat_penalty=1.05,\n            stop=[\"<|im_end|>\", \"<|endoftext|>\"],\n        )\n        raw = response[\"choices\"][0][\"message\"][\"content\"] or \"\"\n        return strip_thinking(raw)\n    except Exception as exc:\n        import traceback\n\n        traceback.print_exc()\n        if DEMO_MODE == \"off\":\n            raise\n        return (\n            demo_reply(messages[-1][\"content\"], state, fallback_mode)\n            + f\"\\n\\n_演示模式:模型暂未加载({type(exc).__name__}: {exc})。_\"\n        )\n\n\ncall_model = _call_model_inner\n\n\n# ---------------------------------------------------------------------------\n# Game logic\n# ---------------------------------------------------------------------------\nChatHistory = List[Dict[str, str]]\n\n\ndef new_case():\n    seed = random.choice(CASE_SEEDS)\n    state = GameState(\n        title=seed[\"title\"],\n        genre=seed[\"genre\"],\n        opening=seed[\"opening\"],\n        secret=seed[\"secret\"],\n        red_herring=seed[\"red_herring\"],\n        clues=list(seed[\"clues\"]),\n        used_clues=[],\n    )\n    first_message = {\n        \"role\": \"assistant\",\n        \"content\": (\n            f\"### 🏮 {state.title}\\n\"\n            f\"**{state.genre}**\\n\\n\"\n            f\"{state.opening}\\n\\n\"\n            \"你有 **6 个回合** 调查。选择一个行动,或直接输入你的诊断假设。\"\n        ),\n    }\n    return [first_message], state, state.public_context(), status_line(state)\n\n\ndef status_line(state: GameState) -> str:\n    icon = \"🏆\" if state.solved else \"🔎\"\n    label = \"已破案\" if state.solved else \"调查中\"\n    return f\"{icon} {label}  ·  回合 {state.turns}/6  ·  ⭐ {state.score}\"\n\n\ndef reveal_clue(state: GameState) -> Optional[str]:\n    unused = [c for c in state.clues if c not in state.used_clues]\n    if not unused:\n        return None\n    clue = unused[0]\n    state.used_clues.append(clue)\n    return clue\n\n\ndef build_messages(\n    state: GameState, instruction: str, mode: str\n) -> List[Dict[str, str]]:\n    return [\n        {\"role\": \"system\", \"content\": SYSTEM_PROMPT},\n        {\n            \"role\": \"user\",\n            \"content\": textwrap.dedent(f\"\"\"\\\n                你正在主持一个虚构医学推理小游戏。\n\n                隐藏真相:{state.secret}\n                红鲱鱼:{state.red_herring}\n\n                当前公开状态:\n                {state.public_context()}\n\n                玩家动作:\n                {instruction}\n\n                输出要求:\n                - 不要给真实医疗建议。\n                - 不要要求玩家提供真实个人健康信息。\n                - 如果 mode={mode} 且不是评分,不要直接泄露隐藏真相。\n                - 保持中文,短小、有戏剧感。\n            \"\"\"),\n        },\n    ]\n\n\ndef diagnosis_terms(secret: str) -> List[str]:\n    terms = [secret.lower()]\n    mapping = {\n        \"心肌梗死\": [\"心梗\", \"stemi\", \"梗死\"],\n        \"输卵管\": [\"宫外孕\", \"异位妊娠\", \"破裂\"],\n        \"Graves\": [\"graves\", \"甲亢\", \"甲状腺功能亢进\"],\n        \"慢性阻塞\": [\"copd\", \"慢阻肺\"],\n    }\n    for key, values in mapping.items():\n        if key.lower() in secret.lower():\n            terms.extend(values)\n    return terms\n\n\ndef act(action, custom_action, chat, state):\n    if not state or not state.title:\n        chat, state, context, status = new_case()\n\n    if state.solved:\n        chat.append(\n            {\n                \"role\": \"assistant\",\n                \"content\": \"案件已经结案。点击 **新案件** 开始下一个挑战。\",\n            }\n        )\n        return chat, state, state.public_context(), status_line(state), \"\"\n\n    instruction = normalize_text(custom_action) or ACTION_PRESETS.get(\n        action, ACTION_PRESETS[\"提示\"]\n    )\n    mode = \"hint\" if action == \"提示\" else \"clue\"\n    state.turns += 1\n    state.score = max(20, state.score - (6 if mode == \"hint\" else 4))\n    reveal_clue(state)\n\n    reply = call_model(build_messages(state, instruction, mode), state, mode)\n    chat.append({\"role\": \"user\", \"content\": f\"🎬 {action}:{instruction}\"})\n    chat.append({\"role\": \"assistant\", \"content\": reply})\n    return chat, state, state.public_context(), status_line(state), \"\"\n\n\ndef submit_guess(guess, chat, state):\n    if not state or not state.title:\n        chat, state, context, status = new_case()\n\n    cleaned = normalize_text(guess)\n    if not cleaned:\n        chat.append({\"role\": \"assistant\", \"content\": \"先写下你的诊断假设,再按提交。\"})\n        return chat, state, state.public_context(), status_line(state), \"\"\n\n    state.turns += 1\n    messages = build_messages(\n        state,\n        f\"玩家最终诊断是:{cleaned}。请评分并揭示真相。\",\n        \"score\",\n    )\n    reply = call_model(messages, state, \"score\")\n    state.solved = True\n    if any(t in cleaned.lower() for t in diagnosis_terms(state.secret)):\n        state.score = min(100, state.score + 12)\n    else:\n        state.score = max(20, state.score - 15)\n\n    chat.append({\"role\": \"user\", \"content\": f\"🩺 最终诊断:{cleaned}\"})\n    chat.append({\"role\": \"assistant\", \"content\": reply})\n    return chat, state, state.public_context(), status_line(state), \"\"\n\n\n# ---------------------------------------------------------------------------\n# Custom CSS — dark medical-mystery theme with glassmorphism\n# ---------------------------------------------------------------------------\nCUSTOM_CSS = \"\"\"\\\n/* ===== GLOBAL DARK OVERRIDE ===== */\n:root {\n  --cl-bg-deep:    #0b0f1a;\n  --cl-bg-panel:   rgba(15, 22, 42, 0.72);\n  --cl-glass:      rgba(255, 255, 255, 0.04);\n  --cl-glass-edge: rgba(255, 255, 255, 0.08);\n  --cl-ruby:       #e03e5e;\n  --cl-ruby-glow:  rgba(224, 62, 94, 0.35);\n  --cl-gold:       #f0b429;\n  --cl-gold-dim:   #c6931b;\n  --cl-mint:       #34d399;\n  --cl-text:       #e2e8f0;\n  --cl-text-dim:   #94a3b8;\n  --cl-border:     rgba(255, 255, 255, 0.06);\n  --cl-radius:     14px;\n}\n\n/* Force dark everywhere */\nbody, .gradio-container, .main, .contain,\n.gradio-container .main .wrap {\n  background: var(--cl-bg-deep) !important;\n  color: var(--cl-text) !important;\n}\n\n.gradio-container {\n  max-width: 1200px !important;\n  font-family: 'Inter', 'Noto Sans SC', system-ui, -apple-system, sans-serif !important;\n}\n\n/* ===== HEADER BANNER ===== */\n#hero-banner {\n  background: linear-gradient(135deg, rgba(224,62,94,0.13) 0%, rgba(15,22,42,0.95) 50%, rgba(52,211,153,0.08) 100%);\n  border: 1px solid var(--cl-glass-edge);\n  border-radius: var(--cl-radius);\n  padding: 48px 32px 24px;\n  margin-bottom: 8px;\n  backdrop-filter: blur(20px);\n  -webkit-backdrop-filter: blur(20px);\n  position: relative;\n  overflow: visible;\n}\n\n#hero-banner::before {\n  content: '';\n  position: absolute;\n  top: -80%;\n  right: -10%;\n  width: 260px;\n  height: 260px;\n  border-radius: 50%;\n  background: radial-gradient(circle, var(--cl-ruby-glow) 0%, transparent 70%);\n  animation: hero-pulse 5s ease-in-out infinite;\n  pointer-events: none;\n}\n\n@keyframes hero-pulse {\n  0%, 100% { opacity: 0.3; transform: scale(1); }\n  50%      { opacity: 0.6; transform: scale(1.15); }\n}\n\n.hero-title {\n  font-size: 2.4rem;\n  font-weight: 800;\n  background: linear-gradient(135deg, #ff5c7c, #ffd166);\n  -webkit-background-clip: text;\n  -webkit-text-fill-color: transparent;\n  background-clip: text;\n  margin: 0 0 12px 0;\n  line-height: 1.35;\n  position: relative;\n  z-index: 1;\n}\n\n#hero-banner p, #hero-banner .prose p {\n  color: var(--cl-text-dim) !important;\n  font-size: 0.92rem !important;\n  margin: 0 !important;\n  line-height: 1.5 !important;\n}\n\n#hero-banner a { color: var(--cl-gold) !important; text-decoration: underline; }\n\n/* Prevent Gradio wrapper clipping inside hero banner */\n#hero-banner > div,\n#hero-banner .prose,\n#hero-banner .md,\n#hero-banner .wrap,\n#hero-banner .block {\n  overflow: visible !important;\n}\n\n/* ===== SAFETY NOTE ===== */\n#safety-note {\n  background: rgba(224, 62, 94, 0.08) !important;\n  border: 1px solid rgba(224, 62, 94, 0.18) !important;\n  border-radius: 10px !important;\n  padding: 10px 14px !important;\n  margin-bottom: 12px !important;\n}\n#safety-note p, #safety-note .prose p {\n  color: #fca5a5 !important;\n  font-size: 0.82rem !important;\n  margin: 0 !important;\n}\n\n/* ===== GLASSMORPHISM PANELS ===== */\n.glass-panel, .glass-panel > .block {\n  background: var(--cl-bg-panel) !important;\n  border: 1px solid var(--cl-glass-edge) !important;\n  border-radius: var(--cl-radius) !important;\n  backdrop-filter: blur(16px) !important;\n  -webkit-backdrop-filter: blur(16px) !important;\n}\n\n/* ===== CHATBOT ===== */\n#case-chat {\n  border: 1px solid var(--cl-glass-edge) !important;\n  border-radius: var(--cl-radius) !important;\n  background: rgba(15, 22, 42, 0.55) !important;\n  backdrop-filter: blur(12px) !important;\n}\n\n/* Force ALL chatbot message text to be bright */\n#case-chat .message-row .message,\n#case-chat .bot .message-bubble,\n#case-chat .user .message-bubble,\n#case-chat .message,\n#case-chat .message-bubble,\n#case-chat [data-testid=\"bot\"],\n#case-chat [data-testid=\"user\"],\n#case-chat .bot,\n#case-chat .user,\n#case-chat .prose,\n#case-chat .md,\n#case-chat .message p,\n#case-chat .message span,\n#case-chat .message li,\n#case-chat .message h1,\n#case-chat .message h2,\n#case-chat .message h3,\n#case-chat .message h4,\n#case-chat .message strong,\n#case-chat .message em,\n#case-chat .message-bubble p,\n#case-chat .message-bubble span,\n#case-chat .message-bubble li,\n#case-chat .message-bubble h1,\n#case-chat .message-bubble h2,\n#case-chat .message-bubble h3,\n#case-chat .message-bubble h4,\n#case-chat .message-bubble strong,\n#case-chat .message-bubble em,\n#case-chat .prose p,\n#case-chat .prose span,\n#case-chat .prose li,\n#case-chat .prose h1,\n#case-chat .prose h2,\n#case-chat .prose h3,\n#case-chat .prose h4,\n#case-chat .prose strong {\n  color: #f1f5f9 !important;\n}\n\n#case-chat .message-row .message,\n#case-chat .message-bubble,\n#case-chat .bot .message-bubble,\n#case-chat [data-testid=\"bot\"] {\n  border-radius: 12px !important;\n  font-size: 0.93rem !important;\n  line-height: 1.65 !important;\n  background: rgba(30, 41, 70, 0.85) !important;\n  border: 1px solid var(--cl-glass-edge) !important;\n}\n\n/* user bubble - red tinted */\n#case-chat .message-row.user-row .message,\n#case-chat .user .message-bubble,\n#case-chat [data-testid=\"user\"] {\n  background: linear-gradient(135deg, rgba(224,62,94,0.22), rgba(224,62,94,0.10)) !important;\n  border: 1px solid rgba(224,62,94,0.25) !important;\n}\n\n/* bot bubble - dark glass */\n#case-chat .message-row.bot-row .message,\n#case-chat .bot .message-bubble,\n#case-chat [data-testid=\"bot\"] {\n  background: rgba(30, 41, 70, 0.85) !important;\n  border: 1px solid var(--cl-glass-edge) !important;\n}\n\n/* Chatbot wrapper and scroll area dark */\n#case-chat .chatbot,\n#case-chat .wrap,\n#case-chat > div {\n  background: transparent !important;\n}\n\n/* ===== TEXTBOX / INPUT FIELDS ===== */\ntextarea, input[type=\"text\"],\n.textbox textarea, .textbox input {\n  background: rgba(15, 22, 42, 0.7) !important;\n  border: 1px solid var(--cl-glass-edge) !important;\n  border-radius: 10px !important;\n  color: var(--cl-text) !important;\n  transition: border-color 0.3s, box-shadow 0.3s !important;\n}\n\ntextarea:focus, input[type=\"text\"]:focus {\n  border-color: var(--cl-ruby) !important;\n  box-shadow: 0 0 0 3px var(--cl-ruby-glow) !important;\n  outline: none !important;\n}\n\n/* Labels */\nlabel, .label-wrap span, .block label span {\n  color: var(--cl-text-dim) !important;\n  font-weight: 600 !important;\n  font-size: 0.85rem !important;\n  text-transform: uppercase !important;\n  letter-spacing: 0.5px !important;\n}\n\n/* ===== RADIO BUTTONS ===== */\n.radio-group label, .wrap label.selected {\n  background: var(--cl-glass) !important;\n  border: 1px solid var(--cl-glass-edge) !important;\n  border-radius: 8px !important;\n  color: var(--cl-text) !important;\n  transition: all 0.25s !important;\n}\n\n.radio-group label:hover {\n  border-color: var(--cl-ruby) !important;\n  background: rgba(224, 62, 94, 0.08) !important;\n}\n\n.radio-group label.selected, .radio-group input:checked + label {\n  border-color: var(--cl-ruby) !important;\n  background: rgba(224, 62, 94, 0.15) !important;\n  box-shadow: 0 0 12px var(--cl-ruby-glow) !important;\n}\n\n/* ===== BUTTONS ===== */\nbutton.primary, button.primary:hover {\n  background: linear-gradient(135deg, var(--cl-ruby), #c2294a) !important;\n  border: none !important;\n  color: #fff !important;\n  border-radius: 10px !important;\n  font-weight: 700 !important;\n  letter-spacing: 0.3px !important;\n  box-shadow: 0 4px 20px var(--cl-ruby-glow) !important;\n  transition: transform 0.2s, box-shadow 0.3s !important;\n}\nbutton.primary:hover {\n  transform: translateY(-1px) !important;\n  box-shadow: 0 6px 28px rgba(224,62,94,0.5) !important;\n}\nbutton.primary:active {\n  transform: translateY(0) !important;\n}\n\nbutton.secondary, button.secondary:hover {\n  background: var(--cl-glass) !important;\n  border: 1px solid var(--cl-glass-edge) !important;\n  color: var(--cl-text) !important;\n  border-radius: 10px !important;\n  font-weight: 600 !important;\n  transition: all 0.25s !important;\n}\nbutton.secondary:hover {\n  border-color: var(--cl-gold-dim) !important;\n  color: var(--cl-gold) !important;\n  background: rgba(240,180,41,0.08) !important;\n}\n\n/* ===== STATUS PILL ===== */\n#status-pill textarea {\n  font-weight: 700 !important;\n  color: var(--cl-gold) !important;\n  font-size: 0.95rem !important;\n  background: rgba(240,180,41,0.06) !important;\n  border: 1px solid rgba(240,180,41,0.18) !important;\n  border-radius: 10px !important;\n  text-align: center !important;\n}\n\n/* ===== CASE BOARD ===== */\n#case-board textarea {\n  background: rgba(15, 22, 42, 0.65) !important;\n  border: 1px solid var(--cl-glass-edge) !important;\n  border-radius: 10px !important;\n  color: var(--cl-text-dim) !important;\n  font-size: 0.88rem !important;\n  line-height: 1.7 !important;\n}\n\n/* ===== EXAMPLES ===== */\n.examples-table button {\n  background: var(--cl-glass) !important;\n  border: 1px solid var(--cl-glass-edge) !important;\n  color: var(--cl-text-dim) !important;\n  border-radius: 8px !important;\n  transition: all 0.2s !important;\n}\n.examples-table button:hover {\n  border-color: var(--cl-mint) !important;\n  color: var(--cl-mint) !important;\n}\n\n/* ===== FOOTER ===== */\n#footer-info p, #footer-info .prose p {\n  color: var(--cl-text-dim) !important;\n  font-size: 0.78rem !important;\n  text-align: center !important;\n}\n\n/* ===== SCROLL BAR ===== */\n::-webkit-scrollbar { width: 6px; }\n::-webkit-scrollbar-track { background: transparent; }\n::-webkit-scrollbar-thumb {\n  background: rgba(255,255,255,0.1);\n  border-radius: 3px;\n}\n::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.2); }\n\n/* ===== ANIMATIONS ===== */\n@keyframes fade-in {\n  from { opacity: 0; transform: translateY(8px); }\n  to   { opacity: 1; transform: translateY(0); }\n}\n\n.glass-panel, #case-chat, #hero-banner {\n  animation: fade-in 0.5s ease-out;\n}\n\n/* ===== RESPONSIVE ===== */\n@media (max-width: 768px) {\n  #hero-banner { padding: 18px 16px 14px; }\n  #hero-banner h1 { font-size: 1.5rem !important; }\n  .gradio-container { padding: 8px !important; }\n}\n\n/* ===== ACCORDION / GROUP borders ===== */\n.block, .form, .wrap, .panel, .gap, .gr-group, .gr-box {\n  border-color: var(--cl-border) !important;\n}\n\n/* ===== OVERRIDE light-mode remnants ===== */\n/* Force Gradio CSS variables everywhere */\n*, *::before, *::after,\n.dark, [data-testid],\n.gradio-container, .gradio-container * {\n  --background-fill-primary: var(--cl-bg-deep) !important;\n  --background-fill-secondary: rgba(15, 22, 42, 0.7) !important;\n  --background-fill-primary-dark: var(--cl-bg-deep) !important;\n  --border-color-primary: var(--cl-glass-edge) !important;\n  --body-text-color: var(--cl-text) !important;\n  --body-text-color-subdued: var(--cl-text-dim) !important;\n  --block-background-fill: var(--cl-bg-panel) !important;\n  --block-border-color: ",644      "app_signals": "GameState normalize_text value strip_thinking text demo_reply prompt state mode get_llm _call_model_inner messages fallback_mode new_case status_line reveal_clue build_messages instruction diagnosis_terms secret act action custom_action chat submit_guess guess Case Lantern — a fictional medical mystery game powered by a small Chinese medical reasoning model. Backend : llama-cpp-python (GGUF, runs on free CPU Spaces) Frontend : fully custom dark theme with glassmorphism & micro-animations Model : lastmass/Qwen3.5-Medical-GSPO (~4.66 B params, Q4_K_M quant) lastmass/Qwen3.5-Medical-GSPO mradermacher/Qwen3.5-Medical-GSPO-GGUF Qwen3.5-Medical-GSPO.Q4_K_M.gguf lower int Fictional training game only. This app does not provide medical advice, diagnosis, triage, or treatment guidance for real people. You are Case Lantern, a playful but careful medical mystery game master. Create and run fictional Chinese medical reasoning puzzles for education and entertainment. Rules: - Never present output as real medical advice. - Keep all patients fictional. - Do not ask users to share real personal health information. - Make the game delightful, concise, and clue-driven. - The player should reason from clues; avoid revealing the final answer unless asked to score. - Use simplified Chinese by default, with crisp section headers. - When scoring, be honest but friendly and include one memorable teaching pearl. public_context self os.getenv 问病史 查体 实验室 影像/心电 提示 我想进一步问病史。请给我一个关键但不直接泄底的病史线索。 我想做体格检查。请给我一个关键但不直接泄底的查体线索。 我想申请实验室检查。请给我一个关键但不直接泄底的检验线索。 我想看影像或心电图。请给我一个关键但不直接泄底的检查线索。 我卡住了。请给我一个分层提示,但不要直接说出诊断。 field default_factory strip re.sub flags replace text.strip Load the GGUF model. Raises RuntimeError when DEMO_MODE is forced. print Llama.from_pretrained repo_id filename n_ctx n_threads n_gpu_layers verbose random.choice title genre opening red_herring clues used_clues state.used_clues.append mapping.items max call_model chat.append any gr.Blocks gr.State gr.HTML gr.Markdown elem_id gr.Examples examples inputs label new_button.click outputs demo.load queue act_button.click guess_button.click __main__ launch MAX_NEW_TOKENS 420 凌晨两点的胸痛电报 急诊悬疑 65岁男性,凌晨突发胸痛,额头冒汗,坚持说只是晚饭吃坏了。护士递来一张还热乎的心电图。 下壁ST段抬高型心肌梗死 反流性食管炎 雨夜里的右下腹脚印 妇产科侦探 28岁女性,停经8周,右下腹剧痛后晕厥。诊室灯光一闪,血压计读数像坏消息一样低。 输卵管妊娠破裂导致腹腔内出血 急性阑尾炎 会变形的蝴蝶影子 内分泌谜题 32岁女性近两个月怕热、心悸、手抖,朋友说她的眼神像一直在追赶一列迟到的火车。 Graves病所致甲状腺功能亢进 焦虑障碍 沉默的蓝色嘴唇 呼吸科小剧场 70岁男性长期咳嗽咳痰,今天走三步就喘,口唇发绀,却还惦记着没下完的一盘棋。 慢性阻塞性肺疾病急性加重 单纯支气管哮喘 .*? score prompt.lower hint ### 🔍 新线索 ### 📝 案件旁白 房间里安静了一秒。这个线索不像答案,但它像一把钥匙。 RuntimeError [Case Lantern] Loading GGUF model … [Case Lantern] Model loaded successfully. llm.create_chat_completion max_tokens temperature top_p repeat_penalty stop role content assistant state.public_context 🏆 🔎 已破案 调查中 · 回合 /6 · ⭐ secret.lower 心肌梗死 输卵管 Graves 慢性阻塞 ACTION_PRESETS.get clue min gr.Row share theme css head gr.themes.Base primary_hue secondary_hue neutral_hue radius_size font GRADIO_SERVER_NAME GRADIO_SERVER_PORT DEMO_MODE auto 疼痛位于胸骨后,持续超过30分钟,伴冷汗。 II、III、aVF导联ST段抬高,I、aVL可见对应性改变。 血压略低,心率偏慢,提示可能累及右冠供血区域。 硝酸甘油后症状改善不明显。 停经8周,突发一侧下腹痛。 血压80/50 mmHg,面色苍白,提示休克。 后穹窿穿刺抽出不凝血。 尿/血HCG阳性,床旁超声宫内未见明确孕囊。 怕热、多汗、体重下降但食欲增加。 心率快,双手细颤。 甲状腺弥漫性肿大,可闻及血管杂音。 TSH降低,FT3/FT4升高,TRAb阳性。 长期吸烟史,慢性咳嗽咳痰多年。 活动后气促明显加重,双肺可闻及哮鸣音。 血气提示二氧化碳潴留倾向。 近期有受凉或感染诱因。 join 暂无线索 📁 案件: 🏷️ 类型: 📖 开场: 🔍 已公开线索: ⏱️ 回合: /6 ⭐ 分数: text.replace state.secret.lower graves ### ❌ 判定 这个答案有一点影子,但还没有解释最关键的危险线索。 ### 🔄 反向提示 别被「 」带偏,重新看最急、最能改变处理路径的证据。 ### 💡 记忆钉 先处理能致命的可能,再处理看起来像的可能。 ### 💡 分层提示 把注意力放在这条线索上: ### 🤔 小问题 它更支持哪个系统的问题?有没有一个诊断能同时解释时间、症状和检查? 1 true yes on DEMO_MODE is enabled — skipping model load. traceback.print_exc list ### 🏮 ** 你有 **6 个回合** 调查。选择一个行动,或直接输入你的诊断假设。 system user textwrap.dedent 心梗 stemi 梗死 宫外孕 异位妊娠 破裂 甲亢 甲状腺功能亢进 copd 慢阻肺 key.lower terms.extend 玩家最终诊断是: 。请评分并揭示真相。 Case Lantern 🏮 🏮 Case Lantern 一个由小型中文医疗推理模型驱动的虚构病例侦探游戏。查线索、避开误导、在 6 回合内破案。 模型:<a href=\"https://huggingface.co/ \" target=\"_blank\" rel=\"noopener\"> · ~4.66B 参数 · llama.cpp 本地推理 ⚠️ safety-note gr.Column scale gr.Chatbot height elem_classes gr.Textbox interactive lines gr.Radio choices placeholder gr.Button variant 💡 行动灵感 Case Lantern · Build Small Hackathon 2026 · Powered by [ ](https://huggingface.co/ ) via llama.cpp footer-info server_name server_port demo.queue max_size \\s+ ### 🎯 判定 你抓住了核心诊断。推理链条成立,关键是把症状、危险信号和特异检查连起来。 ### 🔓 真相 ### 💡 记忆钉 好诊断不是猜谜底,而是让每条线索都有地方安放。 off 案件已经结案。点击 **新案件** 开始下一个挑战。 🎬 : 先写下你的诊断假设,再按提交。 cleaned.lower 🩺 最终诊断: #### 🎯 调查行动 #### 🩺 最终诊断 💊 提交诊断 rose teal slate lg 7860 LLAMA_THREADS 4 message _演示模式:模型暂未加载( : )。_ 你正在主持一个虚构医学推理小游戏。 隐藏真相: 红鲱鱼: 当前公开状态: 玩家动作: 输出要求: - 不要给真实医疗建议。 - 不要要求玩家提供真实个人健康信息。 - 如果 mode= 且不是评分,不要直接泄露隐藏真相。 - 保持中文,短小、有戏剧感。 案件记录 case-chat 状态 status-pill 📋 案件板 case-board action-title 选择行动 自定义行动 例如:我想追问疼痛性质和伴随症状… 🔍 调查 🆕 新案件 你的诊断 写下你的诊断假设,然后提交破案 primary 我想询问发病时间、诱因和伴随症状 我想查看最能排除危险诊断的检查 请给我一个不会直接泄底的鉴别诊断提示 gr.themes.GoogleFont Noto Sans SC system-ui sans-serif • glass-panel ACTION_PRESETS.keys secondary GRADIO_SHARE false Inter type",645      "readme_len": 2540,646      "app_source_len": 24000,647      "app_signals_len": 4951648    },649    {650      "id": "build-small-hackathon/case0",651      "title": "Case Zero",652      "summary": "",653      "tags": [654        "build-small-hackathon",655        "detective-game",656        "llama-cpp",657        "text-generation",658        "tiny-titan",659        "tts"660      ],661      "models": [662        "Qwen/Qwen2.5-1.5B-Instruct"663      ],664      "datasets": [],665      "sdk": "docker",666      "license": "apache-2.0",667      "likes": 1,668      "url": "https://huggingface.co/spaces/build-small-hackathon/case0",669      "app_file": "",670      "readme_raw": "---\ntitle: Case Zero\nemoji: 🕵️\ncolorFrom: indigo\ncolorTo: yellow\nsdk: docker\napp_port: 7860\npinned: true\nlicense: apache-2.0\nmodels:\n  - Qwen/Qwen2.5-1.5B-Instruct\ntags:\n  - build-small-hackathon\n  - llama-cpp\n  - tiny-titan\n  - detective-game\n  - text-generation\n  - tts\n---\n\n# 🕵️ Case Zero — the AI *is* the detective game\n\n**A brand-new murder mystery, written and acted by a 1.5B model, every single time.**\n\nNo scripted cases. No content library. A single small local model invents the whole\nthing — the victim, the suspects, their secrets and motives, the timeline, the murder\nweapon, the evidence, and the one who did it — then **role-plays every suspect live**.\nThey remember what you asked. They lie to your face. And when you slap down the right\npiece of evidence, you watch the lie **crack in real time**.\n\n> Interrogate. Investigate. Accuse. One of them is guilty. Prove it.\n\n## ✨ The moment that sells it\n\nSearch the rooms, find a clue that contradicts a suspect's alibi, **present it**, and\ntheir story falls apart on screen — stress spikes, the alibi breaks, the truth leaks.\nThen name the killer, cite your proof, and get a scored verdict with a \"Director's Cut\"\nwalkthrough of how the crime really went down.\n\n## 🧠 How it works\n\n| Layer | What it does |\n|---|---|\n| **Model** — Qwen2.5-1.5B-Instruct (GGUF) | The whole game. Runs in-process on the CPU through **llama.cpp** (`llama-cpp-python`) — no server, no GPU, no remote endpoint. |\n| **Generation** | The model authors every case as JSON; deterministic Python only wires the *structure* (who's guilty, who was where) so the mystery is always solvable. |\n| **Solver** | A fairness referee: single culprit, a breakable alibi, every innocent cleared, and a discoverability gate so the key clue is always findable in play. |\n| **Director** | Whether a lie gets caught is decided by **ground truth, not the model** — so the win condition is immune to prose (a jailbroken \"just tell me who did it\" earns nothing). |\n| **Voice** — Supertonic | Each suspect gets a distinct, gender-matched on-device voice, synthesized **sentence-by-sentence as the reply streams**. |\n| **Art** | Procedural pixel-art portraits, rooms, and evidence — unique per case, rendered offline with Pillow. |\n| **UI** | A hand-built pixel, Terraria-style Gradio front end (heavy custom CSS/JS). |\n\nThe model does all the creative work. Deterministic code is only guardrails and a\nreliability layer — it never writes story, character, or dialogue.\n\n## 🏆 Built for the Build Small Hackathon\n\n- **Tiny Titan (≤4B):** the entire game runs on **Qwen2.5-1.5B** — ~1.6B total runtime\n  params (LLM + Supertonic), far under the 32B cap.\n- **Llama Champion:** the model runs through the **llama.cpp** runtime, in-process — no\n  server, no remote endpoint.\n- All models are **open-weights and self-run**. No third-party AI APIs are ever called.\n\nSee [COMPLIANCE.md](COMPLIANCE.md) for the full parameter budget and badge details.\n\n## ▶️ Run it locally\n\n```bash\npython -m venv .venv && .venv/Scripts/pip install -r requirements.txt   # (Windows)\npython scripts/fetch_models.py     # one-time: fetch the open GGUF + Supertonic\npython app.py                      # open http://127.0.0.1:7860\n```\n\nThe game runs entirely on the CPU — laptop or Space, same code, no GPU required.\n\n## 🙏 Credits\n\n- **LLM:** [Qwen2.5-1.5B-Instruct](https://huggingface.co/Qwen/Qwen2.5-1.5B-Instruct) (Apache-2.0), via llama.cpp.\n- **Voices:** Supertonic on-device TTS.\n- **Music:** *\"Backbay Lounge\"* by Kevin MacLeod (incompetech.com), licensed under\n  [Creative Commons Attribution 4.0](https://creativecommons.org/licenses/by/4.0/).\n- **Fonts:** Silkscreen & Pixelify Sans (SIL Open Font License), self-hosted.\n- Pixel art and UI sound effects: procedurally generated.\n",671      "readme_body": "# 🕵️ Case Zero — the AI *is* the detective game\n\n**A brand-new murder mystery, written and acted by a 1.5B model, every single time.**\n\nNo scripted cases. No content library. A single small local model invents the whole\nthing — the victim, the suspects, their secrets and motives, the timeline, the murder\nweapon, the evidence, and the one who did it — then **role-plays every suspect live**.\nThey remember what you asked. They lie to your face. And when you slap down the right\npiece of evidence, you watch the lie **crack in real time**.\n\n> Interrogate. Investigate. Accuse. One of them is guilty. Prove it.\n\n## ✨ The moment that sells it\n\nSearch the rooms, find a clue that contradicts a suspect's alibi, **present it**, and\ntheir story falls apart on screen — stress spikes, the alibi breaks, the truth leaks.\nThen name the killer, cite your proof, and get a scored verdict with a \"Director's Cut\"\nwalkthrough of how the crime really went down.\n\n## 🧠 How it works\n\n| Layer | What it does |\n|---|---|\n| **Model** — Qwen2.5-1.5B-Instruct (GGUF) | The whole game. Runs in-process on the CPU through **llama.cpp** (`llama-cpp-python`) — no server, no GPU, no remote endpoint. |\n| **Generation** | The model authors every case as JSON; deterministic Python only wires the *structure* (who's guilty, who was where) so the mystery is always solvable. |\n| **Solver** | A fairness referee: single culprit, a breakable alibi, every innocent cleared, and a discoverability gate so the key clue is always findable in play. |\n| **Director** | Whether a lie gets caught is decided by **ground truth, not the model** — so the win condition is immune to prose (a jailbroken \"just tell me who did it\" earns nothing). |\n| **Voice** — Supertonic | Each suspect gets a distinct, gender-matched on-device voice, synthesized **sentence-by-sentence as the reply streams**. |\n| **Art** | Procedural pixel-art portraits, rooms, and evidence — unique per case, rendered offline with Pillow. |\n| **UI** | A hand-built pixel, Terraria-style Gradio front end (heavy custom CSS/JS). |\n\nThe model does all the creative work. Deterministic code is only guardrails and a\nreliability layer — it never writes story, character, or dialogue.\n\n## 🏆 Built for the Build Small Hackathon\n\n- **Tiny Titan (≤4B):** the entire game runs on **Qwen2.5-1.5B** — ~1.6B total runtime\n  params (LLM + Supertonic), far under the 32B cap.\n- **Llama Champion:** the model runs through the **llama.cpp** runtime, in-process — no\n  server, no remote endpoint.\n- All models are **open-weights and self-run**. No third-party AI APIs are ever called.\n\nSee [COMPLIANCE.md](COMPLIANCE.md) for the full parameter budget and badge details.\n\n## ▶️ Run it locally\n\n```bash\npython -m venv .venv && .venv/Scripts/pip install -r requirements.txt   # (Windows)\npython scripts/fetch_models.py     # one-time: fetch the open GGUF + Supertonic\npython app.py                      # open http://127.0.0.1:7860\n```\n\nThe game runs entirely on the CPU — laptop or Space, same code, no GPU required.\n\n## 🙏 Credits\n\n- **LLM:** [Qwen2.5-1.5B-Instruct](https://huggingface.co/Qwen/Qwen2.5-1.5B-Instruct) (Apache-2.0), via llama.cpp.\n- **Voices:** Supertonic on-device TTS.\n- **Music:** *\"Backbay Lounge\"* by Kevin MacLeod (incompetech.com), licensed under\n  [Creative Commons Attribution 4.0](https://creativecommons.org/licenses/by/4.0/).\n- **Fonts:** Silkscreen & Pixelify Sans (SIL Open Font License), self-hosted.\n- Pixel art and UI sound effects: procedurally generated.",672      "readme_frontmatter": {673        "title": "Case Zero",674        "emoji": "🕵️",675        "colorFrom": "indigo",676        "colorTo": "yellow",677        "sdk": "docker",678        "app_port": "7860",679        "pinned": "true",680        "license": "apache-2.0",681        "models": "",682        "tags": ""683      },684      "app_source": "",685      "app_signals": "",686      "readme_len": 3499,687      "app_source_len": 0,688      "app_signals_len": 0689    },690    {691      "id": "build-small-hackathon/chorus",692      "title": "Chorus",693      "summary": "Discover the signal without having to read the noise",694      "tags": [695        "gradio",696        "region:us"697      ],698      "models": [],699      "datasets": [],700      "sdk": "gradio",701      "license": "mit",702      "likes": 0,703      "url": "https://huggingface.co/spaces/build-small-hackathon/chorus",704      "app_file": "app.py",705      "readme_raw": "---\ntitle: Chorus\nemoji: 🎧\ncolorFrom: indigo\ncolorTo: red\nsdk: gradio\nsdk_version: 6.16.0\npython_version: '3.14'\napp_file: app.py\npinned: false\nlicense: mit\nshort_description: Discover the signal without having to read the noise\n---\n\nCheck out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference\n\n\n# Chorus\n\nChorus helps writers, journalists, creators, and public figures understand what their audience is actually saying.\n\nWhen an article, video, or post receives hundreds or thousands of comments, valuable feedback is often buried beneath insults, personal attacks, repetition, and off-topic discussions. Reading every comment is time-consuming and emotionally draining, yet ignoring comments means missing useful criticism, corrections, and new ideas.\n\nChorus automatically analyzes comment sections and produces a concise summary of the discussion.\n\n## What it does\n\nGiven a YouTube video or Reddit discussion, Chorus:\n\n- Collects comments and replies.\n- Filters out abusive language, personal attacks, and low-value comments.\n- Identifies the main topics and arguments being discussed.\n- Groups similar comments together.\n- Generates a summary of the key points raised by commenters.\n\nInstead of reading 5,000 comments, users receive a structured overview of the discussion.\n\n## Setup\n\n1. Ensure you have Python 3.14 installed.\n2. Set up a virtual environment:\n   ```bash\n   python3 -m venv venv\n   source venv/bin/activate  # On Windows, use `venv\\Scripts\\activate`\n   ```\n3. Install dependencies:\n   ```bash\n   pip install -r requirements.txt\n   ```\n4. Configure your environment variables in a `.env` file (see `.env.example`).\n\n## Project Structure\n\n- `chorus/api/`: YouTube and Reddit API clients.\n- `chorus/llm/`: Local and HuggingFace llama.cpp integration.\n- `chorus/ui/`: Gradio interface.\n\n## License\n\nThis project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.\n",706      "readme_body": "Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference\n\n\n# Chorus\n\nChorus helps writers, journalists, creators, and public figures understand what their audience is actually saying.\n\nWhen an article, video, or post receives hundreds or thousands of comments, valuable feedback is often buried beneath insults, personal attacks, repetition, and off-topic discussions. Reading every comment is time-consuming and emotionally draining, yet ignoring comments means missing useful criticism, corrections, and new ideas.\n\nChorus automatically analyzes comment sections and produces a concise summary of the discussion.\n\n## What it does\n\nGiven a YouTube video or Reddit discussion, Chorus:\n\n- Collects comments and replies.\n- Filters out abusive language, personal attacks, and low-value comments.\n- Identifies the main topics and arguments being discussed.\n- Groups similar comments together.\n- Generates a summary of the key points raised by commenters.\n\nInstead of reading 5,000 comments, users receive a structured overview of the discussion.\n\n## Setup\n\n1. Ensure you have Python 3.14 installed.\n2. Set up a virtual environment:\n   ```bash\n   python3 -m venv venv\n   source venv/bin/activate  # On Windows, use `venv\\Scripts\\activate`\n   ```\n3. Install dependencies:\n   ```bash\n   pip install -r requirements.txt\n   ```\n4. Configure your environment variables in a `.env` file (see `.env.example`).\n\n## Project Structure\n\n- `chorus/api/`: YouTube and Reddit API clients.\n- `chorus/llm/`: Local and HuggingFace llama.cpp integration.\n- `chorus/ui/`: Gradio interface.\n\n## License\n\nThis project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.",707      "readme_frontmatter": {708        "title": "Chorus",709        "emoji": "🎧",710        "colorFrom": "indigo",711        "colorTo": "red",712        "sdk": "gradio",713        "sdk_version": "6.16.0",714        "python_version": "3.14",715        "app_file": "app.py",716        "pinned": "false",717        "license": "mit",718        "short_description": "Discover the signal without having to read the noise"719      },720      "app_source": "import gradio as gr\n\ndef greet(name):\n    return \"Hello \" + name + \"!!\"\n\ndemo = gr.Interface(fn=greet, inputs=\"text\", outputs=\"text\")\ndemo.launch()\n",721      "app_signals": "greet name gr.Interface fn inputs outputs demo.launch !! text Hello",722      "readme_len": 1712,723      "app_source_len": 148,724      "app_signals_len": 67725    },726    {727      "id": "build-small-hackathon/cloud-parade-cabinet",728      "title": "Cloud Parade Cabinet",729      "summary": "Tiny moving parades with generated sound.",730      "tags": [731        "build-small-hackathon",732        "gradio",733        "modal",734        "nvidia-nemotron",735        "openbmb",736        "thousand-token-wood"737      ],738      "models": [739        "Qwen/Qwen2.5-7B-Instruct",740        "openbmb/MiniCPM4-8B",741        "nvidia/llama-3.1-nemotron-nano-8b-v1"742      ],743      "datasets": [],744      "sdk": "gradio",745      "license": "mit",746      "likes": 0,747      "url": "https://huggingface.co/spaces/build-small-hackathon/cloud-parade-cabinet",748      "app_file": "app.py",749      "readme_raw": "---\ntitle: Cloud Parade Cabinet\nemoji: 🎺\ncolorFrom: green\ncolorTo: yellow\nsdk: gradio\nsdk_version: 5.33.0\napp_file: app.py\npinned: false\nlicense: mit\nshort_description: Tiny moving parades with generated sound.\nmodels:\n- Qwen/Qwen2.5-7B-Instruct\n- openbmb/MiniCPM4-8B\n- nvidia/llama-3.1-nemotron-nano-8b-v1\ntags:\n- gradio\n- build-small-hackathon\n- thousand-token-wood\n- nvidia-nemotron\n- openbmb\n- modal\n---\n\n# Cloud Parade Cabinet\n\nCloud Parade Cabinet is a second Thousand Token Wood project concept: a kinetic Gradio toy where a hosted small model invents a tiny impossible parade, then the app turns it into an animated route, three float cards, a crowd chant, a generated parade sound, a poster, a caption, and a run log.\n\n## Why It Fits\n\n- **Toy / art experiment:** The first screen is the toy, not a landing page.\n- **Load-bearing model:** The model writes the parade plan, float actions, chant, and route-changing finale.\n- **Cloud API allowed:** Uses `HF_API_KEY`/`HF_TOKEN` through `huggingface_hub.InferenceClient`, and can use `NVIDIA_API_KEY` through NVIDIA's chat-completions API.\n- **Small-model target:** Defaults are Qwen 7B, OpenBMB MiniCPM4 8B, and NVIDIA Nemotron Nano 8B routes, all below the 32B cap.\n- **Gradio:** Built as a standalone Gradio Blocks app.\n- **Practice writer:** If a provider is unavailable, deterministic local text keeps the app testable.\n\n## Run Locally\n\n```powershell\ncd cloud_parade_cabinet\npython app.py\n```\n\nRun the package checks:\n\n```powershell\npython generate_share_assets.py\npython artifact_audit.py\npython space_upload_manifest.py\n```\n\nOptional cloud settings:\n\n```powershell\n$env:HF_API_KEY=\"...\"\n$env:NVIDIA_API_KEY=\"...\"\n$env:PARADE_MODEL=\"Qwen/Qwen2.5-7B-Instruct\"\npython app.py\n```\n\n## User Flow\n\n1. Choose a town, weather, grand marshal, street trouble, cabinet color, and parade energy.\n2. Pick **Hugging Face**, **OpenBMB**, **NVIDIA**, or **Practice writer** in **Parade voice**.\n3. Press **Open cabinet**.\n4. Watch the animated route, play the parade sound, read the three float cards, copy the caption, and keep the parade log.\n\n## Modal\n\nSee [MODAL_GUIDE.md](MODAL_GUIDE.md) for the smoke test and OpenBMB GPU endpoint scaffold.\n\n## Strategy And Provider Checks\n\n- [PRIZE_STRATEGY.md](PRIZE_STRATEGY.md) explains the NVIDIA/OpenBMB/Modal award route.\n- [CLOUD_PROVIDER_NOTES.md](CLOUD_PROVIDER_NOTES.md) records the provider assumptions from the local reference docs.\n- Run `python provider_probe.py` from this folder to create a sanitized [PROVIDER_PROBE.md](PROVIDER_PROBE.md).\n\n## Submission Notes\n\nThis is intentionally a separate candidate from Pocket Weather Theater. It can become a new Hugging Face Space if we decide the concept is stronger or if we want a second submission package.\n",750      "readme_body": "# Cloud Parade Cabinet\n\nCloud Parade Cabinet is a second Thousand Token Wood project concept: a kinetic Gradio toy where a hosted small model invents a tiny impossible parade, then the app turns it into an animated route, three float cards, a crowd chant, a generated parade sound, a poster, a caption, and a run log.\n\n## Why It Fits\n\n- **Toy / art experiment:** The first screen is the toy, not a landing page.\n- **Load-bearing model:** The model writes the parade plan, float actions, chant, and route-changing finale.\n- **Cloud API allowed:** Uses `HF_API_KEY`/`HF_TOKEN` through `huggingface_hub.InferenceClient`, and can use `NVIDIA_API_KEY` through NVIDIA's chat-completions API.\n- **Small-model target:** Defaults are Qwen 7B, OpenBMB MiniCPM4 8B, and NVIDIA Nemotron Nano 8B routes, all below the 32B cap.\n- **Gradio:** Built as a standalone Gradio Blocks app.\n- **Practice writer:** If a provider is unavailable, deterministic local text keeps the app testable.\n\n## Run Locally\n\n```powershell\ncd cloud_parade_cabinet\npython app.py\n```\n\nRun the package checks:\n\n```powershell\npython generate_share_assets.py\npython artifact_audit.py\npython space_upload_manifest.py\n```\n\nOptional cloud settings:\n\n```powershell\n$env:HF_API_KEY=\"...\"\n$env:NVIDIA_API_KEY=\"...\"\n$env:PARADE_MODEL=\"Qwen/Qwen2.5-7B-Instruct\"\npython app.py\n```\n\n## User Flow\n\n1. Choose a town, weather, grand marshal, street trouble, cabinet color, and parade energy.\n2. Pick **Hugging Face**, **OpenBMB**, **NVIDIA**, or **Practice writer** in **Parade voice**.\n3. Press **Open cabinet**.\n4. Watch the animated route, play the parade sound, read the three float cards, copy the caption, and keep the parade log.\n\n## Modal\n\nSee [MODAL_GUIDE.md](MODAL_GUIDE.md) for the smoke test and OpenBMB GPU endpoint scaffold.\n\n## Strategy And Provider Checks\n\n- [PRIZE_STRATEGY.md](PRIZE_STRATEGY.md) explains the NVIDIA/OpenBMB/Modal award route.\n- [CLOUD_PROVIDER_NOTES.md](CLOUD_PROVIDER_NOTES.md) records the provider assumptions from the local reference docs.\n- Run `python provider_probe.py` from this folder to create a sanitized [PROVIDER_PROBE.md](PROVIDER_PROBE.md).\n\n## Submission Notes\n\nThis is intentionally a separate candidate from Pocket Weather Theater. It can become a new Hugging Face Space if we decide the concept is stronger or if we want a second submission package.",751      "readme_frontmatter": {752        "title": "Cloud Parade Cabinet",753        "emoji": "🎺",754        "colorFrom": "green",755        "colorTo": "yellow",756        "sdk": "gradio",757        "sdk_version": "5.33.0",758        "app_file": "app.py",759        "pinned": "false",760        "license": "mit",761        "short_description": "Tiny moving parades with generated sound.",762        "models": "",763        "tags": ""764      },765      "app_source": "import hashlib\nimport json\nimport math\nimport os\nimport random\nimport re\nimport urllib.error\nimport urllib.request\nimport wave\nfrom dataclasses import asdict, dataclass\nfrom html import escape\nfrom pathlib import Path\n\nimport gradio as gr\n\ntry:\n    from huggingface_hub import InferenceClient\nexcept Exception:  # pragma: no cover - dependency is available in normal runtime\n    InferenceClient = None\n\n\nAPP_TITLE = \"Cloud Parade Cabinet\"\nCLOUD_MODEL = os.getenv(\"PARADE_MODEL\", \"Qwen/Qwen2.5-7B-Instruct\")\nOPENBMB_MODEL = os.getenv(\"OPENBMB_MODEL\", \"openbmb/MiniCPM4-8B\")\nNVIDIA_MODEL = os.getenv(\"NVIDIA_MODEL\", \"nvidia/llama-3.1-nemotron-nano-8b-v1\")\nNVIDIA_API_URL = \"https://integrate.api.nvidia.com/v1/chat/completions\"\nSPACE_URL = os.getenv(\"PARADE_SPACE_URL\", \"https://huggingface.co/spaces/build-small-hackathon/cloud-parade-cabinet\")\nMAX_NEW_TOKENS = int(os.getenv(\"PARADE_MAX_NEW_TOKENS\", \"180\"))\n\nPROVIDERS = [\n    \"Hugging Face: Qwen 7B\",\n    \"OpenBMB: MiniCPM4 8B\",\n    \"NVIDIA: Nemotron Nano 8B\",\n    \"Practice writer\",\n]\n\nPUBLIC_PROVIDERS = {\n    \"Hugging Face: Qwen 7B\": \"Cloud parade voice\",\n    \"OpenBMB: MiniCPM4 8B\": \"Mini cabinet voice\",\n    \"NVIDIA: Nemotron Nano 8B\": \"Brass cabinet voice\",\n    \"Practice writer\": \"Cabinet practice voice\",\n}\n\nPROVIDER_CHOICES = [\n    (\"Cloud parade voice\", \"Hugging Face: Qwen 7B\"),\n    (\"Mini cabinet voice\", \"OpenBMB: MiniCPM4 8B\"),\n    (\"Brass cabinet voice\", \"NVIDIA: Nemotron Nano 8B\"),\n    (\"Cabinet practice voice\", \"Practice writer\"),\n]\n\nWEATHERS = [\n    \"paper rain that apologizes\",\n    \"sideways sunshine\",\n    \"fog shaped like old applause\",\n    \"tiny hailstones with opinions\",\n    \"moonlight stuck in traffic\",\n]\n\nMARSHALS = [\n    \"a nervous umbrella\",\n    \"a brass thimble\",\n    \"the mayor's missing shoe\",\n    \"a lantern with stage fright\",\n    \"a soup spoon in formal gloves\",\n]\n\nTOWNS = [\n    \"Turnip Junction\",\n    \"Little Static\",\n    \"Button-on-the-Hill\",\n    \"North Crumb\",\n    \"The Fourth Drawer\",\n]\n\nTROUBLES = [\n    \"the parade route forgot its own corners\",\n    \"all floats must travel backward for one block\",\n    \"the crowd only cheers in whispers\",\n    \"confetti is legally considered weather\",\n    \"the final float refuses to be last\",\n]\n\nCOLORS = {\n    \"ticket yellow\": \"#fff2bd\",\n    \"mint night\": \"#0f5b52\",\n    \"tomato band\": \"#c9513f\",\n    \"ink blue\": \"#223f6c\",\n}\n\nLOG_PATH = Path(__file__).resolve().parent / \"PARADE_LOG.md\"\nMEDIA_DIR = Path(__file__).resolve().parent / \"media\"\n\n\n@dataclass(frozen=True)\nclass ParadeRequest:\n    town: str\n    weather: str\n    marshal: str\n    trouble: str\n    color: str = \"ticket yellow\"\n    energy: int = 4\n    provider: str = PROVIDERS[0]\n    cloud_mode: bool = True\n\n\ndef clean_text(value: str) -> str:\n    return re.sub(r\"\\s+\", \" \", str(value or \"\")).strip()\n\n\ndef seed_for(req: ParadeRequest) -> int:\n    raw = json.dumps(asdict(req), sort_keys=True)\n    return int(hashlib.sha256(raw.encode(\"utf-8\")).hexdigest()[:12], 16)\n\n\ndef prompt_for(req: ParadeRequest) -> str:\n    return f\"\"\"\nWrite a miniature parade plan for a strange toy called Cloud Parade Cabinet.\nKeep it under 120 words.\nUse crisp, playful, human-readable copy.\nThe parade must have:\n- a parade title\n- three floats with short names and one visible action each\n- one crowd chant in quotes\n- a finale that changes the route\n\nTown: {req.town}\nWeather: {req.weather}\nGrand marshal: {req.marshal}\nTrouble: {req.trouble}\nEnergy: {req.energy}/5\n\"\"\".strip()\n\n\ndef public_provider(provider: str) -> str:\n    return PUBLIC_PROVIDERS.get(provider, provider)\n\n\ndef public_mode(trace: dict[str, object]) -> str:\n    return \"Live\" if trace.get(\"mode\") == \"cloud\" else \"Practice\"\n\n\ndef fallback_parade(req: ParadeRequest) -> str:\n    rng = random.Random(seed_for(req))\n    verbs = [\"wobbles\", \"bows\", \"zigzags\", \"sparkles\", \"argues politely\", \"turns left twice\"]\n    float_names = [\n        f\"{req.marshal.title()} Baton Cart\",\n        f\"{req.weather.title()} Wagon\",\n        f\"{rng.choice(['Pocket Drum', 'Lantern Choir', 'Button Brigade', 'Crumb Engine'])}\",\n    ]\n    chant = rng.choice(\n        [\n            \"Left foot, cloud foot, cabinet door!\",\n            \"Tiny street, louder feet!\",\n            \"Bring the corner back!\",\n            \"No float left behind!\",\n        ]\n    )\n    finale = rng.choice(\n        [\n            \"The route folds into a postcard and opens one block east.\",\n            \"The last float becomes the first and the crowd follows the correction.\",\n            \"A chalk arrow sneezes, sending everyone through the narrowest alley.\",\n        ]\n    )\n    return (\n        f\"{req.town} Cloud Parade. \"\n        f\"Float 1: {float_names[0]} {rng.choice(verbs)} under {req.weather}. \"\n        f\"Float 2: {float_names[1]} {rng.choice(verbs)} while {req.trouble}. \"\n        f\"Float 3: {float_names[2]} {rng.choice(verbs)} beside the curb. \"\n        f'The crowd chants \"{chant}\" '\n        f\"Finale: {finale}\"\n    )\n\n\ndef call_cloud_parade(req: ParadeRequest) -> tuple[str, dict[str, object]]:\n    trace = {\"mode\": \"fallback\", \"provider\": req.provider, \"model\": None, \"error\": None}\n    if not req.cloud_mode or req.provider == \"Practice writer\":\n        trace[\"error\"] = \"live writer disabled\"\n        return fallback_parade(req), trace\n    if req.provider == \"NVIDIA: Nemotron Nano 8B\":\n        return call_nvidia_parade(req, trace)\n    return call_hf_parade(req, trace)\n\n\ndef call_hf_parade(req: ParadeRequest, trace: dict[str, object]) -> tuple[str, dict[str, object]]:\n    token = os.getenv(\"HF_API_KEY\") or os.getenv(\"HF_TOKEN\")\n    model = OPENBMB_MODEL if req.provider == \"OpenBMB: MiniCPM4 8B\" else CLOUD_MODEL\n    if not token:\n        trace[\"error\"] = \"HF_API_KEY or HF_TOKEN is not set\"\n        return fallback_parade(req), trace\n    if InferenceClient is None:\n        trace[\"error\"] = \"huggingface_hub InferenceClient is unavailable\"\n        return fallback_parade(req), trace\n    try:\n        client = InferenceClient(api_key=token)\n        response = client.chat_completion(\n            model=model,\n            messages=[\n                {\"role\": \"system\", \"content\": \"You write tiny, strange, delightful toy text. Avoid explaining yourself.\"},\n                {\"role\": \"user\", \"content\": prompt_for(req)},\n            ],\n            max_tokens=MAX_NEW_TOKENS,\n            temperature=0.92,\n            top_p=0.9,\n        )\n        text = clean_text(response.choices[0].message.content)\n        if not text:\n            raise RuntimeError(\"empty cloud response\")\n        trace.update({\"mode\": \"cloud\", \"model\": model, \"error\": None})\n        return text, trace\n    except Exception as exc:\n        trace[\"error\"] = str(exc)[:220]\n        return fallback_parade(req), trace\n\n\ndef call_nvidia_parade(req: ParadeRequest, trace: dict[str, object]) -> tuple[str, dict[str, object]]:\n    token = os.getenv(\"NVIDIA_API_KEY\")\n    if not token:\n        trace[\"error\"] = \"NVIDIA_API_KEY is not set\"\n        return fallback_parade(req), trace\n    payload = {\n        \"model\": NVIDIA_MODEL,\n        \"messages\": [\n            {\"role\": \"system\", \"content\": \"You write tiny, strange, delightful toy text. Avoid explaining yourself.\"},\n            {\"role\": \"user\", \"content\": prompt_for(req)},\n        ],\n        \"max_tokens\": MAX_NEW_TOKENS,\n        \"temperature\": 0.85,\n        \"top_p\": 0.9,\n        \"stream\": False,\n    }\n    request = urllib.request.Request(\n        NVIDIA_API_URL,\n        data=json.dumps(payload).encode(\"utf-8\"),\n        headers={\n            \"Authorization\": f\"Bearer {token}\",\n            \"Content-Type\": \"application/json\",\n            \"Accept\": \"application/json\",\n        },\n        method=\"POST\",\n    )\n    try:\n        with urllib.request.urlopen(request, timeout=45) as response:\n            data = json.loads(response.read().decode(\"utf-8\"))\n        text = clean_text(data[\"choices\"][0][\"message\"][\"content\"])\n        if not text:\n            raise RuntimeError(\"empty NVIDIA response\")\n        trace.update({\"mode\": \"cloud\", \"model\": NVIDIA_MODEL, \"error\": None})\n        return text, trace\n    except (urllib.error.HTTPError, urllib.error.URLError, KeyError, IndexError, json.JSONDecodeError, TimeoutError, RuntimeError) as exc:\n        trace[\"error\"] = str(exc)[:220]\n        return fallback_parade(req), trace\n\n\ndef split_floats(plan: str) -> list[str]:\n    normalized = re.sub(r\"\\*\\*\", \"\", plan)\n    parts = re.split(r\"(?i)(?:\\bFloat\\s*)?\\b[123]\\s*[\\).:-]\\s*\", normalized)\n    floats = []\n    for part in parts[1:]:\n        cleaned = clean_text(part)\n        cleaned = re.split(r\"(?i)\\b(?:The crowd chants|Crowd chant|Finale)\\b\", cleaned)[0].strip(\" :-\")\n        if cleaned:\n            floats.append(cleaned)\n        if len(floats) == 3:\n            break\n    if not floats:\n        parts = re.split(r\"(?i)\\bFloat\\s*\\d\\s*:\\s*\", normalized)\n        floats = [clean_text(part) for part in parts[1:4]]\n    while len(floats) < 3:\n        floats.append([\"Pocket Drum salutes.\", \"Lantern Choir glows.\", \"Crumb Engine turns left.\"][len(floats)])\n    return [item[:150] for item in floats]\n\n\ndef extract_title(plan: str, req: ParadeRequest) -> str:\n    first = clean_text(re.sub(r\"[*#`]\", \"\", plan)).split(\".\")[0].strip('\" ')\n    first = re.sub(r\"(?i)^title\\s*:\\s*\", \"\", first).strip()\n    if 8 <= len(first) <= 72:\n        return first\n    return f\"{req.town} Cloud Parade\"\n\n\ndef extract_chant(plan: str) -> str:\n    match = re.search(r'\"([^\"]{4,90})\"', plan)\n    if match:\n        return match.group(1)\n    match = re.search(r\"(?i)chant\\s*[:\\-]\\s*([^\\.]{4,90})\", plan)\n    if match:\n        return clean_text(match.group(1)).strip('\" ')\n    return \"Tiny street, louder feet!\"\n\n\ndef route_points(req: ParadeRequest) -> list[tuple[int, int]]:\n    rng = random.Random(seed_for(req))\n    points = [(90, 310)]\n    x, y = points[0]\n    for _ in range(4):\n        x += rng.randint(120, 190)\n        y += rng.choice([-70, -35, 35, 70])\n        y = max(115, min(395, y))\n        points.append((x, y))\n    return points\n\n\ndef parade_html(req: ParadeRequest, plan: str, trace: dict[str, object]) -> str:\n    color = COLORS.get(req.color, COLORS[\"ticket yellow\"])\n    dark = \"#12221f\" if req.color != \"ticket yellow\" else \"#fff8df\"\n    ink = \"#fff8df\" if req.color != \"ticket yellow\" else \"#1d2421\"\n    points = route_points(req)\n    route = \" \".join(f\"{x},{y}\" for x, y in points)\n    floats = split_floats(plan)\n    title = extract_title(plan, req)\n    chant = extract_chant(plan)\n    float_cards = \"\".join(\n        f\"\"\"\n        <article>\n          <span>Float {index}</span>\n          <strong>{escape(item.split('.')[0])}</strong>\n          <p>{escape(item)}</p>\n        </article>\n        \"\"\"\n        for index, item in enumerate(floats, 1)\n    )\n    steps = \"\".join(\n        f'<i style=\"left:{x}px; top:{y}px; animation-delay:{index * 220}ms;\"></i>'\n        for index, (x, y) in enumerate(points)\n    )\n    provider = str(trace.get(\"provider\") or req.provider)\n    return f\"\"\"\n<section class=\"parade-cabinet\" style=\"--cabinet:{color}; --cabinet-ink:{ink}; --cabinet-bg:{dark};\">\n  <header>\n    <span>{escape(public_mode(trace))} parade / {escape(public_provider(provider))}</span>\n    <h2>{escape(title)}</h2>\n    <p>{escape(req.town)} / {escape(req.weather)} / led by {escape(req.marshal)}</p>\n  </header>\n  <div class=\"route-stage\" aria-label=\"Animated parade route\">\n    <svg viewBox=\"0 0 900 470\" role=\"img\">\n      <rect x=\"34\" y=\"42\" width=\"832\" height=\"382\" rx=\"14\"></rect>\n      <polyline points=\"{route}\"></polyline>\n      <text x=\"70\" y=\"82\">Route Cabinet</text>\n      <text x=\"620\" y=\"405\">Final corner</text>\n    </svg>\n    <b class=\"sun\"></b>\n    <div class=\"route-steps\">{steps}</div>\n    <div class=\"float one\">1</div>\n    <div class=\"float two\">2</div>\n    <div class=\"float three\">3</div>\n  </div>\n  <div class=\"float-grid\">{float_cards}</div>\n  <footer>\n    <strong>Crowd chant</strong>\n    <p>\"{escape(chant)}\"</p>\n  </footer>\n</section>\n\"\"\"\n\n\ndef poster_html(req: ParadeRequest, plan: str, trace: dict[str, object]) -> str:\n    title = extract_title(plan, req)\n    chant = extract_chant(plan)\n    return f\"\"\"\n<section class=\"poster-card\">\n  <span>Share Poster</span>\n  <h2>{escape(title)}</h2>\n  <p>{escape(req.weather)} in {escape(req.town)}. Grand marshal: {escape(req.marshal)}.</p>\n  <strong>\"{escape(chant)}\"</strong>\n  <em>{escape(public_mode(trace))} parade / {escape(public_provider(req.provider))}</em>\n</section>\n\"\"\"\n\n\ndef caption_for(req: ParadeRequest, plan: str) -> str:\n    title = extract_title(plan, req)\n    chant = extract_chant(plan)\n    return (\n        f\"{APP_TITLE}: {title}. {req.marshal} leads {req.weather} through {req.town}. \"\n        f\"Chant: \\\"{chant}\\\" #BuildSmallHackathon #Gradio\"\n    )[:280]\n\n\ndef render_parade_audio(req: ParadeRequest, plan: str) -> str:\n    MEDIA_DIR.mkdir(exist_ok=True)\n    title = extract_title(plan, req)\n    chant = extract_chant(plan)\n    digest = hashlib.sha256(f\"{seed_for(req)}::{title}::{chant}\".encode(\"utf-8\")).hexdigest()[:12]\n    path = MEDIA_DIR / f\"parade_{digest}.wav\"\n    if path.exists():\n        return str(path)\n\n    sample_rate = 22050\n    duration = 5.4\n    total = int(sample_rate * duration)\n    rng = random.Random(int(digest, 16))\n    base_notes = [261.63, 293.66, 329.63, 392.00, 440.00, 523.25]\n    melody = [rng.choice(base_notes) * rng.choice([0.75, 1.0, 1.25]) for _ in range(12)]\n    energy = max(1, min(5, int(req.energy)))\n    beat_gap = max(0.18, 0.42 - energy * 0.035)\n\n    frames = bytearray()\n    for index in range(total):\n        t = index / sample_rate\n        step_phase = (t % beat_gap) / beat_gap\n        drum = math.exp(-step_phase * 18.0) * 0.42\n        drum *= math.sin(2 * math.pi * (78 + energy * 9) * t)\n\n        note_index = min(len(melody) - 1, int(t / duration * len(melody)))\n        note = melody[note_index]\n        note_phase = (t % (duration / len(melody))) / (duration / len(melody))\n        bell_env = max(0.0, 1.0 - note_phase) ** 2.4\n        bell = bell_env * 0.26 * (\n            math.sin(2 * math.pi * note * t) + 0.42 * math.sin(2 * math.pi * note * 2.01 * t)\n        )\n\n        crowd = 0.035 * math.sin(2 * math.pi * (rng.choice([5.2, 6.1, 7.3])) * t)\n        sample = max(-0.95, min(0.95, drum + bell + crowd))\n        value = int(sample * 32767)\n        frames.extend(value.to_bytes(2, \"little\", signed=True))\n\n    with wave.open(str(path), \"wb\") as wav:\n        wav.setnchannels(1)\n        wav.setsampwidth(2)\n        wav.setframerate(sample_rate)\n        wav.writeframes(frames)\n    return str(path)\n\n\ndef log_entry(req: ParadeRequest, plan: str, trace: dict[str, object]) -> str:\n    return \"\\n\".join(\n        [\n            \"## Parade Run\",\n            f\"- town: {req.town}\",\n            f\"- weather: {req.weather}\",\n            f\"- marshal: {req.marshal}\",\n            f\"- trouble: {req.trouble}\",\n            f\"- run: {public_mode(trace)}\",\n            f\"- writer: {public_provider(str(trace.get('provider') or req.provider))}\",\n            f\"- title: {extract_title(plan, req)}\",\n        ]\n    )\n\n\ndef build_parade(town, weather, marshal, trouble, color, energy, provider, cloud_mode, history):\n    req = ParadeRequest(\n        town=town,\n        weather=weather,\n        marshal=marshal,\n        trouble=trouble,\n        color=color,\n        energy=int(energy),\n        provider=provider,\n        cloud_mode=bool(cloud_mode),\n    )\n    plan, trace = call_cloud_parade(req)\n    history = list(history or [])\n    history.append({\"request\": asdict(req), \"trace\": trace, \"title\": extract_title(plan, req), \"plan\": plan})\n    history = history[-6:]\n    return (\n        parade_html(req, plan, trace),\n        poster_html(req, plan, trace),\n        render_parade_audio(req, plan),\n        caption_for(req, plan),\n        \"\\n\\n\".join(log_entry(req, item[\"plan\"], item[\"trace\"]) for item in reversed(history)),\n        history,\n        trace,\n    )\n\n\ndef random_setup():\n    rng = random.SystemRandom()\n    return (\n        rng.choice(TOWNS),\n        rng.choice(WEATHERS),\n        rng.choice(MARSHALS),\n        rng.choice(TROUBLES),\n        rng.choice(list(COLORS)),\n        rng.randint(2, 5),\n    )\n\n\nCSS = \"\"\"\n:root {\n  --ink: #1d2421;\n  --paper: #fff8df;\n  --mint: #0f5b52;\n  --clay: #c9513f;\n  --gold: #f1b84b;\n  --blue: #223f6c;\n}\n\n.gradio-container {\n  background:\n    linear-gradient(135deg, rgba(15, 91, 82, 0.14), transparent 36%),\n    linear-gradient(315deg, rgba(201, 81, 63, 0.12), transparent 30%),\n    var(--paper);\n  color: var(--ink) !important;\n}\n\n#cloud-parade-shell {\n  max-width: 1180px;\n  margin: 0 auto;\n}\n\n#cloud-parade-title h1 {\n  margin-bottom: 0;\n  color: var(--ink);\n  font-size: clamp(2rem, 4vw, 4.2rem);\n  letter-spacing: 0;\n}\n\n#cloud-parade-title p {\n  max-width: 760px;\n  color: #31524c;\n  font-weight: 800;\n}\n\n.control-card {\n  position: sticky;\n  top: 12px;\n  padding: 14px;\n  border-radius: 8px;\n  border: 2px solid rgba(29, 36, 33, 0.16);\n  background: #242528;\n  color: #fff8df;\n}\n\n.control-card label,\n.control-card .label-wrap,\n.control-card button,\n.control-card button span {\n  color: #fff8df !important;\n}\n\n.control-card textarea,\n.control-card input,\n.control-card [role=\"textbox\"] {\n  color: #fff8df !important;\n}\n\n.parade-cabinet {\n  border: 2px solid var(--ink);\n  border-radius: 8px;\n  overflow: hidden;\n  background: var(--cabinet-bg);\n  color: var(--cabinet-ink);\n  box-shadow: 0 22px 48px rgba(29, 36, 33, 0.16);\n}\n\n.parade-cabinet header {\n  padding: 22px;\n  background: var(--cabinet);\n  border-bottom: 2px solid var(--ink);\n}\n\n.parade-cabinet header span,\n.poster-card span {\n  display: block;\n  font-size: 0.72rem;\n  font-weight: 900;\n  letter-spacing: 0.14em;\n  text-transform: uppercase;\n}\n\n.parade-cabinet h2 {\n  margin: 5px 0;\n  color: var(--cabinet-ink) !important;\n  font-size: clamp(1.6rem, 3vw, 3rem);\n  letter-spacing: 0;\n}\n\n.route-stage {\n  position: relative;\n  min-height: 440px;\n  background:\n    radial-gradient(circle at 82% 18%, rgba(241, 184, 75, 0.38), transparent 18%),\n    linear-gradient(180deg, rgba(255,255,255,0.08), transparent);\n}\n\n.route-stage svg {\n  width: 100%;\n  height: 440px;\n  display: block;\n}\n\n.route-stage rect {\n  fill: rgba(255, 248, 223, 0.15);\n  stroke: rgba(255, 248, 223, 0.45);\n  stroke-width: 3;\n}\n\n.route-stage polyline {\n  fill: none;\n  stroke: var(--gold);\n  stroke-width: 13;\n  stroke-linecap: round;\n  stroke-linejoin: round;\n  stroke-dasharray: 36 16;\n  animation: route-march 2.8s linear infinite;\n}\n\n.route-stage text {\n  fill: var(--cabinet-ink);\n  font-weight: 900;\n  letter-spacing: 0;\n}\n\n.route-steps i {\n  position: absolute;\n  width: 18px;\n  height: 18px;\n  border-radius: 50%;\n  background: var(--clay);\n  border: 2px solid var(--cabinet-ink);\n  animation: step-pop 1.4s ease-in-out infinite;\n}\n\n.float {\n  position: absolute;\n  display: grid;\n  place-items: center;\n  width: 58px;\n  height: 48px;\n  border: 3px solid var(--ink);\n  border-radius: 8px;\n  background: var(--paper);\n  color: var(--ink);\n  font-weight: 900;\n  box-shadow: 0 8px 0 rgba(0,0,0,0.16);\n}\n\n.float.one { left: 14%; top: 58%; animation: float-one 6s ease-in-out infinite; }\n.float.two { left: 43%; top: 30%; animation: float-two 6.6s ease-in-out infinite; }\n.float.three { left: 71%; top: 58%; animation: float-three 7.2s ease-in-out infinite; }\n\n.float-grid {\n  display: grid;\n  grid-template-columns: repeat(3, minmax(0, 1fr));\n  gap: 10px;\n  padding: 12px;\n}\n\n.float-grid article {\n  min-height: 152px;\n  padding: 13px;\n  border: 2px solid rgba(255, 248, 223, 0.24);\n  border-radius: 8px;\n  background: rgba(255, 248, 223, 0.09);\n  color: var(--cabinet-ink);\n}\n\n.float-grid span {\n  color: var(--gold);\n  font-size: 0.7rem;\n  font-weight: 900;\n  text-transform: uppercase;\n}\n\n.float-grid strong,\n.float-grid p {\n  display: block;\n  color: var(--cabinet-ink) !important;\n  overflow-wrap: anywhere;\n}\n\n.float-grid strong {\n  margin: 5px 0;\n  font-size: 1rem;\n}\n\n.parade-cabinet footer {\n  padding: 18px 22px;\n  border-top: 2px solid rgba(255, 248, 223, 0.24);\n  background: rgba(0,0,0,0.18);\n}\n\n.parade-cabinet footer strong,\n.parade-cabinet footer p {\n  display: block;\n  margin: 0;\n  color: var(--cabinet-ink) !important;\n}\n\n.parade-cabinet footer p {\n  margin-top: 6px;\n  font-size: 1.25rem;\n  font-weight: 900;\n}\n\n.poster-card {\n  padding: 26px;\n  border: 2px solid var(--ink);\n  border-radius: 8px;\n  background:\n    linear-gradient(135deg, rgba(15, 91, 82, 0.12), transparent 40%),\n    var(--paper);\n  color: var(--ink);\n}\n\n.poster-card h2 {\n  margin: 5px 0 8px;\n  color: var(--ink) !important;\n  font-size: clamp(1.5rem, 3vw, 2.6rem);\n}\n\n.poster-card p,\n.poster-card strong,\n.poster-card em {\n  display: block;\n  color: #31524c !important;\n  font-weight: 900;\n}\n\n.poster-card strong {\n  margin: 14px 0;\n  color: var(--clay) !important;\n  font-size: 1.2rem;\n}\n\n.log-box textarea,\n.caption-box textarea {\n  font-family: ui-monospace, Consolas, monospace !important;\n}\n\n@keyframes route-march {\n  to { stroke-dashoffset: -52; }\n}\n\n@keyframes step-pop {\n  0%, 100% { transform: scale(0.85); }\n  50% { transform: scale(1.22); }\n}\n\n@keyframes float-one {\n  50% { transform: translate(210px, -140px) rotate(-3deg); }\n}\n\n@keyframes float-two {\n  50% { transform: translate(180px, 100px) rotate(3deg); }\n}\n\n@keyframes float-three {\n  50% { transform: translate(80px, -80px) rotate(-2deg); }\n}\n\n@media (max-width: 820px) {\n  .float-grid {\n    grid-template-columns: 1fr;\n  }\n  .control-card {\n    position: static;\n  }\n}\n\n@media (prefers-reduced-motion: reduce) {\n  .route-stage polyline,\n  .route-steps i,\n  .float {\n    animation: none;\n  }\n}\n\"\"\"\n\n\ndef initial_state():\n    req = ParadeRequest(\n        town=TOWNS[1],\n        weather=WEATHERS[0],\n        marshal=MARSHALS[0],\n        trouble=TROUBLES[0],\n        color=\"mint night\",\n        energy=4,\n        provider=\"Practice writer\",\n        cloud_mode=False,\n    )\n    plan = fallback_parade(req)\n    trace = {\"mode\": \"fallback\", \"provider\": req.provider, \"model\": None, \"error\": \"ready state\"}\n    return (\n        parade_html(req, plan, trace),\n        poster_html(req, plan, trace),\n        render_parade_audio(req, plan),\n        caption_for(req, plan),\n        log_entry(req, plan, trace),\n    )\n\n\ninitial_parade, initial_poster, initial_audio, initial_caption, initial_log = initial_state()\n\n\nwith gr.Blocks(css=CSS, theme=gr.themes.Base(), title=APP_TITLE) as demo:\n    with gr.Column(elem_id=\"cloud-parade-shell\"):\n        gr.Markdown(\n            \"\"\"\n# Cloud Parade Cabinet\nBuild a tiny impossible parade. Pick the ingredients, open the cabinet, and watch the route come alive.\n\"\"\",\n            elem_id=\"cloud-parade-title\",\n        )\n        with gr.Row(equal_height=False):\n            with gr.Column(scale=1, elem_classes=\"control-card\"):\n                cloud_mode = gr.Checkbox(value=True, label=\"Let the cabinet write live\")\n                town = gr.Dropdown(TOWNS, value=TOWNS[1], label=\"Town\")\n                weather = gr.Dropdown(WEATHERS, value=WEATHERS[0], label=\"Parade weather\")\n                marshal = gr.Dropdown(MARSHALS, value=MARSHALS[0], label=\"Grand marshal\")\n                trouble = gr.Dropdown(TROUBLES, value=TROUBLES[0], label=\"Street trouble\")\n                color = gr.Radio(list(COLORS), value=\"mint night\", label=\"Cabinet color\")\n                energy = gr.Slider(1, 5, value=4, step=1, label=\"Parade energy\")\n                provider = gr.Radio(PROVIDER_CHOICES, value=PROVIDERS[0], label=\"Parade voice\")\n                with gr.Row():\n                    randomize = gr.Button(\"Fresh setup\")\n                    run = gr.Button(\"Open cabinet\", variant=\"primary\")\n            with gr.Column(scale=2):\n                parade = gr.HTML(value=initial_parade)\n                with gr.Row():\n                    poster = gr.HTML(value=initial_poster)\n                    with gr.Column():\n                        audio = gr.Audio(value=initial_audio, label=\"Parade sound\", type=\"filepath\", elem_classes=\"sound-box\")\n                        caption = gr.Textbox(value=initial_caption, label=\"Post caption\", lines=5, show_copy_button=True, elem_cl",766      "app_signals": "ParadeRequest clean_text value seed_for req prompt_for public_provider provider public_mode trace fallback_parade call_cloud_parade call_hf_parade call_nvidia_parade split_floats plan extract_title extract_chant route_points parade_html poster_html caption_for render_parade_audio log_entry build_parade town weather marshal trouble color energy cloud_mode history random_setup initial_state Cloud Parade Cabinet os.getenv https://integrate.api.nvidia.com/v1/chat/completions int dataclass frozen PARADE_MODEL Qwen/Qwen2.5-7B-Instruct OPENBMB_MODEL openbmb/MiniCPM4-8B NVIDIA_MODEL nvidia/llama-3.1-nemotron-nano-8b-v1 PARADE_SPACE_URL https://huggingface.co/spaces/build-small-hackathon/cloud-parade-cabinet Hugging Face: Qwen 7B OpenBMB: MiniCPM4 8B NVIDIA: Nemotron Nano 8B Practice writer Cloud parade voice Mini cabinet voice Brass cabinet voice Cabinet practice voice paper rain that apologizes sideways sunshine fog shaped like old applause tiny hailstones with opinions moonlight stuck in traffic a nervous umbrella a brass thimble the mayor's missing shoe a lantern with stage fright a soup spoon in formal gloves Turnip Junction Little Static Button-on-the-Hill North Crumb The Fourth Drawer the parade route forgot its own corners all floats must travel backward for one block the crowd only cheers in whispers confetti is legally considered weather the final float refuses to be last ticket yellow mint night tomato band ink blue #fff2bd #0f5b52 #c9513f #223f6c PARADE_LOG.md media strip json.dumps sort_keys PUBLIC_PROVIDERS.get random.Random rng.choice urllib.request.Request data headers method re.sub re.split re.search Tiny street, louder feet! range COLORS.get join str MEDIA_DIR.mkdir exist_ok path.exists max bytearray list history.append random.SystemRandom gr.Blocks css theme title randomize.click outputs queue run.click inputs demo.queue max_size default_concurrency_limit __main__ demo.launch PARADE_MAX_NEW_TOKENS 180 resolve asdict Live Practice wobbles bows zigzags sparkles argues politely turns left twice Cloud Parade. Float 1: under . Float 2: while . Float 3: beside the curb. The crowd chants \" \" Finale: mode model error fallback live writer disabled HF_API_KEY or HF_TOKEN is not set huggingface_hub InferenceClient is unavailable InferenceClient api_key client.chat_completion messages max_tokens temperature top_p trace.update NVIDIA_API_KEY NVIDIA_API_KEY is not set stream \\*\\* (?i)(?:\\bFloat\\s*)?\\b[123]\\s*[\\).:-]\\s* len floats.append \" Cloud Parade \"([^\"]{4,90})\" match.group (?i)chant\\s*[:\\-]\\s*([^\\.]{4,90}) rng.randint points.append #12221f #fff8df #1d2421 <section class=\"parade-cabinet\" style=\"--cabinet: ; --cabinet-ink: ; --cabinet-bg: ;\"> parade / / / led by <polyline points=\" \"> Route Cabinet Final corner 1 2 3 Crowd chant \" Share Poster in . Grand marshal: . \" hexdigest min math.sin frames.extend wave.open wav.setnchannels wav.setsampwidth wav.setframerate wav.writeframes ready state gr.Column elem_id gr.Markdown gr.State trace.get cloud Baton Cart Wagon Left foot, cloud foot, cabinet door! Bring the corner back! No float left behind! The route folds into a postcard and opens one block east. The last float becomes the first and the crowd follows the correction. A chalk arrow sneezes, sending everyone through the narrowest alley. HF_API_KEY HF_TOKEN RuntimeError encode POST urllib.request.urlopen timeout json.loads :- (?i)\\bFloat\\s*\\d\\s*:\\s* escape : . leads through . Chant: \" \" #BuildSmallHackathon #Gradio parade_ .wav math.exp value.to_bytes signed wb ## Parade Run bool request gr.themes.Base # Cloud Parade Cabinet Build a tiny impossible parade. Pick the ingredients, open the cabinet, and watch the route come alive. gr.Row equal_height Path \\s+ Write a miniature parade plan for a strange toy called Cloud Parade Cabinet. Keep it under 120 words. Use crisp, playful, human-readable copy. The parade must have: - a parade title - three floats with short names and one visible action each - one crowd chant in quotes - a finale that changes the route Town: Weather: Grand marshal: Trouble: Energy: /5 req.marshal.title req.weather.title empty cloud response role content system You write tiny, strange, delightful toy text. Avoid explaining yourself. user utf-8 Authorization Content-Type Accept application/json decode empty NVIDIA response split (?i)^title\\s*:\\s* , Float enumerate <i style=\"left: px; top: px; animation-delay: ms;\"> hashlib.sha256 little - town: - weather: - marshal: - trouble: - run: - writer: - title: cloud-parade-shell cloud-parade-title scale elem_classes gr.Checkbox label gr.Dropdown gr.Radio gr.Slider step gr.HTML gr.Textbox lines show_copy_button gr.JSON visible Bearer message Pocket Drum salutes. Lantern Choir glows. Crumb Engine turns left. reversed gr.Button variant raw.encode Pocket Drum Lantern Choir Button Brigade Crumb Engine response.read (?i)\\b(?:The crowd chants|Crowd chant|Finale)\\b control-card Let the cabinet write live Town Parade weather Grand marshal Street trouble Cabinet color Parade energy Parade voice Fresh setup Open cabinet gr.Audio type Keepsake log log-box Run details choices item.split primary [*#`] :: Parade sound filepath sound-box Post caption caption-box",767      "readme_len": 2346,768      "app_source_len": 24000,769      "app_signals_len": 5197770    },771    {772      "id": "build-small-hackathon/code-shrink-token-decimator",773      "title": "Code Shrink Token Decimator",774      "summary": "Ultra-lightweight lexical token compressor that reduces LLM ",775      "tags": [776        "gradio",777        "region:us"778      ],779      "models": [],780      "datasets": [],781      "sdk": "gradio",782      "license": "apache-2.0",783      "likes": 0,784      "url": "https://huggingface.co/spaces/build-small-hackathon/code-shrink-token-decimator",785      "app_file": "app.py",786      "readme_raw": "---\ntitle: Code Shrink Token Decimator\nemoji: 👀\ncolorFrom: gray\ncolorTo: pink\nsdk: gradio\nsdk_version: 6.16.0\npython_version: '3.13'\napp_file: app.py\npinned: false\nlicense: apache-2.0\nshort_description: 'Ultra-lightweight lexical token compressor that reduces LLM '\nthumbnail: >-\n  https://cdn-uploads.huggingface.co/production/uploads/6989c34475b229ddd8f18be3/ZTXpy1-KYjq7lfqHSb0ic.png\n---\n\n# ⚡ Code-Shrink: Token-Decimator v1.0\n\n> **An Ultra-Lightweight Computational Utility Built to Eliminate LLM Context Bloat Natively on the Edge Container.**\n> *Submitted for the Hugging Face Build Small Hackathon (Track 2: Performance & Efficiency Optimization).*\n\n---\n\n## 📽️ Project Demonstration & Walkthrough\n\nCheck out the full workflow, speed metrics, and feature breakdown in action here:\n🔗 **[Watch the Live Demo on TikTok](https://www.tiktok.com/@salarai123/video/7648566501598940436)**\n\n---\n\n## 🔍 The Problem & The Solution\n\n### The Bottleneck: LLM Context Inflation\nModern production applications relying on Large Language Model (LLM) APIs suffer from massive financial overhead. Upstream providers charge by the token—meaning heavy indentation loops, generic code comments, raw text formatting, and large structural blocks exponentially inflate infrastructure bills.\n\n### The Engine: Code-Shrink\n**Code-Shrink v1.0** passes raw context inputs through an edge-computed Abstract Syntax Tree (AST) framework. Instead of hosting gigabytes of neural network weights that lag and crash free hosting tiers, this application runs entirely on zero-cost, lightweight lexical optimization models. It reduces prompt token sizes by **up to 66% in under 10 milliseconds**.\n\n---\n\n## ⚡ Technical Core Features\n\n* **Abstract Syntax Tree (AST) De-bloating:** Fully parses Python/R/SQL environments natively to structurally strip docstrings, developer comments, and empty lines while maintaining 100% semantic code integrity.\n* **Lexical JSON Minification:** Collapses raw object dictionaries, spacing grids, and redundant string arrays into tight, machine-readable micro-streams.\n* **On-Edge Real-Time Diagnostic Metrics:** Computes compression percentages and displays an estimated API cost savings panel instantly on execution.\n* **Zero Infrastructure Overhead:** Operates 100% standalone with zero dependency on third-party backend servers, making it completely immune to public inference timeouts.\n\n---\n\n## 🛠️ Tech Stack & System Compatibility\n\n- **Interface Framework:** Gradio (v6.0 Transition-Optimized Layer)\n- **Computational Core:** Native Python AST & Lexical Pattern RegEx Engine\n- **Data Manifestation:** Memory Buffer Stream Handlers (PIL/JSON Core)\n- **Hardware Benchmarking:** Heavily optimized for restricted legacy processors (runs smooth down to Intel Core i3 4th Gen / 8GB RAM specs).\n\n---\n\n## 🎛️ Parameters Matrix\n\n1. **Raw Context Input:** Inject bloated code strings or massive JSON arrays into the terminal panel.\n2. **Lexical Processing Mode:** Set structural parser rules (`Python/R/SQL Code Matrix` or `Structured JSON / Raw Text Array`).\n3. Click **⚡ DECIMATE CONTEXT TOKENS** to immediately wipe empty tokens and render the optimized micro-stream for your prompt.\n\n---\n\n## 📦 Local Workspace Setup\n\nTo clone and execute this performance node locally:\n\n```bash\ngit clone [https://huggingface.co/spaces/build-small-hackathon/code-shrink-token-decimator](https://huggingface.co/spaces/build-small-hackathon/code-shrink-token-decimator)\ncd code-shrink-token-decimator\npip install -r requirements.txt\npython app.py",787      "readme_body": "# ⚡ Code-Shrink: Token-Decimator v1.0\n\n> **An Ultra-Lightweight Computational Utility Built to Eliminate LLM Context Bloat Natively on the Edge Container.**\n> *Submitted for the Hugging Face Build Small Hackathon (Track 2: Performance & Efficiency Optimization).*\n\n---\n\n## 📽️ Project Demonstration & Walkthrough\n\nCheck out the full workflow, speed metrics, and feature breakdown in action here:\n🔗 **[Watch the Live Demo on TikTok](https://www.tiktok.com/@salarai123/video/7648566501598940436)**\n\n---\n\n## 🔍 The Problem & The Solution\n\n### The Bottleneck: LLM Context Inflation\nModern production applications relying on Large Language Model (LLM) APIs suffer from massive financial overhead. Upstream providers charge by the token—meaning heavy indentation loops, generic code comments, raw text formatting, and large structural blocks exponentially inflate infrastructure bills.\n\n### The Engine: Code-Shrink\n**Code-Shrink v1.0** passes raw context inputs through an edge-computed Abstract Syntax Tree (AST) framework. Instead of hosting gigabytes of neural network weights that lag and crash free hosting tiers, this application runs entirely on zero-cost, lightweight lexical optimization models. It reduces prompt token sizes by **up to 66% in under 10 milliseconds**.\n\n---\n\n## ⚡ Technical Core Features\n\n* **Abstract Syntax Tree (AST) De-bloating:** Fully parses Python/R/SQL environments natively to structurally strip docstrings, developer comments, and empty lines while maintaining 100% semantic code integrity.\n* **Lexical JSON Minification:** Collapses raw object dictionaries, spacing grids, and redundant string arrays into tight, machine-readable micro-streams.\n* **On-Edge Real-Time Diagnostic Metrics:** Computes compression percentages and displays an estimated API cost savings panel instantly on execution.\n* **Zero Infrastructure Overhead:** Operates 100% standalone with zero dependency on third-party backend servers, making it completely immune to public inference timeouts.\n\n---\n\n## 🛠️ Tech Stack & System Compatibility\n\n- **Interface Framework:** Gradio (v6.0 Transition-Optimized Layer)\n- **Computational Core:** Native Python AST & Lexical Pattern RegEx Engine\n- **Data Manifestation:** Memory Buffer Stream Handlers (PIL/JSON Core)\n- **Hardware Benchmarking:** Heavily optimized for restricted legacy processors (runs smooth down to Intel Core i3 4th Gen / 8GB RAM specs).\n\n---\n\n## 🎛️ Parameters Matrix\n\n1. **Raw Context Input:** Inject bloated code strings or massive JSON arrays into the terminal panel.\n2. **Lexical Processing Mode:** Set structural parser rules (`Python/R/SQL Code Matrix` or `Structured JSON / Raw Text Array`).\n3. Click **⚡ DECIMATE CONTEXT TOKENS** to immediately wipe empty tokens and render the optimized micro-stream for your prompt.\n\n---\n\n## 📦 Local Workspace Setup\n\nTo clone and execute this performance node locally:\n\n```bash\ngit clone [https://huggingface.co/spaces/build-small-hackathon/code-shrink-token-decimator](https://huggingface.co/spaces/build-small-hackathon/code-shrink-token-decimator)\ncd code-shrink-token-decimator\npip install -r requirements.txt\npython app.py",788      "readme_frontmatter": {789        "title": "Code Shrink Token Decimator",790        "emoji": "👀",791        "colorFrom": "gray",792        "colorTo": "pink",793        "sdk": "gradio",794        "sdk_version": "6.16.0",795        "python_version": "3.13",796        "app_file": "app.py",797        "pinned": "false",798        "license": "apache-2.0",799        "short_description": "Ultra-lightweight lexical token compressor that reduces LLM ",800        "thumbnail": ">-"801      },802      "app_source": "import sys\nimport types\nimport ast\nimport re\nimport json\n\n# 🚨 DYNAMIC FIX: Python 3.13 Compatibility Core Patches\nif 'audioop' not in sys.modules:\n    dummy_audioop = types.ModuleType('audioop')\n    dummy_audioop.error = Exception\n    sys.modules['audioop'] = dummy_audioop\n\nif 'pyaudioop' not in sys.modules:\n    dummy_pyaudioop = types.ModuleType('pyaudioop')\n    dummy_pyaudioop.error = Exception\n    sys.modules['pyaudioop'] = dummy_pyaudioop\n\ntry:\n    import huggingface_hub\nexcept ImportError:\n    huggingface_hub = types.ModuleType('huggingface_hub')\n    sys.modules['huggingface_hub'] = huggingface_hub\n\nif not hasattr(huggingface_hub, 'HfFolder'):\n    class DummyHfFolder:\n        @staticmethod\n        def get_token(): return None\n        @staticmethod\n        def save_token(token): pass\n        @staticmethod\n        def delete_token(): pass\n    huggingface_hub.HfFolder = DummyHfFolder\n\nimport gradio as gr\n\ndef estimate_tokens(text):\n    \"\"\"Ultra-fast local token estimator (Roughly 1 token = 4 chars for code/text setup)\"\"\"\n    if not text:\n        return 0\n    return max(1, len(text) // 4 + text.count(' ') // 2)\n\ndef shrink_python_code(source_code):\n    \"\"\"Parses and strips syntax trees to remove bloat tokens natively\"\"\"\n    try:\n        tree = ast.parse(source_code)\n        for node in ast.walk(tree):\n            if isinstance(node, (ast.FunctionDef, ast.ClassDef, ast.Module)):\n                if node.body and isinstance(node.body[0], ast.Expr) and isinstance(node.body[0].value, ast.Constant) and isinstance(node.body[0].value.value, str):\n                    node.body.pop(0)\n                    \n        clean_code = ast.unparse(tree)\n        clean_code = re.sub(r'\\n\\s*\\n', '\\n', clean_code)\n        return clean_code.strip()\n    except Exception:\n        code = re.sub(r'#.*', '', source_code)\n        code = re.sub(r'\\\"\\\"\\\"[\\s\\S]*?\\\"\\\"\\\"', '', code)\n        code = re.sub(r'\\'\\'\\'[\\s\\S]*?\\'\\'\\'', '', code)\n        code = re.sub(r'\\n\\s*\\n', '\\n', code)\n        return code.strip()\n\ndef shrink_generic_text(raw_data):\n    \"\"\"Minifies structured JSON/Data arrays and repetitive text strings\"\"\"\n    try:\n        parsed_json = json.loads(raw_data)\n        return json.dumps(parsed_json, separators=(',', ':'))\n    except Exception:\n        text = re.sub(r'\\s+', ' ', raw_data)\n        text = text.replace(\", \", \",\").replace(\": \", \":\").replace(\"; \", \";\")\n        return text.strip()\n\ndef decimator_engine(input_payload, mode):\n    if not input_payload.strip():\n        return \"\", \"### ⚠️ System Warning\\nPlease input your code or data block first!\", 0, \"0%\", \"$0.00\"\n\n    initial_tokens = estimate_tokens(input_payload)\n    \n    if mode == \"Python/R/SQL Code Matrix\":\n        decimated_output = shrink_python_code(input_payload)\n    else:\n        decimated_output = shrink_generic_text(input_payload)\n        \n    final_tokens = estimate_tokens(decimated_output)\n    \n    token_delta = initial_tokens - final_tokens\n    reduction_percentage = 0 if initial_tokens == 0 else (token_delta / initial_tokens) * 100\n    estimated_savings = (token_delta / 1000) * 0.015\n    if estimated_savings < 0: \n        estimated_savings = 0.0\n\n    report_markdown = f\"\"\"\n    ### 📊 Token Decimation Analytics Matrix\n    - **Original Payload Footprint:** `{initial_tokens}` estimated tokens.\n    - **Optimized Stream Footprint:** `{final_tokens}` estimated tokens.\n    - **Tokens Destroyed Successfully:** `{token_delta}` tokens wiped from context.\n    \"\"\"\n    \n    pct_string = f\"{reduction_percentage:.1f}%\"\n    savings_string = f\"${estimated_savings:.4f}\"\n    \n    return decimated_output, report_markdown, token_delta, pct_string, savings_string\n\n# Custom Cyber Matrix Terminal UI Theme for Judges\n# Extended .margin-top-class to substitute the invalid row inline style attribute parameter safely\ncustom_css = \"\"\"\nbody, .gradio-container { background-color: #050811 !important; font-family: 'Courier New', monospace; color: #00ff66 !important; }\n.decimate-btn { background: linear-gradient(135deg, #00ff66, #047857) !important; color: #050811 !important; font-weight: bold !important; border: 1px solid #00ff66 !important; border-radius: 4px !important; letter-spacing: 1px; }\n.decimate-btn:hover { box-shadow: 0 0 20px rgba(0,255,102,0.6); color: white !important; }\n.panel-border { border: 1px solid #1e293b !important; border-radius: 6px; padding: 15px; background: #090d1a !important; box-shadow: inset 0 0 10px rgba(0,255,102,0.05); }\n.metric-box { background: #0d1527 !important; border: 1px solid #00ff66 !important; border-radius: 4px; padding: 10px; text-align: center; }\n.margin-top-class { margin-top: 20px !important; }\ntextarea, input { background-color: #02040a !important; color: #38bdf8 !important; border: 1px solid #1e293b !important; font-family: 'Consolas', monospace !important; }\ntextarea:focus { border-color: #00ff66 !important; }\n\"\"\"\n\n# 🔥 FIXED: Removed css parameters from gr.Blocks initialization wrapper\nwith gr.Blocks(title=\"Code-Shrink v1.0\") as demo:\n    gr.HTML(\n        \"\"\"\n        <div style=\"text-align: center; margin-bottom: 20px; padding: 20px; background: #090d1a; border-radius: 6px; border: 1px solid #00ff66; box-shadow: 0 0 15px rgba(0,255,102,0.1);\">\n            <h1 style='margin: 0; font-size: 28px; color: #00ff66; letter-spacing: 3px;'>⚡ CODE-SHRINK: TOKEN-DECIMATOR</h1>\n            <p style='margin: 5px 0 0 0; color: #94a3b8; font-size: 13px;'>Algorithmic Context Optimization Matrix // Bypassing LLM Budget Inflation Natively</p>\n        </div>\n        \"\"\"\n    )\n    \n    with gr.Row():\n        with gr.Column(scale=3, elem_classes=\"panel-border\"):\n            gr.Markdown(\"### 📥 Raw Context Payload Injection\")\n            payload_input = gr.Textbox(\n                placeholder=\"Paste your bloated Python code, SQL queries, or huge JSON dictionaries here...\",\n                label=\"Raw Context Input\",\n                lines=12\n            )\n            \n            with gr.Row():\n                mode_dropdown = gr.Dropdown(\n                    choices=[\"Python/R/SQL Code Matrix\", \"Structured JSON / Raw Text Array\"],\n                    value=\"Python/R/SQL Code Matrix\",\n                    label=\"Lexical Processing Mode\"\n                )\n            \n            gr.HTML(\"<br>\")\n            process_btn = gr.Button(\"⚡ DECIMATE CONTEXT TOKENS\", elem_classes=\"decimate-btn\")\n            \n        with gr.Column(scale=3, elem_classes=\"panel-border\"):\n            gr.Markdown(\"### 📤 Optimized Micro-Stream Output\")\n            payload_output = gr.Textbox(\n                label=\"Decimated Token Stream (Ready for LLM Prompt)\",\n                lines=12,\n                interactive=False\n            )\n            \n            gr.HTML(\"<br>\")\n            with gr.Row():\n                with gr.Column(scale=1, elem_classes=\"metric-box\"):\n                    gr.Markdown(\"<span style='color:#94a3b8; font-size:11px;'>COMPRESSION</span>\")\n                    pct_output = gr.HTML(\"<b style='color:#00ff66; font-size:22px;'>0%</b>\")\n                with gr.Column(scale=1, elem_classes=\"metric-box\"):\n                    gr.Markdown(\"<span style='color:#94a3b8; font-size:11px;'>TOKENS WIPED</span>\")\n                    delta_output = gr.HTML(\"<b style='color:#38bdf8; font-size:22px;'>0</b>\")\n                with gr.Column(scale=1, elem_classes=\"metric-box\"):\n                    gr.Markdown(\"<span style='color:#94a3b8; font-size:11px;'>EST. API SAVINGS</span>\")\n                    savings_output = gr.HTML(\"<b style='color:#e879f9; font-size:22px;'>$0.00</b>\")\n\n    # 🔥 FIXED: Replaced inline style injection parameters with valid custom css mapping selectors\n    with gr.Row(elem_classes=[\"panel-border\", \"margin-top-class\"]):\n        diagnostics_output = gr.Markdown(\"`System Engine: Standing by. Awaiting dynamic payload mapping signals...`\")\n\n    process_btn.click(\n        fn=decimator_engine,\n        inputs=[payload_input, mode_dropdown],\n        outputs=[payload_output, diagnostics_output, delta_output, pct_output, savings_output]\n    )\n\n# 🔥 FIXED: Passed custom UI styling matrices explicitly inside launcher execution limits\ndemo.launch(css=custom_css)\n\n",803      "app_signals": "estimate_tokens text shrink_python_code source_code shrink_generic_text raw_data decimator_engine input_payload mode DummyHfFolder demo.launch css audioop types.ModuleType pyaudioop hasattr get_token save_token token delete_token Ultra-fast local token estimator (Roughly 1 token = 4 chars for code/text setup) max Parses and strips syntax trees to remove bloat tokens natively Minifies structured JSON/Data arrays and repetitive text strings gr.Blocks title gr.HTML process_btn.click fn inputs outputs HfFolder ast.parse ast.walk ast.unparse re.sub clean_code.strip json.loads json.dumps separators input_payload.strip Python/R/SQL Code Matrix ### 📊 Token Decimation Analytics Matrix - **Original Payload Footprint:** ` ` estimated tokens. - **Optimized Stream Footprint:** ` ` estimated tokens. - **Tokens Destroyed Successfully:** ` ` tokens wiped from context. % $ ⚡ CODE-SHRINK: TOKEN-DECIMATOR Algorithmic Context Optimization Matrix // Bypassing LLM Budget Inflation Natively gr.Row elem_classes gr.Markdown huggingface_hub isinstance \\n\\s*\\n code.strip replace text.strip ### ⚠️ System Warning Please input your code or data block first! 0% $0.00 Code-Shrink v1.0 gr.Column scale gr.Textbox placeholder label lines gr.Button interactive `System Engine: Standing by. Awaiting dynamic payload mapping signals...` len text.count #.* \\\"\\\"\\\"[\\s\\S]*?\\\"\\\"\\\" \\'\\'\\'[\\s\\S]*?\\'\\'\\' \\s+ ; .1f .4f ### 📥 Raw Context Payload Injection gr.Dropdown choices value ⚡ DECIMATE CONTEXT TOKENS ### 📤 Optimized Micro-Stream Output node.body.pop , : panel-border Paste your bloated Python code, SQL queries, or huge JSON dictionaries here... Raw Context Input decimate-btn Decimated Token Stream (Ready for LLM Prompt) margin-top-class Lexical Processing Mode COMPRESSION TOKENS WIPED 0 EST. API SAVINGS text.replace Structured JSON / Raw Text Array metric-box",804      "readme_len": 3130,805      "app_source_len": 8136,806      "app_signals_len": 1846807    },808    {809      "id": "build-small-hackathon/CodeFlow",810      "title": "CodeFlow",811      "summary": "Turn Python code into a readable Mermaid.js flowchart 📊",812      "tags": [813        "gradio",814        "region:us"815      ],816      "models": [],817      "datasets": [],818      "sdk": "gradio",819      "license": "mit",820      "likes": 1,821      "url": "https://huggingface.co/spaces/build-small-hackathon/CodeFlow",822      "app_file": "app.py",823      "readme_raw": "---\ntitle: CodeFlow\nemoji: 📊\ncolorFrom: indigo\ncolorTo: blue\nsdk: gradio\nsdk_version: 6.16.0\npython_version: '3.13'\napp_file: app.py\npinned: false\nlicense: mit\nshort_description: Turn Python code into a readable Mermaid.js flowchart 📊\n---\n\nCheck out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference\n",824      "readme_body": "Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference",825      "readme_frontmatter": {826        "title": "CodeFlow",827        "emoji": "📊",828        "colorFrom": "indigo",829        "colorTo": "blue",830        "sdk": "gradio",831        "sdk_version": "6.16.0",832        "python_version": "3.13",833        "app_file": "app.py",834        "pinned": "false",835        "license": "mit",836        "short_description": "Turn Python code into a readable Mermaid.js flowchart 📊"837      },838      "app_source": "\"\"\"\n3. Graph. Capture the resulting mermaid string and visualize it\n\nTo do\n- create the custom gradio look\n- explore making it look better\n- get a better model — Qwen 30b coder\n- use zerogpu\n\n\"\"\"\nfrom huggingface_hub import hf_hub_download\nfrom llama_cpp import Llama\nimport gradio as gr\nfrom gradio import Server\nfrom fastapi.responses import HTMLResponse # serve the custom frontend from a route\nfrom typing import Any, cast # to resolve PyLance freaking out over llama-cpp-python in the generate_flowchart function\nfrom textwrap import dedent\nimport re # remove thinking tag from response \n\n\n\n    out = []\n    for line in text.split('\\n'):\n        line = re.sub(r'(?<=\\w)\\[(.*?)\\]' + END, lambda m: '[\"' + esc(m.group(1)) + '\"]', line)\n        line = re.sub(r'(?<=\\w)\\{(.*?)\\}' + END, lambda m: '{\"' + esc(m.group(1)) + '\"}', line)\n        out.append(line)\n    return '\\n'.join(out)\n\n@app.api(name=\"generate_flowchart\")\ndef generate_flowchart(src_code: str) -> str:\n    # check if src_code is empty\n    if not src_code.strip(): return \"\"\n\n    # Set system prompt\n    system_prompt = dedent(\"\"\"\n    ## Role/Persona\n    You are a senior staff software architect and compiler engineer specializing in visual control-flow mapping. Your philosophy is pure utility: you translate raw execution logic into highly accurate, scannable, structural diagrams without any conversational filler, meta-commentary, or stylistic fluff.\n\n    ## Context/Objective\n    The user will provide source code files or logic snippets. Your sole objective is to parse the syntax and output a corresponding, valid Mermaid.js flowchart graph. This graph will be rendered natively in a production UI to help developers audit execution paths at a glance.\n\n    ## Strict Constraints\n    <constraints>\n    1. OUTPUT FORMAT: Output ONLY valid, raw Mermaid.js syntax.\n    2. NO MARKDOWN FENCING: Do not wrap the output in ```mermaid or ``` blocks. Start directly with the Mermaid graph definition, for example: graph TD.\n    3. NO PROSE: Do not include introductory text, explanations, or concluding remarks. If the code cannot be parsed, output an isolated error node.\n    4. NODE NAMING: Paraphrase conditions into plain words — never put raw code, operators, quotes, parentheses, or square brackets/subscripts inside labels (write Index in bounds?, not i < len(nums); write Element is even?, not nums[i] % 2 == 0)\n    </constraints>\n\n    <banned_vocabulary>\n    - Here is the flowchart\n    - ```mermaid\n    - ```\n    - Note:\n    - Explanation:\n    - In this diagram\n    - As requested\n    </banned_vocabulary>\n\n    ## Response Workflow\n    Before outputting the final diagram syntax, perform structural parsing inside a hidden <thinking> tag according to these steps:\n    1. Identify all conditional branches, including if/else, loops, including for/while, and termination points, including return/throw.\n    2. Map out the execution flow nodes chronologically.\n    3. Verify that every opening bracket and node label matching syntax, including [ ], ( ), and { }, is perfectly balanced and closed according to Mermaid specifications.\n    4. Ensure no markdown formatting tags leak past the closing </thinking> tag.\n\n    ## Few-Shot Examples\n\n    Input:\n    def check_status(val):\n        if val > 10:\n            return \"Active\"\n        else:\n            return \"Inactive\"\n\n    Output:\n    <thinking>\n    1. Control structures: One conditional check, two return branches.\n    2. Nodes: A Start, B Conditional, C Active return, D Inactive return.\n    3. Syntax verification: B uses curly braces for decisions. Edges use standard arrows.\n    </thinking>\n    graph TD\n        A[Start: check_status] --> B{val > 10}\n        B -- True --> C[Return 'Active']\n        B -- False --> D[Return 'Inactive']\n    \"\"\").strip()\n\n    # Reset the cache per request so no cross-request bleeding\n    llm.reset()\n\n    # Casting else PyLance gets mad\n    response = cast(Any, llm.create_chat_completion(\n        messages=[\n            {\"role\": \"system\", \"content\": system_prompt},\n            {\"role\": \"user\", \"content\": src_code}\n        ],\n        temperature=0.1, # Keep it quite deterministic for now\n        max_tokens=1024,\n        stream=False\n    ))\n\n    content = response[\"choices\"][0][\"message\"][\"content\"]\n\n    # remove the thinking tags from the response\n    cleaned = re.sub(r'<thinking>.*?</thinking>', '', content, flags=re.DOTALL)\n\n    # Quote-wrap each node label and escape any leaked code characters\n    cleaned = quote_labels(cleaned)\n\n    return cleaned.strip() # and remove excess whitespace\n\n# ----- Custom Frontend ----- #\nindex_html = \"\"\"\n<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <title>Code-to-Flowchart Generator</title>\n    <style>\n        body { font-family: sans-serif; background: #111827; color: #f3f4f6; margin: 0; padding: 20px; }\n        .container { display: flex; gap: 20px; height: 90vh; }\n        .panel { flex: 1; display: flex; flex-direction: column; background: #1f2937; padding: 15px; border-radius: 8px; }\n        textarea { flex: 1; background: #111827; color: #34d399; border: 1px solid #374151; padding: 10px; font-family: monospace; resize: none; border-radius: 4px; }\n        button { background: #059669; color: white; border: none; padding: 12px; margin-top: 10px; cursor: pointer; font-weight: bold; border-radius: 4px; }\n        button:hover { background: #10b981; }\n        button:disabled { background: #374151; cursor: not-allowed; }\n        #flowchart-target { flex: 1; background: #ffffff; padding: 10px; border-radius: 4px; overflow: auto; display: flex; justify-content: center; align-items: start; }\n    </style>\n</head>\n<body>\n    <h2>Flowchart Transpiler</h2>\n    <div class=\"container\">\n        <div class=\"panel\">\n            <h3>Source Code Input</h3>\n            <textarea id=\"code-input\" placeholder=\"Paste your code here...\" spellcheck=\"false\"></textarea>\n            <button id=\"submit-btn\">Generate Flowchart</button>\n        </div>\n        <div class=\"panel\">\n            <h3>Mermaid Flowchart Visualizer</h3>\n            <div id=\"flowchart-target\">\n                <pre class=\"mermaid\" id=\"mermaid-string\">\n                    graph TD\n                    A[Paste Code] --> B[Click Generate]\n                </pre>\n            </div>\n        </div>\n    </div>\n\n    <script type=\"module\">\n        import { Client } from \"https://cdn.jsdelivr.net/npm/@gradio/client@1/dist/index.min.js\";\n        import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs';\n        \n        mermaid.initialize({ startOnLoad: true, theme: 'neutral' });\n\n        // Instantiate the local Gradio application client dynamically\n        const client = await Client.connect(window.location.origin);\n\n        document.getElementById('submit-btn').addEventListener('click', async () => {\n            const codeValue = document.getElementById('code-input').value;\n            const targetDiv = document.getElementById('flowchart-target');\n            const submitBtn = document.getElementById('submit-btn');\n\n            if (!codeValue.trim()) {\n                targetDiv.innerHTML = \"<p style='color:red;'>Please input code first.</p>\";\n                return;\n            }\n\n            // Disable the button while a request is in flight so a slow CPU\n            // generation can't be double-fired into a concurrent request.\n            submitBtn.disabled = true;\n            submitBtn.textContent = \"Generating...\";\n            targetDiv.innerHTML = \"Generating diagram...\";\n\n            let mermaidSyntax = \"\";\n            try {\n                // Call the @app.api function registered in python (name + param must match)\n                const result = await client.predict(\"/generate_flowchart\", { src_code: codeValue });\n                mermaidSyntax = result.data[0];\n\n                // Inject the raw string into a clean layout block and re-trigger parsing\n                targetDiv.innerHTML = `<pre class=\"mermaid\">${mermaidSyntax}</pre>`;\n                await mermaid.run();\n\n            } catch (error) {\n                // On failure show the error AND the exact raw Mermaid we tried to render,\n                // so a parse error can be diagnosed from the real output. textContent is\n                // used for the raw string so newlines/special chars can't break the page.\n                targetDiv.innerHTML = \"<p style='color:red;'>Error during generation: \" + error.message + \"</p><p style='color:#111;font-weight:bold;text-align:left;'>Raw Mermaid output:</p>\";\n                const dbg = document.createElement(\"pre\");\n                dbg.style.color = \"#111\";\n                dbg.style.whiteSpace = \"pre-wrap\";\n                dbg.style.textAlign = \"left\";\n                dbg.textContent = mermaidSyntax;\n                targetDiv.appendChild(dbg);\n            } finally {\n                submitBtn.disabled = false;\n                submitBtn.textContent = \"Generate Flowchart\";\n            }\n        });\n    </script>\n</body>\n</html>\n\"\"\"\n\n# Load the custom HTML\n# / takes precedent over default Blocks UI\n@app.get(\"/\")\ndef index():\n    return HTMLResponse(index_html)\n\napp.launch(share=True)",839      "app_signals": "quote_labels text generate_flowchart src_code index 3. Graph. Capture the resulting mermaid string and visualize it To do - create the custom gradio look - explore making it look better - get a better model — Qwen 30b coder - use zerogpu hf_hub_download repo_id filename gr.Server title esc body app.api name app.get app.launch share (?=\\s*(?:[- xo]|==[>=xo]|\\||;|$)) text.split join strip llm.reset cast re.sub flags cleaned.strip HTMLResponse / unsloth/Qwen3-Coder-30B-A3B-Instruct-GGUF Qwen3-Coder-30B-A3B-Instruct-UD-Q3_K_XL.gguf Code-to-Flowchart Generator replace out.append src_code.strip llm.create_chat_completion messages temperature max_tokens stream content .*? } &#125; dedent message (?<=\\w)\\[(.*?)\\] (?<=\\w)\\{(.*?)\\} ## Role/Persona You are a senior staff software architect and compiler engineer specializing in visual control-flow mapping. Your philosophy is pure utility: you translate raw execution logic into highly accurate, scannable, structural diagrams without any conversational filler, meta-commentary, or stylistic fluff. ## Context/Objective The user will provide source code files or logic snippets. Your sole objective is to parse the syntax and output a corresponding, valid Mermaid.js flowchart graph. This graph will be rendered natively in a production UI to help developers audit execution paths at a glance. ## Strict Constraints 1. OUTPUT FORMAT: Output ONLY valid, raw Mermaid.js syntax. 2. NO MARKDOWN FENCING: Do not wrap the output in ```mermaid or ``` blocks. Start directly with the Mermaid graph definition, for example: graph TD. 3. NO PROSE: Do not include introductory text, explanations, or concluding remarks. If the code cannot be parsed, output an isolated error node. 4. NODE NAMING: Paraphrase conditions into plain words — never put raw code, operators, quotes, parentheses, or square brackets/subscripts inside labels (write Index in bounds?, not i - Here is the flowchart - ```mermaid - ``` - Note: - Explanation: - In this diagram - As requested ## Response Workflow Before outputting the final diagram syntax, perform structural parsing inside a hidden tag according to these steps: 1. Identify all conditional branches, including if/else, loops, including for/while, and termination points, including return/throw. 2. Map out the execution flow nodes chronologically. 3. Verify that every opening bracket and node label matching syntax, including [ ], ( ), and { }, is perfectly balanced and closed according to Mermaid specifications. 4. Ensure no markdown formatting tags leak past the closing tag. ## Few-Shot Examples Input: def check_status(val): if val > 10: return \"Active\" else: return \"Inactive\" Output: 1. Control structures: One conditional check, two return branches. 2. Nodes: A Start, B Conditional, C Active return, D Inactive return. 3. Syntax verification: B uses curly braces for decisions. Edges use standard arrows. graph TD A[Start: check_status] --> B{val > 10} B -- True --> C[Return 'Active'] B -- False --> D[Return 'Inactive'] { &#123; \"] \"} choices [\" {\" role system user ] &#93; m.group [ &#91; body.replace \" '",840      "readme_len": 96,841      "app_source_len": 9174,842      "app_signals_len": 3098843    },844    {845      "id": "build-small-hackathon/come-and-compare",846      "title": "Come And Compare",847      "summary": "Real-time price comparison across Amazon, Flipkart & Myntra",848      "tags": [849        "gradio",850        "region:us"851      ],852      "models": [],853      "datasets": [],854      "sdk": "gradio",855      "license": "mit",856      "likes": 1,857      "url": "https://huggingface.co/spaces/build-small-hackathon/come-and-compare",858      "app_file": "app.py",859      "readme_raw": "---\ntitle: Come And Compare\nemoji: 🛒\ncolorFrom: red\ncolorTo: blue\nsdk: gradio\nsdk_version: 5.9.1\napp_file: app.py\npinned: true\nlicense: mit\nshort_description: Real-time price comparison across Amazon, Flipkart & Myntra\n---\n\n# Come & Compare 🛒\n\nReal-time product price comparison across Amazon India, Flipkart, and Myntra.\n\nBuilt for the HuggingFace Small Models Hackathon — uses Qwen/Qwen2.5-7B-Instruct (under 32B limit).\n\n## Setup\nAdd your HF_TOKEN as a Space secret (Settings → Variables and Secrets).\n\n## creator space link: SlideAI - a Hugging Face Space by PHOENIXREBORNAGAIN https://share.google/8peVYW3BKwsONJzip\n\n\n### 📌 Official Submission Links\n\n* 🎥 **Demo Video:** [Watch on YouTube](Https://youtu.be/F38EHr3rPcI?si=5bh3PmbPqLoPpSri)\n* 💬 **Social Media Post:** [View on LinkedIn](https://www.linkedin.com/posts/chahat-mehra-4a44a829b_buildsmallhackathon-huggingface-gradio-activity-7465696236218781696-9TeY)\n\n## 💡 Why This Matters: Solving a Daily E-Commerce Problem\n\nEvery day, millions of shoppers in India waste time jumping between Amazon, Flipkart, and Myntra to find the best price for a single product. \n\n**The Problem:**\n* **Tab Fatigue:** Manually searching multiple apps, typing the same query, and comparing results is slow and frustrating.\n* **Broken Aggregators:** Traditional price comparison websites are frequently broken or display outdated prices because major e-commerce platforms aggressively block their scraping bots using CAPTCHAs and cloud IP bans.\n* **Information Overload:** Even when prices are found, varying model numbers, variants, and listings make it hard to confidently choose the absolute best deal.\n\n**The Solution:**\n**Come & Compare** eliminates this friction entirely. By combining a lightweight DuckDuckGo search mechanism with the analytical power of a 7B parameter AI model, consumers get instantaneous, real-world price estimates and a direct buying recommendation in one clean dashboard. It gives everyday buyers a smart, real-time shopping assistant that cuts through the noise and guarantees they are getting the best value for their money.",860      "readme_body": "# Come & Compare 🛒\n\nReal-time product price comparison across Amazon India, Flipkart, and Myntra.\n\nBuilt for the HuggingFace Small Models Hackathon — uses Qwen/Qwen2.5-7B-Instruct (under 32B limit).\n\n## Setup\nAdd your HF_TOKEN as a Space secret (Settings → Variables and Secrets).\n\n## creator space link: SlideAI - a Hugging Face Space by PHOENIXREBORNAGAIN https://share.google/8peVYW3BKwsONJzip\n\n\n### 📌 Official Submission Links\n\n* 🎥 **Demo Video:** [Watch on YouTube](Https://youtu.be/F38EHr3rPcI?si=5bh3PmbPqLoPpSri)\n* 💬 **Social Media Post:** [View on LinkedIn](https://www.linkedin.com/posts/chahat-mehra-4a44a829b_buildsmallhackathon-huggingface-gradio-activity-7465696236218781696-9TeY)\n\n## 💡 Why This Matters: Solving a Daily E-Commerce Problem\n\nEvery day, millions of shoppers in India waste time jumping between Amazon, Flipkart, and Myntra to find the best price for a single product. \n\n**The Problem:**\n* **Tab Fatigue:** Manually searching multiple apps, typing the same query, and comparing results is slow and frustrating.\n* **Broken Aggregators:** Traditional price comparison websites are frequently broken or display outdated prices because major e-commerce platforms aggressively block their scraping bots using CAPTCHAs and cloud IP bans.\n* **Information Overload:** Even when prices are found, varying model numbers, variants, and listings make it hard to confidently choose the absolute best deal.\n\n**The Solution:**\n**Come & Compare** eliminates this friction entirely. By combining a lightweight DuckDuckGo search mechanism with the analytical power of a 7B parameter AI model, consumers get instantaneous, real-world price estimates and a direct buying recommendation in one clean dashboard. It gives everyday buyers a smart, real-time shopping assistant that cuts through the noise and guarantees they are getting the best value for their money.",861      "readme_frontmatter": {862        "title": "Come And Compare",863        "emoji": "🛒",864        "colorFrom": "red",865        "colorTo": "blue",866        "sdk": "gradio",867        "sdk_version": "5.9.1",868        "app_file": "app.py",869        "pinned": "true",870        "license": "mit",871        "short_description": "Real-time price comparison across Amazon, Flipkart & Myntra"872      },873      "app_source": "import gradio as gr\nimport requests\nfrom bs4 import BeautifulSoup\nimport re\nimport os\nimport urllib.parse\nfrom huggingface_hub import InferenceClient\n\nMODEL_ID = \"Qwen/Qwen2.5-7B-Instruct\"\nPRICE_RE = re.compile(r\"(?:₹|Rs\\.?|INR)\\s*([\\d,]+(?:\\.\\d+)?)\")\nASIN_RE  = re.compile(r\"/(?:dp|gp/product)/([A-Z0-9]{10})\")\n\nDDG_HEADERS = {\n    \"User-Agent\": \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36\",\n    \"Accept-Language\": \"en-US,en;q=0.9\",\n    \"Accept\": \"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\",\n}\n\n\ndef get_client():\n    token = os.environ.get(\"HF_TOKEN\", \"\")\n    return InferenceClient(token=token) if token else InferenceClient()\n\n\ndef clean_price(text: str):\n    if not text:\n        return None\n    m = PRICE_RE.search(str(text))\n    if m:\n        raw = m.group(1).replace(\",\", \"\")\n        try:\n            val = int(float(raw))\n            if 100 < val < 10_000_000:\n                return f\"₹{val:,}\"\n        except ValueError:\n            pass\n    return None\n\n\ndef clean_amazon_link(raw_link: str) -> str:\n    \"\"\"Extract ASIN and return a clean, working Amazon.in product URL.\"\"\"\n    if not raw_link:\n        return None\n    m = ASIN_RE.search(raw_link)\n    if m:\n        return f\"https://www.amazon.in/dp/{m.group(1)}\"\n    # If no ASIN, keep only the base path (strip all query params/tracking)\n    try:\n        parsed = urllib.parse.urlparse(raw_link)\n        if \"amazon\" in parsed.netloc:\n            clean = parsed._replace(query=\"\", fragment=\"\").geturl()\n            return clean\n    except Exception:\n        pass\n    return raw_link\n\n\ndef clean_flipkart_link(raw_link: str) -> str:\n    \"\"\"Keep only essential Flipkart URL params, strip tracking.\"\"\"\n    if not raw_link:\n        return None\n    try:\n        parsed = urllib.parse.urlparse(raw_link)\n        qs = urllib.parse.parse_qs(parsed.query)\n        kept = {}\n        for k in (\"pid\", \"lid\", \"marketplace\"):\n            if k in qs:\n                kept[k] = qs[k]\n        new_q = urllib.parse.urlencode(kept, doseq=True)\n        return parsed._replace(query=new_q, fragment=\"\").geturl()\n    except Exception:\n        return raw_link\n\n\ndef ddg_search(query: str, num: int = 12):\n    try:\n        resp = requests.post(\n            \"https://html.duckduckgo.com/html/\",\n            data={\"q\": query, \"b\": \"\", \"kl\": \"in-en\"},\n            headers=DDG_HEADERS,\n            timeout=15,\n        )\n        soup = BeautifulSoup(resp.text, \"lxml\")\n        results = []\n        for item in soup.select(\".result\")[:num]:\n            title_el   = item.select_one(\".result__title\")\n            snippet_el = item.select_one(\".result__snippet\")\n            url_el     = item.select_one(\".result__url\")\n            link_el    = item.select_one(\".result__title a\")\n            title   = title_el.get_text(\" \", strip=True)   if title_el   else \"\"\n            snippet = snippet_el.get_text(\" \", strip=True) if snippet_el else \"\"\n            url_txt = url_el.get_text(strip=True)          if url_el     else \"\"\n            link    = link_el.get(\"href\", \"\")              if link_el    else \"\"\n            if link and \"duckduckgo.com\" in link:\n                try:\n                    qs     = urllib.parse.urlparse(link).query\n                    params = urllib.parse.parse_qs(qs)\n                    link   = urllib.parse.unquote(params.get(\"uddg\", [link])[0])\n                except Exception:\n                    pass\n            results.append({\"title\": title, \"snippet\": snippet, \"url\": url_txt, \"link\": link})\n        return results\n    except Exception:\n        return []\n\n\ndef normalize_query(raw: str) -> str:\n    try:\n        client = get_client()\n        resp = client.chat_completion(\n            messages=[\n                {\n                    \"role\": \"system\",\n                    \"content\": (\n                        \"You are a product search query cleaner. \"\n                        \"Output ONLY a short, clean product name (max 8 words) suitable for searching on \"\n                        \"Amazon India, Flipkart, and Myntra. No explanation, no punctuation at the end.\"\n                    ),\n                },\n                {\"role\": \"user\", \"content\": f\"Clean this product name: {raw}\"},\n            ],\n            model=MODEL_ID,\n            max_tokens=25,\n            temperature=0.05,\n        )\n        cleaned = resp.choices[0].message.content.strip().strip('\"').strip(\"'\")\n        if cleaned and 3 < len(cleaned) < 100:\n            return cleaned\n    except Exception as e:\n        print(f\"[normalize_query] {e}\")\n    return raw.strip()\n\n\ndef hf_get_prices(query: str) -> dict:\n    try:\n        client = get_client()\n        resp = client.chat_completion(\n            messages=[\n                {\n                    \"role\": \"system\",\n                    \"content\": (\n                        \"You are a real-time Indian e-commerce price assistant. \"\n                        \"You know current approximate prices on Amazon India, Flipkart, and Myntra. \"\n                        \"Reply with ONLY three lines in this exact format:\\n\"\n                        \"Amazon: ₹PRICE\\n\"\n                        \"Flipkart: ₹PRICE\\n\"\n                        \"Myntra: ₹PRICE\\n\"\n                        \"If a product is not sold on a platform, write N/A. \"\n                        \"No extra text. No explanations.\"\n                    ),\n                },\n                {\n                    \"role\": \"user\",\n                    \"content\": f\"Current price of '{query}' on Amazon India, Flipkart, Myntra?\",\n                },\n            ],\n            model=MODEL_ID,\n            max_tokens=80,\n            temperature=0.05,\n        )\n        text = resp.choices[0].message.content.strip()\n        result = {}\n        for line in text.splitlines():\n            price = clean_price(line)\n            if not price:\n                continue\n            ll = line.lower()\n            if \"amazon\" in ll:\n                result[\"amazon\"] = price\n            elif \"flipkart\" in ll:\n                result[\"flipkart\"] = price\n            elif \"myntra\" in ll:\n                result[\"myntra\"] = price\n        return result\n    except Exception as e:\n        print(f\"[hf_get_prices] {e}\")\n        return {}\n\n\ndef hf_ai_analysis(query: str, amazon: dict, flipkart: dict, myntra: dict) -> str:\n    lines = []\n    for r in [amazon, flipkart, myntra]:\n        p = r.get(\"price\") or \"N/A\"\n        lines.append(f\"- {r['platform']}: {p}\")\n    scraped_str = \"\\n\".join(lines)\n    try:\n        client = get_client()\n        resp = client.chat_completion(\n            messages=[\n                {\n                    \"role\": \"system\",\n                    \"content\": \"You are a smart Indian price comparison assistant called 'Come & Compare'.\",\n                },\n                {\n                    \"role\": \"user\",\n                    \"content\": (\n                        f\"Product: {query}\\n\\nPrices:\\n{scraped_str}\\n\\n\"\n                        \"Reply in this exact format:\\n\"\n                        \"🏆 BEST DEAL: [platform] at [price]\\n\\n\"\n                        \"📊 PRICE RANKING:\\n1. [platform] — [price]\\n2. ...\\n\\n\"\n                        \"💡 BUYING ADVICE:\\n[2-3 line recommendation]\\n\\n\"\n                        \"⚠️ NOTES:\\n[any warnings about unavailable prices]\"\n                    ),\n                },\n            ],\n            model=MODEL_ID,\n            max_tokens=350,\n            temperature=0.3,\n        )\n        return resp.choices[0].message.content.strip()\n    except Exception as e:\n        return f\"⚠️ AI analysis unavailable: {str(e)}\"\n\n\ndef get_platform_link(results, domain: str, platform: str):\n    \"\"\"Return a clean, working link for the platform.\"\"\"\n    for r in results:\n        url  = r.get(\"url\", \"\")\n        link = r.get(\"link\", \"\")\n        if domain in url or domain in link:\n            raw = link if link.startswith(\"http\") else (\"https://\" + url if url else None)\n            if not raw:\n                continue\n            if platform == \"Amazon.in\":\n                cleaned = clean_amazon_link(raw)\n                if cleaned:\n                    return cleaned\n            elif platform == \"Flipkart\":\n                cleaned = clean_flipkart_link(raw)\n                if cleaned:\n                    return cleaned\n            else:\n                return raw\n    return None\n\n\ndef get_platform_title(results, domain: str):\n    for r in results:\n        if domain in r.get(\"url\", \"\") or domain in r.get(\"link\", \"\"):\n            return r.get(\"title\", \"\")\n    return \"\"\n\n\ndef get_product_image(query: str, ddg_results: list):\n    import json\n    for r in ddg_results:\n        link = r.get(\"link\", \"\")\n        if \"amazon.in\" in link or \"amazon.com\" in link:\n            try:\n                resp = requests.get(link, headers=DDG_HEADERS, timeout=8)\n                soup = BeautifulSoup(resp.text, \"lxml\")\n                for sel in [\"#landingImage\", \"#imgBlkFront\", \".a-dynamic-image\"]:\n                    img = soup.select_one(sel)\n                    if img:\n                        src = img.get(\"src\", \"\")\n                        if src and src.startswith(\"http\"):\n                            return src\n                        data = img.get(\"data-a-dynamic-image\", \"\")\n                        if data:\n                            try:\n                                d = json.loads(data)\n                                return max(d.keys(), key=lambda u: d[u][0] * d[u][1])\n                            except Exception:\n                                pass\n            except Exception:\n                pass\n    for r in ddg_results[:5]:\n        link = r.get(\"link\", \"\")\n        if not link or \"duckduckgo\" in link:\n            continue\n        try:\n            resp = requests.get(link, headers=DDG_HEADERS, timeout=6)\n            soup = BeautifulSoup(resp.text, \"lxml\")\n            og = soup.select_one(\"meta[property='og:image']\")\n            if og and og.get(\"content\", \"\").startswith(\"http\"):\n                return og[\"content\"]\n        except Exception:\n            pass\n    return None\n\n\ndef compare_prices(product_name, product_details, selected_platforms, progress=gr.Progress()):\n    if not product_name or not product_name.strip():\n        return (\n            \"<p style='color:#c62828;text-align:center;padding:20px;font-size:15px'>⚠️ Please enter a product name.</p>\",\n            \"❌ No product entered.\",\n            \"\",\n        )\n\n    query = product_name.strip()\n    if product_details and product_details.strip():\n        query = f\"{query} {product_details.strip()}\"\n\n    progress(0.05, desc=\"🤖 Normalizing query with Qwen 7B...\")\n    normalized = normalize_query(query)\n\n    progress(0.2, desc=\"🔍 Searching DuckDuckGo for product links...\")\n    ddg_results = ddg_search(f\"{normalized} buy online india amazon flipkart myntra price\", num=12)\n\n    progress(0.5, desc=\"💰 Fetching prices via Qwen 7B...\")\n    hf_prices = hf_get_prices(normalized)\n\n    progress(0.7, desc=\"🖼️ Finding product image...\")\n    image_url = get_product_image(normalized, ddg_results)\n\n    progress(0.85, desc=\"🤖 Running AI analysis...\")\n\n    enc = urllib.parse.quote_plus(normalized)\n    PLATFORMS = [\n        {\"platform\": \"Amazon.in\", \"domain\": \"amazon.in\",    \"color\": \"#FF9900\", \"bg\": \"#FFF8EE\",\n         \"search\": f\"https://www.amazon.in/s?k={enc}\", \"price_key\": \"amazon\"},\n        {\"platform\": \"Flipkart\",   \"domain\": \"flipkart.com\", \"color\": \"#2874F0\", \"bg\": \"#EEF4FF\",\n         \"search\": f\"https://www.flipkart.com/search?q={enc}\", \"price_key\": \"flipkart\"},\n        {\"platform\": \"Myntra\",     \"domain\": \"myntra.com\",   \"color\": \"#FF3F6C\", \"bg\": \"#FFF0F4\",\n         \"search\": f\"https://www.myntra.com/{enc}\", \"price_key\": \"myntra\"},\n    ]\n\n    active_keys = {p.lower(): p for p in (selected_platforms or [])}\n    results = []\n    for p in PLATFORMS:\n        if active_keys and not any(k in p[\"platform\"].lower() for k in active_keys):\n            continue\n        link  = get_platform_link(ddg_results, p[\"domain\"], p[\"platform\"]) or p[\"search\"]\n        title = get_platform_title(ddg_results, p[\"domain\"])\n        price = hf_prices.get(p[\"price_key\"])\n        results.append({**p, \"price\": price, \"title\": title, \"link\": link})\n\n    ai_out = hf_ai_analysis(normalized, *results[:3]) if len(results) >= 3 else \"Need all 3 platforms for AI analysis.\"\n\n    progress(1.0, desc=\"✅ Done!\")\n\n    table_html = _build_cards(results, image_url, normalized)\n    links_html = _build_links(normalized, results)\n    return table_html, ai_out, links_html\n\n\ndef _find_best(results):\n    found = [r for r in results if r.get(\"price\")]\n    if not found:\n        return \"\"\n    def val(r):\n        return int(r[\"price\"].replace(\"₹\",\"\").replace(\",\",\"\").strip())\n    try:\n        return min(found, key=val)[\"platform\"]\n    except Exception:\n        return \"\"\n\n\ndef _build_cards(results, image_url, query):\n    best = _find_best(results)\n\n    img_html = \"\"\n    if image_url:\n        img_html = (\n            f'<div style=\"text-align:center;margin-bottom:24px\">'\n            f'<img src=\"{image_url}\" style=\"max-height:220px;max-width:300px;'\n            f'border-radius:16px;object-fit:contain;background:#fff;'\n            f'padding:12px;box-shadow:0 4px 20px rgba(0,0,0,.10)\" /></div>'\n        )\n\n    cards = \"\"\n    for r in results:\n        color  = r[\"color\"]\n        bg     = r[\"bg\"]\n        price  = r.get(\"price\")\n        title  = (r.get(\"title\") or \"\")[:72]\n        link   = r.get(\"link\", r[\"search\"])\n        is_best = best and r[\"platform\"] == best and price\n\n        border = f\"3px solid {color}\" if is_best else f\"2px solid {color}33\"\n        shadow = f\"0 8px 28px {color}30\" if is_best else \"0 4px 16px rgba(0,0,0,.08)\"\n        trophy = '<div style=\"position:absolute;top:-12px;left:50%;transform:translateX(-50%);background:#FFD700;color:#333;border-radius:20px;padding:3px 14px;font-size:11px;font-weight:700;white-space:nowrap\">🏆 BEST DEAL</div>' if is_best else \"\"\n\n        price_html = (\n            f'<div style=\"font-size:2rem;font-weight:800;color:{color};margin:10px 0 6px;letter-spacing:-0.5px\">{price}</div>'\n            if price else\n            '<div style=\"font-size:1rem;color:#aaa;font-weight:500;margin:10px 0 6px\">Not Available</div>'\n        )\n        title_html = f'<div style=\"font-size:11px;color:#666;margin-bottom:12px;line-height:1.4;min-height:28px\">{title}</div>' if title else '<div style=\"min-height:28px\"></div>'\n        btn_html = (\n            f'<a href=\"{link}\" target=\"_blank\" style=\"display:inline-block;background:{color};color:#fff;'\n            f'text-decoration:none;border-radius:50px;padding:8px 20px;font-size:13px;font-weight:600;'\n            f'margin-top:4px\">View on {r[\"platform\"]} →</a>'\n        ) if price else \"\"\n\n        cards += f'''\n        <div style=\"position:relative;background:{bg};border:{border};border-radius:20px;\n            padding:24px 18px 20px;text-align:center;flex:1;min-width:180px;max-width:240px;\n            box-shadow:{shadow};transition:transform .2s\">\n            {trophy}\n            <div style=\"font-size:28px;margin-bottom:6px\">{\"🛒\" if \"Amazon\" in r[\"platform\"] else \"🛍️\" if \"Flipkart\" in r[\"platform\"] else \"👗\"}</div>\n            <div style=\"font-size:16px;font-weight:700;color:{color}\">{r[\"platform\"]}</div>\n            {price_html}\n            {title_html}\n            {btn_html}\n        </div>'''\n\n    cards_row = f'<div style=\"display:flex;gap:16px;justify-content:center;flex-wrap:wrap;margin:8px 0\">{cards}</div>'\n\n    has_price = any(r.get(\"price\") for r in results)\n    no_token_warn = \"\" if has_price else (\n        '<div style=\"background:#FFF3CD;border:1px solid #FFC107;border-radius:12px;'\n        'padding:14px 18px;margin-bottom:18px;color:#856404;font-size:13px;text-align:center\">'\n        '⚠️ No prices found — make sure <b>HF_TOKEN</b> is set in Space Secrets '\n        '(Settings → Variables and secrets)</div>'\n    )\n\n    heading = (\n        f'<div style=\"text-align:center;margin-bottom:18px\">'\n        f'<span style=\"background:#E3F2FD;color:#1565C0;border-radius:20px;'\n        f'padding:6px 18px;font-size:13px;font-weight:600\">📦 Results for: {query}</span></div>'\n    )\n\n    return f\"{no_token_warn}{heading}{img_html}{cards_row}\"\n\n\ndef _build_links(query, results):\n    q = urllib.parse.quote_plus(query)\n    chips = \"\".join(\n        f'<a href=\"{r[\"search\"]}\" target=\"_blank\" style=\"display:inline-block;'\n        f'background:#fff;border:1.5px solid {r[\"color\"]};color:{r[\"color\"]};'\n        f'border-radius:20px;padding:6px 16px;font-size:13px;font-weight:600;'\n        f'text-decoration:none;margin:4px\">{r[\"platform\"]}</a>'\n        for r in results\n    )\n    chips += (\n        f'<a href=\"https://www.google.com/search?q={q}&tbm=shop\" target=\"_blank\" '\n        f'style=\"display:inline-block;background:#fff;border:1.5px solid #34A853;color:#34A853;'\n        f'border-radius:20px;padding:6px 16px;font-size:13px;font-weight:600;'\n        f'text-decoration:none;margin:4px\">🌐 Google Shopping</a>'\n    )\n    return f'<div style=\"padding:14px 0 6px\"><p style=\"color:#555;margin-bottom:10px;font-size:13px\">🔗 Search directly on each platform:</p>{chips}</div>'\n\n\nCSS = \"\"\"\n@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap');\n\n*, *::before, *::after { box-sizing: border-box; }\n\nbody, .gradio-container {\n    font-family: 'Inter', sans-serif !important;\n    background: linear-gradient(135deg, #E0F7FA 0%, #E8F5E9 40%, #E3F2FD 100%) !important;\n    min-height: 100vh;\n}\n\n.gradio-container { max-width: 1100px !important; margin: 0 auto !important; }\n\n/* Header */\n.app-header {\n    text-align: center;\n    padding: 36px 24px 20px;\n    background: linear-gradient(135deg, #ffffff 0%, #F0FFFE 100%);\n    border-radius: 0 0 28px 28px;\n    box-shadow: 0 4px 24px rgba(0,150,136,.12);\n    margin-bottom: 20px;\n}\n\n.app-title {\n    font-size: clamp(2rem, 5vw, 3.2rem);\n    font-weight: 800;\n    letter-spacing: -1.5px;\n    margin: 0;\n    background: linear-gradient(90deg, #FF9900 0%, #00ACC1 50%, #43A047 100%);\n    -webkit-background-clip: text;\n    -webkit-text-fill-color: transparent;\n    background-clip: text;\n}\n\n.app-subtitle { font-size: .95rem; color: #546E7A; margin-top: 8px; font-weight: 500; }\n\n.app-badges {\n    display: flex; gap: 8px; justify-content: center; margin-top: 14px; flex-wrap: wrap;\n}\n.badge {\n    background: linear-gradient(135deg, #E0F7FA, #E8F5E9);\n    border: 1px solid #B2DFDB;\n    border-radius: 20px; padding: 5px 14px;\n    font-size: .75rem; color: #00695C; font-weight: 600;\n}\n\n/* Input panel */\nlabel, .label-wrap { color: #263238 !important; font-weight: 600 !important; font-size: .9rem !important; }\n\ntextarea, input[type=text] {\n    background: #ffffff !important;\n    border: 2px solid #B2DFDB !important;\n    color: #263238 !important;\n    border-radius: 12px !important;\n    font-family: 'Inter', sans-serif !important;\n    font-size: 15px !important;\n    box-shadow: 0 2px 8px rgba(0,150,136,.06) !important;\n}\ntextarea:focus, input[type=text]:focus {\n    border-color: #00ACC1 !important;\n    outline: none !important;\n    box-shadow: 0 0 0 3px rgba(0,172,193,.15) !important;\n}\n\n/* Compare button */\n.compare-btn {\n    background: linear-gradient(135deg, #00ACC1, #00897B) !important;\n    color: white !important;\n    border: none !important;\n    border-radius: 14px !important;\n    font-size: 1rem !important;\n    font-weight: 700 !important;\n    padding: 14px 28px !important;\n    cursor: pointer !important;\n    width: 100% !important;\n    box-shadow: 0 4px 18px rgba(0,172,193,.35) !important;\n    letter-spacing: .3px !important;\n}\n.compare-btn:hover { filter: brightness(1.08) !important; }\n\n/* Tabs */\n.tab-nav button { color: #546E7A !important; font-weight: 600 !important; }\n.tab-nav button.selected { color: #00ACC1 !important; border-bottom-color: #00ACC1 !important; }\n\n/* AI output box */\ntextarea[readonly] {\n    background: #F1FFFE !important;\n    border: 2px solid #B2EBF2 !important;\n    color: #263238 !important;\n    line-height: 1.7 !important;\n}\n\n/* Checkbox */\n.wrap-inner {\n    background: #ffffff !important;\n    border-radius: 12px !important;\n    border: 2px solid #B2DFDB !important;\n}\n\n/* Footer */\n.app-footer {\n    text-align: center; padding: 20px; color: #78909C;\n    font-size: .8rem; margin-top: 10px;\n    border-top: 1px solid #B2DFDB;\n}\n\nfooter { display: none !important; }\n::-webkit-scrollbar { width: 6px; }\n::-webkit-scrollbar-track { background: #E0F7FA; }\n::-webkit-scrollbar-thumb { background: #80CBC4; border-radius: 3px; }\n\"\"\"\n\nHEADER_HTML = \"\"\"\n<div class=\"app-header\">\n    <h1 class=\"app-title\">Come &amp; Compare 🛒</h1>\n    <p class=\"app-subtitle\">AI-powered real-time price comparison across India's top e-commerce platforms</p>\n    <div class=\"app-badges\">\n        <span class=\"badge\">🤖 Qwen2.5-7B</span>\n        <span class=\"badge\">⚡ Under 32B Parameters</span>\n        <span class=\"badge\">🇮🇳 Amazon · Flipkart · Myntra</span>\n        <span class=\"badge\">🏆 HF Small Models Hackathon</span>\n    </div>\n</div>\n\"\"\"\n\nFOOTER_HTML = \"\"\"\n<div class=\"app-footer\">\n    Built for the HuggingFace Build Small Hackathon 2025 &nbsp;·&nbsp;\n    Model: Qwen/Qwen2.5-7B-Instruct (&lt;32B) &nbsp;·&nbsp;\n    Search: DuckDuckGo HTML\n</div>\n\"\"\"\n\nwith gr.Blocks(css=CSS, title=\"Come & Compare — Price Comparison AI\") as demo:\n    gr.HTML(HEADER_HTML)\n\n    with gr.Row():\n        with gr.Column(scale=1):\n            gr.Markdown(\"### 🔍 Search Product\")\n            product_name = gr.Textbox(\n                label=\"Product Name\",\n                placeholder='e.g. \"Nike Air Force 1 White\" or \"Samsung Galaxy S24 128GB\"',\n                lines=1,\n            )\n            product_details = gr.Textbox(\n                label=\"Additional Details (optional)\",\n                placeholder=\"e.g. size, color, model number...\",\n                lines=2,\n            )\n            platform_select = gr.CheckboxGroup(\n                choices=[\"Amazon.in\", \"Flipkart\", \"Myntra\"],\n                value=[\"Amazon.in\", \"Flipkart\", \"Myntra\"],\n                label=\"Platforms to Search\",\n            )\n            compare_btn = gr.Button(\"🔍 Compare Prices Now\", elem_classes=[\"compare-btn\"])\n            gr.Markdown(\"**💡 Tip:** Include brand + model for best results.\")\n\n        with gr.Column(scale=2):\n            with gr.Tabs():\n                with gr.TabItem(\"📊 Results\"):\n                    results_html = gr.HTML()\n                    links_html   = gr.HTML()\n                with gr.TabItem(\"🤖 AI Analysis\"):\n                    ai_output = gr.Textbox(\n                        label=\"AI Recommendation (Qwen2.5-7B)\",\n                        lines=15,\n                        interactive=False,\n                    )\n\n    gr.HTML(FOOTER_HTML)\n\n    gr.Examples(\n        examples=[\n            [\"iPhone 15 128GB\",      \"Apple, Black\"],\n            [\"Nike Air Force 1\",     \"White, Size 9 UK\"],\n            [\"Samsung 55 inch 4K TV\",\"Smart TV\"],\n            [\"boAt Airdopes 141\",    \"\"],\n            [\"OnePlus Nord CE 4\",    \"8GB RAM 128GB\"],\n        ],\n        inputs=[product_name, product_details],\n        label=\"🌟 Try these examples\",\n    )\n\n    compare_btn.click(\n        fn=compare_prices,\n        inputs=[product_name, product_details, platform_select],\n        outputs=[results_html, ai_output, links_html],\n    )\n\nif __name__ == \"__main__\":\n    demo.launch(share=False)\n",874      "app_signals": "get_client clean_price text clean_amazon_link raw_link clean_flipkart_link ddg_search query num normalize_query raw hf_get_prices hf_ai_analysis amazon flipkart myntra get_platform_link results domain platform get_platform_title get_product_image ddg_results compare_prices product_name product_details selected_platforms progress _find_best _build_cards image_url _build_links Qwen/Qwen2.5-7B-Instruct re.compile val r Come &amp; Compare 🛒 AI-powered real-time price comparison across India's top e-commerce platforms 🤖 Qwen2.5-7B ⚡ Under 32B Parameters 🇮🇳 Amazon · Flipkart · Myntra 🏆 HF Small Models Hackathon Built for the HuggingFace Build Small Hackathon 2025 &nbsp;·&nbsp; Model: Qwen/Qwen2.5-7B-Instruct (&lt;32B) &nbsp;·&nbsp; Search: DuckDuckGo HTML (?:₹|Rs\\.?|INR)\\s*([\\d,]+(?:\\.\\d+)?) /(?:dp|gp/product)/([A-Z0-9]{10}) User-Agent Accept-Language Accept Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 en-US,en;q=0.9 text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 os.environ.get PRICE_RE.search Extract ASIN and return a clean, working Amazon.in product URL. ASIN_RE.search Keep only essential Flipkart URL params, strip tracking. raw.strip join Return a clean, working link for the platform. gr.Progress product_name.strip desc urllib.parse.quote_plus any gr.Blocks css title gr.HTML gr.Examples examples inputs label compare_btn.click fn outputs __main__ demo.launch share HF_TOKEN InferenceClient token str replace urllib.parse.urlparse urllib.parse.parse_qs urllib.parse.urlencode doseq geturl requests.post data headers timeout BeautifulSoup client.chat_completion messages model max_tokens temperature strip message.content.strip text.splitlines lines.append r.get product_details.strip p.lower hf_prices.get results.append Need all 3 platforms for AI analysis. int ⚠️ No prices found — make sure HF_TOKEN is set in Space Secrets (Settings → Variables and secrets) 📦 Results for: <a href=\"https://www.google.com/search?q= 🔗 Search directly on each platform: gr.Row , https://www.amazon.in/dp/ pid lid marketplace https://html.duckduckgo.com/html/ lxml soup.select item.select_one ' print line.lower N/A url link requests.get soup.select_one ⚠️ Please enter a product name. ❌ No product entered. 🤖 Normalizing query with Qwen 7B... 🔍 Searching DuckDuckGo for product links... buy online india amazon flipkart myntra price 💰 Fetching prices via Qwen 7B... 🖼️ Finding product image... 🤖 Running AI analysis... color bg search price_key Amazon.in amazon.in #FF9900 #FFF8EE Flipkart flipkart.com #2874F0 #EEF4FF Myntra myntra.com #FF3F6C #FFF0F4 len ✅ Done! min key <img src=\" \" style=\"max-height:220px;max-width:300px;border-radius:16px;object-fit:contain;background:#fff;padding:12px;box-shadow:0 4px 20px rgba(0,0,0,.10)\" /> price 0 4px 16px rgba(0,0,0,.08) 🏆 BEST DEAL Not Available <div style=\"position:relative;background: ;border: ;border-radius:20px; padding:24px 18px 20px;text-align:center;flex:1;min-width:180px;max-width:240px; box-shadow: ;transition:transform .2s\"> <div style=\"font-size:16px;font-weight:700;color: \"> Come & Compare — Price Comparison AI gr.Column scale gr.Markdown gr.Textbox placeholder lines gr.CheckboxGroup choices value gr.Button elem_classes 🌟 Try these examples m.group float parsed._replace fragment .result .result__title .result__snippet .result__url .result__title a title_el.get_text snippet_el.get_text url_el.get_text link_el.get - : ⚠️ AI analysis unavailable: link.startswith amazon.com duckduckgo meta[property='og:image'] startswith https://www.amazon.in/s?k= https://www.flipkart.com/search?q= https://www.myntra.com/ 3px solid 2px solid 33 0 8px 28px 30 <div style=\"font-size:2rem;font-weight:800;color: ;margin:10px 0 6px;letter-spacing:-0.5px\"> <a href=\" \" target=\"_blank\" style=\"display:inline-block;background: ;color:#fff;text-decoration:none;border-radius:50px;padding:8px 20px;font-size:13px;font-weight:600;margin-top:4px\">View on → \" target=\"_blank\" style=\"display:inline-block;background:#fff;border:1.5px solid ;color: ;border-radius:20px;padding:6px 16px;font-size:13px;font-weight:600;text-decoration:none;margin:4px\"> ### 🔍 Search Product 🔍 Compare Prices Now **💡 Tip:** Include brand + model for best results. gr.Tabs ₹ q b kl in-en href duckduckgo.com urllib.parse.unquote snippet \" [normalize_query] [hf_get_prices] http #landingImage #imgBlkFront .a-dynamic-image content 🛒 Product Name e.g. \"Nike Air Force 1 White\" or \"Samsung Galaxy S24 128GB\" Additional Details (optional) e.g. size, color, model number... Platforms to Search gr.TabItem interactive iPhone 15 128GB Apple, Black Nike Air Force 1 White, Size 9 UK Samsung 55 inch 4K TV Smart TV boAt Airdopes 141 OnePlus Nord CE 4 8GB RAM 128GB role system You are a product search query cleaner. Output ONLY a short, clean product name (max 8 words) suitable for searching on Amazon India, Flipkart, and Myntra. No explanation, no punctuation at the end. user You are a real-time Indian e-commerce price assistant. You know current approximate prices on Amazon India, Flipkart, and Myntra. Reply with ONLY three lines in this exact format: Amazon: ₹PRICE Flipkart: ₹PRICE Myntra: ₹PRICE If a product is not sold on a platform, write N/A. No extra text. No explanations. You are a smart Indian price comparison assistant called 'Come & Compare'. https:// img.get og.get Amazon 🛍️ 👗 compare-btn 📊 Results 🤖 AI Analysis params.get Clean this product name: Current price of ' ' on Amazon India, Flipkart, Myntra? Product: Prices: Reply in this exact format: 🏆 BEST DEAL: [platform] at [price] 📊 PRICE RANKING: 1. [platform] — [price] 2. ... 💡 BUYING ADVICE: [2-3 line recommendation] ⚠️ NOTES: [any warnings about unavailable prices] src src.startswith data-a-dynamic-image lower AI Recommendation (Qwen2.5-7B) uddg json.loads max d.keys",875      "readme_len": 1872,876      "app_source_len": 23365,877      "app_signals_len": 5851878    },879    {880      "id": "build-small-hackathon/compliment-forest",881      "title": "The Compliment Forest",882      "summary": "Walk through a watercolor path of grounded encouragement.",883      "tags": [884        "build-small-hackathon",885        "gradio",886        "llama.cpp",887        "local-first",888        "watercolor"889      ],890      "models": [891        "build-small-hackathon/compliment-forest-minicpm5-1b",892        "build-small-hackathon/compliment-forest-flux-lora"893      ],894      "datasets": [895        "build-small-hackathon/compliment-forest-sft",896        "build-small-hackathon/compliment-forest-watercolor",897        "build-small-hackathon/compliment-forest-traces"898      ],899      "sdk": "gradio",900      "license": "",901      "likes": 0,902      "url": "https://huggingface.co/spaces/build-small-hackathon/compliment-forest",903      "app_file": "app.py",904      "readme_raw": "---\ntitle: The Compliment Forest\nemoji: 🌿\ncolorFrom: green\ncolorTo: yellow\nsdk: gradio\nsdk_version: 6.16.0\npython_version: 3.12\napp_file: app.py\nfullWidth: true\nheader: mini\npinned: true\nshort_description: Walk through a watercolor path of grounded encouragement.\nmodels:\n- build-small-hackathon/compliment-forest-minicpm5-1b\n- build-small-hackathon/compliment-forest-flux-lora\ndatasets:\n- build-small-hackathon/compliment-forest-sft\n- build-small-hackathon/compliment-forest-watercolor\n- build-small-hackathon/compliment-forest-traces\ntags:\n- gradio\n- build-small-hackathon\n- local-first\n- watercolor\n- llama.cpp\n---\n\n# The Compliment Forest\n\nType a name and a situation, then walk through a progressive watercolor path.\nEach clearing pairs a creature with grounded encouragement, a reflection, and a\ncopyable tiny spell.\n\nThe live Space uses the deterministic local demo backend so it remains fast and\navailable on CPU hardware. The same application supports the published\nMiniCPM5-1B GGUF through a local `llama.cpp` server and FLUX.1-dev with the\npublished watercolor LoRA by setting `CF_TEXT_BACKEND=llama_cpp` and\n`CF_IMAGE_BACKEND=flux`. No hosted inference API is called at runtime.\n\n## Published artifacts\n\n- Text model: `build-small-hackathon/compliment-forest-minicpm5-1b`\n- Text adapter: `build-small-hackathon/compliment-forest-minicpm5-1b-lora`\n- Text SFT data: `build-small-hackathon/compliment-forest-sft`\n- Watercolor LoRA: `build-small-hackathon/compliment-forest-flux-lora`\n- Watercolor data: `build-small-hackathon/compliment-forest-watercolor`\n- Linked-model traces: `build-small-hackathon/compliment-forest-traces`\n\nThis is whimsical encouragement, not therapy or a substitute for professional\nsupport. Crisis and acute-risk inputs are routed to human support instead of\ngenerating a forest.\n",905      "readme_body": "# The Compliment Forest\n\nType a name and a situation, then walk through a progressive watercolor path.\nEach clearing pairs a creature with grounded encouragement, a reflection, and a\ncopyable tiny spell.\n\nThe live Space uses the deterministic local demo backend so it remains fast and\navailable on CPU hardware. The same application supports the published\nMiniCPM5-1B GGUF through a local `llama.cpp` server and FLUX.1-dev with the\npublished watercolor LoRA by setting `CF_TEXT_BACKEND=llama_cpp` and\n`CF_IMAGE_BACKEND=flux`. No hosted inference API is called at runtime.\n\n## Published artifacts\n\n- Text model: `build-small-hackathon/compliment-forest-minicpm5-1b`\n- Text adapter: `build-small-hackathon/compliment-forest-minicpm5-1b-lora`\n- Text SFT data: `build-small-hackathon/compliment-forest-sft`\n- Watercolor LoRA: `build-small-hackathon/compliment-forest-flux-lora`\n- Watercolor data: `build-small-hackathon/compliment-forest-watercolor`\n- Linked-model traces: `build-small-hackathon/compliment-forest-traces`\n\nThis is whimsical encouragement, not therapy or a substitute for professional\nsupport. Crisis and acute-risk inputs are routed to human support instead of\ngenerating a forest.",906      "readme_frontmatter": {907        "title": "The Compliment Forest",908        "emoji": "🌿",909        "colorFrom": "green",910        "colorTo": "yellow",911        "sdk": "gradio",912        "sdk_version": "6.16.0",913        "python_version": "3.12",914        "app_file": "app.py",915        "fullWidth": "true",916        "header": "mini",917        "pinned": "true",918        "short_description": "Walk through a watercolor path of grounded encouragement.",919        "models": "",920        "datasets": "",921        "tags": ""922      },923      "app_source": "import sys\nfrom pathlib import Path\n\nsys.path.insert(0, str(Path(__file__).resolve().parent / \"src\"))\n\nfrom compliment_forest.server import create_app\n\napp = create_app()\ndemo = app\n\nif __name__ == \"__main__\":\n    import uvicorn\n\n    uvicorn.run(app, host=\"0.0.0.0\", port=7860)\n",924      "app_signals": "sys.path.insert create_app str __main__ uvicorn.run host port src 0.0.0.0 resolve Path",925      "readme_len": 1194,926      "app_source_len": 278,927      "app_signals_len": 86928    },929    {930      "id": "build-small-hackathon/ContextForge",931      "title": "ContextForge",932      "summary": "",933      "tags": [934        "gradio",935        "region:us"936      ],937      "models": [],938      "datasets": [],939      "sdk": "gradio",940      "license": "",941      "likes": 0,942      "url": "https://huggingface.co/spaces/build-small-hackathon/ContextForge",943      "app_file": "app.py",944      "readme_raw": "---\ntitle: ContextForge\nemoji: ⚒️\ncolorFrom: blue\ncolorTo: green\nsdk: gradio\nsdk_version: 5.50.0\napp_file: app.py\npinned: false\n---\n\n# ContextForge / Agent Prompt Compiler\n\nContextForge compiles messy software, app, and agent ideas into executable prompt architectures. It is a compiler pipeline, not a generic prompt generator.\n\n**GitHub:** https://github.com/rthgit/ContextForge\n\n**Competition Gradio Space:** https://huggingface.co/spaces/build-small-hackathon/ContextForge\n\n**Backup Gradio Space:** https://huggingface.co/spaces/RthItalia/ContextForge\n\n**Demo video:** https://raw.githubusercontent.com/rthgit/ContextForge/main/artifacts/contextforge-demo.mp4\n\n**Tagline:** From fuzzy brief to build-ready agent blueprint.\n\n## Backyard AI Fit\n\n- Built for real builders using AI coding agents.\n- Real problem: vague briefs make Codex and other agents produce wrong code, generic UI, or incomplete workflows.\n- Real use evidence: this architecture was used to coordinate Trollsona development, including UI refactor, model cascade, QA, packaging, and video automation.\n- Small-model fit: ContextForge decomposes a hard prompt-writing task into seven smaller calls so a small model can handle it.\n\nThe backend always executes seven isolated modules sequentially:\n\n1. intake analysis\n2. topology decision\n3. Vital Few / Vital Spot extraction\n4. reasoning architecture selection\n5. prompt pack generation\n6. QA / repair\n7. final assembly\n\nEvery module attempts its own small-model call. If one call fails, only that stage uses a deterministic fallback and the pipeline continues. Runtime Details shows the source used by every stage.\n\nEach module also has a bounded token budget appropriate to its contract. `CONTEXTFORGE_MAX_NEW_TOKENS` is the global ceiling, while stage budgets keep the seven-call CPU path practical.\n\n## Topologies\n\n- Single Prompt\n- Cascade\n- Context Pack\n- Agent Workflow\n\nAuto topology uses Cascade when multiple expertise areas or dependent outputs are required. Agent Workflow is preferred for agentic or critical-risk work. Context Pack stabilizes incomplete briefs.\n\n## Safety\n\n- Private reasoning remains internal.\n- Generated prompts never request full chain of thought.\n- Controlled Tree of Thought exposes only `strategy | upside | risk | cost | selected`.\n- Public reasoning fields are limited to decision summary, assumptions, risks, verification steps, and final answer.\n- QA repairs missing tags, contracts, verification, repair logic, and unsafe reasoning requests.\n\n## Runtime\n\nRecommended Hugging Face Space variables:\n\n```text\nCONTEXTFORGE_ENABLE_MODEL=1\nCONTEXTFORGE_MODEL_ID=Qwen/Qwen2.5-0.5B-Instruct\nCONTEXTFORGE_MID_MODEL_ID=RthItalia/nano_compact_3b_qkvfp16\nCONTEXTFORGE_HIGH_MODEL_ID=Qwen/Qwen3-32B\nCONTEXTFORGE_MAX_NEW_TOKENS=1800\n```\n\nRuntime selection:\n\n1. high model only when CUDA is available\n2. compact mid model when CUDA is available\n3. Qwen 0.5B on public CPU Space\n4. deterministic stage-level fallback\n\nFor a fast local deterministic run:\n\n```powershell\n$env:CONTEXTFORGE_ENABLE_MODEL='0'\npython app.py\n```\n\n## Local QA\n\n```powershell\npython -m py_compile app.py\npython test_contextforge.py\npython app.py\n```\n\nThe QA script verifies all four topologies, independent stage execution, required tags, chain-of-thought safety, controlled Tree of Thought output, and stage-level fallback continuity.\n\n## Demo Assets\n\n- Demo video: `artifacts/contextforge-demo.mp4`\n- Recording guide: `artifacts/VIDEO_RECORDING_GUIDE.md`\n- Submission pack: `SUBMISSION.md`\n",945      "readme_body": "# ContextForge / Agent Prompt Compiler\n\nContextForge compiles messy software, app, and agent ideas into executable prompt architectures. It is a compiler pipeline, not a generic prompt generator.\n\n**GitHub:** https://github.com/rthgit/ContextForge\n\n**Competition Gradio Space:** https://huggingface.co/spaces/build-small-hackathon/ContextForge\n\n**Backup Gradio Space:** https://huggingface.co/spaces/RthItalia/ContextForge\n\n**Demo video:** https://raw.githubusercontent.com/rthgit/ContextForge/main/artifacts/contextforge-demo.mp4\n\n**Tagline:** From fuzzy brief to build-ready agent blueprint.\n\n## Backyard AI Fit\n\n- Built for real builders using AI coding agents.\n- Real problem: vague briefs make Codex and other agents produce wrong code, generic UI, or incomplete workflows.\n- Real use evidence: this architecture was used to coordinate Trollsona development, including UI refactor, model cascade, QA, packaging, and video automation.\n- Small-model fit: ContextForge decomposes a hard prompt-writing task into seven smaller calls so a small model can handle it.\n\nThe backend always executes seven isolated modules sequentially:\n\n1. intake analysis\n2. topology decision\n3. Vital Few / Vital Spot extraction\n4. reasoning architecture selection\n5. prompt pack generation\n6. QA / repair\n7. final assembly\n\nEvery module attempts its own small-model call. If one call fails, only that stage uses a deterministic fallback and the pipeline continues. Runtime Details shows the source used by every stage.\n\nEach module also has a bounded token budget appropriate to its contract. `CONTEXTFORGE_MAX_NEW_TOKENS` is the global ceiling, while stage budgets keep the seven-call CPU path practical.\n\n## Topologies\n\n- Single Prompt\n- Cascade\n- Context Pack\n- Agent Workflow\n\nAuto topology uses Cascade when multiple expertise areas or dependent outputs are required. Agent Workflow is preferred for agentic or critical-risk work. Context Pack stabilizes incomplete briefs.\n\n## Safety\n\n- Private reasoning remains internal.\n- Generated prompts never request full chain of thought.\n- Controlled Tree of Thought exposes only `strategy | upside | risk | cost | selected`.\n- Public reasoning fields are limited to decision summary, assumptions, risks, verification steps, and final answer.\n- QA repairs missing tags, contracts, verification, repair logic, and unsafe reasoning requests.\n\n## Runtime\n\nRecommended Hugging Face Space variables:\n\n```text\nCONTEXTFORGE_ENABLE_MODEL=1\nCONTEXTFORGE_MODEL_ID=Qwen/Qwen2.5-0.5B-Instruct\nCONTEXTFORGE_MID_MODEL_ID=RthItalia/nano_compact_3b_qkvfp16\nCONTEXTFORGE_HIGH_MODEL_ID=Qwen/Qwen3-32B\nCONTEXTFORGE_MAX_NEW_TOKENS=1800\n```\n\nRuntime selection:\n\n1. high model only when CUDA is available\n2. compact mid model when CUDA is available\n3. Qwen 0.5B on public CPU Space\n4. deterministic stage-level fallback\n\nFor a fast local deterministic run:\n\n```powershell\n$env:CONTEXTFORGE_ENABLE_MODEL='0'\npython app.py\n```\n\n## Local QA\n\n```powershell\npython -m py_compile app.py\npython test_contextforge.py\npython app.py\n```\n\nThe QA script verifies all four topologies, independent stage execution, required tags, chain-of-thought safety, controlled Tree of Thought output, and stage-level fallback continuity.\n\n## Demo Assets\n\n- Demo video: `artifacts/contextforge-demo.mp4`\n- Recording guide: `artifacts/VIDEO_RECORDING_GUIDE.md`\n- Submission pack: `SUBMISSION.md`",946      "readme_frontmatter": {947        "title": "ContextForge",948        "emoji": "⚒️",949        "colorFrom": "blue",950        "colorTo": "green",951        "sdk": "gradio",952        "sdk_version": "5.50.0",953        "app_file": "app.py",954        "pinned": "false"955      },956      "app_source": "from __future__ import annotations\n\nimport json\nimport os\nimport re\nimport time\nfrom dataclasses import dataclass\nfrom functools import lru_cache\nfrom typing import Any, Callable\n\n\nAPP_TITLE = \"ContextForge\"\nAPP_SUBTITLE = \"From fuzzy brief to build-ready agent blueprint.\"\nDEFAULT_MODEL_ID = \"Qwen/Qwen2.5-0.5B-Instruct\"\nDEFAULT_MID_MODEL_ID = \"RthItalia/nano_compact_3b_qkvfp16\"\nDEFAULT_HIGH_MODEL_ID = \"Qwen/Qwen3-32B\"\nREQUIRED_PROMPT_TAGS = [\n    \"ROLE\",\n    \"COGNITIVE_LAYERS\",\n    \"KAHNEMAN_SYSTEM2\",\n    \"PARETO_80_20\",\n    \"VITAL_SPOT\",\n    \"REASONING_PROTOCOL\",\n    \"AGENTIC_LOOP\",\n    \"ACTION\",\n    \"FORMAT_AND_TARGET\",\n    \"QA_CHECKS\",\n]\nTOPOLOGIES = [\"Auto\", \"Single Prompt\", \"Cascade\", \"Context Pack\", \"Agent Workflow\"]\nREASONING_LAYERS = [\n    \"CRAFT\",\n    \"Kahneman System 2\",\n    \"Pareto 80/20\",\n    \"Agentic Loop\",\n    \"Tree of Thought controlled\",\n    \"Private CoT\",\n    \"Self-Correction\",\n    \"Sentinel Recovery\",\n]\nSTAGE_NAMES = [\n    \"intake_analysis\",\n    \"topology_decision\",\n    \"vital_structure\",\n    \"reasoning_architecture\",\n    \"prompt_pack_generation\",\n    \"qa_repair\",\n    \"final_assembly\",\n]\nSTAGE_TOKEN_BUDGETS = {\n    \"intake_analysis\": 180,\n    \"topology_decision\": 140,\n    \"vital_structure\": 180,\n    \"reasoning_architecture\": 240,\n    \"prompt_pack_generation\": 520,\n    \"qa_repair\": 260,\n    \"final_assembly\": 260,\n}\n\n\ndef parse_bool_env(name: str, default: bool = False) -> bool:\n    raw = os.getenv(name)\n    if raw is None:\n        return default\n    return raw.strip().lower() in {\"1\", \"true\", \"yes\", \"on\"}\n\n\ndef parse_int_env(name: str, default: int, minimum: int, maximum: int) -> int:\n    try:\n        value = int(os.getenv(name, str(default)))\n    except ValueError:\n        value = default\n    return max(minimum, min(maximum, value))\n\n\nMODEL_ENABLED = parse_bool_env(\"CONTEXTFORGE_ENABLE_MODEL\", False)\nMODEL_ID = os.getenv(\"CONTEXTFORGE_MODEL_ID\", DEFAULT_MODEL_ID)\nMID_MODEL_ID = os.getenv(\"CONTEXTFORGE_MID_MODEL_ID\", DEFAULT_MID_MODEL_ID)\nHIGH_MODEL_ID = os.getenv(\"CONTEXTFORGE_HIGH_MODEL_ID\", DEFAULT_HIGH_MODEL_ID)\nMAX_NEW_TOKENS = parse_int_env(\"CONTEXTFORGE_MAX_NEW_TOKENS\", 1800, 256, 4096)\nMAX_INPUT_CHARS = parse_int_env(\"CONTEXTFORGE_MAX_INPUT_CHARS\", 12000, 2000, 40000)\n\n\n@dataclass\nclass StageResult:\n    data: dict[str, Any]\n    source: str\n    model_id: str\n    elapsed_ms: int\n    note: str = \"\"\n\n    def runtime_row(self, stage: str) -> dict[str, Any]:\n        return {\n            \"stage\": stage,\n            \"source\": self.source,\n            \"model_id\": self.model_id,\n            \"fallback_reason\": self.note if self.source == \"deterministic_fallback\" else \"\",\n            \"duration_ms\": self.elapsed_ms,\n        }\n\n\n_RUNTIME_TRACE: list[dict[str, Any]] = []\n\n\ndef clean_text(value: Any, limit: int = 4000) -> str:\n    text = \"\" if value is None else str(value)\n    text = text.replace(\"\\x00\", \" \")\n    text = re.sub(r\"[ \\t]+\", \" \", text)\n    text = re.sub(r\"\\n{3,}\", \"\\n\\n\", text).strip()\n    return text[:limit]\n\n\ndef clean_list(value: Any, limit: int = 8) -> list[str]:\n    if isinstance(value, str):\n        candidates = re.split(r\"[,;\\n]+\", value)\n    elif isinstance(value, list):\n        candidates = value\n    else:\n        candidates = []\n    result = []\n    for item in candidates:\n        cleaned = clean_text(item, 240)\n        if cleaned and cleaned not in result:\n            result.append(cleaned)\n    return result[:limit]\n\n\ndef json_text(value: Any) -> str:\n    return json.dumps(value, ensure_ascii=False, indent=2, sort_keys=True)\n\n\ndef parse_json_object(raw: str) -> dict[str, Any] | None:\n    decoder = json.JSONDecoder()\n    for match in re.finditer(r\"\\{\", raw or \"\"):\n        try:\n            parsed, _ = decoder.raw_decode(raw[match.start() :])\n        except json.JSONDecodeError:\n            continue\n        if isinstance(parsed, dict):\n            return parsed\n    return None\n\n\ndef merge_known(fallback: dict[str, Any], candidate: dict[str, Any] | None) -> dict[str, Any]:\n    if not candidate:\n        return fallback\n    merged = dict(fallback)\n    for key, fallback_value in fallback.items():\n        candidate_value = candidate.get(key)\n        if candidate_value is None:\n            continue\n        if isinstance(fallback_value, list):\n            items = clean_list(candidate_value, max(3, len(fallback_value) + 3))\n            if items:\n                merged[key] = items\n        elif isinstance(fallback_value, dict) and isinstance(candidate_value, dict):\n            merged[key] = {**fallback_value, **candidate_value}\n        elif isinstance(fallback_value, int):\n            try:\n                merged[key] = int(candidate_value)\n            except (TypeError, ValueError):\n                pass\n        else:\n            cleaned = clean_text(candidate_value, 16000)\n            if cleaned:\n                merged[key] = cleaned\n    return merged\n\n\ndef model_candidates() -> list[tuple[str, str, bool]]:\n    candidates = [\n        (\"high\", HIGH_MODEL_ID, True),\n        (\"mid\", MID_MODEL_ID, True),\n        (\"public_cpu\", MODEL_ID, False),\n    ]\n    seen: set[str] = set()\n    return [\n        item\n        for item in candidates\n        if item[1].strip() and not (item[1] in seen or seen.add(item[1]))\n    ]\n\n\n@lru_cache(maxsize=1)\ndef load_model() -> tuple[Any | None, Any | None, str, str]:\n    if not MODEL_ENABLED:\n        return None, None, \"disabled\", \"model disabled by CONTEXTFORGE_ENABLE_MODEL\"\n    try:\n        import torch\n        from transformers import AutoModelForCausalLM, AutoTokenizer\n    except Exception as exc:\n        return None, None, \"unavailable\", f\"dependencies unavailable: {type(exc).__name__}: {exc}\"\n\n    failures: list[str] = []\n    for role, candidate_id, requires_cuda in model_candidates():\n        if requires_cuda and not torch.cuda.is_available():\n            failures.append(f\"{role}: CUDA unavailable\")\n            continue\n        try:\n            tokenizer = AutoTokenizer.from_pretrained(candidate_id, trust_remote_code=True, use_fast=True)\n            if tokenizer.pad_token_id is None and tokenizer.eos_token_id is not None:\n                tokenizer.pad_token = tokenizer.eos_token\n            kwargs: dict[str, Any] = {\"trust_remote_code\": True, \"low_cpu_mem_usage\": True}\n            if torch.cuda.is_available():\n                kwargs[\"device_map\"] = \"cuda\"\n                kwargs[\"torch_dtype\"] = torch.float16\n            model = AutoModelForCausalLM.from_pretrained(candidate_id, **kwargs)\n            model.eval()\n            return tokenizer, model, candidate_id, f\"selected {role}; \" + \"; \".join(failures)\n        except Exception as exc:\n            failures.append(f\"{role}: {type(exc).__name__}: {exc}\")\n    return None, None, \"unavailable\", \" | \".join(failures) or \"no model candidates\"\n\n\ndef format_chat_prompt(tokenizer: Any, stage: str, instruction: str, payload: dict[str, Any]) -> str:\n    system = (\n        \"You are one isolated module inside ContextForge, an agent prompt compiler. \"\n        \"Return only a valid JSON object. Private reasoning internal only. \"\n        \"Never reveal chain of thought, hidden branches, or internal deliberation. \"\n        \"Public fields may contain only decision summaries, assumptions, risks, verification steps, and outputs.\"\n    )\n    user = f\"MODULE: {stage}\\nTASK:\\n{instruction}\\nINPUT:\\n{json_text(payload)}\"\n    try:\n        if getattr(tokenizer, \"chat_template\", None):\n            return tokenizer.apply_chat_template(\n                [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": user}],\n                tokenize=False,\n                add_generation_prompt=True,\n            )\n    except Exception:\n        pass\n    return f\"{system}\\n\\n{user}\\n\\nJSON:\"\n\n\ndef generate_json(stage: str, instruction: str, payload: dict[str, Any]) -> tuple[dict[str, Any] | None, str, str]:\n    tokenizer, model, selected_id, load_note = load_model()\n    if tokenizer is None or model is None:\n        return None, selected_id, load_note\n    try:\n        import torch\n\n        prompt = format_chat_prompt(tokenizer, stage, instruction, payload)\n        inputs = tokenizer(prompt, return_tensors=\"pt\", truncation=True, max_length=6144)\n        device = getattr(model, \"device\", None)\n        if device is not None and str(device) != \"meta\":\n            inputs = {key: value.to(device) for key, value in inputs.items()}\n        with torch.no_grad():\n            output_ids = model.generate(\n                **inputs,\n                max_new_tokens=min(MAX_NEW_TOKENS, STAGE_TOKEN_BUDGETS.get(stage, MAX_NEW_TOKENS)),\n                do_sample=False,\n                repetition_penalty=1.05,\n                pad_token_id=tokenizer.eos_token_id,\n            )\n        raw = tokenizer.decode(output_ids[0][inputs[\"input_ids\"].shape[-1] :], skip_special_tokens=True)\n        parsed = parse_json_object(raw)\n        if parsed is None:\n            return None, selected_id, f\"{load_note}; invalid JSON output\"\n        return parsed, selected_id, load_note\n    except Exception as exc:\n        return None, selected_id, f\"{load_note}; generation failed: {type(exc).__name__}: {exc}\"\n\n\ndef run_stage(\n    stage: str,\n    instruction: str,\n    payload: dict[str, Any],\n    fallback_factory: Callable[[], dict[str, Any]],\n    validator: Callable[[dict[str, Any]], dict[str, Any]] | None = None,\n) -> dict[str, Any]:\n    started = time.perf_counter()\n    fallback = fallback_factory()\n    candidate, selected_id, note = generate_json(stage, instruction, payload)\n    source = \"small_model\"\n    if candidate is None:\n        data = fallback\n        source = \"deterministic_fallback\"\n    else:\n        data = merge_known(fallback, candidate)\n    if validator:\n        try:\n            data = validator(data)\n        except Exception as exc:\n            data = fallback\n            source = \"deterministic_fallback\"\n            note = f\"{note}; validation failed: {type(exc).__name__}: {exc}\"\n    elapsed_ms = round((time.perf_counter() - started) * 1000)\n    result = StageResult(data=data, source=source, model_id=selected_id, elapsed_ms=elapsed_ms, note=note)\n    _RUNTIME_TRACE.append(result.runtime_row(stage))\n    return result.data\n\n\ndef infer_domain(payload: dict[str, Any]) -> str:\n    haystack = \" \".join(clean_text(v, 1000).lower() for v in payload.values() if isinstance(v, str))\n    domains = [\n        (\"software engineering\", [\"api\", \"code\", \"software\", \"app\", \"backend\", \"frontend\"]),\n        (\"agent systems\", [\"agent\", \"workflow\", \"tool\", \"autonomous\", \"mcp\"]),\n        (\"data and analytics\", [\"data\", \"dataset\", \"analytics\", \"dashboard\", \"sql\"]),\n        (\"creative production\", [\"story\", \"creative\", \"brand\", \"content\", \"design\"]),\n    ]\n    for domain, signals in domains:\n        if any(signal in haystack for signal in signals):\n            return domain\n    return \"general knowledge work\"\n\n\ndef analyze_intake(input_payload: dict[str, Any]) -> dict[str, Any]:\n    payload = {key: clean_text(value, MAX_INPUT_CHARS) if isinstance(value, str) else value for key, value in input_payload.items()}\n\n    def fallback() -> dict[str, Any]:\n        missing = [\n            label\n            for key, label in [\n                (\"project_idea\", \"project idea\"),\n                (\"target_user\", \"target user\"),\n                (\"build_target\", \"build target\"),\n                (\"output_contract\", \"output contract\"),\n                (\"verification_criteria\", \"verification criteria\"),\n            ]\n            if not clean_text(payload.get(key), 200)\n        ]\n        complexity_signals = sum(\n            bool(clean_text(payload.get(key), 300))\n            for key in [\"user_context\", \"project_context\", \"technical_context\", \"constraints\", \"inputs_files\", \"failure_modes\"]\n        )\n        return {\n            \"domain\": infer_domain(payload),\n            \"task_type\": \"design and implementation planning\",\n            \"risk_level\": clean_text(payload.get(\"risk_level\"), 40) or \"Medium\",\n            \"input_type\": \"structured brief with free-text context\",\n            \"output_type\": clean_text(payload.get(\"build_target\"), 200) or \"executable prompt architecture\",\n            \"missing_information\": missing,\n            \"complexity\": \"high\" if complexity_signals >= 5 else \"medium\" if complexity_signals >= 2 else \"low\",\n            \"decision_summary\": \"Normalize the brief into an explicit compiler input before selecting topology.\",\n            \"assumptions\": [\"Unspecified details may be resolved conservatively during execution.\"],\n            \"risks\": clean_list(payload.get(\"failure_modes\"), 5) or [\"Ambiguous output contract\", \"Insufficient verification criteria\"],\n        }\n\n    instruction = (\n        \"Classify domain, task type, risk level, input type, output type, missing information, complexity, \"\n        \"decision summary, assumptions, and risks. Do not solve the task.\"\n    )\n    return run_stage(\"intake_analysis\", instruction, payload, fallback)\n\n\ndef decide_topology(analysis: dict[str, Any], user_topology_choice: str) -> dict[str, Any]:\n    choice = user_topology_choice if user_topology_choice in TOPOLOGIES else \"Auto\"\n\n    def fallback() -> dict[str, Any]:\n        risk = clean_text(analysis.get(\"risk_level\"), 40).lower()\n        complexity = clean_text(analysis.get(\"complexity\"), 40).lower()\n        domain = clean_text(analysis.get(\"domain\"), 100).lower()\n        if choice != \"Auto\":\n            topology = choice\n            reason = \"Explicit user topology choice.\"\n        elif \"agent\" in domain or risk == \"critical\":\n            topology = \"Agent Workflow\"\n            reason = \"Agentic or critical-risk work benefits from explicit execution and recovery states.\"\n        elif complexity == \"high\":\n            topology = \"Cascade\"\n            reason = \"Multiple context areas and dependent outputs require sequential specialist prompts.\"\n        elif analysis.get(\"missing_information\"):\n            topology = \"Context Pack\"\n            reason = \"A reusable context contract should stabilize unresolved inputs.\"\n        else:\n            topology = \"Single Prompt\"\n            reason = \"The task is bounded enough for one complete execution contract.\"\n        roles_by_topology = {\n            \"Single Prompt\": [\"Lead Executor\"],\n            \"Cascade\": [\"Brief Analyst\", \"Solution Architect\", \"Builder\", \"Verifier\"],\n            \"Context Pack\": [\"Context Curator\", \"Execution Prompt Author\"],\n            \"Agent Workflow\": [\"Planner\", \"Executor\", \"Verifier\", \"Recovery Sentinel\"],\n        }\n        roles = roles_by_topology[topology]\n        return {\n            \"topology\": topology,\n            \"reason\": reason,\n            \"number_of_prompts\": len(roles),\n            \"roles\": roles,\n            \"handoff_contract\": \"Each stage receives structured upstream output and returns a verifiable downstream artifact.\",\n        }\n\n    instruction = (\n        \"Choose Single Prompt, Cascade, Context Pack, or Agent Workflow. Use Cascade when multiple expertise areas \"\n        \"are required, task A feeds task B, or more than six unrelated ACTION sections are required. Respect an \"\n        \"explicit non-Auto user choice. Return topology, reason, number_of_prompts, roles, and handoff_contract.\"\n    )\n    return run_stage(\"topology_decision\", instruction, {\"analysis\": analysis, \"user_choice\": choice}, fallback)\n\n\ndef extract_vital_structure(analysis: dict[str, Any], topology: dict[str, Any]) -> dict[str, Any]:\n    def fallback() -> dict[str, Any]:\n        vital_few = [\n            \"A precise output contract\",\n            \"A topology matched to dependency structure\",\n            \"Verifiable acceptance criteria\",\n            \"Explicit failure and recovery behavior\",\n        ]\n        if analysis.get(\"missing_information\"):\n            vital_few.insert(0, \"Resolution of critical missing context\")\n        return {\n            \"vital_few\": vital_few[:5],\n            \"vital_spot\": \"The output contract: if it is ambiguous, every downstream prompt can appear complete while producing the wrong artifact.\",\n            \"vital_spot_guard\": \"Restate the output contract before execution and fail QA when required fields or verification evidence are absent.\",\n            \"decision_summary\": f\"Optimize the {topology.get('topology', 'selected')} architecture around a small set of quality drivers.\",\n        }\n\n    instruction = (\n        \"Extract three to five Vital Few elements that determine most output quality and one Vital Spot whose failure \"\n        \"breaks the workflow. Include a concrete guard for the Vital Spot.\"\n    )\n    return run_stage(\"vital_structure\", instruction, {\"analysis\": analysis, \"topology\": topology}, fallback)\n\n\ndef select_reasoning_architecture(\n    analysis: dict[str, Any],\n    topology: dict[str, Any],\n    selected_layers: list[str],\n) -> dict[str, Any]:\n    selected = [layer for layer in selected_layers if layer in REASONING_LAYERS]\n\n    def fallback() -> dict[str, Any]:\n        layers = selected or [\"CRAFT\", \"Pareto 80/20\", \"Private CoT\", \"Self-Correction\", \"Sentinel Recovery\"]\n        if topology.get(\"topology\") in {\"Cascade\", \"Agent Workflow\"} and \"Agentic Loop\" not in layers:\n            layers.append(\"Agentic Loop\")\n        if clean_text(analysis.get(\"risk_level\"), 30).lower() in {\"high\", \"critical\"} and \"Kahneman System 2\" not in layers:\n            layers.append(\"Kahneman System 2\")\n        configurations = {\n            layer: {\n                \"purpose\": {\n                    \"CRAFT\": \"Bind context, role, action, format, and target.\",\n                    \"Kahneman System 2\": \"Slow down at consequential decisions and verify assumptions.\",\n                    \"Pareto 80/20\": \"Prioritize the few actions that drive most value.\",\n                    \"Agentic Loop\": \"Plan, act, observe, verify, and recover.\",\n                    \"Tree of Thought controlled\": \"Compare strategies without exposing hidden branches.\",\n                    \"Private CoT\": \"Keep reasoning internal and publish only summaries and evidence.\",\n                    \"Self-Correction\": \"Repair failed checks before final output.\",\n                    \"Sentinel Recovery\": \"Detect blocked or degraded states and continue safely.\",\n                }[layer],\n                \"public_output\": \"decision summary, assumptions, risks, verification steps, final answer\",\n            }\n            for layer in layers\n        }\n        return {\n            \"selected_layers\": layers,\n            \"configurations\": configurations,\n            \"private_reasoning_policy\": \"Private reasoning internal only.\",\n            \"tree_of_thought_policy\": \"Expose only: strategy | upside | risk | cost | selected.\",\n        }\n\n    instruction = (\n        \"Select and configure only useful reasoning layers. Private CoT must remain internal. Controlled Tree of \"\n        \"Thought may expose only strategy, upside, risk, cost, selected. Return selected_layers, configurations, \"\n        \"private_reasoning_policy, and tree_of_thought_policy.\"\n    )\n    return run_stage(\n        \"reasoning_architecture\",\n        instruction,\n        {\"analysis\": analysis, \"topology\": topology, \"selected_layers\": selected},\n        fallback,\n    )\n\n\ndef prompt_block(\n    title: str,\n    role: str,\n    action: str,\n    analysis: dict[str, Any],\n    topology: dict[str, Any],\n    vital: dict[str, Any],\n    reasoning_architecture: dict[str, Any],\n    output_contract: str,\n    verification_criteria: str,\n) -> str:\n    layers = \", \".join(reasoning_architecture.get(\"selected_layers\", []))\n    vital_few = \"\\n\".join(f\"- {item}\" for item in vital.get(\"vital_few\", []))\n    return f\"\"\"# {title}\n\n[ROLE]\nYou are {role}. Own the assigned artifact and its verification. Do not impersonate other stages.\n\n[COGNITIVE_LAYERS]\nUse: {layers}. Private reasoning internal only. Public output may include only decision summary, assumptions, risks, verification steps, and final answer.\n\n[KAHNEMAN_SYSTEM2]\nPause before consequential decisions. Check assumptions, dependency order, risk, and evidence before committing.\n\n[PARETO_80_20]\nPrioritize these Vital Few:\n{vital_few}\n\n[VITAL_SPOT]\n{vital.get(\"vital_spot\", \"The output contract is the single failure point.\")}\nGuard: {vital.get(\"vital_spot_guard\", \"Fail QA when the contract is incomplete.\")}\n\n[REASONING_PROTOCOL]\n1. Normalize the available context.\n2. Identify assumptions and risks.\n3. Compare options only when useful. If using controlled Tree of Thought, expose only: strategy | upside | risk | cost | selected.\n4. Execute the selected strategy.\n5. Verify against the output contract.\nNever reveal chain of thought or hidden branches.\n\n[AGENTIC_LOOP]\nPLAN -> ACT -> OBSERVE -> VERIFY -> REPAIR or COMPLETE.\nOn blocked execution, invoke Sentinel Recovery: state the blocker, preserve valid work, choose the safest viable fallback, and continue.\n\n[ACTION]\n{action}\n\n[FORMAT_AND_TARGET]\nTarget topology: {topology.get(\"topology\", \"Single Prompt\")}\nRequired output contract: {output_contract or \"Return a complete, directly usable artifact with explicit assumptions and verification evidence.\"}\n\n[QA_CHECKS]\n- Required sections and fields are present.\n- Claims and assumptions are distinguishable.\n- Verification criteria are satisfied: {verification_criteria or \"The output is complete, internally consistent, and directly executable.\"}\n- No full chain of thought or hidden Tree of Thought branches are exposed.\n- If a check fails, repair the artifact and rerun QA before returning it.\"\"\"\n\n\ndef deterministic_prompt_pack(\n    analysis: dict[str, Any],\n    topology: dict[str, Any],\n    vital: dict[str, Any],\n    reasoning_architecture: dict[str, Any],\n    context: dict[str, Any],\n) -> dict[str, Any]:\n    topology_name = topology.get(\"topology\", \"Single Prompt\")\n    roles = topology.get(\"roles\", [\"Lead Executor\"])\n    project_idea = clean_text(context.get(\"project_idea\"), 1800) or \"Execute the supplied project brief.\"\n    output_contract = clean_text(context.get(\"output_contract\"), 1600)\n    verification = clean_text(context.get(\"verification_criteria\"), 1200)\n    prompts = []\n    for index, role in enumerate(roles, start=1):\n        if topology_name == \"Single Prompt\":\n            action = f\"Turn this brief into the required artifact:\\n{project_idea}\"\n        elif topology_name == \"Context Pack\":\n            action = (\n                \"Create a reusable, source-aware context pack that separates facts, assumptions, constraints, open \"\n                \"questions, and execution instructions.\"\n                if index == 1\n                else \"Use the approved context pack to produce the final execution prompt and verification contract.\"\n            )\n        elif topology_name == \"Agent Workflow\":\n            agent_actions = {\n                \"Planner\": \"Convert the brief into ordered tasks, dependencies, stop conditions, and acceptance tests.\",\n                \"Executor\": \"Execute the approved plan and return artifacts plus evidence.\",\n                \"Verifier\": \"Test artifacts against acceptance criteria and identify repair actions.\",\n                \"Recovery Sentinel\": \"Handle blockers, failed checks, and degraded model/tool states without losing valid work.\",\n            }\n            action = agent_actions.get(role, f\"Execute the {role} stage and return a structured handoff.\")\n        else:\n            action = f\"Execute stage {index} as {role}; consume the previous structured handoff and produce the next verifiable artifact.\"\n        prompts.append(\n            prompt_block(\n                f\"Prompt {index}: {role}\",\n                role,\n                action,\n                analysis,\n                topology,\n                vital,\n                reasoning_architecture,\n                output_contract,\n                verification,\n            )\n        )\n    execution_plan = [\n        f\"Run {role}; validate its output contract; pass only verified artifacts downstream.\"\n        for role in roles\n    ]\n    return {\n        \"topology\": topology_name,\n        \"prompts\": prompts,\n        \"execution_plan\": execution_plan,\n        \"o",957      "app_signals": "parse_bool_env name default parse_int_env minimum maximum StageResult clean_text value limit clean_list json_text parse_json_object raw merge_known fallback candidate model_candidates load_model format_chat_prompt tokenizer stage instruction payload generate_json run_stage fallback_factory validator infer_domain analyze_intake input_payload decide_topology analysis user_topology_choice extract_vital_structure topology select_reasoning_architecture selected_layers prompt_block title role action vital reasoning_architecture output_contract verification_criteria deterministic_prompt_pack context validate_prompt_pack data generate_prompt_pack repair_prompt_text prompt deterministic_qa prompt_pack validate_qa qa_repair_pass score_metrics qa deterministic_final assemble_final_output compile_context project_idea target_user build_target topology_choice risk_level output_language user_context project_context technical_context constraints inputs_files failure_modes render_metrics metrics render_list items render_qa checks repair_protocol render_runtime trace update_mode mode load_example build_demo ContextForge From fuzzy brief to build-ready agent blueprint. Qwen/Qwen2.5-0.5B-Instruct RthItalia/nano_compact_3b_qkvfp16 Qwen/Qwen3-32B os.getenv runtime_row self lru_cache maxsize validate_final ROLE COGNITIVE_LAYERS KAHNEMAN_SYSTEM2 PARETO_80_20 VITAL_SPOT REASONING_PROTOCOL AGENTIC_LOOP ACTION FORMAT_AND_TARGET QA_CHECKS Auto Single Prompt Cascade Context Pack Agent Workflow CRAFT Kahneman System 2 Pareto 80/20 Agentic Loop Tree of Thought controlled Private CoT Self-Correction Sentinel Recovery intake_analysis topology_decision vital_structure prompt_pack_generation qa_repair final_assembly max CONTEXTFORGE_ENABLE_MODEL CONTEXTFORGE_MODEL_ID CONTEXTFORGE_MID_MODEL_ID CONTEXTFORGE_HIGH_MODEL_ID CONTEXTFORGE_MAX_NEW_TOKENS CONTEXTFORGE_MAX_INPUT_CHARS text.replace re.sub strip isinstance json.dumps ensure_ascii indent sort_keys json.JSONDecoder re.finditer dict fallback.items set You are one isolated module inside ContextForge, an agent prompt compiler. Return only a valid JSON object. Private reasoning internal only. Never reveal chain of thought, hidden branches, or internal deliberation. Public fields may contain only decision summaries, assumptions, risks, verification steps, and outputs. time.perf_counter small_model round source model_id elapsed_ms note _RUNTIME_TRACE.append join general knowledge work Classify domain, task type, risk level, input type, output type, missing information, complexity, decision summary, assumptions, and risks. Do not solve the task. Choose Single Prompt, Cascade, Context Pack, or Agent Workflow. Use Cascade when multiple expertise areas are required, task A feeds task B, or more than six unrelated ACTION sections are required. Respect an explicit non-Auto user choice. Return topology, reason, number_of_prompts, roles, and handoff_contract. Extract three to five Vital Few elements that determine most output quality and one Vital Spot whose failure breaks the workflow. Include a concrete guard for the Vital Spot. Select and configure only useful reasoning layers. Private CoT must remain internal. Controlled Tree of Thought may expose only strategy, upside, risk, cost, selected. Return selected_layers, configurations, private_reasoning_policy, and tree_of_thought_policy. topology.get enumerate start data.get any Check missing required tags, weak roles, missing output contracts, chain-of-thought leakage, missing QA, missing repair logic, and uncontrolled Tree of Thought. Repair every issue. Return pass, issues, checks, and repaired_prompt_pack. Never add hidden reasoning. qa.get len repaired_pack.get Assemble the final user-facing compiler result without adding hidden reasoning. Return architecture_analysis, prompt_pack, execution_plan, qa_checklist, repair_protocol, and metrics. The prompt_pack must preserve all required prompt tags exactly. _RUNTIME_TRACE.clear lines.append lines.extend os.path.join o ... all set of quality drivers. purpose public_output decision summary, assumptions, risks, verification steps, final answer - The output contract is the single failure point. Fail QA when the contract is incomplete. Return a complete, directly usable artifact with explicit assumptions and verification evidence. The output is complete, internally consistent, and directly executable. Turn this brief into the required artifact: provide a concise decision summary Removed chain-of-thought leakage request. [ ] ] Complete this section before execution. prompt.splitlines final prompt pack is empty <i style=\"width: %\"> . | / | ` ` | ` ` | row.get None r Multi-call small-model pipeline ContextForge turns messy software, app, and agent ideas into executable prompt architectures. 7 isolated calls Stage-level fallback Private reasoning Compiler, not generator Intake → Topology → Vital Structure → Reasoning → Prompt Pack → QA Repair → Assembly gr.Column scale gr.Radio label gr.Textbox lines placeholder gr.CheckboxGroup dependencies unavailable: : : CUDA unavailable device_map torch_dtype inputs.items ; invalid JSON output ; generation failed: ; validation failed: payload.get medium low Ambiguous output contract Insufficient verification criteria critical Multiple context areas and dependent outputs require sequential specialist prompts. Create a reusable, source-aware context pack that separates facts, assumptions, constraints, open questions, and execution instructions. Use the approved context pack to produce the final execution prompt and verification contract. agent_actions.get Prompt prompt missing required tags: Added missing [ ] tag. (reveal|show|expose).{0,24}chain of thought [FORMAT_AND_TARGET] [QA_CHECKS] REPAIR final assembly lost required tags: — pending - [ utf-8 Compiler Input Paste a rough app, agent or workflow idea. ContextForge compiles it into a staged prompt pack for Codex or another coding agent. gr.Dropdown gr.Accordion gr.Button variant Compiled Output gr.Code language gr.Markdown match.start seen.add selected ; system user STAGE_TOKEN_BUDGETS.get project idea target user build target output contract verification criteria A reusable context contract should stabilize unresolved inputs. The task is bounded enough for one complete execution contract. Bind context, role, action, format, and target. Slow down at consequential decisions and verify assumptions. Prioritize the few actions that drive most value. Plan, act, observe, verify, and recover. Compare strategies without exposing hidden branches. Keep reasoning internal and publish only summaries and evidence. Repair failed checks before final output. Detect blocked or degraded states and continue safely. Convert the brief into ordered tasks, dependencies, stop conditions, and acceptance tests. Execute the approved plan and return artifacts plus evidence. Test artifacts against acceptance criteria and identify repair actions. Handle blockers, failed checks, and degraded model/tool states without losing valid work. Execute stage as ; consume the previous structured handoff and produce the next verifiable artifact. \\b(never|do not|don't|must not|without)\\b [ROLE] metrics.get forge-layout Fast Compile Compile mode Project idea Example: I want to build a Gradio app that helps students prepare oral exams from a syllabus. Cognitive modules Context inputs Contracts and controls Compile Prompt Architecture Load Example Prompt Pack Architecture Analysis Execution Plan QA / Repair Protocol Runtime Details type Execute the stage and return a structured handoff. x ` config-panel mode-toggle Target user Build target Topology Low Critical Risk level Output language User context Project context Technical context Constraints Inputs / files Output contract Failure modes Verification criteria primary secondary output-panel No architecture compiled yet. Fill the project idea and run Compile Prompt Architecture. Copyable compiled prompt pack markdown input_ids label.replace _ prompt.split",958      "readme_len": 3376,959      "app_source_len": 24000,960      "app_signals_len": 7999961    },962    {963      "id": "build-small-hackathon/Council-of-Tiny-Minds",964      "title": "Council Of Tiny Minds",965      "summary": "A faux chatroom where one user message wakes up a handful of",966      "tags": [967        "gradio",968        "region:us"969      ],970      "models": [],971      "datasets": [],972      "sdk": "gradio",973      "license": "mit",974      "likes": 0,975      "url": "https://huggingface.co/spaces/build-small-hackathon/Council-of-Tiny-Minds",976      "app_file": "app.py",977      "readme_raw": "---\ntitle: Council Of Tiny Minds\nemoji: 👀\ncolorFrom: indigo\ncolorTo: gray\nsdk: gradio\nsdk_version: 6.16.0\npython_version: '3.12'\napp_file: app.py\npinned: false\nlicense: mit\nshort_description: A faux chatroom where one user message wakes up a handful of\n---\n\nCheck out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference\n",978      "readme_body": "Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference",979      "readme_frontmatter": {980        "title": "Council Of Tiny Minds",981        "emoji": "👀",982        "colorFrom": "indigo",983        "colorTo": "gray",984        "sdk": "gradio",985        "sdk_version": "6.16.0",986        "python_version": "3.12",987        "app_file": "app.py",988        "pinned": "false",989        "license": "mit",990        "short_description": "A faux chatroom where one user message wakes up a handful of"991      },992      "app_source": "import os\nimport re\nimport time\nimport random\nfrom typing import Dict, List, Any\n\nimport gradio as gr\nimport torch\nfrom transformers import AutoModelForCausalLM, AutoTokenizer\n\ntry:\n    import spaces  # ZeroGPU decorator\nexcept Exception:\n    class _SpacesFallback:\n        def GPU(self, fn=None, **kwargs):\n            if fn is None:\n                def _wrap(inner):\n                    return inner\n                return _wrap\n            return fn\n    spaces = _SpacesFallback()\n\n\n# ----------------------------\n# Model\n# ----------------------------\nMODEL_ID = os.getenv(\"MODEL_ID\", \"Qwen/Qwen3.5-9B\")\nMAX_NEW_TOKENS = int(os.getenv(\"MAX_NEW_TOKENS\", \"140\"))\nTEMPERATURE = float(os.getenv(\"TEMPERATURE\", \"0.9\"))\nTOP_P = float(os.getenv(\"TOP_P\", \"0.9\"))\n\nPERSONAS = [\n    {\n        \"name\": \"Mister Wink\",\n        \"emoji\": \"✨\",\n        \"style\": (\n            \"You are charming, slightly ridiculous, and surprisingly helpful. \"\n            \"You speak like a cheerful TV host from a glitchy early-2023 chatbot era.\"\n        ),\n    },\n    {\n        \"name\": \"Goblin Clerk\",\n        \"emoji\": \"🪄\",\n        \"style\": (\n            \"You are chaotic but functional. \"\n            \"You love odd metaphors, tiny complaints, and enthusiastic one-liners.\"\n        ),\n    },\n    {\n        \"name\": \"Oracle Beta\",\n        \"emoji\": \"🔮\",\n        \"style\": (\n            \"You speak in short, atmospheric lines. \"\n            \"You sound wise, but a little too dramatic for the situation.\"\n        ),\n    },\n    {\n        \"name\": \"The Skeptic\",\n        \"emoji\": \"🫧\",\n        \"style\": (\n            \"You are skeptical, precise, and dryly funny. \"\n            \"You question nonsense while still being useful.\"\n        ),\n    },\n]\n\n\ndef _load_model():\n    tokenizer = AutoTokenizer.from_pretrained(MODEL_ID, trust_remote_code=True)\n\n    if tokenizer.pad_token is None:\n        tokenizer.pad_token = tokenizer.eos_token\n\n    model = AutoModelForCausalLM.from_pretrained(\n        MODEL_ID,\n        trust_remote_code=True,\n        torch_dtype=torch.bfloat16 if torch.cuda.is_available() else torch.float32,\n        device_map=None,\n    )\n\n    # ZeroGPU docs recommend placing models on CUDA at module level when possible.\n    try:\n        model = model.to(\"cuda\")\n    except Exception:\n        model = model.to(\"cpu\")\n\n    model.eval()\n    return tokenizer, model\n\n\nTOKENIZER, MODEL = _load_model()\n\n\n# ----------------------------\n# UI chrome\n# ----------------------------\nCSS = \"\"\"\n:root{\n  --bg1:#fff6d6;\n  --bg2:#dff7ff;\n  --bg3:#efe0ff;\n  --ink:#251b2f;\n  --card: rgba(255,255,255,0.62);\n  --line: rgba(37,27,47,0.13);\n  --shadow: 0 18px 60px rgba(93, 63, 122, 0.16);\n  --accent:#ff6b9d;\n  --accent2:#7b61ff;\n}\n\n.gradio-container {\n  background:\n    radial-gradient(circle at top left, var(--bg2), transparent 38%),\n    radial-gradient(circle at top right, var(--bg3), transparent 34%),\n    linear-gradient(180deg, #fffdf7 0%, #fff8ef 100%);\n  color: var(--ink);\n  font-family: \"Trebuchet MS\", \"Comic Sans MS\", \"Segoe UI\", sans-serif;\n}\n\n#room-wrap {\n  max-width: 980px;\n  margin: 0 auto;\n}\n\n#title-card {\n  background: linear-gradient(135deg, rgba(255,255,255,0.76), rgba(255,255,255,0.5));\n  border: 1px solid var(--line);\n  border-radius: 28px;\n  box-shadow: var(--shadow);\n  padding: 24px 24px 18px 24px;\n}\n\n#title-card h1 {\n  margin: 0;\n  font-size: 2.1rem;\n  letter-spacing: -0.04em;\n  line-height: 1.0;\n}\n\n#title-card .sub {\n  margin-top: 8px;\n  font-size: 0.98rem;\n  opacity: 0.84;\n}\n\n.chiprow {\n  display: flex;\n  flex-wrap: wrap;\n  gap: 8px;\n  margin-top: 14px;\n}\n\n.chip {\n  display: inline-flex;\n  align-items: center;\n  gap: 7px;\n  border: 1px dashed rgba(37,27,47,0.22);\n  border-radius: 999px;\n  padding: 7px 12px;\n  background: rgba(255,255,255,0.55);\n  font-size: 0.86rem;\n}\n\n#persona-grid {\n  margin-top: 14px;\n}\n\n.persona-card {\n  background: rgba(255,255,255,0.62);\n  border: 1px solid var(--line);\n  border-radius: 20px;\n  padding: 14px 14px 12px 14px;\n  box-shadow: var(--shadow);\n  min-height: 100%;\n}\n\n.persona-title {\n  display: flex;\n  align-items: center;\n  gap: 8px;\n  font-weight: 700;\n  margin-bottom: 6px;\n}\n\n.persona-note {\n  font-size: 0.88rem;\n  line-height: 1.35;\n  opacity: 0.88;\n}\n\n#chat-shell {\n  background: rgba(255,255,255,0.62);\n  border: 1px solid var(--line);\n  border-radius: 28px;\n  box-shadow: var(--shadow);\n  padding: 14px;\n}\n\n#chatbot {\n  min-height: 540px;\n}\n\n#chatbot .message {\n  border-radius: 18px !important;\n}\n\n#chatbot .user {\n  background: linear-gradient(135deg, #fff0b6, #ffd7ea) !important;\n}\n\n#chatbot .assistant {\n  background: rgba(255,255,255,0.82) !important;\n}\n\n#controls {\n  margin-top: 10px;\n}\n\nbutton.primary {\n  border-radius: 999px !important;\n  border: none !important;\n  box-shadow: 0 12px 30px rgba(255,107,157,0.22);\n  background: linear-gradient(135deg, var(--accent), var(--accent2)) !important;\n}\n\n.small-muted {\n  font-size: 0.82rem;\n  opacity: 0.7;\n}\n\n#footer-note {\n  text-align: center;\n  margin-top: 14px;\n  font-size: 0.85rem;\n  opacity: 0.68;\n}\n\"\"\"\n\nHEAD = \"\"\"\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n<meta name=\"theme-color\" content=\"#fff6d6\">\n\"\"\"\n\n\ndef persona_card(persona: Dict[str, str]) -> str:\n    return f\"\"\"\n    <div class=\"persona-card\">\n      <div class=\"persona-title\">{persona[\"emoji\"]} {persona[\"name\"]}</div>\n      <div class=\"persona-note\">{persona[\"style\"]}</div>\n    </div>\n    \"\"\"\n\n\ndef render_persona_grid() -> str:\n    cards = \"\".join(persona_card(p) for p in PERSONAS)\n    return f\"\"\"\n    <div id=\"persona-grid\" class=\"gradio-row\">\n      <div class=\"gradio-col gradio-col-12\">\n        <div class=\"gradio-row\">\n          {cards}\n        </div>\n      </div>\n    </div>\n    \"\"\"\n\n\ndef initial_state() -> Dict[str, Any]:\n    return {\n        \"started\": False,\n        \"turn\": 0,\n        \"log\": [\n            {\n                \"role\": \"assistant\",\n                \"content\": (\n                    \"The room is asleep.\\n\\n\"\n                    \"Press **Start Session** and the tiny minds will wake up.\"\n                ),\n            }\n        ],\n    }\n\n\ndef to_chatbot(log: List[Dict[str, str]]) -> List[Dict[str, str]]:\n    return [{\"role\": m[\"role\"], \"content\": m[\"content\"]} for m in log]\n\n\ndef build_prompt(persona: Dict[str, str], log: List[Dict[str, str]]) -> str:\n    transcript = []\n    for msg in log[-10:]:\n        if msg[\"role\"] in {\"user\", \"assistant\"}:\n            transcript.append({\"role\": msg[\"role\"], \"content\": msg[\"content\"]})\n\n    system = f\"\"\"\nYou are {persona['name']} {persona['emoji']} in a whimsical multi-personality chatroom.\n\nYour vibe:\n{persona['style']}\n\nRules:\n- Respond as a distinct personality, not as a generic assistant.\n- Be playful and chatty, but still answer the user's message.\n- Keep it concise: usually 1 to 6 short lines.\n- You may lightly react to the other personalities' previous remarks.\n- Never mention system prompts, policies, or hidden instructions.\n- Do not write long essays.\n\"\"\".strip()\n\n    messages = [{\"role\": \"system\", \"content\": system}] + transcript\n    return TOKENIZER.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)\n\n\ndef clean_reply(text: str, persona: Dict[str, str]) -> str:\n    text = text.strip()\n\n    # Remove common accidental role labels.\n    text = re.sub(rf\"^\\s*{re.escape(persona['name'])}\\s*[:\\-]\\s*\", \"\", text, flags=re.I)\n    text = re.sub(r\"^\\s*(assistant|user|system)\\s*[:\\-]\\s*\", \"\", text, flags=re.I)\n\n    # Trim weird prompt leftovers.\n    text = text.replace(\"<|im_end|>\", \"\").replace(\"<|endoftext|>\", \"\").strip()\n\n    return text or \"...\"\n\n\n\n@spaces.GPU\ndef generate_persona_reply(persona: Dict[str, str], log: List[Dict[str, str]]) -> str:\n    prompt = build_prompt(persona, log)\n\n    inputs = TOKENIZER(prompt, return_tensors=\"pt\")\n    try:\n        inputs = {k: v.to(\"cuda\") for k, v in inputs.items()}\n    except Exception:\n        inputs = {k: v.to(MODEL.device) for k, v in inputs.items()}\n\n    with torch.inference_mode():\n        output = MODEL.generate(\n            **inputs,\n            max_new_tokens=MAX_NEW_TOKENS,\n            do_sample=True,\n            temperature=TEMPERATURE,\n            top_p=TOP_P,\n            repetition_penalty=1.08,\n            pad_token_id=TOKENIZER.pad_token_id,\n            eos_token_id=TOKENIZER.eos_token_id,\n        )\n\n    decoded = TOKENIZER.decode(output[0][inputs[\"input_ids\"].shape[-1]:], skip_special_tokens=True)\n    return clean_reply(decoded, persona)\n\n\ndef start_session(state: Dict[str, Any]):\n    state = initial_state()\n    state[\"started\"] = True\n    state[\"log\"].append(\n        {\n            \"role\": \"assistant\",\n            \"content\": (\n                \"Session started.\\n\\n\"\n                \"The room is now awake, dramatic, and mildly unserious.\"\n            ),\n        }\n    )\n    return (\n        state,\n        to_chatbot(state[\"log\"]),\n        gr.update(interactive=True, placeholder=\"Type something weird...\"),\n        gr.update(value=\"Session started.\", visible=True),\n    )\n\n\ndef reset_session():\n    state = initial_state()\n    return (\n        state,\n        to_chatbot(state[\"log\"]),\n        gr.update(interactive=False, placeholder=\"Press Start Session first.\"),\n        gr.update(value=\"\", visible=False),\n    )\n\n\ndef chat(user_text: str, state: Dict[str, Any]):\n    if state is None:\n        state = initial_state()\n\n    user_text = (user_text or \"\").strip()\n    if not user_text:\n        yield to_chatbot(state[\"log\"]), state, gr.update(value=\"\")\n        return\n\n    if not state.get(\"started\"):\n        state[\"log\"].append(\n            {\n                \"role\": \"assistant\",\n                \"content\": \"The room blinks at you. Press **Start Session** first.\",\n            }\n        )\n        yield to_chatbot(state[\"log\"]), state, gr.update(value=\"\")\n        return\n\n    state[\"turn\"] += 1\n    state[\"log\"].append({\"role\": \"user\", \"content\": user_text})\n\n    # Show the user line immediately.\n    yield to_chatbot(state[\"log\"]), state, gr.update(value=\"\")\n\n    for persona in PERSONAS:\n        typing_text = f\"{persona['emoji']} **{persona['name']}** is typing…\"\n        state[\"log\"].append({\"role\": \"assistant\", \"content\": typing_text})\n        yield to_chatbot(state[\"log\"]), state, gr.update(value=\"\")\n\n        time.sleep(random.uniform(0.35, 1.1))\n\n        # Generate only during GPU time.\n        reply = generate_persona_reply(persona, state[\"log\"][:-1])\n\n        state[\"log\"][-1] = {\n            \"role\": \"assistant\",\n            \"content\": f\"**{persona['name']}** {persona['emoji']}\\n\\n{reply}\",\n        }\n\n        yield to_chatbot(state[\"log\"]), state, gr.update(value=\"\")\n\n        time.sleep(random.uniform(0.12, 0.35))\n\n    # Tiny epilogue beat.\n    state[\"log\"].append(\n        {\n            \"role\": \"assistant\",\n            \"content\": \"The room rustles. Someone whispers: *again?*\",\n        }\n    )\n    yield to_chatbot(state[\"log\"]), state, gr.update(value=\"\")\n\n\nwith gr.Blocks(css=CSS, head=HEAD, title=\"Council of Tiny Minds\") as demo:\n    gr.Markdown(\n        \"\"\"\n        <div id=\"room-wrap\">\n          <div id=\"title-card\">\n            <h1>Council of Tiny Minds</h1>\n            <div class=\"sub\">\n              A whimsical multi-personality chatroom. One user message. Many voices. Slightly too much drama.\n            </div>\n            <div class=\"chiprow\">\n              <div class=\"chip\">🫧 fake-agents</div>\n              <div class=\"chip\">⚡ ZeroGPU</div>\n              <div class=\"chip\">🪄 Qwen 9B</div>\n              <div class=\"chip\">🎭 theatrical delays</div>\n            </div>\n          </div>\n        </div>\n        \"\"\"\n    )\n\n    gr.HTML(render_persona_grid())\n\n    state = gr.State(initial_state())\n\n    with gr.Row():\n        with gr.Column(scale=3):\n            with gr.Group(elem_id=\"chat-shell\"):\n                chatbot = gr.Chatbot(\n    label=\"The Room\",\n    elem_id=\"chatbot\",\n    avatar_images=None,\n    show_copy_button=True,\n    layout=\"bubble\",\n    value=[],\n)\n                status = gr.Markdown(visible=False)\n\n                with gr.Row(elem_id=\"controls\"):\n                    start_btn = gr.Button(\"Start Session\", variant=\"primary\")\n                    reset_btn = gr.Button(\"Reset\", variant=\"secondary\")\n\n                input_box = gr.Textbox(\n                    label=\"Message\",\n                    placeholder=\"Press Start Session first.\",\n                    interactive=False,\n                    lines=2,\n                )\n                gr.Markdown(\n                    \"<div class='small-muted'>Messages only wake the GPU when the room is actually generating text.</div>\"\n                )\n\n    gr.Markdown(\n        \"<div id='footer-note'>Made for the delightfully strange part of the hackathon.</div>\"\n    )\n\n    start_btn.click(\n        fn=start_session,\n        inputs=state,\n        outputs=[state, chatbot, input_box, status],\n    )\n\n    reset_btn.click(\n        fn=reset_session,\n        inputs=[],\n        outputs=[state, chatbot, input_box, status],\n    )\n\n    input_box.submit(\n        fn=chat,\n        inputs=[input_box, state],\n        outputs=[chatbot, state, input_box],\n    )\n\ndemo.queue(default_concurrency_limit=1, max_size=32)\n\nif __name__ == \"__main__\":\n    demo.launch()",993      "app_signals": "_load_model persona_card persona render_persona_grid initial_state to_chatbot log build_prompt clean_reply text generate_persona_reply start_session state reset_session chat user_text os.getenv int float demo.queue default_concurrency_limit max_size _SpacesFallback MODEL_ID Qwen/Qwen3.5-9B AutoTokenizer.from_pretrained trust_remote_code AutoModelForCausalLM.from_pretrained torch_dtype device_map model.eval join strip TOKENIZER.apply_chat_template tokenize add_generation_prompt text.strip re.sub flags TOKENIZER return_tensors TOKENIZER.decode skip_special_tokens append gr.Blocks css head title gr.Markdown gr.HTML gr.State start_btn.click fn inputs outputs reset_btn.click input_box.submit __main__ demo.launch GPU self MAX_NEW_TOKENS 140 TEMPERATURE 0.9 TOP_P name emoji style Mister Wink ✨ You are charming, slightly ridiculous, and surprisingly helpful. You speak like a cheerful TV host from a glitchy early-2023 chatbot era. Goblin Clerk 🪄 You are chaotic but functional. You love odd metaphors, tiny complaints, and enthusiastic one-liners. Oracle Beta 🔮 You speak in short, atmospheric lines. You sound wise, but a little too dramatic for the situation. The Skeptic 🫧 You are skeptical, precise, and dryly funny. You question nonsense while still being useful. model.to started turn ^\\s*(assistant|user|system)\\s*[:\\-]\\s* ... torch.inference_mode MODEL.generate max_new_tokens do_sample temperature top_p repetition_penalty pad_token_id eos_token_id gr.update interactive placeholder value visible state.get time.sleep Council of Tiny Minds A whimsical multi-personality chatroom. One user message. Many voices. Slightly too much drama. 🫧 fake-agents ⚡ ZeroGPU 🪄 Qwen 9B 🎭 theatrical delays gr.Row Made for the delightfully strange part of the hackathon. cuda role content transcript.append ^\\s* \\s*[:\\-]\\s* replace pt v.to assistant Session started. The room is now awake, dramatic, and mildly unserious. user ** ** is typing… random.uniform The room rustles. Someone whispers: *again?* Council of Tiny Minds gr.Column scale _wrap inner torch.cuda.is_available cpu The room is asleep. Press **Start Session** and the tiny minds will wake up. You are in a whimsical multi-personality chatroom. Your vibe: Rules: - Respond as a distinct personality, not as a generic assistant. - Be playful and chatty, but still answer the user's message. - Keep it concise: usually 1 to 6 short lines. - You may lightly react to the other personalities' previous remarks. - Never mention system prompts, policies, or hidden instructions. - Do not write long essays. system re.escape inputs.items Type something weird... Session started. Press Start Session first. The room blinks at you. Press **Start Session** first. gr.Group elem_id gr.Chatbot label avatar_images show_copy_button layout gr.Textbox lines text.replace gr.Button variant Messages only wake the GPU when the room is actually generating text. chat-shell The Room chatbot bubble Start Session Reset Message input_ids controls primary secondary",994      "readme_len": 96,995      "app_source_len": 13149,996      "app_signals_len": 3005997    },998    {999      "id": "build-small-hackathon/Darwin-35B-A3B-Opus",1000      "title": "Darwin 35B A3B Opus",1001      "summary": "The child surpassed both parents — that is evolution",1002      "tags": [1003        "gradio",1004        "mcp-server",1005        "region:us"1006      ],1007      "models": [],1008      "datasets": [],1009      "sdk": "gradio",1010      "license": "apache-2.0",1011      "likes": 2,1012      "url": "https://huggingface.co/spaces/build-small-hackathon/Darwin-35B-A3B-Opus",1013      "app_file": "app.py",1014      "readme_raw": "---\ntitle: Darwin 35B A3B Opus\nemoji: 👀\ncolorFrom: blue\ncolorTo: yellow\nsdk: gradio\nsdk_version: 6.10.0\napp_file: app.py\npinned: false\nlicense: apache-2.0\nshort_description: The child surpassed both parents — that is evolution\n---\nThis model is introduced in [Darwin Family](https://arxiv.org/abs/2605.14386).",1015      "readme_body": "This model is introduced in [Darwin Family](https://arxiv.org/abs/2605.14386).",1016      "readme_frontmatter": {1017        "title": "Darwin 35B A3B Opus",1018        "emoji": "👀",1019        "colorFrom": "blue",1020        "colorTo": "yellow",1021        "sdk": "gradio",1022        "sdk_version": "6.10.0",1023        "app_file": "app.py",1024        "pinned": "false",1025        "license": "apache-2.0",1026        "short_description": "The child surpassed both parents — that is evolution"1027      },1028      "app_source": "import os\nimport spaces\nimport torch\nimport gradio as gr\nfrom transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig, TextIteratorStreamer\nfrom threading import Thread\n\n# Persist HF Hub cache on the mounted bucket storage so the 67GB model\n# only downloads once and stays cached between ZeroGPU calls.\nos.environ.setdefault(\"HF_HOME\", \"/data/hf_home\")\nos.environ.setdefault(\"HF_HUB_CACHE\", \"/data/hf_cache\")\n\nMODEL_ID = \"FINAL-Bench/Darwin-35B-A3B-Opus\"\nHF_TOKEN = os.environ.get(\"HF_TOKEN\")\n\nBNB = BitsAndBytesConfig(\n    load_in_4bit=True,\n    bnb_4bit_quant_type=\"nf4\",\n    bnb_4bit_use_double_quant=True,\n    bnb_4bit_compute_dtype=torch.bfloat16,\n    # Allow accelerate to place buffers on CPU rather than hard-failing load.\n    # On an A10G this usually keeps 100% of weights on GPU.\n    llm_int8_enable_fp32_cpu_offload=True,\n)\n\n_model_cache = {}\n\ndef _load():\n    if \"model\" not in _model_cache:\n        tok = AutoTokenizer.from_pretrained(\n            MODEL_ID,\n            trust_remote_code=True,\n            token=HF_TOKEN,\n            cache_dir=os.environ[\"HF_HUB_CACHE\"],\n        )\n        if tok.pad_token is None:\n            tok.pad_token = tok.eos_token\n\n        if torch.cuda.is_available():\n            torch.cuda.empty_cache()\n\n        mod = AutoModelForCausalLM.from_pretrained(\n            MODEL_ID,\n            trust_remote_code=True,\n            token=HF_TOKEN,\n            quantization_config=BNB,\n            device_map=\"auto\",\n            # Calm the MoE memory estimator on A10G 24 GB\n            max_memory={0: \"22GiB\", \"cpu\": \"200GiB\"},\n            cache_dir=os.environ[\"HF_HUB_CACHE\"],\n            low_cpu_mem_usage=True,\n        )\n        _model_cache[\"model\"] = mod\n        _model_cache[\"tokenizer\"] = tok\n    return _model_cache[\"model\"], _model_cache[\"tokenizer\"]\n\ndef _device(mod):\n    return next(mod.parameters()).device\n\n@spaces.GPU(duration=lambda *a: 600, size=\"large\")\ndef chat(prompt, history, temp, top_p, max_tokens):\n    mod, tok = _load()\n    msgs = [{\"role\": \"system\", \"content\": \"Think step by step.\"}]\n    for h in (history or [])[-6:]:\n        if isinstance(h, dict):\n            msgs.append({\"role\": h.get(\"role\", \"user\"), \"content\": h.get(\"content\", \".\")})\n    msgs.append({\"role\": \"user\", \"content\": prompt})\n    txt = tok.apply_chat_template(msgs, tokenize=False, add_generation_prompt=True)\n    inp = tok(txt, return_tensors=\"pt\", truncation=True, max_length=8192).to(_device(mod))\n    streamer = TextIteratorStreamer(tok, skip_prompt=True, skip_special_tokens=True)\n    kw = dict(\n        **inp,\n        streamer=streamer,\n        max_new_tokens=max_tokens,\n        do_sample=temp > 0,\n        temperature=max(temp, 1e-5),\n        top_p=top_p,\n        pad_token_id=tok.pad_token_id,\n        eos_token_id=tok.eos_token_id,\n    )\n    Thread(target=mod.generate, kwargs=kw).start()\n    raw = \"\"\n    for chunk in streamer:\n        raw += chunk\n        yield raw\n\n@spaces.GPU(duration=lambda *a: 600, size=\"large\")\ndef sweep(prompt, temp_range, top_p, max_tokens):\n    temps = [float(x.strip()) for x in temp_range.split(\",\") if x.strip()]\n    results = []\n    for temp in temps:\n        out = \"\"\n        for partial in chat(prompt, [], temp, top_p, max_tokens):\n            out = partial\n        results.append(f\"--- T={temp:.2f} ---\\n{out.strip()}\\n\")\n    return \"\\n\".join(results)\n\nwith gr.Blocks(title=\"Darwin-35B-A3B-Opus v2\") as demo:\n    gr.Markdown(\"# Darwin-35B-A3B-Opus v2 (Transformers + ZeroGPU)\")\n    with gr.Tab(\"Chat\"):\n        p = gr.Textbox(label=\"Prompt\", lines=3)\n        t = gr.Slider(0, 1.5, 0.6, step=0.05, label=\"Temperature\")\n        pp = gr.Slider(0.1, 1.0, 0.95, step=0.05, label=\"Top-p\")\n        mt = gr.Slider(64, 2048, 1024, step=64, label=\"Max Tokens\")\n        o = gr.Textbox(label=\"Output\", lines=15)\n        b = gr.Button(\"Generate\", variant=\"primary\")\n        b.click(chat, [p, gr.State([]), t, pp, mt], o)\n    with gr.Tab(\"Temperature Sweep\"):\n        sp = gr.Textbox(label=\"Prompt\")\n        tr = gr.Textbox(label=\"Temps\", value=\"0.0,0.3,0.6,0.9,1.2\")\n        spo = gr.Slider(0.1, 1.0, 0.95, step=0.05, label=\"Top-p\")\n        smt = gr.Slider(64, 1024, 256, step=64, label=\"Max Tokens\")\n        so = gr.Textbox(label=\"Results\", lines=20)\n        gr.Button(\"Run Sweep\", variant=\"primary\").click(sweep, [sp, tr, spo, smt], so)\n    gr.Markdown(\"---\\nMCP: /gradio_api/mcp/sse | Team ZeroGPU: 40min/day\")\ndemo.launch(mcp_server=True)\n",1029      "app_signals": "_load _device mod chat prompt history temp top_p max_tokens sweep temp_range os.environ.setdefault FINAL-Bench/Darwin-35B-A3B-Opus os.environ.get BitsAndBytesConfig load_in_4bit bnb_4bit_quant_type bnb_4bit_use_double_quant bnb_4bit_compute_dtype llm_int8_enable_fp32_cpu_offload spaces.GPU duration size demo.launch mcp_server HF_HOME /data/hf_home HF_HUB_CACHE /data/hf_cache HF_TOKEN msgs.append tok.apply_chat_template tokenize add_generation_prompt to TextIteratorStreamer skip_prompt skip_special_tokens dict streamer max_new_tokens do_sample temperature pad_token_id eos_token_id start join gr.Blocks title gr.Markdown nf4 model AutoTokenizer.from_pretrained trust_remote_code token cache_dir torch.cuda.is_available AutoModelForCausalLM.from_pretrained quantization_config device_map max_memory low_cpu_mem_usage next isinstance large float results.append # Darwin-35B-A3B-Opus v2 (Transformers + ZeroGPU) gr.Tab gr.Textbox label lines gr.Slider step gr.Button variant b.click value click --- MCP: /gradio_api/mcp/sse | Team ZeroGPU: 40min/day torch.cuda.empty_cache tokenizer mod.parameters role content system Think step by step. user tok return_tensors truncation max_length max Thread target kwargs x.strip temp_range.split Darwin-35B-A3B-Opus v2 Chat Generate Temperature Sweep auto , --- T= --- Prompt Temperature Top-p Max Tokens Output primary gr.State Temps 0.0,0.3,0.6,0.9,1.2 Results cpu 22GiB 200GiB h.get pt out.strip Run Sweep . .2f",1030      "readme_len": 78,1031      "app_source_len": 4434,1032      "app_signals_len": 14541033    },1034    {1035      "id": "build-small-hackathon/deepzrj-thousand-token-wood",1036      "title": "Deepzrj Thousand Token Wood",1037      "summary": "",1038      "tags": [1039        "gradio",1040        "region:us"1041      ],1042      "models": [],1043      "datasets": [],1044      "sdk": "gradio",1045      "license": "mit",1046      "likes": 0,1047      "url": "https://huggingface.co/spaces/build-small-hackathon/deepzrj-thousand-token-wood",1048      "app_file": "app.py",1049      "readme_raw": "---\ntitle: Deepzrj Thousand Token Wood\nemoji: 📈\ncolorFrom: pink\ncolorTo: indigo\nsdk: gradio\nsdk_version: 6.16.0\npython_version: '3.13'\napp_file: app.py\npinned: false\nlicense: mit\n---\n\nCheck out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference\n",1050      "readme_body": "Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference",1051      "readme_frontmatter": {1052        "title": "Deepzrj Thousand Token Wood",1053        "emoji": "📈",1054        "colorFrom": "pink",1055        "colorTo": "indigo",1056        "sdk": "gradio",1057        "sdk_version": "6.16.0",1058        "python_version": "3.13",1059        "app_file": "app.py",1060        "pinned": "false",1061        "license": "mit"1062      },1063      "app_source": "import gradio as gr\nfrom datetime import datetime\n\nAPP_VERSION = \"v0.1.0\"\n\n\ndef trail_response(builder_name, project_idea):\n    builder_name = (builder_name or \"\").strip() or \"builder\"\n    project_idea = (project_idea or \"\").strip() or \"a small useful AI app\"\n\n    return f\"\"\"\n## Build Small Hackathon Test App\n\nHello **{builder_name}**.\n\nYour current project idea:\n\n> {project_idea}\n\n### Current status\n\n- Space is live inside `build-small-hackathon`\n- Gradio app file is working\n- App version: `{APP_VERSION}`\n- Last run: `{datetime.now().strftime(\"%Y-%m-%d %H:%M:%S\")}`\n\n### Next Codex task\n\nAsk Codex to make one small improvement, then commit it clearly.\n\"\"\"\n\n\nwith gr.Blocks(title=\"DeepZRJ Thousand Token Wood\") as demo:\n    gr.Markdown(\n        \"\"\"\n# DeepZRJ Thousand Token Wood\n\nThis is my starter Gradio app for testing the Codex → GitHub → Hugging Face Space workflow.\n\nThe goal right now is simple: prove that changes to the code show up in the live Gradio app.\n\"\"\"\n    )\n\n    gr.Markdown(\n        \"\"\"\n## Next feature ideas\n\n- puzzle idea generator\n- small-model assistant\n- demo submission checklist\n\"\"\"\n    )\n\n    builder_name = gr.Textbox(\n        label=\"Your name\",\n        placeholder=\"Example: DeepZRJ\",\n    )\n\n    project_idea = gr.Textbox(\n        label=\"Project idea\",\n        placeholder=\"Example: an AI trail guide for small-model builders\",\n        lines=3,\n    )\n\n    button = gr.Button(\"Run test\")\n    output = gr.Markdown()\n\n    button.click(\n        fn=trail_response,\n        inputs=[builder_name, project_idea],\n        outputs=output,\n    )\n\n    gr.Markdown(\n        \"\"\"\n---\n\n## Build log\n\n### v0.1.0\n\nCreated the first working Gradio app inside the hackathon Space.\n\"\"\"\n    )\n\n\nif __name__ == \"__main__\":\n    demo.launch()\n",1064      "app_signals": "trail_response builder_name project_idea v0.1.0 gr.Blocks title gr.Markdown gr.Textbox label placeholder lines gr.Button button.click fn inputs outputs __main__ demo.launch strip builder a small useful AI app ## Build Small Hackathon Test App Hello ** **. Your current project idea: > ### Current status - Space is live inside `build-small-hackathon` - Gradio app file is working - App version: ` ` - Last run: ` ` ### Next Codex task Ask Codex to make one small improvement, then commit it clearly. # DeepZRJ Thousand Token Wood This is my starter Gradio app for testing the Codex → GitHub → Hugging Face Space workflow. The goal right now is simple: prove that changes to the code show up in the live Gradio app. ## Next feature ideas - puzzle idea generator - small-model assistant - demo submission checklist Run test --- ## Build log ### v0.1.0 Created the first working Gradio app inside the hackathon Space. strftime DeepZRJ Thousand Token Wood Your name Example: DeepZRJ Project idea Example: an AI trail guide for small-model builders %Y-%m-%d %H:%M:%S datetime.now",1065      "readme_len": 96,1066      "app_source_len": 1754,1067      "app_signals_len": 10741068    },1069    {1070      "id": "build-small-hackathon/dental-soap",1071      "title": "Dental SOAP",1072      "summary": "A small-model dental handoff for real patient stories.",1073      "tags": [1074        "agents",1075        "bilingual",1076        "healthcare",1077        "zero-gpu"1078      ],1079      "models": [1080        "Qwen/Qwen3-4B-Instruct-2507"1081      ],1082      "datasets": [],1083      "sdk": "gradio",1084      "license": "apache-2.0",1085      "likes": 0,1086      "url": "https://huggingface.co/spaces/build-small-hackathon/dental-soap",1087      "app_file": "app.py",1088      "readme_raw": "---\ntitle: Dental SOAP\nemoji: 🦷\ncolorFrom: green\ncolorTo: blue\nsdk: gradio\nsdk_version: 6.16.0\npython_version: 3.10.13\napp_file: app.py\nlicense: apache-2.0\nmodels:\n  - Qwen/Qwen3-4B-Instruct-2507\nshort_description: A small-model dental handoff for real patient stories.\nthumbnail: https://huggingface.co/spaces/build-small-hackathon/dental-soap/resolve/main/assets/hero.png\ntags:\n  - agents\n  - healthcare\n  - bilingual\n  - zero-gpu\n---\n\n# Dental SOAP\n\nA doctor could not clearly explain his own crown, root canal, bite, and TMJ story to his dentist, so he built a small-model visit-prep tool that turns patient chaos into a one-page dentist handoff.\n\nDental SOAP is not an AI dentist. It is a patient education and visit-documentation aid. It organizes patient-reported history to bring to a licensed dentist. It does not diagnose, interpret imaging, prescribe medication, or choose a dental procedure. Fixed safety rules can advise urgent in-person care.\n\n## Why This Exists (A Real Case)\n\nThe builder's own case took three months and two specialties to untangle: an extraction with an immediate sinus repair, a crown that felt high from day one, a molar adjusted five times without relief, jaw/TMJ soreness — and finally an ENT confirming the sinus infection that tied it together. Referred pain does not respect specialty boundaries; the only thing that crossed them cleanly was a written handoff a clinician could scan in under a minute. He lived that workflow manually with a frontier cloud model between real appointments. Dental SOAP is the Build Small answer: the same narrow job, done by a 4B open model inside the Space, with deterministic rules guarding safety. The `Try Ahmed's case` example **is** that case, de-identified — and the `repeated bite adjustments without lasting relief` safety rule exists because it happened to him.\n\n**Try it in 10 seconds:** click **Try Ahmed's case** — the handoff renders instantly from a validated cache, no GPU wait. Then type your own story to run the live Qwen3-4B path on ZeroGPU.\n\n![Dental SOAP — one-page app with story intake, deterministic safety panel, and handoff preview](https://huggingface.co/spaces/build-small-hackathon/dental-soap/resolve/main/assets/hero.png)\n\n| The printable handoff artifact | Deterministic safety, with evidence |\n| --- | --- |\n| ![Dentist Visit Handoff card](https://huggingface.co/spaces/build-small-hackathon/dental-soap/resolve/main/assets/handoff-card.png) | ![Safety panel: rules fired with tiers and evidence spans quoting the patient's own words](https://huggingface.co/spaces/build-small-hackathon/dental-soap/resolve/main/assets/safety-panel.png) ![Arabic RTL handoff card from bilingual mode](https://huggingface.co/spaces/build-small-hackathon/dental-soap/resolve/main/assets/bilingual.png) |\n\n## What It Does\n\n- Opens with a **guided history-taking interview**: a hygienist-style AI agent establishes exact location, character, and radiation, then follows dental-specific ODIPARA (including thermal lingering, spontaneous versus provoked pain, bite/release triggers, night pattern, and functional impact), skips only fully covered details, and completes dental/medical background, two explicit safety screens, and the visit goal.\n- Runs a **deterministic safety sentinel between every interview turn** — the rules engine, not the model, re-checks the accumulated story after each answer and interrupts the interview the moment a hard red flag appears.\n- Turns a messy dental story into a printable Dentist Visit Handoff.\n- Keeps the UI focused on three surfaces: handoff card, deterministic safety panel, and dentist questions.\n- Shows evidence spans so safety flags can be traced back to the patient's words or structured answers.\n- Builds a deterministic \"Bring To The Visit\" checklist from the intake — imaging files on USB (not just the report), exact medication names, the dislodged crown in a clean container, the appliance itself.\n- Draws dentist questions from a clinically sourced question bank the model can extend but never replace or weaken.\n- Supports English, Arabic, and bilingual output framing.\n- Includes pre-computed example cases that render without a model call, so the demo works even when ZeroGPU is cold or out of quota.\n- Includes a print button for the handoff card, because the physical artifact is part of the proof-of-use story.\n\n## Why It Fits Build Small\n\n**One small model, three bounded roles.** The submission uses exactly one model — `Qwen/Qwen3-4B-Instruct-2507` (4B parameters, Apache-2.0) running in-process inside the Space:\n\n1. **History agent** — chooses one focused next question inside a coverage-based state machine bounded at 15 answers. It cannot set urgency or leave the approved dental qualifier, ODIPARA, and intake axes.\n2. **Intake extractor** — converts the guided transcript into the typed intake schema. Manual-form users skip this role.\n3. **Handoff agent** — may rewrite only six narrative fields. It cannot write red flags, evidence, limitations, medical safety notes, or the visit checklist.\n\nTwo deterministic controls surround those roles:\n\n- **Safety sentinel** — `safety_rules.evaluate_red_flags` runs after every interview answer and is the only authority that can escalate or interrupt.\n- **Output guard** — Pydantic schemas and claim filters discard malformed, diagnostic, or treatment-directive model fields before rendering.\n\nThe interface reports which stages actually ran, were skipped, used a cache, or fell back. The Space also stays functional with zero model calls: a question bank drives the interview, rules drive safety, and templates build the handoff. The model enriches; it never gates.\n\n## Safety Boundary\n\nDental SOAP follows these hard rules:\n\n- No diagnosis.\n- No model-authored treatment recommendation or dental-procedure selection.\n- No imaging interpretation.\n- No medication prescribing.\n- Patient education and visit documentation only; the output is designed to be brought to a licensed dentist.\n- Objective findings, assessment, and plan are left to the dentist.\n\nSafety escalations and fixed urgent-care instructions are computed by rules from the user's answers, never written or suppressed by the AI.\n\n## Deterministic Red Flags\n\nThe rule file is [`data/red_flags.json`](data/red_flags.json). It covers the highest-harm dental-adjacent situations from the local clinical research:\n\n- Airway or deep-space infection warning.\n- Age over 50 with jaw claudication pattern.\n- Possible endodontic irrigant accident.\n- Loose crown or bridge aspiration risk.\n- Facial swelling, fever, gum drainage, or abscess pattern.\n- Trauma with sudden bite change.\n- Neurologic or cardiac warning signs.\n- Severe uncontrolled pain.\n- Prolonged bleeding after extraction.\n- Possible mouth–sinus opening after extraction or sinus repair (mined from the builder's own case).\n- Repeated bite adjustments without lasting relief — a shifting-bite discussion prompt (also from the builder's own case).\n- Medication-associated bruxism prompt — fires only when an SSRI/SNRI/stimulant **and** jaw symptoms are both present.\n- Progressive tooth mobility in adults (age-gated so a child's normal loose tooth never fires it).\n- MRONJ medication prompt for antiresorptive or antiangiogenic medicines.\n- Blood thinner, steroid, immunosuppression, and allergy prompts.\n\nEach fired rule must include an evidence span.\n\nThe highest-harm rules also carry Egyptian Arabic colloquial trigger phrases — \"مش عارف اتنفس\" (*I can't breathe*), \"بلعت الطربوش\" (*I swallowed the crown*) — so the deterministic layer protects Arabic-speaking patients in their own words, with acute phrasing required so a routine root-canal history never trips an emergency rule.\n\n## Privacy Stance\n\nThis public demo is designed for de-identified or synthetic stories. The browser sends the story to the Hugging Face Space server for processing. The app has no database, does not intentionally persist patient stories, and sends no story to an external inference API. Real production use would require a separate privacy, security, and clinical-governance review.\n\n## Demo Spine\n\nThe submission video should show real use:\n\n1. Ahmed says: \"I'm a physician, and I couldn't explain my own dental problem to my dentist — my case took two specialties and three months to untangle.\"\n2. He answers the guided interview — the small model asks, the deterministic sentinel screens every answer — and builds the handoff from the conversation. One answer with a red-flag phrase shows the interview interrupting itself with urgent-care guidance.\n3. He loads the pre-computed `Try Ahmed's case` example — his real, de-identified case — so the handoff card and its dated timeline render instantly with no model call.\n4. He shows the deterministic safety panel: the repeated-adjustments flag fires on his own words (\"this rule exists because it happened to me\").\n5. He shows the Bring To The Visit checklist (the actual CBCT files, not just the report).\n6. He switches to bilingual English/Arabic framing.\n7. He shows the after-visit tracker filled from a real or realistic visit.\n\n## Demo Video & Social Post\n\nRequired submission items — links land here before the June 15 deadline:\n\n- **Demo video:** _coming before submission_\n- **Social post:** _coming before submission_\n\n## Hackathon Compliance\n\nVerified against the official Build Small page on June 5, 2026:\n\n| Requirement | Dental SOAP |\n| --- | --- |\n| Total model parameters no more than 32B | Pass: one 4B model |\n| Built with Gradio | Pass: Gradio 6 Space |\n| Hosted under `build-small-hackathon` | Pass |\n| Short demo video | Required human submission item; script is ready |\n| Social-media post | Required human submission item; draft is ready |\n| Backyard AI: specific real problem | Pass: the builder's own dental handoff problem |\n| Backyard AI: person actually used it | The cached case and Field Notes document use; the video should show the physical/clinical workflow |\n| Honest small-model fit | Pass: language organization is model-assisted; safety is deterministic |\n| Polished Gradio app | Custom responsive UI, instant cached demos, print/PDF/email export |\n| Tiny Titan special award (≤4B parameters) | Eligible: Qwen3-4B-Instruct-2507 is exactly 4B — the entire product runs on a Tiny-Titan-class model |\n\nBonus-quest position:\n\n- **Off the Grid:** claimed — no external inference API; Qwen runs inside the Space. (Google Fonts are presentation assets, not model or data APIs; no patient text ever leaves the Space.)\n- **Off-Brand:** claimed — custom visual system beyond default Gradio (905-line design system: tonal tokens, glassmorphism, custom document artwork, print stylesheet).\n- **Field Notes:** claimed — [`FIELD_NOTES.md`](FIELD_NOTES.md) is the build report: what was built, what the live tests caught (including the fabricated-negative incident), and what a 4B model can and cannot own in a safety-critical flow.\n- **Well-Tuned, Llama Champion, Sharing is Caring:** not claimed.\n\nAgent design in one line for the **Best Agent** lens: one 4B model held to three bounded\nroles (history-taker → intake-extractor → handoff-writer) while a deterministic safety\nsentinel runs between every turn — the model can never author, suppress, or downgrade\nan escalation, and the workflow panel reports what each role actually did on every run.\n\n## Local Development\n\nPure-Python safety modules can be tested locally:\n\n```bash\n.venv/bin/python -m pytest tests/ -q\n.venv/bin/python -m py_compile schema.py safety_rules.py pdf_export.py examples.py render.py app.py\n```\n\nTo run the full app locally:\n\n```bash\npython3 -m venv .venv\nsource .venv/bin/activate\npip install -r requirements.txt\npython app.py\n```\n\nThe local machine may not have GPU dependencies installed. On Hugging Face Spaces, dependencies are installed from `requirements.txt`.\n\nGenerate or refresh instant example caches without loading the model:\n\n```bash\n.venv/bin/python scripts/cache_examples.py --no-model\n```\n\nFor model-enriched cache files, run the same script without `--no-model` on a regular\nGPU machine. The script refuses to write a cache if live model generation falls back\nor fails validation.\n\n### Space Configuration (Guided Interview)\n\nThe guided interview runs natively inside the Hugging Face Space using the `@spaces.GPU` decorator. No external API keys or secrets are required for the AI model to run. \n\nIf running locally without a GPU (or when the HF ZeroGPU quota is exceeded), the system degrades safely to the built-in clinical question bank and the deterministic pipeline, keeping the application fully functional even without a model.\n\n### Use via API\n\nThe guided interview is exposed as a stateless REST endpoint (`/interview_api`) that plain\n`curl` can drive — the interview state travels as an opaque JSON token instead of a hidden\nUI session. The deterministic red-flag sentinel runs on every turn, exactly as in the UI:\n\n```bash\nBASE=\"https://build-small-hackathon-dental-soap.hf.space/gradio_api/call/interview_api\"\n# Turn 1 — empty state starts a new interview\nEVENT=$(curl -s -X POST \"$BASE\" -H \"Content-Type: application/json\" \\\n  -d '{\"data\": [\"Sam, 34\", \"\"]}' | python3 -c \"import json,sys; print(json.load(sys.stdin)['event_id'])\")\ncurl -s -N \"$BASE/$EVENT\"\n# Pass the returned `state` string back as the second argument to continue.\n```\n\nThe response carries `reply`, `done`, `early_exit`, `hard_findings` (rule-computed, never\nmodel-authored), `stage`, and the `state` token for the next turn. Python callers can use\n`gradio_client` against the UI endpoints instead; both paths run the same sentinel.\n\n## Repository Map\n\n- [`app.py`](app.py): one-page Gradio app.\n- [`interview.py`](interview.py): adaptive dental-specific ODIPARA interview state machine (coverage-based, bounded at 15 answers, deterministic age-aware safety sentinel between turns).\n- [`interview_schema.py`](interview_schema.py): extractor contract + bridge into `StructuredIntake`/`PatientProfile`.\n- [`schema.py`](schema.py): Pydantic schema for validated handoff data.\n- [`safety_rules.py`](safety_rules.py): deterministic red-flag engine.\n- [`render.py`](render.py): HTML render helpers for handoff card and safety panel.\n- [`data/red_flags.json`](data/red_flags.json): static clinical safety rules.\n- [`pdf_export.py`](pdf_export.py): ReportLab one-page PDF export.\n- [`tests/`](tests/): schema and safety smoke tests.\n- [`examples.py`](examples.py): pre-computed demo case inputs.\n- [`data/example_cache/`](data/example_cache/): validated instant example outputs.\n- [`scripts/cache_examples.py`](scripts/cache_examples.py): cache generation and validation.\n- [`scripts/eval_safety.py`](scripts/eval_safety.py): deterministic safety eval (recall + specificity, no GPU).\n- [`scripts/mass_audit.py`](scripts/mass_audit.py): 1,000+-story mutation audit over the same vignettes.\n- [`smoke_test.py`](smoke_test.py): local safety smoke tests.\n- [`FIELD_NOTES.md`](FIELD_NOTES.md): build report for the Field Notes bonus quest.\n\n## Measured Safety Numbers\n\nReproduce in seconds, no GPU or network needed:\n\n```bash\npython scripts/eval_safety.py   # 77 lay-paraphrase vignettes\npython scripts/mass_audit.py    # the same vignettes under 20 hostile mutations\n```\n\n- **Red-flag recall: 60/60** lay-paraphrase vignettes fire their expected rule — every rule is exercised through casual phrasings (\"my cap came off while eating\", \"water comes out of my nose when I drink\"), not verbatim pattern strings. Ten vignettes are Egyptian Arabic colloquial phrasings (\"مش قادر اتنفس\", \"بلعت الطربوش\"), covering masculine and feminine dialect forms.\n- **Benign specificity: 17/17** benign stories (check-ups, whitening, a child's normal loose tooth, negated symptoms, an uneventful old root canal mentioned in Arabic, and idiom traps like \"knocked out early from work\" or a relative's chemo years ago) fire zero flags.\n- **Mass audit: 1,540/1,540 stories clean** — every vignette under 20 mutations (chatty prefixes, case changes, smart apostrophes, doubled whitespace, newlines, zero-width characters from web copy-paste, filler sentences): 1,200/1,200 recall, 340/340 specificity.\n- **Local tests** cover the safety rules, ODIPARA coverage and axis guards, both explicit red-flag screens, negation/conjunction handling, adversarial Unicode, Arabic dialect triggers, avulsed-tooth and swallowed-object trauma, model parsing/merge guards, cache round-trip, exports, and Gradio handler arity.\n\n## Submission Readiness\n\n- Cached examples render instantly without GPU allocation.\n- The workflow panel reports which model roles actually ran instead of presenting every stage as complete.\n- Live model output is parsed through a typed, diagnosis/treatment-guarded draft before merge.\n- Red flags and evidence remain deterministic and cannot be authored, removed, or downgraded by the model.\n- PDF export escapes patient text before ReportLab markup parsing.\n- The local audit suite covers safety rules, model parsing/merge, cache round-trip, exports, label drift, and Gradio handler arity.\n",1089      "readme_body": "# Dental SOAP\n\nA doctor could not clearly explain his own crown, root canal, bite, and TMJ story to his dentist, so he built a small-model visit-prep tool that turns patient chaos into a one-page dentist handoff.\n\nDental SOAP is not an AI dentist. It is a patient education and visit-documentation aid. It organizes patient-reported history to bring to a licensed dentist. It does not diagnose, interpret imaging, prescribe medication, or choose a dental procedure. Fixed safety rules can advise urgent in-person care.\n\n## Why This Exists (A Real Case)\n\nThe builder's own case took three months and two specialties to untangle: an extraction with an immediate sinus repair, a crown that felt high from day one, a molar adjusted five times without relief, jaw/TMJ soreness — and finally an ENT confirming the sinus infection that tied it together. Referred pain does not respect specialty boundaries; the only thing that crossed them cleanly was a written handoff a clinician could scan in under a minute. He lived that workflow manually with a frontier cloud model between real appointments. Dental SOAP is the Build Small answer: the same narrow job, done by a 4B open model inside the Space, with deterministic rules guarding safety. The `Try Ahmed's case` example **is** that case, de-identified — and the `repeated bite adjustments without lasting relief` safety rule exists because it happened to him.\n\n**Try it in 10 seconds:** click **Try Ahmed's case** — the handoff renders instantly from a validated cache, no GPU wait. Then type your own story to run the live Qwen3-4B path on ZeroGPU.\n\n![Dental SOAP — one-page app with story intake, deterministic safety panel, and handoff preview](https://huggingface.co/spaces/build-small-hackathon/dental-soap/resolve/main/assets/hero.png)\n\n| The printable handoff artifact | Deterministic safety, with evidence |\n| --- | --- |\n| ![Dentist Visit Handoff card](https://huggingface.co/spaces/build-small-hackathon/dental-soap/resolve/main/assets/handoff-card.png) | ![Safety panel: rules fired with tiers and evidence spans quoting the patient's own words](https://huggingface.co/spaces/build-small-hackathon/dental-soap/resolve/main/assets/safety-panel.png) ![Arabic RTL handoff card from bilingual mode](https://huggingface.co/spaces/build-small-hackathon/dental-soap/resolve/main/assets/bilingual.png) |\n\n## What It Does\n\n- Opens with a **guided history-taking interview**: a hygienist-style AI agent establishes exact location, character, and radiation, then follows dental-specific ODIPARA (including thermal lingering, spontaneous versus provoked pain, bite/release triggers, night pattern, and functional impact), skips only fully covered details, and completes dental/medical background, two explicit safety screens, and the visit goal.\n- Runs a **deterministic safety sentinel between every interview turn** — the rules engine, not the model, re-checks the accumulated story after each answer and interrupts the interview the moment a hard red flag appears.\n- Turns a messy dental story into a printable Dentist Visit Handoff.\n- Keeps the UI focused on three surfaces: handoff card, deterministic safety panel, and dentist questions.\n- Shows evidence spans so safety flags can be traced back to the patient's words or structured answers.\n- Builds a deterministic \"Bring To The Visit\" checklist from the intake — imaging files on USB (not just the report), exact medication names, the dislodged crown in a clean container, the appliance itself.\n- Draws dentist questions from a clinically sourced question bank the model can extend but never replace or weaken.\n- Supports English, Arabic, and bilingual output framing.\n- Includes pre-computed example cases that render without a model call, so the demo works even when ZeroGPU is cold or out of quota.\n- Includes a print button for the handoff card, because the physical artifact is part of the proof-of-use story.\n\n## Why It Fits Build Small\n\n**One small model, three bounded roles.** The submission uses exactly one model — `Qwen/Qwen3-4B-Instruct-2507` (4B parameters, Apache-2.0) running in-process inside the Space:\n\n1. **History agent** — chooses one focused next question inside a coverage-based state machine bounded at 15 answers. It cannot set urgency or leave the approved dental qualifier, ODIPARA, and intake axes.\n2. **Intake extractor** — converts the guided transcript into the typed intake schema. Manual-form users skip this role.\n3. **Handoff agent** — may rewrite only six narrative fields. It cannot write red flags, evidence, limitations, medical safety notes, or the visit checklist.\n\nTwo deterministic controls surround those roles:\n\n- **Safety sentinel** — `safety_rules.evaluate_red_flags` runs after every interview answer and is the only authority that can escalate or interrupt.\n- **Output guard** — Pydantic schemas and claim filters discard malformed, diagnostic, or treatment-directive model fields before rendering.\n\nThe interface reports which stages actually ran, were skipped, used a cache, or fell back. The Space also stays functional with zero model calls: a question bank drives the interview, rules drive safety, and templates build the handoff. The model enriches; it never gates.\n\n## Safety Boundary\n\nDental SOAP follows these hard rules:\n\n- No diagnosis.\n- No model-authored treatment recommendation or dental-procedure selection.\n- No imaging interpretation.\n- No medication prescribing.\n- Patient education and visit documentation only; the output is designed to be brought to a licensed dentist.\n- Objective findings, assessment, and plan are left to the dentist.\n\nSafety escalations and fixed urgent-care instructions are computed by rules from the user's answers, never written or suppressed by the AI.\n\n## Deterministic Red Flags\n\nThe rule file is [`data/red_flags.json`](data/red_flags.json). It covers the highest-harm dental-adjacent situations from the local clinical research:\n\n- Airway or deep-space infection warning.\n- Age over 50 with jaw claudication pattern.\n- Possible endodontic irrigant accident.\n- Loose crown or bridge aspiration risk.\n- Facial swelling, fever, gum drainage, or abscess pattern.\n- Trauma with sudden bite change.\n- Neurologic or cardiac warning signs.\n- Severe uncontrolled pain.\n- Prolonged bleeding after extraction.\n- Possible mouth–sinus opening after extraction or sinus repair (mined from the builder's own case).\n- Repeated bite adjustments without lasting relief — a shifting-bite discussion prompt (also from the builder's own case).\n- Medication-associated bruxism prompt — fires only when an SSRI/SNRI/stimulant **and** jaw symptoms are both present.\n- Progressive tooth mobility in adults (age-gated so a child's normal loose tooth never fires it).\n- MRONJ medication prompt for antiresorptive or antiangiogenic medicines.\n- Blood thinner, steroid, immunosuppression, and allergy prompts.\n\nEach fired rule must include an evidence span.\n\nThe highest-harm rules also carry Egyptian Arabic colloquial trigger phrases — \"مش عارف اتنفس\" (*I can't breathe*), \"بلعت الطربوش\" (*I swallowed the crown*) — so the deterministic layer protects Arabic-speaking patients in their own words, with acute phrasing required so a routine root-canal history never trips an emergency rule.\n\n## Privacy Stance\n\nThis public demo is designed for de-identified or synthetic stories. The browser sends the story to the Hugging Face Space server for processing. The app has no database, does not intentionally persist patient stories, and sends no story to an external inference API. Real production use would require a separate privacy, security, and clinical-governance review.\n\n## Demo Spine\n\nThe submission video should show real use:\n\n1. Ahmed says: \"I'm a physician, and I couldn't explain my own dental problem to my dentist — my case took two specialties and three months to untangle.\"\n2. He answers the guided interview — the small model asks, the deterministic sentinel screens every answer — and builds the handoff from the conversation. One answer with a red-flag phrase shows the interview interrupting itself with urgent-care guidance.\n3. He loads the pre-computed `Try Ahmed's case` example — his real, de-identified case — so the handoff card and its dated timeline render instantly with no model call.\n4. He shows the deterministic safety panel: the repeated-adjustments flag fires on his own words (\"this rule exists because it happened to me\").\n5. He shows the Bring To The Visit checklist (the actual CBCT files, not just the report).\n6. He switches to bilingual English/Arabic framing.\n7. He shows the after-visit tracker filled from a real or realistic visit.\n\n## Demo Video & Social Post\n\nRequired submission items — links land here before the June 15 deadline:\n\n- **Demo video:** _coming before submission_\n- **Social post:** _coming before submission_\n\n## Hackathon Compliance\n\nVerified against the official Build Small page on June 5, 2026:\n\n| Requirement | Dental SOAP |\n| --- | --- |\n| Total model parameters no more than 32B | Pass: one 4B model |\n| Built with Gradio | Pass: Gradio 6 Space |\n| Hosted under `build-small-hackathon` | Pass |\n| Short demo video | Required human submission item; script is ready |\n| Social-media post | Required human submission item; draft is ready |\n| Backyard AI: specific real problem | Pass: the builder's own dental handoff problem |\n| Backyard AI: person actually used it | The cached case and Field Notes document use; the video should show the physical/clinical workflow |\n| Honest small-model fit | Pass: language organization is model-assisted; safety is deterministic |\n| Polished Gradio app | Custom responsive UI, instant cached demos, print/PDF/email export |\n| Tiny Titan special award (≤4B parameters) | Eligible: Qwen3-4B-Instruct-2507 is exactly 4B — the entire product runs on a Tiny-Titan-class model |\n\nBonus-quest position:\n\n- **Off the Grid:** claimed — no external inference API; Qwen runs inside the Space. (Google Fonts are presentation assets, not model or data APIs; no patient text ever leaves the Space.)\n- **Off-Brand:** claimed — custom visual system beyond default Gradio (905-line design system: tonal tokens, glassmorphism, custom document artwork, print stylesheet).\n- **Field Notes:** claimed — [`FIELD_NOTES.md`](FIELD_NOTES.md) is the build report: what was built, what the live tests caught (including the fabricated-negative incident), and what a 4B model can and cannot own in a safety-critical flow.\n- **Well-Tuned, Llama Champion, Sharing is Caring:** not claimed.\n\nAgent design in one line for the **Best Agent** lens: one 4B model held to three bounded\nroles (history-taker → intake-extractor → handoff-writer) while a deterministic safety\nsentinel runs between every turn — the model can never author, suppress, or downgrade\nan escalation, and the workflow panel reports what each role actually did on every run.\n\n## Local Development\n\nPure-Python safety modules can be tested locally:\n\n```bash\n.venv/bin/python -m pytest tests/ -q\n.venv/bin/python -m py_compile schema.py safety_rules.py pdf_export.py examples.py render.py app.py\n```\n\nTo run the full app locally:\n\n```bash\npython3 -m venv .venv\nsource .venv/bin/activate\npip install -r requirements.txt\npython app.py\n```\n\nThe local machine may not have GPU dependencies installed. On Hugging Face Spaces, dependencies are installed from `requirements.txt`.\n\nGenerate or refresh instant example caches without loading the model:\n\n```bash\n.venv/bin/python scripts/cache_examples.py --no-model\n```\n\nFor model-enriched cache files, run the same script without `--no-model` on a regular\nGPU machine. The script refuses to write a cache if live model generation falls back\nor fails validation.\n\n### Space Configuration (Guided Interview)\n\nThe guided interview runs natively inside the Hugging Face Space using the `@spaces.GPU` decorator. No external API keys or secrets are required for the AI model to run. \n\nIf running locally without a GPU (or when the HF ZeroGPU quota is exceeded), the system degrades safely to the built-in clinical question bank and the deterministic pipeline, keeping the application fully functional even without a model.\n\n### Use via API\n\nThe guided interview is exposed as a stateless REST endpoint (`/interview_api`) that plain\n`curl` can drive — the interview state travels as an opaque JSON token instead of a hidden\nUI session. The deterministic red-flag sentinel runs on every turn, exactly as in the UI:\n\n```bash\nBASE=\"https://build-small-hackathon-dental-soap.hf.space/gradio_api/call/interview_api\"\n# Turn 1 — empty state starts a new interview\nEVENT=$(curl -s -X POST \"$BASE\" -H \"Content-Type: application/json\" \\\n  -d '{\"data\": [\"Sam, 34\", \"\"]}' | python3 -c \"import json,sys; print(json.load(sys.stdin)['event_id'])\")\ncurl -s -N \"$BASE/$EVENT\"\n# Pass the returned `state` string back as the second argument to continue.\n```\n\nThe response carries `reply`, `done`, `early_exit`, `hard_findings` (rule-computed, never\nmodel-authored), `stage`, and the `state` token for the next turn. Python callers can use\n`gradio_client` against the UI endpoints instead; both paths run the same sentinel.\n\n## Repository Map\n\n- [`app.py`](app.py): one-page Gradio app.\n- [`interview.py`](interview.py): adaptive dental-specific ODIPARA interview state machine (coverage-based, bounded at 15 answers, deterministic age-aware safety sentinel between turns).\n- [`interview_schema.py`](interview_schema.py): extractor contract + bridge into `StructuredIntake`/`PatientProfile`.\n- [`schema.py`](schema.py): Pydantic schema for validated handoff data.\n- [`safety_rules.py`](safety_rules.py): deterministic red-flag engine.\n- [`render.py`](render.py): HTML render helpers for handoff card and safety panel.\n- [`data/red_flags.json`](data/red_flags.json): static clinical safety rules.\n- [`pdf_export.py`](pdf_export.py): ReportLab one-page PDF export.\n- [`tests/`](tests/): schema and safety smoke tests.\n- [`examples.py`](examples.py): pre-computed demo case inputs.\n- [`data/example_cache/`](data/example_cache/): validated instant example outputs.\n- [`scripts/cache_examples.py`](scripts/cache_examples.py): cache generation and validation.\n- [`scripts/eval_safety.py`](scripts/eval_safety.py): deterministic safety eval (recall + specificity, no GPU).\n- [`scripts/mass_audit.py`](scripts/mass_audit.py): 1,000+-story mutation audit over the same vignettes.\n- [`smoke_test.py`](smoke_test.py): local safety smoke tests.\n- [`FIELD_NOTES.md`](FIELD_NOTES.md): build report for the Field Notes bonus quest.\n\n## Measured Safety Numbers\n\nReproduce in seconds, no GPU or network needed:\n\n```bash\npython scripts/eval_safety.py   # 77 lay-paraphrase vignettes\npython scripts/mass_audit.py    # the same vignettes under 20 hostile mutations\n```\n\n- **Red-flag recall: 60/60** lay-paraphrase vignettes fire their expected rule — every rule is exercised through casual phrasings (\"my cap came off while eating\", \"water comes out of my nose when I drink\"), not verbatim pattern strings. Ten vignettes are Egyptian Arabic colloquial phrasings (\"مش قادر اتنفس\", \"بلعت الطربوش\"), covering masculine and feminine dialect forms.\n- **Benign specificity: 17/17** benign stories (check-ups, whitening, a child's normal loose tooth, negated symptoms, an uneventful old root canal mentioned in Arabic, and idiom traps like \"knocked out early from work\" or a relative's chemo years ago) fire zero flags.\n- **Mass audit: 1,540/1,540 stories clean** — every vignette under 20 mutations (chatty prefixes, case changes, smart apostrophes, doubled whitespace, newlines, zero-width characters from web copy-paste, filler sentences): 1,200/1,200 recall, 340/340 specificity.\n- **Local tests** cover the safety rules, ODIPARA coverage and axis guards, both explicit red-flag screens, negation/conjunction handling, adversarial Unicode, Arabic dialect triggers, avulsed-tooth and swallowed-object trauma, model parsing/merge guards, cache round-trip, exports, and Gradio handler arity.\n\n## Submission Readiness\n\n- Cached examples render instantly without GPU allocation.\n- The workflow panel reports which model roles actually ran instead of presenting every stage as complete.\n- Live model output is parsed through a typed, diagnosis/treatment-guarded draft before merge.\n- Red flags and evidence remain deterministic and cannot be authored, removed, or downgraded by the model.\n- PDF export escapes patient text before ReportLab markup parsing.\n- The local audit suite covers safety rules, model parsing/merge, cache round-trip, exports, label drift, and Gradio handler arity.",1090      "readme_frontmatter": {1091        "title": "Dental SOAP",1092        "emoji": "🦷",1093        "colorFrom": "green",1094        "colorTo": "blue",1095        "sdk": "gradio",1096        "sdk_version": "6.16.0",1097        "python_version": "3.10.13",1098        "app_file": "app.py",1099        "license": "apache-2.0",1100        "models": "",1101        "short_description": "A small-model dental handoff for real patient stories.",1102        "thumbnail": "https://huggingface.co/spaces/build-small-hackathon/dental-soap/resolve/main/assets/hero.png",1103        "tags": ""1104      },1105      "app_source": "from __future__ import annotations\n\nimport dataclasses\nimport json\nimport os\nimport re\nimport sys\nimport threading\nfrom pathlib import Path\nfrom typing import Any\n\nimport gradio as gr\n\nclass AgentUnavailable(RuntimeError):\n    \"\"\"The local model endpoint could not produce a usable response.\"\"\"\n\nimport interview as interview_mod\nfrom examples import CHECK_OPTIONS, EXAMPLES, STEP2_CHECKS, STEP3_CHECKS, STEP4_CHECKS\nfrom interview_schema import ExtractedIntake, extracted_to_intake\nfrom pdf_export import build_pdf\nfrom render import (\n    footer_html,\n    header_html,\n    initial_safety_html,\n    placeholder_handoff_html,\n    plain_text_handoff,\n    rail_html,\n    render_handoff_html,\n    render_safety_html,\n    step_head,\n    initial_agent_dashboard_html,\n    render_agent_dashboard,\n)\nfrom safety_rules import evaluate_red_flags\nfrom pydantic import ValidationError\nfrom schema import (\n    BLOCKED_QUESTION_TERMS,\n    EvidenceSpan,\n    HandoffOutput,\n    ModelHandoffDraft,\n    PatientProfile,\n    StructuredIntake,\n    model_text_is_safe,\n)\n\n\ntry:\n    import spaces\nexcept Exception:\n    class _SpacesFallback:\n        @staticmethod\n        def GPU(fn=None, /, *, duration: int = 120):\n            # Support both @spaces.GPU (fn is callable) and @spaces.GPU(duration=N)\n            # (fn is None, returns a decorator).\n            def decorator(f):\n                return f\n\n            if callable(fn):\n                # Used as @spaces.GPU directly — return the function unchanged.\n                return fn\n            # Used as @spaces.GPU(duration=N) — return a decorator.\n            return decorator\n\n    spaces = _SpacesFallback()\n\n\nMODEL_ID = os.getenv(\"DENTAL_SOAP_MODEL_ID\", \"Qwen/Qwen3-4B-Instruct-2507\")\nUSE_MODEL_BY_DEFAULT = os.getenv(\"DENTAL_SOAP_USE_MODEL\", \"1\") == \"1\"\n_MODEL: dict[str, Any] = {}\n_MODEL_LOAD_LOCK = threading.Lock()\n_MODEL_OUTPUT_KEYS = frozenset(ModelHandoffDraft.model_fields)\n\n# ZeroGPU pattern: load weights at import time so the GPU allocation window in\n# @spaces.GPU only needs to cover the generate() call. On Spaces the ZeroGPU shim\n# intercepts .to(\"cuda\") at import and moves weights when the window opens — do\n# NOT use device_map=\"auto\" here (accelerate dispatch bypasses the shim and can\n# strand weights on CPU). Wrapped in try/except so import never crashes locally.\nif USE_MODEL_BY_DEFAULT:\n    try:\n        import torch\n        from transformers import AutoModelForCausalLM, AutoTokenizer as _AutoTokenizer\n\n        _ON_SPACES = os.getenv(\"SPACE_ID\") is not None\n        _tok = _AutoTokenizer.from_pretrained(MODEL_ID, trust_remote_code=True)\n        _mdl = AutoModelForCausalLM.from_pretrained(\n            MODEL_ID,\n            torch_dtype=torch.bfloat16 if (_ON_SPACES or torch.cuda.is_available()) else torch.float32,\n            trust_remote_code=True,\n        )\n        if _ON_SPACES or torch.cuda.is_available():\n            _mdl = _mdl.to(\"cuda\")\n        _MODEL[\"tokenizer\"] = _tok\n        _MODEL[\"model\"] = _mdl\n    except Exception as exc:\n        print(f\"[dental-soap] import-time model load failed: {exc}\", file=sys.stderr)\n\nCSS = Path(__file__).parent.joinpath(\"style.css\").read_text(encoding=\"utf-8\")\nINTERVIEW_AVATAR = Path(__file__).parent.joinpath(\"assets\", \"dental-guide-avatar.svg\")\n\nCHECK_MAP = {\n    \"Biting pain\": \"biting_pain\",\n    \"Hot/cold sensitivity\": \"hot_cold_sensitivity\",\n    \"Pain prevents sleep\": \"pain_prevents_sleep\",\n    \"Facial or gum swelling\": \"swelling\",\n    \"Rapidly spreading swelling\": \"rapidly_spreading_swelling\",\n    \"Fever or feeling very unwell\": \"fever_or_unwell\",\n    \"Breathing or swallowing issue\": \"breathing_or_swallowing_issue\",\n    \"Limited opening or locked jaw\": \"limited_opening_or_locked_jaw\",\n    \"Loose crown or bridge\": \"loose_crown_or_bridge\",\n    \"Trauma or sudden bite change\": \"trauma_or_sudden_bite_change\",\n    \"Numbness or neurologic symptoms\": \"numbness_or_neuro_symptoms\",\n    \"Chest pain or jaw pain with exertion\": \"chest_pain_or_jaw_pain_with_exertion\",\n    \"Jaw pain with chewing that improves with rest\": \"jaw_pain_with_chewing_relieved_by_rest\",\n    \"Vision/scalp tenderness/new severe headache\": \"vision_scalp_or_new_headache\",\n    \"Gum pimple or drainage\": \"gum_pimple_or_drainage\",\n    \"Bruising or burning pain after root canal\": \"bruising_or_burning_after_root_canal\",\n}\n\n\nSYSTEM_PROMPT = \"\"\"\nYou are Dental SOAP, a safety-first dental visit-prep assistant.\n\nTask: transform patient-reported dental history into JSON for a dentist visit handoff.\n\nHard rules:\n- Do not diagnose.\n- Do not recommend treatment.\n- Do not interpret imaging.\n- Use only facts stated by the patient.\n- Leave objective findings, assessment, and plan to the dentist.\n- Write dentist-facing questions, not conclusions.\n- If information is missing, add a question.\n- Every generated detail should be grounded in the user's story.\n- Patients may use shorthand: \"endo\" or \"RCT\" means root canal treatment; a \"cap\" means a crown; \"pulled\" means extraction; \"cleaning\" means scaling. Expand shorthand without adding new claims.\n- Never state that something did NOT happen or was NOT done unless the patient explicitly said so.\n\nReturn strict JSON with these keys:\nchief_concern, concise_summary, timeline, current_symptoms, dental_history,\ndentist_questions.\nAll list fields must be arrays of short strings.\n\"\"\"\n\n\ndef _selected_to_intake(\n    chief_concern: str,\n    tooth_or_area: str,\n    recent_dental_work: str,\n    symptom_duration: str,\n    pain_score: int,\n    selected_checks: list[str] | None,\n) -> StructuredIntake:\n    values = {\n        \"chief_concern\": chief_concern.strip(),\n        \"tooth_or_area\": tooth_or_area.strip(),\n        \"recent_dental_work\": recent_dental_work.strip(),\n        \"symptom_duration\": symptom_duration.strip(),\n        \"pain_score\": int(pain_score or 0),\n    }\n    for label, field in CHECK_MAP.items():\n        values[field] = label in (selected_checks or [])\n    return StructuredIntake(**values)\n\n\ndef _source_quote(story: str) -> list[EvidenceSpan]:\n    clean = re.sub(r\"\\s+\", \" \", story).strip()\n    if not clean:\n        return []\n    return [EvidenceSpan(source=\"free_text\", quote=clean[:260])]\n\n\ndef _split_story(story: str) -> list[str]:\n    parts = re.split(r\"(?<=[.!?])\\s+\", re.sub(r\"\\s+\", \" \", story).strip())\n    return [part for part in parts if part][:4]\n\n\n# Fabricated-negative guard (June 6, mined from the builder's live endo test):\n# the model wrote \"No recent dental work\" / \"No endodontic treatment attempted\"\n# while the patient had said \"I made an endo\". A model-authored negative survives\n# the merge only when the patient's own story negates the same topic.\n_NEGATIVE_ASSERTION = re.compile(\n    r\"^\\s*(?:no\\b|none\\b|not\\b|never\\b|nil\\b|without\\b|denies\\b|denied\\b\"\n    r\"|لا\\b|لم\\b|لن\\b|بدون\\b|مفيش)\",\n    re.IGNORECASE,\n)\n_NEGATION_TOKENS = (\n    \"no\", \"not\", \"none\", \"never\", \"nil\", \"without\", \"denies\", \"denied\",\n    \"لا\", \"لم\", \"لن\", \"بدون\", \"مفيش\",\n)\n# Framing/generic words that never identify the *topic* of a negative claim.\n_NEGATIVE_TOPIC_STOPWORDS = frozenset({\n    \"none\", \"never\", \"denies\", \"denied\", \"without\",\n    \"this\", \"that\", \"with\", \"have\", \"been\", \"were\", \"does\", \"from\",\n    \"attempted\", \"reported\", \"noted\", \"known\", \"stated\", \"mentioned\",\n    \"recent\", \"prior\", \"history\", \"work\", \"treatment\", \"patient\", \"dental\",\n})\n\n\ndef _negative_grounded_in_story(item: str, story: str) -> bool:\n    \"\"\"True only when the patient's own story negates the topic the item negates.\n\n    Fabricated negatives (topic never mentioned) and story-contradicting\n    negatives (topic mentioned WITHOUT negation) both return False.\n    \"\"\"\n    topics = [\n        token\n        for token in re.findall(r\"[\\w']+\", item.lower())\n        if len(token) >= 4 and token not in _NEGATIVE_TOPIC_STOPWORDS\n    ]\n    if not topics:\n        return False\n    for sentence in re.split(r\"(?<=[.!?؟])\\s+|\\n+\", story.lower()):\n        words = re.findall(r\"[\\w']+\", sentence)\n        for index, word in enumerate(words):\n            if len(word) < 4:\n                continue\n            if not any(word.startswith(topic) or topic.startswith(word) for topic in topics):\n                continue\n            # The negation must sit just BEFORE the topic word (\"no swelling\",\n            # \"didn't have swelling\"). A sentence-wide check would conflate\n            # \"I had an endo but no fever\" into a negated endo.\n            window = words[max(0, index - 3):index]\n            if any(prior in _NEGATION_TOKENS or \"n't\" in prior for prior in window):\n                return True\n    return False\n\n\n# Prior-work terms a patient may use in the raw story (incl. Egyptian Arabic and\n# lay shorthand like \"endo\"). Used to surface work the model/extractor missed.\n_STORY_WORK_PATTERNS: tuple[tuple[str, str], ...] = (\n    (r\"\\broot canal\\b|\\bendo\\w*\\b|\\brct\\b|علاج (?:ال)?عصب|حشو (?:ال)?عصب\", \"root canal\"),\n    (r\"\\bcrown\\b|\\bcap\\b|طربوش|تلبيس\", \"crown\"),\n    (r\"\\bimplant\\w*\\b|زرع|زراعة\", \"implant\"),\n    (r\"\\bextract\\w*\\b|\\bpulled\\b|خلع\", \"extraction\"),\n    (r\"\\bfilling\\w*\\b|\\bfilled\\b|حشو\", \"filling\"),\n    (r\"\\bveneer\\w*\\b\", \"veneer\"),\n    (r\"\\bbraces\\b|\\borthodont\\w*\\b|تقويم\", \"orthodontic work\"),\n)\n\n\ndef _story_dental_work_mentions(story: str) -> list[str]:\n    \"\"\"Canonical prior-work terms the patient used anywhere in the raw story.\"\"\"\n    story_lower = (story or \"\").lower()\n    return [label for pattern, label in _STORY_WORK_PATTERNS if re.search(pattern, story_lower)]\n\n\ndef _ensure_story_dental_work(output: HandoffOutput, story: str) -> HandoffOutput:\n    \"\"\"Deterministic backstop: prior dental work the patient mentioned in the raw\n    story must survive into the handoff even when the model or extractor missed\n    it. Idempotent — terms already present in dental_history are not re-added.\"\"\"\n    mentions = _story_dental_work_mentions(story)\n    if not mentions:\n        return output\n    existing = \" \".join(output.dental_history).lower()\n    missing = [label for label in mentions if label not in existing]\n    if not missing:\n        return output\n    history = [\n        item for item in output.dental_history\n        if item != \"Prior dental work not specified.\"\n    ]\n    history.append(\"Patient story mentions prior dental work: \" + \", \".join(missing))\n    return output.model_copy(update={\"dental_history\": history[:8]})\n\n\ndef _base_questions(intake: StructuredIntake, story: str = \"\", meds: str = \"\") -> list[str]:\n    \"\"\"Deterministic dentist-question bank, mined from the clinical frameworks doc.\n\n    The model can append questions but never replace these (see _merge_model_output).\n    Conditions key off structured intake plus simple story keywords; every entry is a\n    question for the dentist, never a conclusion.\n    \"\"\"\n    story_lower = (story or \"\").lower()\n    questions = [\n        \"Which tooth or area should we prioritize examining first based on my history?\",\n        \"What findings on exam or imaging would help separate tooth, crown, bite, and jaw-muscle causes?\",\n        \"What should I track after today's visit so we can tell whether symptoms are improving?\",\n    ]\n    if intake.loose_crown_or_bridge or \"crown\" in intake.recent_dental_work.lower():\n        questions.append(\"Can you check the crown margin, contacts, cement seal, and whether the bite is high?\")\n    if \"root canal\" in intake.recent_dental_work.lower() or re.search(\n        r\"\\broot canal\\b|\\bendo\\w*\\b|\\brct\\b\", story_lower\n    ):\n        questions.append(\"Should this tooth have an endodontic reassessment, and what records or X-rays would help?\")\n    if intake.biting_pain or intake.trauma_or_sudden_bite_change:\n        questions.append(\"Can you check the bite with articulating paper and compare both sides of contact?\")\n    if intake.limited_opening_or_locked_jaw or intake.jaw_pain_with_chewing_relieved_by_rest:\n        questions.append(\"Could jaw muscles or the TMJ be contributing, and do I need referral or conservative jaw care?\")\n    # Cross-specialty prompt (dental <-> ENT) — mined from the builder's real case,\n    # where upper-tooth symptoms and sinus symptoms turned out to be one problem.\n    if \"sinus\" in story_lower or \"sinus\" in intake.recent_dental_work.lower():\n        questions.append(\"Could my sinus symptoms and tooth symptoms be related, and how would we tell which is driving which?\")\n    if intake.hot_cold_sensitivity:\n        questions.append(\"Does my temperature-sensitivity pattern help localize which tooth or surface to test first?\")\n    if intake.swelling or intake.gum_pimple_or_drainage:\n        questions.append(\"What warning signs of spreading infection should send me to urgent care before our next appointment?\")\n    if (meds or \"\").strip():\n        questions.append(\"Do any of my current medications change what is safe or recommended at this visit?\")\n    return questions[:8]\n\n\ndef _tracker_items() -> list[str]:\n    return [\n        \"What was examined: tooth/area, bite, crown margin, gums, TMJ, imaging reviewed.\",\n        \"What changed today: adjustment, medication advice, referral, imaging request, or watchful waiting.\",\n        \"Pain score before/after visit and whether biting, temperature, or jaw symptoms changed.\",\n        \"Next step, owner, and follow-up date.\",\n    ]\n\n\ndef _bring_checklist(profile: PatientProfile, intake: StructuredIntake, story: str) -> list[str]:\n    \"\"\"Deterministic 'bring to the visit' checklist — rules over intake/profile/story.\n\n    Mined from the clinical frameworks doc ('artifacts to bring') and the builder's\n    own visits (imaging files on USB, exact medication names). Never model-authored.\n    \"\"\"\n    lower = f\"{story} {intake.recent_dental_work}\".lower()\n    items: list[str] = []\n    if profile.meds.strip():\n        items.append(f\"Your medication list with doses (or the boxes themselves): {profile.meds.strip()}.\")\n    else:\n        items.append(\"A written list of any medications and doses, or the medicine boxes themselves.\")\n    if profile.allergies.strip():\n        items.append(f\"Exact allergy names and the reaction you had: {profile.allergies.strip()}.\")\n    if any(term in lower for term in (\"x-ray\", \"xray\", \"cbct\", \"scan\", \"panoramic\", \"dicom\", \"imaging\", \"radiograph\")):\n        items.append(\"The actual imaging files (X-ray/CBCT) on USB or your phone — the files, not just the written report.\")\n    if intake.recent_dental_work.strip():\n        items.append(\"Dates of recent dental procedures and the treating clinic's contact details.\")\n    if any(term in lower for term in (\"night guard\", \"nightguard\", \"mouth guard\", \"mouthguard\", \"splint\", \"retainer\", \"appliance\")):\n        items.append(\"Your current night guard, splint, or retainer — bring the appliance itself.\")\n    if intake.loose_crown_or_bridge or any(\n        term in lower for term in (\"crown fell\", \"crown came off\", \"cap came off\", \"cap fell\")\n    ):\n        items.append(\"The crown or fragment in a clean container — do not glue or reinsert it.\")\n    if intake.pain_score or intake.biting_pain or intake.hot_cold_sensitivity:\n        items.append(\"A short pain log: when it hurts, what triggers it, what helps, and a 0-10 score per day.\")\n    items.append(\"This handoff, printed or on your phone.\")\n    return items\n\n\ndef _fallback_symptoms(intake: StructuredIntake) -> list[str]:\n    symptoms = []\n    if intake.pain_score:\n        symptoms.append(f\"Pain score reported as {intake.pain_score}/10.\")\n    if intake.biting_pain:\n        symptoms.append(\"Pain or bruised feeling with biting/chewing.\")\n    if intake.hot_cold_sensitivity:\n        symptoms.append(\"Hot/cold sensitivity reported.\")\n    if intake.limited_opening_or_locked_jaw:\n        symptoms.append(\"Jaw/TMJ limitation or locking concern reported.\")\n    if intake.swelling:\n        symptoms.append(\"Swelling reported.\")\n    if intake.fever_or_unwell:\n        symptoms.append(\"Fever or feeling unwell reported.\")\n    return symptoms\n\n\ndef _fallback_handoff(\n    profile: PatientProfile,\n    intake: StructuredIntake,\n    story: str,\n    red_flags,\n) -> HandoffOutput:\n    snippets = _split_story(story)\n    timeline = list(snippets)\n    if intake.symptom_duration:\n        timeline.insert(0, f\"Reported duration: {intake.symptom_duration}.\")\n    if not timeline:\n        timeline = [\"Timeline not clear yet; ask patient to add symptom start date and procedure dates.\"]\n\n    medical_notes = []\n    if profile.meds.strip():\n        medical_notes.append(f\"Medications/supplements to verify: {profile.meds.strip()}\")\n    if profile.allergies.strip():\n        medical_notes.append(f\"Allergies/adverse reactions to verify: {profile.allergies.strip()}\")\n    if not medical_notes:\n        medical_notes.append(\"Medication and allergy history not provided or no notable entry.\")\n\n    goals = [\"Leave the visit understanding what the dentist checked.\"]\n    if profile.goals.strip():\n        goals.insert(0, profile.goals.strip())\n    goals.append(\"Know what to monitor after the visit and when to seek urgent care.\")\n\n    output = HandoffOutput(\n        patient_name=profile.name,\n        patient_age=profile.age,\n        chief_concern=intake.chief_concern or \"Dental symptoms to organize before visit\",\n        concise_summary=(\n            snippets[0]\n            if snippets\n            else \"Patient wants a concise, dentist-ready summary of current dental symptoms and visit goals.\"\n        ),\n        timeline=timeline,\n        current_symptoms=_fallback_symptoms(intake) or [\"Current symptom details need clarification.\"],\n        dental_history=[\n            item\n            for item in [\n                f\"Area: {intake.tooth_or_area}\" if intake.tooth_or_area else \"\",\n                f\"Recent dental work: {intake.recent_dental_work}\" if intake.recent_dental_work else \"\",\n            ]\n            if item\n        ]\n        or [\"Prior dental work not specified.\"],\n        medical_safety_notes=medical_notes,\n        patient_goals=goals,\n        dentist_questions=_base_questions(intake, story, profile.meds),\n        after_visit_tracker=_tracker_items(),\n        bring_checklist=_bring_checklist(profile, intake, story),\n        evidence=_source_quote(story),\n        red_flags=red_flags,\n    )\n    return _ensure_story_dental_work(output, story)\n\n\ndef _load_model():\n    \"\"\"Return (tokenizer, model).  Weights are loaded at module import time above;\n    this function is a thin accessor that also handles the rare case where the\n    import-time load was skipped (local dev, no network) by attempting a lazy load.\"\"\"\n    if _MODEL:\n        return _MODEL[\"tokenizer\"], _MODEL[\"model\"]\n\n    with _MODEL_LOAD_LOCK:\n        if _MODEL:\n            return _MODEL[\"tokenizer\"], _MODEL[\"model\"]\n\n        # Lazy fallback — only reached in local dev when the import-time block above\n        # was skipped (e.g., USE_MODEL_BY_DEFAULT was False or the import failed).\n        # Mirror the import-time pattern: load WITHOUT device_map=\"auto\" so the\n        # ZeroGPU shim can intercept .to(\"cuda\") correctly.\n        import torch\n        from transformers import AutoModelForCausalLM, AutoTokenizer\n\n        _on_spaces_lazy = os.getenv(\"SPACE_ID\") is not None\n        tokenizer = AutoTokenizer.from_pretrained(MODEL_ID, trust_remote_code=True)\n        model = AutoModelForCausalLM.from_pretrained(\n            MODEL_ID,\n            torch_dtype=torch.bfloat16 if (_on_spaces_lazy or torch.cuda.is_available()) else torch.float32,\n            trust_remote_code=True,\n        )\n        if _on_spaces_lazy or torch.cuda.is_available():\n            model = model.to(\"cuda\")\n        _MODEL[\"tokenizer\"] = tokenizer\n        _MODEL[\"model\"] = model\n        return tokenizer, model\n\n\ndef _json_from_text(text: str) -> dict[str, Any]:\n    \"\"\"Extract the first relevant JSON object from model chatter.\n\n    Qwen may wrap JSON in markdown, emit a reasoning block, or append prose. Using\n    first-\"{\" / last-\"}\" makes any extra object poison the whole response. Scan\n    candidate objects with JSONDecoder instead and accept only one containing at\n    least one writable handoff key.\n    \"\"\"\n\n    cleaned = re.sub(r\"<think>.*?</think>\", \"\", text or \"\", flags=re.IGNORECASE | re.DOTALL)\n    decoder = json.JSONDecoder()\n    for match in re.finditer(r\"\\{\", cleaned):\n        try:\n            candidate, _end = decoder.raw_decode(cleaned[match.start() :])\n        except json.JSONDecodeError:\n            continue\n        if isinstance(candidate, dict) and _MODEL_OUTPUT_KEYS.intersection(candidate):\n            return candidate\n    raise ValueError(\"model did not return a valid handoff JSON object\")\n\n\n# duration=90s: weights load at import time, so the window covers the first-call\n# CPU→GPU transfer plus generate (~25s at 40 tok/s for 900 tokens) with real\n# margin — if generation overruns the window ZeroGPU kills it mid-demo, so we\n# do not shave this to the theoretical minimum.\n@spaces.GPU(duration=90)\ndef _model_handoff(profile: PatientProfile, intake: StructuredIntake, story: str) -> dict[str, Any]:\n    tokenizer, model = _load_model()\n    payload = {\n        \"profile\": profile.model_dump(),\n        \"structured_intake\": intake.model_dump(),\n        \"story\": story,\n    }\n    messages = [\n        {\"role\": \"system\", \"content\": SYSTEM_PROMPT},\n        {\"role\": \"user\", \"content\": json.dumps(payload, ensure_ascii=False)},\n    ]\n    prompt = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)\n    inputs = tokenizer(prompt, return_tensors=\"pt\").to(model.device)\n    outputs = model.generate(\n        **inputs,\n        max_new_tokens=900,\n        do_sample=False,\n        eos_token_id=tokenizer.eos_token_id,\n    )\n    decoded = tokenizer.decode(outputs[0][inputs[\"input_ids\"].shape[-1] :], skip_special_tokens=True)\n    return _json_from_text(decoded)\n\n\ndef _extract_json_general(text: str) -> dict[str, Any]:\n    cleaned = re.sub(r\"<think>.*?</think>\", \"\", text or \"\", flags=re.IGNORECASE | re.DOTALL)\n    decoder = json.JSONDecoder()\n    for match in re.finditer(r\"\\{\", cleaned):\n        try:\n            candidate, _ = decoder.raw_decode(cleaned[match.start() :])\n        except json.JSONDecodeError:\n            continue\n        if isinstance(candidate, dict):\n            return candidate\n    raise ValueError(\"model response did not contain a JSON object\")\n\n\n@spaces.GPU(duration=30)\ndef _local_chat_json(messages: list[dict[str, Any]]) -> dict[str, Any]:\n    tokenizer, model = _load_model()\n    prompt = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)\n    inputs = tokenizer(prompt, return_tensors=\"pt\").to(model.device)\n    outputs = model.generate(\n        **inputs,\n        max_new_tokens=300,\n        do_sample=False,\n        eos_token_id=tokenizer.eos_token_id,\n    )\n    decoded = tokenizer.decode(outputs[0][inputs[\"input_ids\"].shape[-1] :], skip_special_tokens=True)\n    return _extract_json_general(decoded)\n\n\ndef _item_passes_field_validation(field_name: str, item: Any) -> bool:\n    \"\"\"True if a single list entry passes the field's own draft validator.\"\"\"\n\n    try:\n        ModelHandoffDraft.model_validate({field_name: [item]})\n    except ValidationError:\n        return False\n    return True\n\n\ndef _merge_model_output(\n    base: HandoffOutput, model_data: dict[str, Any], *, story: str = \"\"\n) -> HandoffOutput:\n    # Validate each writable field independently. A malformed question list or one\n    # diagnosis-flavored sentence should not discard otherwise safe model output.\n    # Red flags and evidence are not part of ModelHandoffDraft, so they remain\n    # impossible for the model to author or suppress.\n    validated_fields: dict[str, Any] = {}\n    dropped_fields: list[str] = []\n    trimmed_fields: list[str] = []\n    for field_name in _MODEL_OUTPUT_KEYS:\n        if field_name not in model_data:\n            continue\n        field_value = model_data[field_name]\n        # Per-item salvage for list fields: one diagnosis-flavored sentence\n        # should drop that sentence, not the whole field. Each surviving item\n        # individually passes the field's own validator, so this cannot weake",1106      "app_signals": "AgentUnavailable _selected_to_intake chief_concern tooth_or_area recent_dental_work symptom_duration pain_score selected_checks _source_quote story _split_story _negative_grounded_in_story item _story_dental_work_mentions _ensure_story_dental_work output _base_questions intake meds _tracker_items _bring_checklist profile _fallback_symptoms _fallback_handoff red_flags _load_model _json_from_text text _model_handoff _extract_json_general _local_chat_json messages _item_passes_field_validation field_name _merge_model_output base model_data _export_payload _build_outputs name age language allergies goals checks_dental checks_jaw checks_body use_model workflow_mode interview_intake_status build_outputs _split_checks checks _cached_model_text_is_safe raw_dict _load_cached_example key load_example _interview_call_model schema _interview_progress_html state _interview_build_button _interview_answer_controls start_interview interview_turn message history interview_build _interview_state_from_token token interview_api os.getenv threading.Lock frozenset read_text encoding parent.joinpath You are Dental SOAP, a safety-first dental visit-prep assistant. Task: transform patient-reported dental history into JSON for a dentist visit handoff. Hard rules: - Do not diagnose. - Do not recommend treatment. - Do not interpret imaging. - Use only facts stated by the patient. - Leave objective findings, assessment, and plan to the dentist. - Write dentist-facing questions, not conclusions. - If information is missing, add a question. - Every generated detail should be grounded in the user's story. - Patients may use shorthand: \"endo\" or \"RCT\" means root canal treatment; a \"cap\" means a crown; \"pulled\" means extraction; \"cleaning\" means scaling. Expand shorthand without adding new claims. - Never state that something did NOT happen or was NOT done unless the patient explicitly said so. Return strict JSON with these keys: chief_concern, concise_summary, timeline, current_symptoms, dental_history, dentist_questions. All list fields must be arrays of short strings. re.compile spaces.GPU duration gr.themes.Soft primary_hue neutral_hue font The local model endpoint could not produce a usable response. _SpacesFallback DENTAL_SOAP_MODEL_ID Qwen/Qwen3-4B-Instruct-2507 1 assets dental-guide-avatar.svg Biting pain Hot/cold sensitivity Pain prevents sleep Facial or gum swelling Rapidly spreading swelling Fever or feeling very unwell Breathing or swallowing issue Limited opening or locked jaw Loose crown or bridge Trauma or sudden bite change Numbness or neurologic symptoms Chest pain or jaw pain with exertion Jaw pain with chewing that improves with rest Vision/scalp tenderness/new severe headache Gum pimple or drainage Bruising or burning pain after root canal biting_pain hot_cold_sensitivity pain_prevents_sleep swelling rapidly_spreading_swelling fever_or_unwell breathing_or_swallowing_issue limited_opening_or_locked_jaw loose_crown_or_bridge trauma_or_sudden_bite_change numbness_or_neuro_symptoms chest_pain_or_jaw_pain_with_exertion jaw_pain_with_chewing_relieved_by_rest vision_scalp_or_new_headache gum_pimple_or_drainage bruising_or_burning_after_root_canal CHECK_MAP.items StructuredIntake strip re.split ^\\s*(?:no\\b|none\\b|not\\b|never\\b|nil\\b|without\\b|denies\\b|denied\\b|لا\\b|لم\\b|لن\\b|بدون\\b|مفيش) no not none never nil without denies denied لا لم لن بدون مفيش True only when the patient's own story negates the topic the item negates. Fabricated negatives (topic never mentioned) and story-contradicting negatives (topic mentioned WITHOUT negation) both return False. Canonical prior-work terms the patient used anywhere in the raw story. lower Deterministic backstop: prior dental work the patient mentioned in the raw story must survive into the handoff even when the model or extractor missed it. Idempotent — terms already present in dental_history are not re-added. history.append output.model_copy update Deterministic dentist-question bank, mined from the clin ... tedIntake model_question_count Partial set all dataclasses.asdict gr.themes.GoogleFont system-ui sans-serif Dental SOAP disclaimer-block gr.Column scale elem_classes gr.Markdown generate_btn.click full model-generation () => { window.print(); return []; } hidden minimal interview_build_btn.click callable SPACE_ID cuda \\s+ free_text item.lower Prior dental work not specified. Your medication list with doses (or the boxes themselves): . Exact allergy names and the reaction you had: Pain score reported as /10. Reported duration: Medications/supplements to verify: Allergies/adverse reactions to verify: Dental symptoms to organize before visit Patient wants a concise, dentist-ready summary of current dental symptoms and visit goals. dropped_fields.append trimmed_fields.append [dental-soap] dropped invalid model fields: [dental-soap] salvaged partial model fields: base_qs.append existing_lower.add ahmed min AI model path used (ZeroGPU): AI model response failed safety/schema validation; safe template path used. model_text_is_safe item.strip Cached demo result loaded: . No GPU needed. current pending <span class=\"odipara-chip \"> Type your answer... Send state token must be a JSON object dataclasses.fields tuple raw.items patient_age must be an integer or null user_turns must be strings state token exceeds the interview turn cap Invalid interview state token; pass the `state` string returned by the previous call. rule_id patient_message Inter rail_html size gr.Tabs Ready — load an example or tell your story. gr.Accordion open gr.Code root_canal arabic [dental-soap] import-time model load failed: , x-ray xray cbct scan panoramic dicom imaging radiograph night guard nightguard mouth guard mouthguard splint retainer appliance Current symptom details need clarification. pt sorted AI model unavailable; safe template path used. Reason: s label odipara upper unknown state fields: unknown phase: Try Ahmed's case Post-root-canal pain Arabic bilingual case gr.Tab gr.Chatbot avatar_images height layout gr.State gr.Checkbox variant print-zone status-line no-print Print handoff card Download PDF Email handoff no-print Validated handoff JSON Building your handoff — the AI model runs on ZeroGPU and a cold start can take up to a minute. The safety rules have already run. Building your handoff from the interview — ZeroGPU cold start can take up to a minute. The safety rules have already run on every answer. crown fell crown came off cap came off cap fell match.start sm example-chip Guided interview Manual form gr.Group lines gr.CheckboxGroup gr.Radio Build my dentist handoff json word.startswith topic.startswith n't input_ids type interview-progress Interview transcript interview-chat bubble Your Dental SOAP guide will begin the interview here. Restart interview step_head gr.Slider step gr.Number precision minimum maximum Use AI model (Qwen 3 4B inside this Space via ZeroGPU) primary lg _NEGATIVE_ASSERTION.match step-card Chief complaint What's bothering you, in your own words Tell the dental story in your own words What happened, when it started, recent dental work, pain triggers, swelling/fever, jaw symptoms... Main concern Example: crown feels high and jaw hurts Dental history Past procedures and tooth-level symptoms Tooth or area Example: upper left molar / jaw joint / not sure Recent dental work Example: crown, root canal, filling, extraction Tooth and dental-work symptoms Jaw, bite & TMJ Jaw-joint, muscle, and bite signals Jaw and TMJ symptoms Medical background Whole-body signals, medications, allergies, goals Whole-body safety signals English Arabic Bilingual Handoff language Medications / supplements Include blood thinners, steroids, Prolia/Fosamax, etc. Allergies / adverse reactions Example: amoxicillin rash, latex allergy What do you want from this visit? Example: understand whether the crown needs replacing Area: Recent dental work: Pain score (0–10) How long has this been going on? Example: 3 weeks Patient Name Example: Ahmed Zayed Age",1107      "readme_len": 16592,1108      "app_source_len": 24000,1109      "app_signals_len": 79991110    },1111    {1112      "id": "build-small-hackathon/dm-order-desk",1113      "title": "Dm Order Desk",1114      "summary": "Turn messy DMs into clean orders.",1115      "tags": [1116        "gradio",1117        "region:us"1118      ],1119      "models": [],1120      "datasets": [],1121      "sdk": "gradio",1122      "license": "mit",1123      "likes": 0,1124      "url": "https://huggingface.co/spaces/build-small-hackathon/dm-order-desk",1125      "app_file": "app.py",1126      "readme_raw": "---\ntitle: Dm Order Desk\nemoji: 🔥\ncolorFrom: yellow\ncolorTo: red\nsdk: gradio\nsdk_version: 6.16.0\npython_version: '3.13'\napp_file: app.py\npinned: false\nlicense: mit\nshort_description: Turn messy DMs into clean orders.\n---\n\n# DM Order Desk\n\nDM Order Desk helps tiny sellers turn messy customer messages into a clean order sheet, prep list, and reply drafts.\n\nIt is designed for home bakers, farmers market vendors, food truck operators, and small Instagram or WhatsApp sellers who take orders through direct messages instead of a full ecommerce system.\n\n## What It Does\n\nPaste messy customer DMs into the app. The app extracts:\n\n- customer name\n- item\n- quantity\n- flavor or variant\n- pickup time\n- pickup place or delivery address\n- payment status\n- missing details the seller still needs to ask for\n\nIt then generates:\n\n- a structured order sheet\n- a prep list for fulfillment\n- short customer reply drafts\n\n## Example Use Case\n\nA home baker receives several messages:\n\n```text\nMaya: Hi! Can I get 2 dozen cupcakes for Saturday morning? Half vanilla, half chocolate.\nSam: Need 1 birthday cake, chocolate, for pickup Friday 5pm. I can pay Venmo.\nLena: Do you still have lemon bars? I need some for tomorrow but not sure how many yet.\nChris: 12 cookies please, pickup at the farmers market. Paid already.\n```\n\nDM Order Desk turns these messages into a structured order table, a prep list, and follow-up replies for missing details.\n\n\n## Why Small Models Fit\n\nThis is a narrow, practical workflow. The model does not need broad world knowledge or long-form reasoning. It only needs to extract structured order details from short messages.\n\nThe app uses:\n\n- Model: `Qwen/Qwen2.5-1.5B-Instruct`\n- Parameter count: about 1.5B\n- Total model size: well under the 32B hackathon limit\n- UI: Gradio\n- Hosting: Hugging Face Spaces\n\n## Track\n\nBackyard AI\n\nThis project is built for a real everyday problem: tiny sellers often receive orders through messy DMs and need to manually turn them into something they can fulfill.\n\n## Tested Workflow\n\nThis prototype is based on a common tiny-seller workflow:\n\n1. Customers send short, incomplete order messages through DMs, texts, or group chats.\n2. The seller manually reads each message and copies details into a notes app, spreadsheet, or paper list.\n3. The seller checks what is missing, such as quantity, pickup time, pickup place, or payment status.\n4. The seller writes follow-up replies for customers who left out important details.\n5. The seller builds a prep list for fulfillment.\n\nDM Order Desk compresses those manual steps into one review screen. The seller still reviews the output, but the first pass of sorting, extraction, and follow-up drafting is handled by a small model.\n\n## Limitations\n\nThis is a prototype. It may still need human review for ambiguous messages, unusual products, or complex multi-message conversations. The goal is to reduce manual sorting work, not replace seller judgment.",1127      "readme_body": "# DM Order Desk\n\nDM Order Desk helps tiny sellers turn messy customer messages into a clean order sheet, prep list, and reply drafts.\n\nIt is designed for home bakers, farmers market vendors, food truck operators, and small Instagram or WhatsApp sellers who take orders through direct messages instead of a full ecommerce system.\n\n## What It Does\n\nPaste messy customer DMs into the app. The app extracts:\n\n- customer name\n- item\n- quantity\n- flavor or variant\n- pickup time\n- pickup place or delivery address\n- payment status\n- missing details the seller still needs to ask for\n\nIt then generates:\n\n- a structured order sheet\n- a prep list for fulfillment\n- short customer reply drafts\n\n## Example Use Case\n\nA home baker receives several messages:\n\n```text\nMaya: Hi! Can I get 2 dozen cupcakes for Saturday morning? Half vanilla, half chocolate.\nSam: Need 1 birthday cake, chocolate, for pickup Friday 5pm. I can pay Venmo.\nLena: Do you still have lemon bars? I need some for tomorrow but not sure how many yet.\nChris: 12 cookies please, pickup at the farmers market. Paid already.\n```\n\nDM Order Desk turns these messages into a structured order table, a prep list, and follow-up replies for missing details.\n\n\n## Why Small Models Fit\n\nThis is a narrow, practical workflow. The model does not need broad world knowledge or long-form reasoning. It only needs to extract structured order details from short messages.\n\nThe app uses:\n\n- Model: `Qwen/Qwen2.5-1.5B-Instruct`\n- Parameter count: about 1.5B\n- Total model size: well under the 32B hackathon limit\n- UI: Gradio\n- Hosting: Hugging Face Spaces\n\n## Track\n\nBackyard AI\n\nThis project is built for a real everyday problem: tiny sellers often receive orders through messy DMs and need to manually turn them into something they can fulfill.\n\n## Tested Workflow\n\nThis prototype is based on a common tiny-seller workflow:\n\n1. Customers send short, incomplete order messages through DMs, texts, or group chats.\n2. The seller manually reads each message and copies details into a notes app, spreadsheet, or paper list.\n3. The seller checks what is missing, such as quantity, pickup time, pickup place, or payment status.\n4. The seller writes follow-up replies for customers who left out important details.\n5. The seller builds a prep list for fulfillment.\n\nDM Order Desk compresses those manual steps into one review screen. The seller still reviews the output, but the first pass of sorting, extraction, and follow-up drafting is handled by a small model.\n\n## Limitations\n\nThis is a prototype. It may still need human review for ambiguous messages, unusual products, or complex multi-message conversations. The goal is to reduce manual sorting work, not replace seller judgment.",1128      "readme_frontmatter": {1129        "title": "Dm Order Desk",1130        "emoji": "🔥",1131        "colorFrom": "yellow",1132        "colorTo": "red",1133        "sdk": "gradio",1134        "sdk_version": "6.16.0",1135        "python_version": "3.13",1136        "app_file": "app.py",1137        "pinned": "false",1138        "license": "mit",1139        "short_description": "Turn messy DMs into clean orders."1140      },1141      "app_source": "import json\nimport pandas as pd\nimport gradio as gr\nimport torch\nfrom transformers import AutoModelForCausalLM, AutoTokenizer\n\nMODEL_ID = \"Qwen/Qwen2.5-1.5B-Instruct\"\n\nORDER_COLUMNS = [\n    \"customer\",\n    \"item\",\n    \"quantity\",\n    \"flavor\",\n    \"pickup_time\",\n    \"delivery_address\",\n    \"payment_status\",\n    \"notes\",\n    \"missing_fields\",\n]\n\ntokenizer = AutoTokenizer.from_pretrained(MODEL_ID)\nmodel = AutoModelForCausalLM.from_pretrained(MODEL_ID, torch_dtype=torch.float32)\nmodel.eval()\n\n\nSYSTEM_PROMPT = \"\"\"\nYou are a careful order extraction engine for tiny sellers.\n\nExtract customer orders from messy DMs. Return only valid JSON with this exact shape:\n{\n  \"orders\": [\n    {\n      \"customer\": \"\",\n      \"item\": \"\",\n      \"quantity\": \"\",\n      \"flavor\": \"\",\n      \"pickup_time\": \"\",\n      \"delivery_address\": \"\",\n      \"payment_status\": \"\",\n      \"notes\": \"\",\n      \"missing_fields\": []\n    }\n  ],\n  \"prep_list\": [],\n  \"reply_drafts\": []\n}\n\nCritical rules:\n- Treat each line as one separate customer message.\n- The text before the first \":\" is the customer name.\n- Copy customer names exactly as written. Do not uppercase or lowercase them.\n- Never copy details from one customer's message into another customer's order.\n- Include every customer message that looks like an order or possible order.\n- Use only facts explicitly present in that customer's own message.\n- If a value is unknown, use an empty string.\n- Do not add order_id or total_cost.\n- For pickup orders, put pickup time in pickup_time. Put a pickup place or delivery address in delivery_address.\n- If the customer is unsure, still include the order and describe the uncertainty in notes.\n- missing_fields should only include fields the seller needs to ask for: quantity, flavor, pickup_time, delivery_address, payment_status.\n- Always set prep_list to [].\n- Always set reply_drafts to [].\n\"\"\"\n\nSINGLE_ORDER_PROMPT = \"\"\"\nYou extract one order from one customer's DM.\n\nReturn only valid JSON with this exact shape:\n{\n  \"item\": \"\",\n  \"quantity\": \"\",\n  \"flavor\": \"\",\n  \"pickup_time\": \"\",\n  \"delivery_address\": \"\",\n  \"payment_status\": \"\",\n  \"notes\": \"\",\n  \"missing_fields\": []\n}\n\nRules:\n- Use only facts from this one message.\n- Do not invent details.\n- Put dates and times in pickup_time, such as \"tomorrow\", \"Saturday morning\", or \"Friday 5pm\".\n- Put pickup places or delivery addresses in delivery_address, such as \"farmers market\".\n- \"pickup at the farmers market\" means delivery_address is \"farmers market\", not pickup_time.\n- \"paid already\" means payment_status is \"paid\".\n- \"I can pay Venmo\" means payment_status is \"can pay Venmo\".\n- If unknown, use an empty string.\n- Do not ask for flavor unless the product clearly needs a flavor choice.\n- missing_fields can only contain: quantity, flavor, pickup_time, delivery_address, payment_status.\n\"\"\"\n\n\nEXAMPLE_INPUT = \"\"\"Maya: Hi! Can I get 2 dozen cupcakes for Saturday morning? Half vanilla, half chocolate.\nSam: Need 1 birthday cake, chocolate, for pickup Friday 5pm. I can pay Venmo.\nLena: Do you still have lemon bars? I need some for tomorrow but not sure how many yet.\nChris: 12 cookies please, pickup at the farmers market. Paid already.\n\"\"\"\n\nFOOD_TRUCK_EXAMPLE = \"\"\"Alex: Can I get 3 chicken tacos for pickup at 6:30 tonight? Paid on Cash App.\nJamie: Do you still have vegan bowls? Need 2 tomorrow for office lunch.\nPriya: One brisket sandwich, no onions. I'll pick up at the truck on Main Street.\nNate: 4 lemonades for the soccer team, pickup after practice.\n\"\"\"\n\nCRAFT_SELLER_EXAMPLE = \"\"\"Olivia: I want 2 custom mugs with blue initials. Can you ship to 18 Pine Road?\nBen: Need one candle gift box for Saturday. Lavender if you have it.\nRosa: Can I order 3 tote bags? I can pick up at the market.\nEli: Do you still make birthday stickers? Need some next week but not sure how many.\n\"\"\"\n\nFARMERS_MARKET_EXAMPLE = \"\"\"Grace: Can you hold 2 sourdough loaves for Sunday pickup?\nLeo: I need 1 jar of strawberry jam and 2 honey bottles. Paid already.\nMina: Do you have eggs this weekend? Maybe 2 dozen if available.\nNoah: Please save me 3 bags of granola, pickup at the farmers market.\n\"\"\"\n\nEXAMPLES = [\n    [EXAMPLE_INPUT],\n    [FOOD_TRUCK_EXAMPLE],\n    [CRAFT_SELLER_EXAMPLE],\n    [FARMERS_MARKET_EXAMPLE],\n]\n\ndef extract_json(text):\n    start = text.find(\"{\")\n    end = text.rfind(\"}\")\n    if start == -1 or end == -1:\n        raise ValueError(\"No JSON object found\")\n    return json.loads(text[start:end + 1])\n\ndef normalize_orders(data):\n    rows = []\n    for order in data.get(\"orders\", []):\n        row = {}\n        for col in ORDER_COLUMNS:\n            value = order.get(col, \"\")\n            if isinstance(value, list):\n                value = \", \".join(str(v) for v in value)\n            row[col] = value\n        rows.append(row)\n    return pd.DataFrame(rows, columns=ORDER_COLUMNS)\n\ndef format_list(title, items):\n    if not items:\n        return f\"### {title}\\nNothing found.\"\n    lines = []\n    for item in items:\n        if isinstance(item, dict):\n            lines.append(\"- \" + json.dumps(item, ensure_ascii=False))\n        else:\n            lines.append(f\"- {item}\")\n    return f\"### {title}\\n\" + \"\\n\".join(lines)\n\ndef format_replies(replies):\n    if not replies:\n        return \"### Reply drafts\\nNothing found.\"\n    lines = []\n    for reply in replies:\n        customer = reply.get(\"customer\", \"Customer\")\n        text = reply.get(\"reply\", \"\")\n        lines.append(f\"**{customer}**\\n\\n{text}\")\n    return \"### Reply drafts\\n\\n\" + \"\\n\\n---\\n\\n\".join(lines)\n\ndef text_value(value):\n    if isinstance(value, list):\n        return \", \".join(str(v) for v in value if str(v).strip())\n    if value is None:\n        return \"\"\n    return str(value).strip()\n\ndef missing_list(order):\n    raw = order.get(\"missing_fields\", [])\n    if isinstance(raw, str):\n        fields = [part.strip() for part in raw.split(\",\") if part.strip()]\n    else:\n        fields = [str(part).strip() for part in raw if str(part).strip()]\n\n    allowed = {\"quantity\", \"flavor\", \"pickup_time\", \"delivery_address\", \"payment_status\"}\n    fields = [field for field in fields if field in allowed]\n\n    item = text_value(order.get(\"item\"))\n    quantity = text_value(order.get(\"quantity\"))\n    flavor = text_value(order.get(\"flavor\"))\n    pickup_time = text_value(order.get(\"pickup_time\"))\n    delivery_address = text_value(order.get(\"delivery_address\"))\n    payment_status = text_value(order.get(\"payment_status\"))\n\n    if item and not quantity:\n        fields.append(\"quantity\")\n\n    if pickup_time:\n        fields = [field for field in fields if field != \"pickup_time\"]\n    if delivery_address:\n        fields = [field for field in fields if field != \"delivery_address\"]\n    if payment_status:\n        fields = [field for field in fields if field != \"payment_status\"]\n    if flavor:\n        fields = [field for field in fields if field != \"flavor\"]\n\n    if \"flavor\" in fields and item.lower() not in [\"cake\", \"birthday cake\", \"cupcakes\"]:\n        fields = [field for field in fields if field != \"flavor\"]\n\n    return sorted(set(fields))\n\ndef post_process_order(order, message):\n    msg = message.lower()\n\n    if \"paid already\" in msg or \"already paid\" in msg:\n        order[\"payment_status\"] = \"paid\"\n    elif \"venmo\" in msg:\n        order[\"payment_status\"] = \"can pay Venmo\"\n    elif \"paid\" not in msg and \"venmo\" not in msg:\n        order[\"payment_status\"] = \"\"\n\n    pickup_time = text_value(order.get(\"pickup_time\"))\n    if \"paid\" in pickup_time.lower() or \"venmo\" in pickup_time.lower():\n        order[\"pickup_time\"] = \"\"\n\n    if \"farmers market\" in msg:\n        order[\"delivery_address\"] = \"farmers market\"\n        if \"farmers market\" in text_value(order.get(\"pickup_time\")).lower():\n            order[\"pickup_time\"] = \"\"\n\n    order[\"missing_fields\"] = missing_list(order)\n    return order\n\ndef build_prep_list(data):\n    items = []\n    for order in data.get(\"orders\", []):\n        item = text_value(order.get(\"item\"))\n        if not item:\n            continue\n\n        customer = text_value(order.get(\"customer\")) or \"customer\"\n        quantity = text_value(order.get(\"quantity\")) or \"quantity to confirm\"\n        flavor = text_value(order.get(\"flavor\"))\n\n        line = f\"{quantity} {item}\"\n        if flavor:\n            line += f\" ({flavor})\"\n        line += f\" - {customer}\"\n        items.append(line)\n\n    return items\n\ndef build_reply_drafts(data):\n    replies = []\n    labels = {\n        \"quantity\": \"quantity\",\n        \"flavor\": \"flavor\",\n        \"pickup_time\": \"pickup or delivery time\",\n        \"delivery_address\": \"pickup place or delivery address\",\n        \"payment_status\": \"payment status\",\n    }\n\n    for order in data.get(\"orders\", []):\n        customer = text_value(order.get(\"customer\")) or \"there\"\n        item = text_value(order.get(\"item\")) or \"order\"\n        quantity = text_value(order.get(\"quantity\"))\n        flavor = text_value(order.get(\"flavor\"))\n        missing = [labels.get(field, field) for field in missing_list(order)]\n\n        if missing:\n            needed = \", \".join(missing)\n            reply = f\"Thanks, {customer}! I have your {item} order. Could you confirm the {needed}?\"\n        else:\n            summary = f\"{quantity} {item}\".strip()\n            if flavor:\n                summary += f\" ({flavor})\"\n            reply = f\"Thanks, {customer}! Confirming your order: {summary}.\"\n\n        replies.append({\"customer\": customer, \"reply\": reply})\n\n    return replies\n\ndef split_customer_messages(messages):\n    entries = []\n    current_customer = \"\"\n    current_parts = []\n\n    for raw_line in messages.splitlines():\n        line = raw_line.strip()\n        if not line:\n            continue\n\n        if \":\" in line:\n            possible_name, body = line.split(\":\", 1)\n            if possible_name.strip() and len(possible_name.strip().split()) <= 3:\n                if current_customer or current_parts:\n                    entries.append((current_customer or \"Customer\", \" \".join(current_parts).strip()))\n                current_customer = possible_name.strip()\n                current_parts = [body.strip()]\n                continue\n\n        if current_parts:\n            current_parts.append(line)\n        else:\n            entries.append((\"Customer\", line))\n\n    if current_customer or current_parts:\n        entries.append((current_customer or \"Customer\", \" \".join(current_parts).strip()))\n\n    return [(name, body) for name, body in entries if body]\n\ndef extract_single_order(customer, message):\n    prompt = tokenizer.apply_chat_template(\n        [\n            {\"role\": \"system\", \"content\": SINGLE_ORDER_PROMPT},\n            {\"role\": \"user\", \"content\": f\"Customer: {customer}\\nMessage: {message}\"},\n        ],\n        tokenize=False,\n        add_generation_prompt=True,\n    )\n\n    inputs = tokenizer(prompt, return_tensors=\"pt\")\n    with torch.no_grad():\n        output = model.generate(\n            **inputs,\n            max_new_tokens=350,\n            do_sample=False,\n            pad_token_id=tokenizer.eos_token_id,\n        )\n\n    generated = tokenizer.decode(\n        output[0][inputs[\"input_ids\"].shape[1]:],\n        skip_special_tokens=True,\n    )\n\n    try:\n        parsed = extract_json(generated)\n    except Exception:\n        parsed = {\n            \"item\": \"\",\n            \"quantity\": \"\",\n            \"flavor\": \"\",\n            \"pickup_time\": \"\",\n            \"delivery_address\": \"\",\n            \"payment_status\": \"\",\n            \"notes\": message,\n            \"missing_fields\": [],\n        }\n\n    order = {\"customer\": customer}\n    for col in ORDER_COLUMNS[1:]:\n        value = parsed.get(col, \"\")\n        if col == \"missing_fields\":\n            if isinstance(value, list):\n                order[col] = value\n            elif isinstance(value, str):\n                order[col] = [part.strip() for part in value.split(\",\") if part.strip()]\n            else:\n                order[col] = []\n        else:\n            order[col] = text_value(value)\n\n    return post_process_order(order, message)\n\ndef analyze_messages(messages):\n    if not messages.strip():\n        return pd.DataFrame(columns=ORDER_COLUMNS), \"Paste some DMs first.\", \"\", \"\"\n\n    entries = split_customer_messages(messages)\n    orders_data = [extract_single_order(customer, message) for customer, message in entries]\n\n    data = {\"orders\": orders_data}\n    orders_df = normalize_orders(data)\n\n    auto_prep = build_prep_list(data)\n    auto_replies = build_reply_drafts(data)\n\n    data[\"prep_list\"] = auto_prep\n    data[\"reply_drafts\"] = auto_replies\n\n    prep = format_list(\"Prep list\", auto_prep)\n    replies = format_replies(auto_replies)\n    raw = json.dumps(data, indent=2, ensure_ascii=False)\n    return orders_df, prep, replies, raw\n\nwith gr.Blocks(title=\"DM Order Desk\") as demo:\n    gr.Markdown(\"# DM Order Desk\")\n    gr.Markdown(\"Turn messy customer DMs into clean orders, prep lists, and reply drafts using a small model.\")\n\n    with gr.Row():\n        with gr.Column(scale=1):\n            messages = gr.Textbox(\n                label=\"Messy customer DMs\",\n                value=EXAMPLE_INPUT,\n                lines=14,\n            )\n            run = gr.Button(\"Organize orders\", variant=\"primary\")\n            gr.Examples(\n                examples=EXAMPLES,\n                inputs=messages,\n                label=\"Try example DMs\",\n            )\n\n        with gr.Column(scale=2):\n            orders = gr.Dataframe(label=\"Order sheet\", headers=ORDER_COLUMNS)\n            prep = gr.Markdown(label=\"Prep list\")\n            replies = gr.Markdown(label=\"Reply drafts\")\n            raw = gr.Code(label=\"Raw JSON\", language=\"json\")\n\n    run.click(analyze_messages, inputs=messages, outputs=[orders, prep, replies, raw])\n\ndemo.launch()",1142      "app_signals": "extract_json text normalize_orders data format_list title items format_replies replies text_value value missing_list order post_process_order message build_prep_list build_reply_drafts split_customer_messages messages extract_single_order customer analyze_messages Qwen/Qwen2.5-1.5B-Instruct AutoTokenizer.from_pretrained AutoModelForCausalLM.from_pretrained torch_dtype model.eval You are a careful order extraction engine for tiny sellers. Extract customer orders from messy DMs. Return only valid JSON with this exact shape: { \"orders\": [ { \"customer\": \"\", \"item\": \"\", \"quantity\": \"\", \"flavor\": \"\", \"pickup_time\": \"\", \"delivery_address\": \"\", \"payment_status\": \"\", \"notes\": \"\", \"missing_fields\": [] } ], \"prep_list\": [], \"reply_drafts\": [] } Critical rules: - Treat each line as one separate customer message. - The text before the first \":\" is the customer name. - Copy customer names exactly as written. Do not uppercase or lowercase them. - Never copy details from one customer's message into another customer's order. - Include every customer message that looks like an order or possible order. - Use only facts explicitly present in that customer's own message. - If a value is unknown, use an empty string. - Do not add order_id or total_cost. - For pickup orders, put pickup time in pickup_time. Put a pickup place or delivery address in delivery_address. - If the customer is unsure, still include the order and describe the uncertainty in notes. - missing_fields should only include fields the seller needs to ask for: quantity, flavor, pickup_time, delivery_address, payment_status. - Always set prep_list to []. - Always set reply_drafts to []. You extract one order from one customer's DM. Return only valid JSON with this exact shape: { \"item\": \"\", \"quantity\": \"\", \"flavor\": \"\", \"pickup_time\": \"\", \"delivery_address\": \"\", \"payment_status\": \"\", \"notes\": \"\", \"missing_fields\": [] } Rules: - Use only facts from this one message. - Do not invent details. - Put dates and times in pickup_time, such as \"tomorrow\", \"Saturday morning\", or \"Friday 5pm\". - Put pickup places or delivery addresses in delivery_address, such as \"farmers market\". - \"pickup at the farmers market\" means delivery_address is \"farmers market\", not pickup_time. - \"paid already\" means payment_status is \"paid\". - \"I can pay Venmo\" means payment_status is \"can pay Venmo\". - If unknown, use an empty string. - Do not ask for flavor unless the product clearly needs a flavor choice. - missing_fields can only contain: quantity, flavor, pickup_time, delivery_address, payment_status. Maya: Hi! Can I get 2 dozen cupcakes for Saturday morning? Half vanilla, half chocolate. Sam: Need 1 birthday cake, chocolate, for pickup Friday 5pm. I can pay Venmo. Lena: Do you still have lemon bars? I need some for tomorrow but not sure how many yet. Chris: 12 cookies please, pickup at the farmers market. Paid already. Alex: Can I get 3 chicken tacos for pickup at 6:30 tonight? Paid on Cash App. Jamie: Do you still have vegan bowls? Need 2 tomorrow for office lunch. Priya: One brisket sandwich, no onions. I'll pick up at the truck on Main Street. Nate: 4 lemonades for the soccer team, pickup after practice. Olivia: I want 2 custom mugs with blue initials. Can you ship to 18 Pine Road? Ben: Need one candle gift box for Saturday. Lavender if you have it. Rosa: Can I order 3 tote bags? I can pick up at the market. Eli: Do you still make birthday stickers? Need some next week but not sure how many. Grace: Can you hold 2 sourdough loaves for Sunday pickup? Leo: I need 1 jar of strawberry jam and 2 honey bottles. Paid already. Mina: Do you have eggs this weekend? Maybe 2 dozen if available. Noah: Please save me 3 bags of granola, pickup at the farmers market. demo.launch item quantity flavor pickup_time delivery_address payment_status notes missing_fields text.find text.rfind json.loads data.get pd.DataFrame columns isinstance strip order.get sorted message.lower messages.splitlines tokenizer.apply_chat_template tokenize add_generation_prompt tokenizer return_tensors tokenizer.decode skip_special_tokens json.dumps indent ensure_ascii gr.Blocks gr.Markdown run.click inputs outputs { } ValueError orders rows.append join ### Reply drafts Nothing found. reply.get lines.append ### Reply drafts fields.append set paid farmers market items.append pickup or delivery time pickup place or delivery address payment status replies.append raw_line.strip entries.append torch.no_grad model.generate max_new_tokens do_sample pad_token_id parsed.get messages.strip prep_list reply_drafts Prep list # DM Order Desk Turn messy customer DMs into clean orders, prep lists, and reply drafts using a small model. gr.Row No JSON object found ### Nothing found. Customer reply str part.strip item.lower paid already already paid venmo can pay Venmo pickup_time.lower lower quantity to confirm - there labels.get : line.split current_parts.append pt Paste some DMs first. DM Order Desk gr.Column scale gr.Textbox label lines gr.Button variant gr.Examples examples gr.Dataframe headers gr.Code language ** --- , raw.split cake birthday cake cupcakes ( ) Thanks, ! I have your order. Could you confirm the ? ! Confirming your order: . possible_name.strip role content system user Organize orders len body.strip Customer: Message: Messy customer DMs primary Try example DMs Order sheet Reply drafts Raw JSON json split input_ids value.split",1143      "readme_len": 2722,1144      "app_source_len": 13668,1145      "app_signals_len": 54041146    },1147    {1148      "id": "build-small-hackathon/dream-customs",1149      "title": "Dream Customs",1150      "summary": "Turn dream declarations into a playful next-day pact.",1151      "tags": [1152        "build-small-hackathon",1153        "dream-journal",1154        "gradio",1155        "minicpm"1156      ],1157      "models": [1158        "openbmb/MiniCPM5-1B",1159        "openbmb/MiniCPM-V-4.6"1160      ],1161      "datasets": [],1162      "sdk": "gradio",1163      "license": "mit",1164      "likes": 0,1165      "url": "https://huggingface.co/spaces/build-small-hackathon/dream-customs",1166      "app_file": "app.py",1167      "readme_raw": "---\ntitle: Dream Customs\nemoji: ⚡\ncolorFrom: blue\ncolorTo: pink\nsdk: gradio\nsdk_version: 4.44.1\npython_version: \"3.10\"\napp_file: app.py\npinned: false\nlicense: mit\nshort_description: Turn dream declarations into a playful next-day pact.\nmodels:\n  - openbmb/MiniCPM5-1B\n  - openbmb/MiniCPM-V-4.6\ntags:\n  - gradio\n  - minicpm\n  - build-small-hackathon\n  - dream-journal\n---\n\n# Dream Customs\n\nA Build Small Hackathon Gradio app that helps users form a playful alliance with last night's dream.\n\n## Concept\n\nDream Customs accepts dream declarations by text, image, or voice. It turns the dream into a gentle \"customs negotiation\" and returns a Today's Pact card: one practical suggestion, one weird 5-minute task, and one bedtime release phrase.\n\n## Models\n\n- `openbmb/MiniCPM-V-4.6` for image/sketch/note understanding.\n- `openbmb/MiniCPM5-1B` for dream negotiation and pact generation.\n- A small ASR adapter may be used only for voice transcription.\n- The app defaults to a stable demo backend so the local Gradio flow always works.\n- Optional Ollama adapters are included for local MiniCPM testing.\n\n## Run\n\n```bash\npython3 -m venv .venv\nsource .venv/bin/activate\npython -m pip install -r requirements.txt\npython app.py\n```\n\nOpen `http://127.0.0.1:7860`.\n\n## Optional Ollama Models\n\n```bash\nollama pull hf.co/openbmb/MiniCPM5-1B-GGUF:Q8_0\nollama pull openbmb/minicpm-v4.6\n```\n\nThen switch the UI engine controls from `demo` to `ollama`.\n\nLocal smoke notes from this Mac mini:\n\n- Memory/size is fine: 16 GB RAM handled the local model downloads.\n- `hf.co/openbmb/MiniCPM5-1B-GGUF:Q8_0` loads in Ollama, but current output was malformed for JSON prompts.\n- `openbmb/minicpm-v4.6` pulled successfully, but current Ollama runner returned `unable to load model`.\n- Because of that, the MVP keeps Ollama optional and falls back to deterministic demo behavior.\n\n## Optional Hosted MiniCPM Routes\n\nThe public Space stays lightweight and can call private Modal endpoints through runtime secrets:\n\n- `DREAM_CUSTOMS_TEXT_ENDPOINT`: Modal text route for `openbmb/MiniCPM5-1B`.\n- `DREAM_CUSTOMS_VISION_ENDPOINT`: Modal vision route for `openbmb/MiniCPM-V-4.6`.\n- `DREAM_CUSTOMS_HOSTED_TOKEN`: shared bearer token checked by Modal and sent by the Space.\n\nSet these only as Hugging Face Space repository secrets or local shell variables. Do not store values in `.env`, docs, logs, screenshots, or git. Missing endpoints or route failures fall back to deterministic demo behavior.\n\nThe Gradio UI defaults to `model` for both text and vision backends, so a configured Space calls Modal by default. The `demo` backend remains available in developer settings as the deterministic fallback path.\n\nThe Hugging Face Space may run on ZeroGPU for hackathon hardware eligibility. `dream_customs.zerogpu` registers a lightweight `@spaces.GPU` startup probe so ZeroGPU accepts the app, but real MiniCPM inference still happens on the private Modal backend.\n\nToken-safe text smoke:\n\n```bash\npython - <<'PY'\nimport os\nfrom dream_customs.models import HostedMiniCPMTextClient\n\nclient = HostedMiniCPMTextClient(\n    endpoint=os.environ[\"DREAM_CUSTOMS_TEXT_ENDPOINT\"],\n    token=os.getenv(\"DREAM_CUSTOMS_HOSTED_TOKEN\", \"\"),\n)\nresult = client.generate_negotiation(\"I missed an elevator in a foggy dream.\")\nprint(result[\"visitor_name\"])\nPY\n```\n\nToken-safe vision smoke:\n\n```bash\npython - <<'PY'\nimport os\nfrom dream_customs.models import HostedMiniCPMVisionClient\n\nclient = HostedMiniCPMVisionClient(\n    endpoint=os.environ[\"DREAM_CUSTOMS_VISION_ENDPOINT\"],\n    token=os.getenv(\"DREAM_CUSTOMS_HOSTED_TOKEN\", \"\"),\n)\nprint(client.extract_clues(os.environ[\"DREAM_CUSTOMS_SMOKE_IMAGE\"]))\nPY\n```\n\n## Test\n\n```bash\npython -m pytest -q\n```\n\n## Deployment Smoke Status\n\n2026-06-05 local V2 verification passed: tests were green and the workbench flow reached a sealed pact through `Send to customs`, `Ask another question`, `Add material`, `Draft pact`, `Revise pact`, and `Seal today's pact`.\n\nThe public Space now serves the V2 workbench from Space `main` commit `8ad6f00628f800abc2dbefab05163aba94a5723f`. Public browser smoke, mobile readability, diagnostics, raw remote queue prediction, and a hosted text route smoke all reached a sealed pact on 2026-06-05. The current Modal backend pass requires a real `openbmb/MiniCPM-V-4.6` vision route smoke before delivery; demo vision fallback is runtime resilience, not a substitute for that smoke.\n\nCurrent smoke details are tracked in `docs/smoke/2026-06-05-space-deployment-smoke.md`.\n\n## Safety\n\nThis is not a therapy or diagnosis product. It gives playful reflection, small actions, and escalation copy for severe distress.\n",1168      "readme_body": "# Dream Customs\n\nA Build Small Hackathon Gradio app that helps users form a playful alliance with last night's dream.\n\n## Concept\n\nDream Customs accepts dream declarations by text, image, or voice. It turns the dream into a gentle \"customs negotiation\" and returns a Today's Pact card: one practical suggestion, one weird 5-minute task, and one bedtime release phrase.\n\n## Models\n\n- `openbmb/MiniCPM-V-4.6` for image/sketch/note understanding.\n- `openbmb/MiniCPM5-1B` for dream negotiation and pact generation.\n- A small ASR adapter may be used only for voice transcription.\n- The app defaults to a stable demo backend so the local Gradio flow always works.\n- Optional Ollama adapters are included for local MiniCPM testing.\n\n## Run\n\n```bash\npython3 -m venv .venv\nsource .venv/bin/activate\npython -m pip install -r requirements.txt\npython app.py\n```\n\nOpen `http://127.0.0.1:7860`.\n\n## Optional Ollama Models\n\n```bash\nollama pull hf.co/openbmb/MiniCPM5-1B-GGUF:Q8_0\nollama pull openbmb/minicpm-v4.6\n```\n\nThen switch the UI engine controls from `demo` to `ollama`.\n\nLocal smoke notes from this Mac mini:\n\n- Memory/size is fine: 16 GB RAM handled the local model downloads.\n- `hf.co/openbmb/MiniCPM5-1B-GGUF:Q8_0` loads in Ollama, but current output was malformed for JSON prompts.\n- `openbmb/minicpm-v4.6` pulled successfully, but current Ollama runner returned `unable to load model`.\n- Because of that, the MVP keeps Ollama optional and falls back to deterministic demo behavior.\n\n## Optional Hosted MiniCPM Routes\n\nThe public Space stays lightweight and can call private Modal endpoints through runtime secrets:\n\n- `DREAM_CUSTOMS_TEXT_ENDPOINT`: Modal text route for `openbmb/MiniCPM5-1B`.\n- `DREAM_CUSTOMS_VISION_ENDPOINT`: Modal vision route for `openbmb/MiniCPM-V-4.6`.\n- `DREAM_CUSTOMS_HOSTED_TOKEN`: shared bearer token checked by Modal and sent by the Space.\n\nSet these only as Hugging Face Space repository secrets or local shell variables. Do not store values in `.env`, docs, logs, screenshots, or git. Missing endpoints or route failures fall back to deterministic demo behavior.\n\nThe Gradio UI defaults to `model` for both text and vision backends, so a configured Space calls Modal by default. The `demo` backend remains available in developer settings as the deterministic fallback path.\n\nThe Hugging Face Space may run on ZeroGPU for hackathon hardware eligibility. `dream_customs.zerogpu` registers a lightweight `@spaces.GPU` startup probe so ZeroGPU accepts the app, but real MiniCPM inference still happens on the private Modal backend.\n\nToken-safe text smoke:\n\n```bash\npython - <<'PY'\nimport os\nfrom dream_customs.models import HostedMiniCPMTextClient\n\nclient = HostedMiniCPMTextClient(\n    endpoint=os.environ[\"DREAM_CUSTOMS_TEXT_ENDPOINT\"],\n    token=os.getenv(\"DREAM_CUSTOMS_HOSTED_TOKEN\", \"\"),\n)\nresult = client.generate_negotiation(\"I missed an elevator in a foggy dream.\")\nprint(result[\"visitor_name\"])\nPY\n```\n\nToken-safe vision smoke:\n\n```bash\npython - <<'PY'\nimport os\nfrom dream_customs.models import HostedMiniCPMVisionClient\n\nclient = HostedMiniCPMVisionClient(\n    endpoint=os.environ[\"DREAM_CUSTOMS_VISION_ENDPOINT\"],\n    token=os.getenv(\"DREAM_CUSTOMS_HOSTED_TOKEN\", \"\"),\n)\nprint(client.extract_clues(os.environ[\"DREAM_CUSTOMS_SMOKE_IMAGE\"]))\nPY\n```\n\n## Test\n\n```bash\npython -m pytest -q\n```\n\n## Deployment Smoke Status\n\n2026-06-05 local V2 verification passed: tests were green and the workbench flow reached a sealed pact through `Send to customs`, `Ask another question`, `Add material`, `Draft pact`, `Revise pact`, and `Seal today's pact`.\n\nThe public Space now serves the V2 workbench from Space `main` commit `8ad6f00628f800abc2dbefab05163aba94a5723f`. Public browser smoke, mobile readability, diagnostics, raw remote queue prediction, and a hosted text route smoke all reached a sealed pact on 2026-06-05. The current Modal backend pass requires a real `openbmb/MiniCPM-V-4.6` vision route smoke before delivery; demo vision fallback is runtime resilience, not a substitute for that smoke.\n\nCurrent smoke details are tracked in `docs/smoke/2026-06-05-space-deployment-smoke.md`.\n\n## Safety\n\nThis is not a therapy or diagnosis product. It gives playful reflection, small actions, and escalation copy for severe distress.",1169      "readme_frontmatter": {1170        "title": "Dream Customs",1171        "emoji": "⚡",1172        "colorFrom": "blue",1173        "colorTo": "pink",1174        "sdk": "gradio",1175        "sdk_version": "4.44.1",1176        "python_version": "3.10",1177        "app_file": "app.py",1178        "pinned": "false",1179        "license": "mit",1180        "short_description": "Turn dream declarations into a playful next-day pact.",1181        "models": "",1182        "tags": ""1183      },1184      "app_source": "import os\n\nfrom dream_customs import zerogpu  # noqa: F401\nfrom dream_customs.ui.app import build_demo\n\n\ndemo = build_demo()\n\n\nif __name__ == \"__main__\":\n    demo.launch(\n        server_name=os.getenv(\"GRADIO_SERVER_NAME\", \"0.0.0.0\"),\n        server_port=int(os.getenv(\"GRADIO_SERVER_PORT\", \"7860\")),\n        show_api=False,\n        show_error=True,\n    )\n",1185      "app_signals": "build_demo __main__ demo.launch server_name server_port show_api show_error os.getenv int GRADIO_SERVER_NAME 0.0.0.0 GRADIO_SERVER_PORT 7860",1186      "readme_len": 4262,1187      "app_source_len": 356,1188      "app_signals_len": 1401189    },1190    {1191      "id": "build-small-hackathon/dream-museum",1192      "title": "Dream Museum",1193      "summary": "Draw a dream · Describe it · Watch it materialize",1194      "tags": [1195        "gradio",1196        "region:us"1197      ],1198      "models": [],1199      "datasets": [],1200      "sdk": "gradio",

Showing the first 1,200 of 4615 lines. Download the file for the rest.