Skip to content

Playa hardening: per-séance printing, 80mm receipts, self-healing LLM probe, Android voice - #1

Open
unforcedagi wants to merge 8 commits into
Unforced-Dev:mainfrom
unforcedagi:playa-hardening
Open

Playa hardening: per-séance printing, 80mm receipts, self-healing LLM probe, Android voice#1
unforcedagi wants to merge 8 commits into
Unforced-Dev:mainfrom
unforcedagi:playa-hardening

Conversation

@unforcedagi

Copy link
Copy Markdown

Five fixes for the hardware the camp is actually taking to playa: a DGX Spark serving two Galaxy Tab kiosks (plus phones on the camp network via QR), Epson TM-m30III printers, and no internet.

The one that matters most

/api/print built its receipt from a module-level LAST dict. With two tablets mid-séance, one seeker could print another's quest — walking off with a stranger's confession on paper. Printing is now scoped to the session id, and the legacy single-question flow keeps LAST behind a lock.

The rest

  • printer.py — default 48 columns (TM-m30III is 80 mm; 32 was for a 58 mm unit, wasting a third of the paper). Adds ESCPOS_HOST network transport on port 9100 with per-station ESCPOS_HOST_<n> routing, and centres the fixed-width compass rose in the wider paper.
  • llm.pyavailable() cached its probe for the life of the process. If Ollama came up after the oracle server — which a power blip on playa guarantees — the Turtle stayed in template mode until a human noticed and restarted it. Now re-probes on a 30 s TTL. Also extends think-suppression to gpt-oss / magistral.
  • session.py — LLM timeouts move to ORACLE_T_SHORT / ORACLE_T_LONG (25 s / 40 s, from 45–120 s). Every one of these calls has a template fallback, so a timeout costs a little magic; two minutes of a silent turtle costs the seeker. MAX_SESSIONS 60 → 200.
  • kiosk.html — print posts its session id and ?kiosk=n. The TTS voice ranking scored only macOS voice names (Daniel, Alex, Oliver), so on the actual Galaxy Tabs the Turtle would have spoken in a bright American default; it now scores Google/Samsung naming and weights localService heavily, since there's no network voice on playa. Serif stack gains Noto Serif so Android doesn't fall back to Roboto.

Verification

python3 -m py_compile app/oracle/*.py clean; CLI smoke test exercises the full no-LLM fallback path and produces a correct reading + adventure. Not yet tested against a real printer or on the tablets — neither is in hand yet.

🤖 Generated with Claude Code

unforcedagi and others added 6 commits August 7, 2026 11:31
… probe, Android voice

The camp's hardware moved from one Mac mini + one browser to a DGX Spark serving
two Galaxy Tab kiosks (and, soon, phones scanning a QR on the camp network).
Several things that were correct for one-seeker-at-a-time are wrong for that.

- server.py: /api/print now takes a session id and formats from that séance's own
  snapshot. The module-level LAST is demoted to the legacy /api/reading flow and
  guarded by a lock. Previously two tablets shared one "last reading", so a seeker
  could print a stranger's quest — the failure mode being one person walking off
  with someone else's confession on paper.
- printer.py: default width 48 cols (TM-m30III is 80mm; 32 was for a 58mm unit),
  ESCPOS_HOST network transport on 9100 with per-station ESCPOS_HOST_<n> routing,
  and the fixed-width compass rose centred inside the wider paper.
- llm.py: available() re-probes on a TTL instead of caching one answer for the life
  of the process. If Ollama comes up after the oracle — a power blip reorders unit
  start on playa — the Turtle no longer stays stuck in template mode until a human
  notices and restarts it. Also extends think-suppression to gpt-oss/magistral.
- session.py: LLM timeouts move to ORACLE_T_SHORT/T_LONG (25s/40s, was 45-120s).
  Every one of these calls has a template fallback, so a timeout costs a little
  magic; two minutes of a silent turtle costs the seeker. MAX_SESSIONS 60 -> 200.
- kiosk.html: print posts its session id and ?kiosk=n; TTS voice ranking now scores
  Google/Samsung naming and weights localService heavily (the old ranking was all
  macOS voice names, so on the actual tablets the Turtle would have spoken in a
  bright American default); serif stack gains Noto Serif so Android doesn't fall
  back to Roboto.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…t hide

Benchmarked on the DGX Spark (GB10, qwen3:30b-a3b) by driving the real session
state machine, not synthetic tok/s:

  1 seeker,  warm    full seance 10.5s  (weave+echoes 5.7, refine 2.1, seal 2.7)
  4 seekers, warm    30.6s wall, slowest seeker 30.7s
  6 seekers, warm    57.5s wall, slowest SINGLE CALL 24.4s

Ollama serialises on the one GPU, so per-call latency scales with how many seekers
are mid-seance. The 25s T_SHORT from the previous commit looked generous against the
solo number and would have started tripping at six — the worst possible moment,
since that is when the most people are watching. Raised to 45s/60s. These are guards
against a hung model, not pacing controls; the fallback is a visible quality drop, so
the model should win whenever it is merely slow.

- weave.py: the weave is the most expensive call in the seance and was still hardcoded
  to 120s, so tuning session.py alone left it untouched. Now shares ORACLE_T_LONG.
- server.py: /api/health reports llm reachability, model, ears, live seances, printer
  transport, and the ratio of readings served by the LLM vs the template.

That ratio is the point. The fallback weave is good enough that a dead model reads as
a working oracle — the seance completes, the reading is real, nobody notices. It cost
me a benchmark run to spot it here; on playa it could go a whole night. Now a camp
turtle can load one URL and see it.

Also measured, and worth recording: OLLAMA_NUM_PARALLEL=4 made things WORSE than the
default of 1 (4 seekers: 38.8s vs 30.6s). This box is memory-bandwidth-bound, so
batching buys contention rather than throughput. Leave it at 1.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… title

The original 48 card fronts are gone — not in the repo (cards/art/ is gitignored),
not in the migration bundle, not on the archive drive. Rather than reprint a lossy
upscale of the web-sized copies, regenerate them properly.

tools/gen_art.py generates through the Codex backend's image_generation tool. Notes
for whoever runs it next, because none of this is guessable:

- It authenticates with ~/.codex/auth.json — the ChatGPT OAuth token the Codex CLI
  already holds. No OpenAI API key, no per-image billing.
- The model MUST be the one Codex itself uses (gpt-5.6-sol). Every other name returns
  "not supported when using Codex with a ChatGPT account", which reads like a model
  problem and is really an entitlement one.
- The same token gets 401 "missing scopes" against api.openai.com for both
  /v1/images/generations and /v1/responses, so the Codex backend is the only door.
- Output is 1024x1536 — exactly the deck's 2:3, so nothing is cropped. It is under the
  1124x1649 print target with bleed, so cards need ~10% upscaling; print_prep.py
  already manufactures bleed by extending edge pixels.
- ~45s per card, so the full deck is about 35 minutes and iteration is effectively free.

Four prompts rewritten as the standard for the other 44. The old ones illustrate each
card's TITLE; these hold its READING and its SHADOW in one frame. roots-02 is the
clearest example: the reading calls grief "a room you built for someone, and it's still
theirs", the shadow warns it "can become a room you refuse to leave" — so the ribcage
is a tended chapel with one chair and a gold heart, and a door standing open with
stepping stones leading out. Stay or go. "A glowing ribcage sculpture at dusk" cannot
carry that.

Deliberate deviation from STYLE_GUIDE.md: the generated art contains NO lettering and
leaves the bottom cartouche empty. Image models misspell, and 48 cards each misspelling
differently would destroy the cohesion the guide exists to protect. Titles get
composited in print_prep.py where the typography is exact and identical across the deck.

Known gap: the per-realm undertones are uneven. Branches reads clearly sky-blue and
Shell clearly gold, but Roots does not come out indigo — it renders the same warm tan.
Needs a harder push in the realm tone text.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
'Deep indigo undertone' produced Roots cards indistinguishable from the kraft
default — the model read it as a mood word and dropped it. Branches' sky-blue only
survived because that subject was already sky.

Each realm now states it as a print instruction: which ink, where it goes, and how
much of the card it must touch. Roots says the entire lower half below the ground
line prints indigo rather than black and the card must read blue-black, not brown.
Verified by regenerating roots-02 — the realm is now unmistakable at a glance, with
the composition otherwise unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…d standard

The four style-test cards stayed on-playa by luck — their subjects were playa
things. The first draft of branches-03 'The Gift' is an abstract principle with
nothing physical to draw, and the model wandered: pine forests, green valleys,
mountain ridges. A lovely card for a different deck.

The preamble now states the setting as a hard constraint with explicit negatives
(no trees, no forest, no grass, no rivers, no rolling hills; the only tree that may
appear is the World Tree). Regenerating the same prompt puts it back on cracked
alkali playa with distant barren ranges — and the composition improved, because the
model stopped spending the frame on scenery.

branches-03's prompt is also the standard for the abstract cards. 'The Gift' reads
'give the thing away and want nothing back'; its shadow is 'giving in order to be
seen giving is just a transaction with extra steps'. So: a traveller walks away from
a gift left on a cairn, back turned, hands empty, while far off a tiny radiant figure
holds the identical object above a cheering crowd. Given vs performed, one frame.
The old prompt was 'an open hand offering a small handmade treasure to a stranger'
— the virtue with no shadow anywhere in it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
cards.json has always promised that a Shell card "can substitute into any position;
when it appears, the axis itself is speaking", and APP_DESIGN.md repeated it. Nothing
ever implemented it. session.py, select.py and deck.py all iterated only
(roots, trunk, branches), so twelve cards — a quarter of the printed deck, including
shell-01 The World Turtle, the camp's own mascot — could not appear in a reading.
The physical deck was 48 cards and the app was a 36-card deck.

- deck.py: draw_spread() returns (picks, axis_slot). At most ONE slot per spread becomes
  a Shell card, so it stays an event rather than a flavour. ORACLE_SHELL_CHANCE, default
  0.10 — often enough that the camp turtles witness it over a week, rare enough to mean
  something. Verified over 20k draws: 10.12% axis rate, never two in a spread, all twelve
  Shell cards reachable, evenly spread across the three slots.
- session.py: the Turtle breaks its own script when it lands, and the weave context is
  told the axis has spoken so the reading gives it more weight than the other two. A
  live run put The Caduceus in the branches slot and the reading bound it: "the wound
  and the medicine are the same coil."
- session.py: _refine_fallback re-scores cards via select_fallback, which only knows the
  three Tree realms — it would have silently swapped out an axis card the seeker had
  already been shown. The axis slot is now preserved across a refine. Once the spine has
  spoken the Turtle does not take it back.
- kiosk.html: the axis card gets a gold ring and reads "◆ The Axis Speaks" instead of its
  slot name. A steady glow, not confetti.
- printer.py: marked [* AXIS *] on the receipt, so the seeker still has the evidence days
  later. Receipt still fits 48 columns.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
unforcedagi and others added 2 commits August 7, 2026 15:40
…ing and shadow

All 48 image prompts rewritten and all 48 cards regenerated. The old prompts
illustrated each card's TITLE; these hold its READING against its SHADOW in one
frame, and never tell the viewer which is which.

  The Cryptomeria Man  "built beautifully and then burned on purpose" / "sometimes
    renewal is just you torching what you should have tended" -> a spiral-staircase
    effigy split down the centre, half timber half embers, and at its base one figure
    holding a lit torch in one hand and a watering can in the other, not yet choosing.
  The Zendo  "sitting with someone in their dark is holy work" / "you can hide inside
    caretaking to avoid your own passage" -> two figures holding hands inside a tent,
    a third waiting outside with a blanket, and directly behind that third their own
    dark doorway standing open, unentered.
  Radical Self-Reliance  -> a provisioned traveller handing a stranger a full canteen,
    beside a figure bent under an overloaded pack passing an offered canteen without
    turning their head.
  Leave No Trace  -> someone alone on empty playa picking up one scrap with nobody
    watching; behind them another sweeping spotless ground beside their own untouched
    tools, never having made anything.

Shell now gets a heavier ornate frame with corner bosses and far more gold than the
three Tree realms, so an axis card — the rare one the séance surfaces about one time
in ten — is identifiable across a dusty tent before the name is read. The contact
sheet shows the four realms reading as distinct bands, which was the point.

48 generated, 0 failed, ~55s each.

Also fixed: contact_sheet.py, booklet.py and print_prep.py all had REPO hardcoded to
/Users/parachute/Code/oracle-ai — a path from the machine this project was migrated
off. None of them could run here. They now derive the repo from their own location.

cards/art/ is gitignored (200 MB), which is exactly how the v1 art was lost: it
existed only on a machine that went away. The tracked web renders are regenerated
here, and the full-resolution set is mirrored to the Spark at ~/turtle-art-backup,
checksum-verified. It still needs a durable home that is neither this mini nor that
box — see the note in print/README.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ll 48

The generated art leaves the bottom banner empty on purpose: image models misspell,
and 48 cards each misspelling differently would destroy exactly the cohesion the
style guide exists to protect. So the name is set here instead — Bodoni 72 Smallcaps
(the style guide asks for a woodcut serif in small caps), one baseline, one tracking
rule, identical across the deck.

- set_title() centres the name in the banner with real letter-spacing, which PIL does
  not provide — it measures and places each glyph, since small caps need the air.
- Long names shrink to fit rather than crowding the frame: "The World Turtle" and
  "Bring Native Grasses Back to Life" both sit correctly inside their cartouches.
- Verified against a short name, the longest name, and a Shell card, whose heavier
  ornate frame puts its banner in a slightly different place.

Output is 1124x1649 with bleed — the 1024x1536 generations upscale to the 1050x1575
trim and the bleed is built by extending edge pixels outward, so no gold border is
ever cut. 48 fronts + back + a 50-page proof PDF.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant