{
  "schemaVersion": "1.0",
  "item": {
    "slug": "fastapi-studio-template",
    "name": "Fastapi Studio Template",
    "source": "tencent",
    "type": "skill",
    "category": "开发工具",
    "sourceUrl": "https://clawhub.ai/nissan/fastapi-studio-template",
    "canonicalUrl": "https://clawhub.ai/nissan/fastapi-studio-template",
    "targetPlatform": "OpenClaw"
  },
  "install": {
    "downloadMode": "redirect",
    "downloadUrl": "/downloads/fastapi-studio-template",
    "sourceDownloadUrl": "https://wry-manatee-359.convex.site/api/v1/download?slug=fastapi-studio-template",
    "sourcePlatform": "tencent",
    "targetPlatform": "OpenClaw",
    "installMethod": "Manual import",
    "extraction": "Extract archive",
    "prerequisites": [
      "OpenClaw"
    ],
    "packageFormat": "ZIP package",
    "includedAssets": [
      "SKILL.md"
    ],
    "primaryDoc": "SKILL.md",
    "quickSetup": [
      "Download the package from Yavira.",
      "Extract the archive and review SKILL.md first.",
      "Import or place the package into your OpenClaw setup."
    ],
    "agentAssist": {
      "summary": "Hand the extracted package to your coding agent with a concrete install brief instead of figuring it out manually.",
      "steps": [
        "Download the package from Yavira.",
        "Extract it into a folder your agent can access.",
        "Paste one of the prompts below and point your agent at the extracted folder."
      ],
      "prompts": [
        {
          "label": "New install",
          "body": "I downloaded a skill package from Yavira. Read SKILL.md from the extracted folder and install it by following the included instructions. Tell me what you changed and call out any manual steps you could not complete."
        },
        {
          "label": "Upgrade existing",
          "body": "I downloaded an updated skill package from Yavira. Read SKILL.md from the extracted folder, compare it with my current installation, and upgrade it while preserving any custom configuration unless the package docs explicitly say otherwise. Summarize what changed and any follow-up checks I should run."
        }
      ]
    },
    "sourceHealth": {
      "source": "tencent",
      "status": "healthy",
      "reason": "direct_download_ok",
      "recommendedAction": "download",
      "checkedAt": "2026-04-30T16:55:25.780Z",
      "expiresAt": "2026-05-07T16:55:25.780Z",
      "httpStatus": 200,
      "finalUrl": "https://wry-manatee-359.convex.site/api/v1/download?slug=network",
      "contentType": "application/zip",
      "probeMethod": "head",
      "details": {
        "probeUrl": "https://wry-manatee-359.convex.site/api/v1/download?slug=network",
        "contentDisposition": "attachment; filename=\"network-1.0.0.zip\"",
        "redirectLocation": null,
        "bodySnippet": null
      },
      "scope": "source",
      "summary": "Source download looks usable.",
      "detail": "Yavira can redirect you to the upstream package for this source.",
      "primaryActionLabel": "Download for OpenClaw",
      "primaryActionHref": "/downloads/fastapi-studio-template"
    },
    "validation": {
      "installChecklist": [
        "Use the Yavira download entry.",
        "Review SKILL.md after the package is downloaded.",
        "Confirm the extracted package contains the expected setup assets."
      ],
      "postInstallChecks": [
        "Confirm the extracted package includes the expected docs or setup files.",
        "Validate the skill or prompts are available in your target agent workspace.",
        "Capture any manual follow-up steps the agent could not complete."
      ]
    },
    "downloadPageUrl": "https://openagent3.xyz/downloads/fastapi-studio-template",
    "agentPageUrl": "https://openagent3.xyz/skills/fastapi-studio-template/agent",
    "manifestUrl": "https://openagent3.xyz/skills/fastapi-studio-template/agent.json",
    "briefUrl": "https://openagent3.xyz/skills/fastapi-studio-template/agent.md"
  },
  "agentAssist": {
    "summary": "Hand the extracted package to your coding agent with a concrete install brief instead of figuring it out manually.",
    "steps": [
      "Download the package from Yavira.",
      "Extract it into a folder your agent can access.",
      "Paste one of the prompts below and point your agent at the extracted folder."
    ],
    "prompts": [
      {
        "label": "New install",
        "body": "I downloaded a skill package from Yavira. Read SKILL.md from the extracted folder and install it by following the included instructions. Tell me what you changed and call out any manual steps you could not complete."
      },
      {
        "label": "Upgrade existing",
        "body": "I downloaded an updated skill package from Yavira. Read SKILL.md from the extracted folder, compare it with my current installation, and upgrade it while preserving any custom configuration unless the package docs explicitly say otherwise. Summarize what changed and any follow-up checks I should run."
      }
    ]
  },
  "documentation": {
    "source": "clawhub",
    "primaryDoc": "SKILL.md",
    "sections": [
      {
        "title": "FastAPI Studio Template",
        "body": "Bootstrap a dark-themed FastAPI + HTMX studio app for generative AI comparison, A/B testing, and human evaluation with real-time progress streaming."
      },
      {
        "title": "When to Use",
        "body": "Any \"studio\" app: image generation comparison, text model A/B testing, human evaluation UI\nApps needing real-time progress updates (generation can take 30s–15min)\nBlind test / evaluation interfaces where raters shouldn't know which model produced which output\nRapid prototyping of gen AI comparison tools"
      },
      {
        "title": "When NOT to Use",
        "body": "Simple CRUD apps (use standard FastAPI + Jinja2)\nApps that don't need real-time progress (SSE adds complexity)\nProduction-scale apps with 100+ concurrent users (use WebSockets instead of SSE)"
      },
      {
        "title": "SSE Async Pattern (Critical)",
        "body": "MUST use threading.SimpleQueue + asyncio polling. Do NOT use run_in_executor with blocking reads — it deadlocks the event loop.\n\nimport asyncio\nimport threading\nfrom queue import SimpleQueue\n\nfrom fastapi import FastAPI\nfrom fastapi.responses import StreamingResponse\n\napp = FastAPI()\n\nasync def event_stream(queue: SimpleQueue):\n    \"\"\"Yield SSE events from a thread-safe queue.\"\"\"\n    while True:\n        try:\n            msg = queue.get_nowait()\n        except Exception:\n            await asyncio.sleep(0.1)\n            continue\n        if msg is None:  # sentinel\n            yield f\"data: {{\\\"done\\\": true}}\\n\\n\"\n            break\n        yield f\"data: {msg}\\n\\n\"\n\n@app.get(\"/generate/stream\")\nasync def generate_stream(prompt: str, model: str):\n    queue = SimpleQueue()\n\n    def _run():\n        # Heavy generation work in background thread\n        for step in range(10):\n            import time; time.sleep(1)\n            queue.put(f'{{\"step\": {step}, \"total\": 10}}')\n        queue.put(None)  # done sentinel\n\n    threading.Thread(target=_run, daemon=True).start()\n    return StreamingResponse(event_stream(queue), media_type=\"text/event-stream\")\n\nWhy not run_in_executor? FastAPI's executor runs on a thread pool, but SSE needs to yield events incrementally. Blocking in the executor means you can't stream partial progress — you'd have to wait for the entire generation to finish. The queue pattern decouples generation from streaming."
      },
      {
        "title": "Blind Test Mode",
        "body": "Generate N variants (one per model), randomise display order, reveal model identity only after the user rates all variants.\n\nimport random\nimport uuid\n\ndef create_blind_test(prompt: str, models: list[str]) -> dict:\n    test_id = str(uuid.uuid4())\n    variants = []\n    for model in models:\n        variants.append({\n            \"variant_id\": str(uuid.uuid4()),\n            \"model\": model,  # hidden from UI until reveal\n            \"prompt\": prompt,\n        })\n    random.shuffle(variants)\n    return {\n        \"test_id\": test_id,\n        \"variants\": variants,\n        \"display_order\": [v[\"variant_id\"] for v in variants],\n    }\n\nIn the HTMX frontend, render variants as \"Option A\", \"Option B\", etc. On rating submission, return the mapping from option letters to model names."
      },
      {
        "title": "Hot-Loaded Model Singleton (ModelRegistry)",
        "body": "Cold-loading SDXL or similar models takes 6–14 minutes. Cache loaded models in a registry singleton.\n\nclass ModelRegistry:\n    _instance = None\n    _models: dict = {}\n    _lock = threading.Lock()\n\n    @classmethod\n    def get(cls, model_name: str):\n        with cls._lock:\n            if model_name not in cls._models:\n                cls._models[model_name] = cls._load_model(model_name)\n            return cls._models[model_name]\n\n    @classmethod\n    def _load_model(cls, name: str):\n        # Import and load the model\n        if name == \"sdxl\":\n            from mflux import Flux1\n            return Flux1.from_alias(\"schnell\", quantize=8)\n        raise ValueError(f\"Unknown model: {name}\")\n\nPreload at startup via the FastAPI lifespan hook for models you know you'll need."
      },
      {
        "title": "float32 Requirement for SDXL on MPS",
        "body": "torch 2.10 on Apple Silicon (MPS) produces NaN outputs with float16 for SDXL. Force float32:\n\nimport torch\ntorch.set_default_dtype(torch.float32)\n# or per-model: model = model.to(dtype=torch.float32)\n\nThis doubles VRAM usage but is the only reliable option until the MPS float16 bug is fixed."
      },
      {
        "title": "SQLite Schema for Ratings",
        "body": "CREATE TABLE ratings (\n    id INTEGER PRIMARY KEY AUTOINCREMENT,\n    test_id TEXT NOT NULL,\n    variant_id TEXT NOT NULL,\n    model TEXT NOT NULL,\n    rater TEXT DEFAULT 'anonymous',\n    score INTEGER CHECK(score BETWEEN 1 AND 5),\n    preferred BOOLEAN DEFAULT FALSE,  -- winner of pairwise comparison\n    notes TEXT,\n    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP\n);\n\nCREATE INDEX idx_ratings_test ON ratings(test_id);\nCREATE INDEX idx_ratings_model ON ratings(model);"
      },
      {
        "title": "Langfuse Tracing",
        "body": "Wrap generation calls with Langfuse traces for cost tracking and latency monitoring:\n\nfrom langfuse import Langfuse\n\nlangfuse = Langfuse()\n\ndef generate_with_trace(prompt, model_name):\n    trace = langfuse.trace(name=\"studio-generation\", metadata={\"model\": model_name})\n    span = trace.span(name=\"generate\", input={\"prompt\": prompt})\n    result = ModelRegistry.get(model_name).generate(prompt)\n    span.end(output={\"length\": len(result)})\n    return result"
      },
      {
        "title": "Worked Example: Minimal Studio App",
        "body": "\"\"\"Minimal FastAPI+HTMX studio with SSE progress.\"\"\"\nimport asyncio\nimport json\nimport threading\nfrom queue import SimpleQueue\nfrom pathlib import Path\n\nfrom fastapi import FastAPI, Request\nfrom fastapi.responses import HTMLResponse, StreamingResponse\nfrom fastapi.staticfiles import StaticFiles\n\napp = FastAPI()\n\nHTML = \"\"\"\n<!DOCTYPE html>\n<html>\n<head>\n    <title>Studio</title>\n    <script src=\"https://unpkg.com/htmx.org@1.9.12\"></script>\n    <script src=\"https://unpkg.com/htmx.org@1.9.12/dist/ext/sse.js\"></script>\n    <style>\n        body { background: #1a1a2e; color: #e0e0e0; font-family: system-ui; padding: 2rem; }\n        .card { background: #16213e; border-radius: 8px; padding: 1.5rem; margin: 1rem 0; }\n        button { background: #0f3460; color: white; border: none; padding: 0.75rem 1.5rem;\n                 border-radius: 4px; cursor: pointer; }\n        button:hover { background: #533483; }\n        input, textarea { background: #0f3460; color: white; border: 1px solid #333;\n                          padding: 0.5rem; border-radius: 4px; width: 100%; }\n        #progress { color: #e94560; }\n    </style>\n</head>\n<body>\n    <h1>🎨 Studio</h1>\n    <div class=\"card\">\n        <textarea id=\"prompt\" placeholder=\"Enter prompt...\" rows=\"3\"></textarea>\n        <br><br>\n        <button onclick=\"startGeneration()\">Generate</button>\n    </div>\n    <div id=\"progress\" class=\"card\" style=\"display:none\"></div>\n    <div id=\"results\" class=\"card\" style=\"display:none\"></div>\n    <script>\n    function startGeneration() {\n        const prompt = document.getElementById('prompt').value;\n        const progress = document.getElementById('progress');\n        progress.style.display = 'block';\n        progress.textContent = 'Starting...';\n\n        const source = new EventSource('/generate/stream?prompt=' + encodeURIComponent(prompt));\n        source.onmessage = (e) => {\n            const data = JSON.parse(e.data);\n            if (data.done) {\n                source.close();\n                progress.textContent = 'Done!';\n            } else {\n                progress.textContent = `Step ${data.step}/${data.total}`;\n            }\n        };\n    }\n    </script>\n</body>\n</html>\n\"\"\"\n\n@app.get(\"/\", response_class=HTMLResponse)\nasync def index():\n    return HTML\n\nasync def event_stream(queue: SimpleQueue):\n    while True:\n        try:\n            msg = queue.get_nowait()\n        except Exception:\n            await asyncio.sleep(0.1)\n            continue\n        if msg is None:\n            yield f\"data: {json.dumps({'done': True})}\\n\\n\"\n            break\n        yield f\"data: {msg}\\n\\n\"\n\n@app.get(\"/generate/stream\")\nasync def generate_stream(prompt: str):\n    queue = SimpleQueue()\n    def _run():\n        import time\n        for i in range(10):\n            time.sleep(0.5)\n            queue.put(json.dumps({\"step\": i + 1, \"total\": 10}))\n        queue.put(None)\n    threading.Thread(target=_run, daemon=True).start()\n    return StreamingResponse(event_stream(queue), media_type=\"text/event-stream\")\n\nRun with: uvicorn app:app --reload --port 8000"
      },
      {
        "title": "Tips",
        "body": "Dark theme first — gen AI studios are used in long sessions; light themes cause eye strain\nAlways show progress — users will close the tab if they think it's frozen\nLog every generation — Langfuse traces are invaluable for debugging quality issues\nRate-limit generation — SDXL on MPS can only do one image at a time; queue requests\nExport ratings as CSV — researchers need data in portable formats"
      }
    ],
    "body": "FastAPI Studio Template\n\nBootstrap a dark-themed FastAPI + HTMX studio app for generative AI comparison, A/B testing, and human evaluation with real-time progress streaming.\n\nWhen to Use\nAny \"studio\" app: image generation comparison, text model A/B testing, human evaluation UI\nApps needing real-time progress updates (generation can take 30s–15min)\nBlind test / evaluation interfaces where raters shouldn't know which model produced which output\nRapid prototyping of gen AI comparison tools\nWhen NOT to Use\nSimple CRUD apps (use standard FastAPI + Jinja2)\nApps that don't need real-time progress (SSE adds complexity)\nProduction-scale apps with 100+ concurrent users (use WebSockets instead of SSE)\nCore Patterns\nSSE Async Pattern (Critical)\n\nMUST use threading.SimpleQueue + asyncio polling. Do NOT use run_in_executor with blocking reads — it deadlocks the event loop.\n\nimport asyncio\nimport threading\nfrom queue import SimpleQueue\n\nfrom fastapi import FastAPI\nfrom fastapi.responses import StreamingResponse\n\napp = FastAPI()\n\nasync def event_stream(queue: SimpleQueue):\n    \"\"\"Yield SSE events from a thread-safe queue.\"\"\"\n    while True:\n        try:\n            msg = queue.get_nowait()\n        except Exception:\n            await asyncio.sleep(0.1)\n            continue\n        if msg is None:  # sentinel\n            yield f\"data: {{\\\"done\\\": true}}\\n\\n\"\n            break\n        yield f\"data: {msg}\\n\\n\"\n\n@app.get(\"/generate/stream\")\nasync def generate_stream(prompt: str, model: str):\n    queue = SimpleQueue()\n\n    def _run():\n        # Heavy generation work in background thread\n        for step in range(10):\n            import time; time.sleep(1)\n            queue.put(f'{{\"step\": {step}, \"total\": 10}}')\n        queue.put(None)  # done sentinel\n\n    threading.Thread(target=_run, daemon=True).start()\n    return StreamingResponse(event_stream(queue), media_type=\"text/event-stream\")\n\n\nWhy not run_in_executor? FastAPI's executor runs on a thread pool, but SSE needs to yield events incrementally. Blocking in the executor means you can't stream partial progress — you'd have to wait for the entire generation to finish. The queue pattern decouples generation from streaming.\n\nBlind Test Mode\n\nGenerate N variants (one per model), randomise display order, reveal model identity only after the user rates all variants.\n\nimport random\nimport uuid\n\ndef create_blind_test(prompt: str, models: list[str]) -> dict:\n    test_id = str(uuid.uuid4())\n    variants = []\n    for model in models:\n        variants.append({\n            \"variant_id\": str(uuid.uuid4()),\n            \"model\": model,  # hidden from UI until reveal\n            \"prompt\": prompt,\n        })\n    random.shuffle(variants)\n    return {\n        \"test_id\": test_id,\n        \"variants\": variants,\n        \"display_order\": [v[\"variant_id\"] for v in variants],\n    }\n\n\nIn the HTMX frontend, render variants as \"Option A\", \"Option B\", etc. On rating submission, return the mapping from option letters to model names.\n\nHot-Loaded Model Singleton (ModelRegistry)\n\nCold-loading SDXL or similar models takes 6–14 minutes. Cache loaded models in a registry singleton.\n\nclass ModelRegistry:\n    _instance = None\n    _models: dict = {}\n    _lock = threading.Lock()\n\n    @classmethod\n    def get(cls, model_name: str):\n        with cls._lock:\n            if model_name not in cls._models:\n                cls._models[model_name] = cls._load_model(model_name)\n            return cls._models[model_name]\n\n    @classmethod\n    def _load_model(cls, name: str):\n        # Import and load the model\n        if name == \"sdxl\":\n            from mflux import Flux1\n            return Flux1.from_alias(\"schnell\", quantize=8)\n        raise ValueError(f\"Unknown model: {name}\")\n\n\nPreload at startup via the FastAPI lifespan hook for models you know you'll need.\n\nfloat32 Requirement for SDXL on MPS\n\ntorch 2.10 on Apple Silicon (MPS) produces NaN outputs with float16 for SDXL. Force float32:\n\nimport torch\ntorch.set_default_dtype(torch.float32)\n# or per-model: model = model.to(dtype=torch.float32)\n\n\nThis doubles VRAM usage but is the only reliable option until the MPS float16 bug is fixed.\n\nSQLite Schema for Ratings\nCREATE TABLE ratings (\n    id INTEGER PRIMARY KEY AUTOINCREMENT,\n    test_id TEXT NOT NULL,\n    variant_id TEXT NOT NULL,\n    model TEXT NOT NULL,\n    rater TEXT DEFAULT 'anonymous',\n    score INTEGER CHECK(score BETWEEN 1 AND 5),\n    preferred BOOLEAN DEFAULT FALSE,  -- winner of pairwise comparison\n    notes TEXT,\n    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP\n);\n\nCREATE INDEX idx_ratings_test ON ratings(test_id);\nCREATE INDEX idx_ratings_model ON ratings(model);\n\nLangfuse Tracing\n\nWrap generation calls with Langfuse traces for cost tracking and latency monitoring:\n\nfrom langfuse import Langfuse\n\nlangfuse = Langfuse()\n\ndef generate_with_trace(prompt, model_name):\n    trace = langfuse.trace(name=\"studio-generation\", metadata={\"model\": model_name})\n    span = trace.span(name=\"generate\", input={\"prompt\": prompt})\n    result = ModelRegistry.get(model_name).generate(prompt)\n    span.end(output={\"length\": len(result)})\n    return result\n\nWorked Example: Minimal Studio App\n\"\"\"Minimal FastAPI+HTMX studio with SSE progress.\"\"\"\nimport asyncio\nimport json\nimport threading\nfrom queue import SimpleQueue\nfrom pathlib import Path\n\nfrom fastapi import FastAPI, Request\nfrom fastapi.responses import HTMLResponse, StreamingResponse\nfrom fastapi.staticfiles import StaticFiles\n\napp = FastAPI()\n\nHTML = \"\"\"\n<!DOCTYPE html>\n<html>\n<head>\n    <title>Studio</title>\n    <script src=\"https://unpkg.com/htmx.org@1.9.12\"></script>\n    <script src=\"https://unpkg.com/htmx.org@1.9.12/dist/ext/sse.js\"></script>\n    <style>\n        body { background: #1a1a2e; color: #e0e0e0; font-family: system-ui; padding: 2rem; }\n        .card { background: #16213e; border-radius: 8px; padding: 1.5rem; margin: 1rem 0; }\n        button { background: #0f3460; color: white; border: none; padding: 0.75rem 1.5rem;\n                 border-radius: 4px; cursor: pointer; }\n        button:hover { background: #533483; }\n        input, textarea { background: #0f3460; color: white; border: 1px solid #333;\n                          padding: 0.5rem; border-radius: 4px; width: 100%; }\n        #progress { color: #e94560; }\n    </style>\n</head>\n<body>\n    <h1>🎨 Studio</h1>\n    <div class=\"card\">\n        <textarea id=\"prompt\" placeholder=\"Enter prompt...\" rows=\"3\"></textarea>\n        <br><br>\n        <button onclick=\"startGeneration()\">Generate</button>\n    </div>\n    <div id=\"progress\" class=\"card\" style=\"display:none\"></div>\n    <div id=\"results\" class=\"card\" style=\"display:none\"></div>\n    <script>\n    function startGeneration() {\n        const prompt = document.getElementById('prompt').value;\n        const progress = document.getElementById('progress');\n        progress.style.display = 'block';\n        progress.textContent = 'Starting...';\n\n        const source = new EventSource('/generate/stream?prompt=' + encodeURIComponent(prompt));\n        source.onmessage = (e) => {\n            const data = JSON.parse(e.data);\n            if (data.done) {\n                source.close();\n                progress.textContent = 'Done!';\n            } else {\n                progress.textContent = `Step ${data.step}/${data.total}`;\n            }\n        };\n    }\n    </script>\n</body>\n</html>\n\"\"\"\n\n@app.get(\"/\", response_class=HTMLResponse)\nasync def index():\n    return HTML\n\nasync def event_stream(queue: SimpleQueue):\n    while True:\n        try:\n            msg = queue.get_nowait()\n        except Exception:\n            await asyncio.sleep(0.1)\n            continue\n        if msg is None:\n            yield f\"data: {json.dumps({'done': True})}\\n\\n\"\n            break\n        yield f\"data: {msg}\\n\\n\"\n\n@app.get(\"/generate/stream\")\nasync def generate_stream(prompt: str):\n    queue = SimpleQueue()\n    def _run():\n        import time\n        for i in range(10):\n            time.sleep(0.5)\n            queue.put(json.dumps({\"step\": i + 1, \"total\": 10}))\n        queue.put(None)\n    threading.Thread(target=_run, daemon=True).start()\n    return StreamingResponse(event_stream(queue), media_type=\"text/event-stream\")\n\n\nRun with: uvicorn app:app --reload --port 8000\n\nTips\nDark theme first — gen AI studios are used in long sessions; light themes cause eye strain\nAlways show progress — users will close the tab if they think it's frozen\nLog every generation — Langfuse traces are invaluable for debugging quality issues\nRate-limit generation — SDXL on MPS can only do one image at a time; queue requests\nExport ratings as CSV — researchers need data in portable formats"
  },
  "trust": {
    "sourceLabel": "tencent",
    "provenanceUrl": "https://clawhub.ai/nissan/fastapi-studio-template",
    "publisherUrl": "https://clawhub.ai/nissan/fastapi-studio-template",
    "owner": "nissan",
    "version": "1.2.1",
    "license": null,
    "verificationStatus": "Indexed source record"
  },
  "links": {
    "detailUrl": "https://openagent3.xyz/skills/fastapi-studio-template",
    "downloadUrl": "https://openagent3.xyz/downloads/fastapi-studio-template",
    "agentUrl": "https://openagent3.xyz/skills/fastapi-studio-template/agent",
    "manifestUrl": "https://openagent3.xyz/skills/fastapi-studio-template/agent.json",
    "briefUrl": "https://openagent3.xyz/skills/fastapi-studio-template/agent.md"
  }
}