A newer version of the Gradio SDK is available: 6.20.0
Blind Quill Design
Blind Quill is a hidden-canon story grafting game. A reader sees a public capsule for a manuscript, adds a short fragment, and the model privately decides where that fragment belongs in the full canon. The reveal is the important moment: the reader learns where their contribution was stitched and can then read the updated manuscript.
Product Rules
- Each manuscript has a hidden full canon and a public capsule.
- A public capsule contains title, genre, tone, summary, visible characters, open questions, status, and graft count.
- A manuscript accepts at most 30 grafts. At 30 grafts it becomes sealed and read-only.
- Readers are encouraged to add a fragment before reading the full manuscript.
- Readers may still use the escape door,
Read without changing, after a warning modal. This prevents low-quality forced contributions from people who only want to read. - A stitch may replace or append a small local passage. It must not regenerate the whole manuscript.
User Flows
Start a Manuscript
- The user writes a seed, up to 500 characters.
- The binding page explains that the model is creating hidden canon, a public capsule, and persistent storage.
- The creator lands on the capsule for the new manuscript.
- The creator can contribute another fragment, read the manuscript, or share the link.
Continue a Manuscript
- The user opens the gallery or a
?story=share link. - The user sees only the public capsule.
- The primary path is
Contribute a fragment. - The escape path is
Read without changing, which opens a warning modal before revealing the full manuscript. - After a stitch, the reveal stage shows the public reveal, rationale, target chapter, and links to read or return to the bindery.
Read a Manuscript
The reader view shows the full canon, chapter navigation, highlighted grafted paragraphs when present, and a graft ledger.
Architecture
The app is a custom frontend served by a Gradio Server backend.
app.pyexposes queued API endpoints and servesweb/.core.pyowns create, browse, stitch, and read orchestration.story_store.pyowns JSON persistence and file locking.model_client.pyloadsQwen/Qwen3.5-2B, resolves the execution device, runs generation, strips thinking blocks, and validates JSON.patcher.pyapplies model patches deterministically.presenter.pymaps backend story objects into frontend view models.observability.pyconfigures logging and a lightweight per-run profiler.web/contains the production React frontend loaded through Babel in the Space page.
Execution and Progress
BQ_DEVICE (auto | zerogpu | cuda | mps | cpu) selects the base
backend; auto prefers ZeroGPU on a Space, then CUDA, MPS, and CPU.
ZeroGPU quota is per visitor and only known at request time, so device selection cannot be purely static. The flow is:
app.pyattempts each ZeroGPU stitch synchronously on the request thread — ZeroGPU bills against the Gradio request context, which a worker thread would not carry. ZeroGPU runs the function in a forked subprocess, so its arguments are pickled and it cannot stream token callbacks back.- If ZeroGPU raises a per-user quota error (
spacesraisesgradio.Errorwith a "quota exceeded" / "credits exceeded" message),app.pyretries the stitch withforce_cpu=True.model_client.generate_textthen runs in-process on the CPU-resident model instead of the GPU worker. - In-process runs (local CUDA/MPS/CPU, or the CPU fallback) execute through
_stream_stitch, which runscore.stitchon a worker thread and drains itson_progresscallback through a queue into the endpoint's yields. A worker thread is safe here only because no@spaces.GPUcall is involved. core.stitchaccepts an optionalon_progresscallback and aforce_cpuflag, reports stage and token progress, and still returns anAppliedPatchResultsynchronously so tests and callers are unaffected.- The frontend consumes the stream with the Gradio JS client's
submitand shows stage, percentage, ETA, and a fallback note, dropping back to the staged animation for fast GPU runs that emit no token progress.
Observability
Logging and profiling write to stderr only, never the UI. observability.py
configures the blind_quill logger (level via BQ_LOG_LEVEL, default INFO).
Each stitch logs messages processed, total and per-stage timings, and a
best-effort resource snapshot (process memory, CPU, and GPU/MPS memory when
available); a missing metric is omitted rather than raising.
Model Policy
- Model:
Qwen/Qwen3.5-2B. - One model only.
- No embeddings, RAG, ASR, image generation, or secondary LLM.
- Qwen thinking mode is disabled for schema-constrained JSON calls so the model
reaches the required object before
max_new_tokens; other text generation uses the model template default. <think>...</think>content is stripped before parsing, storage, prompts, or UI display.- Generation does not manually tune sampling controls.
- ZeroGPU runs use
spaces.GPU(duration=300); CUDA, MPS, and CPU runs call the model directly. The backend is chosen byBQ_DEVICE.
Environment
The Space targets Python 3.12 and Gradio 6.16. Local development should use uv with the same Python version:
uv sync --python 3.12
uv export --format requirements-txt --no-dev --no-hashes --no-emit-project -o requirements.txt
Hugging Face Spaces still installs from requirements.txt; that file is
generated from uv.lock.
Verification
Required checks before deployment:
uv 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
uv run python -m unittest discover -s tests -v
For frontend changes, also verify the relevant deployed or local UI state with a browser: mobile width, no horizontal overflow, and expected button/modal flows.