---
Date: 2026-07-23
Author: Hermes + Adolfo (interview)
Tags: [kids-book, editor, web-app, plan, grace-poppy, pocketbase, interview-decided]
Status: planned — ready to scaffold
---

# Kids Book Studio — Final Plan (Interviewed)

## Decisions Locked

From interview 2026-07-23:

- **Users:** You + Alicia reviewing/approving (PB native email/pass auth)
- **Review workflow:** Simple — approve / needs-rework + one comment per page (no threads)
- **Data backend:** PocketBase on .110 (family BaaS direction). All family custom software on PB.
- **App hosting:** Custom Svelte editor as Docker container on .110 next to Instatic sites, Caddy route `books.reynafamily.com` or `studio.reynafamily.com`. NOT inside Instatic CMS.
- **Generation worker:** Python service on Mac mini .102 polling PB for `status=queued` jobs — runs Codex imagegen + local TTS, uploads results back to PB files.
- **Frontend stack:** SvelteKit + Tailwind, Playfair for titles, Inter body, Mono for prompts. VOUS/Hillsong clean serif headers.
- **Seed data:** Auto-import Grace/Poppy existing package (21 pages) as first book
- **Image gen:** Codex imagegen (proven) + BYOK configurable model (OpenAI / Gemini / etc via API key in PB settings collection)
- **Audio gen:** Moving OFF Voicebox. Use existing reyna-cli TTS abstraction (`espeak-ng` → `say` → `edge-tts` → `REYNA_TTS_COMMAND`) + small local model on .110 Docker (Piper/Kokoro) as future-proof. No Voicebox dependency.
- **Audio per page:** One narration clip per page for now, schema designed for multi-clip dialogue later (clips[] array forward-compatible)
- **Text:** Support full caption + shorter spoken cut + edit history (caption_history, audio_text_history) for device timing tweaks
- **Images:** 320x240 only for now (ESP32 native). Full-res later as extra rendition
- **New books:** Start with Grace/Poppy only, then New Book wizard

## What We're Building

A web editor to manage kids books — pages with script, generated audio, images, and the prompts used — so you can iterate on specific pages and make new books, with Tactility export.

## System Architecture

```
┌─────────────────────────────────────────────────────────┐
│ .110 FamReynaServer                                     │
│  ┌──────────────┐  ┌────────────────────────┐           │
│  │ PocketBase   │◄─┤ Svelte Editor (Docker) │           │
│  │ :8095 or new │  │ books.reynafamily.com  │           │
│  │ - books      │  │ - Library grid         │           │
│  │ - pages      │  │ - Book strip + reorder │           │
│  │ - assets     │  │ - Page 3-col editor    │           │
│  │ - gen_jobs   │  │ - Device 320x240 prev  │           │
│  │ - users/auth │  │ - Export ZIP/PDF       │           │
│  │ - settings   │  │                        │           │
│  └──────┬───────┘  └────────────────────────┘           │
│         │ ▲                                              │
│         │ │ (PB files + realtime)                        │
└─────────┼─┼──────────────────────────────────────────────┘
          │ │
          │ │  polls jobs, uploads results
          │ │
┌─────────┼─┼──────────────────────────────────────────────┐
│ .102 Mac mini                                            │
│  ┌──────▼──────┐                                          │
│  │ Gen Worker  │  - Codex imagegen skill                  │
│  │ Python svc  │  - reyna-cli TTS (edge-tts/fallback)     │
│  │             │  - future: Kokoro local on .110          │
│  └─────────────┘  - resize + transcode 320x240 / mp3     │
└──────────────────────────────────────────────────────────┘
```

### Network:
- PB reachable via Tailscale + Caddy reverse proxy
- Worker polls PB via private network (no public exposure needed)
- Assets stored as PB files (images, audio) — accessible via PB file URL

## PocketBase Collections (schema)

**books**
- id (PB auto), slug (text unique), title (text), author (text), created (autoDate)
- style_bible: json { art_style, continuity_rules[], color_palette[], reference_images[] file[] }
- display: json { width, height } default 320x240
- default_voice: json { profile, language } e.g. {profile: "en-US-AriaNeural", language:"en-US"}
- cover_page_id -> pages (relation, cover thumbnail)
- status_overview: computed via pages (not stored)

**pages**
- id, book (relation -> books), page_number (number), status (select: draft|needs-rework|approved|generating|queued)
- caption (text, rich — full text), caption_history (json, array of {text, edited_at, by})
- audio_text (text — spoken cut, defaults to caption if empty), audio_text_history (json)
- review_comment (text), reviewed_by (relation users), reviewed_at (date)
- notes (text — author private notes)
- original_page (number optional, for imported books that skipped numbers)
- export_include (bool default true)
- Future: clips[] json for multi-speaker

**image_assets** (one active per page, history preserved via versioned files + history JSON)
- id, page (relation -> pages), is_active (bool)
- file (file, image) — PB stores both original gen + 320x240 variant?
  - Actually: store original file, gen job creates 320x240 as second file field thumb or via hook
- prompt (text), negative_prompt (text), style_bible_applied (bool)
- reference_pages (relation pages[]), generation_params (json: {model, size, seed, provider})
- prompt_version (number auto-increment per page), notes (text)
- generated_at, generated_by (user)
- Simple: to keep history, we don't delete old image_assets, just flip is_active. Page shows history slider.

**audio_assets**
- id, page (relation -> pages), is_active (bool)
- file (file, audio mp3/wav), duration_sec (number)
- text_used (text — snapshot of audio_text at generation time)
- voice_profile (text), language (text), engine (text: edge-tts|piper|kokoro|custom)
- tts_params (json)
- generated_at, generated_by

**generation_jobs** (queue)
- id, page (relation pages), type (select: image|audio), status (queued|running|done|failed)
- input (json): {prompt, negative, style_bible, voice, text, etc}
- provider (text: codex-imagegen|openai|gemini|edge-tts|piper|kokoro)
- result_image / result_audio (relation to image_assets/audio_assets when done)
- error (text), attempts (number)
- created, started, finished (dates)

**settings** (singleton-ish, or one per family)
- id, image_providers (json: {openai_key_encrypted?, gemini_key? ...}) — or use env, not PB at first
- default_image_provider, default_audio_engine
- style_presets (json): preset name → {art_style, continuity_rules}

**users** (PB built-in auth collection: you + Alicia)

## Job Flow

1. User edits prompt in Page Editor → clicks Generate Image
2. Frontend creates generation_jobs record (status=queued, input={prompt:+style_bible, page, provider})
3. Frontend subscribes to job realtime → shows spinner on page
4. Worker on .102 polls PB collection generation_jobs filter=status=queued (or realtime subscribe)
5. Worker marks running, executes:
   - For image: calls Codex imagegen OR OpenAI/Gemini depending on provider → saves PNG → resizes to 320x240 thumb → uploads to PB as new image_assets file
   - For audio: calls reyna-cli tts.py synthesize_wav() → transcodes to mp3 16kHz 24k mono → uploads as audio_assets
6. Worker creates image_assets/audio_assets record (is_active=false initially, then activate after user approval, or auto-activate if generation is direct iteration — TBD: auto-activate with history preserved)
7. Worker updates job to done with relation to new asset
8. Frontend realtime gets event, refreshes page, shows new image with history slider
9. Alicia reviews: changes status to approved or needs-rework + comment

For MVP: steps 5 auto-activates new asset (flips previous is_active=false, new is_active=true), but history still full.

## Frontend Screens (detailed)

**Route / (Library)**
- Grid of books: cover = first page active image_assets file, else placeholder. Title, author, pages count, % approved bar, last edited.
- Button: New Book → modal: title, slug (auto from title), author default Reyna Family, style preset selector (Grace-Poppy watercolor, Minimal, New custom), import tactility zip? toggle.
- Each card: Open, Export zip, Play Preview
- PB query: books list with expand cover_page_id -> image_assets

**Route /books/:slug**
- Header: title, edit pencil for style bible, % approved, Play Preview (autoplay browser reader 320x240 + audio), Export button.
- Page strip: horizontal scrollable, 160x120 thumbs (active image per page), status dot (green approved, yellow needs-rework, gray draft, blue generating). Drag to reorder (updates page_number). Click to open page editor. Plus button to add page.
- Style Bible panel (collapsible right drawer): art_style textarea, continuity_rules as editable list (add/remove), color_palette tags, reference image uploader (PB file).
- Bulk actions: checkboxes on strip → bulk status change, bulk prompt find/replace, bulk re-generate audio.

**Route /books/:slug/pages/:pageNumber (Page Editor — core)**
- Three columns:
  - Left (Script): caption textarea with char count + history icon (show caption_history). audio_text textarea (with Fill from caption button + auto-pace helper that calls edge-tts? simple: button to copy caption and add breaks). Play audio button (uses active audio_assets file URL). Voice profile selector (text input or select from settings list of edge-tts voices). Duration display from active audio. Review section at bottom of this column: status select, review_comment.
  - Center (Image): large preview 320x240 framed + expand to full original. Prompt textarea (Monospace, with toggle: "prepend style bible" on/off — when on, UI shows combined effective prompt). Negative prompt. Reference pages multi-select. Generate button (with provider selector: Codex / OpenAI / Gemini). History carousel below: scrollable thumbs of all image_assets for this page (ordered by created), each shows prompt tooltip, click to make active (swaps). Restore button.
  - Right (Meta): Page number + up/down reorder, original_page mapping, notes textarea, export_include toggle, created/updated meta, generation job log (last job status/error).

**Component: Device Preview Modal**
- 320x240 black frame mimicking ESP32, image displayed, Play/Prev/Next buttons matching Tactility Book Player. Auto-advance when audio finishes (same as device).

**Component: Export Modal**
- Formats: Tactility v1 ZIP (manifest.json + pageXXX.png + mp3). Options: only approved? include draft? file naming page001 vs page_XXX. Download button.

## Importer — Grace/Poppy Seed

Script `scripts/import_grace_poppy.py`:
- Reads `~/output/tactility-book-packages/grace-poppy-tactility-book-example/books/grace-poppy/manifest.json`
- Creates book record slug=grace-poppy
- For each page in manifest, in order:
  - Create page record with caption from manifest, audio_text = caption, status = approved (since already reviewed), original_page, page_number sequential
  - Copy image file to PB file upload as image_assets (prompt empty, notes="imported, prompt unknown", is_active true)
  - Copy audio file to PB file upload as audio_assets (is_active true, voice_profile=Aiden, duration_sec from manifest)
- Also sets style_bible with known Grace continuity rules.

Run once on first deploy.

## Tech Stack Final

- Backend: PocketBase (Go binary, Docker on .110, :8095, Caddy /books api)
- Frontend: SvelteKit + Tailwind, Vite. Hosted as Docker container on .110 next to Instatic (separate Docker Compose service), Caddy reverse proxy to `books.reynafamily.com` (or `studio.reynafamily.com`). Builds talk directly to PB via JS SDK @pocketbase/js-sdk.
- Auth: PB native users collection (email/pass). Two users: you, Alicia. Frontend has login page, guards Library route.
- Realtime: PB realtime subscriptions for jobs and pages status.
- Worker: Python 3.11, runs on Mac mini .102, deps: pocketbase client, pillow, ffmpeg, edge-tts, codex CLI available. Loop: poll every 5s for queued jobs. Could also move to .110 later with Piper local Docker for audio.
- Storage: PB file storage (local filesystem .110). Images and audio files served via PB file URL with auth.

## Project Layout (to scaffold)

```
kids-book-studio/
  README.md
  docker-compose.yml  (frontend + optional worker-for-local-dev)
  pocketbase/
    Dockerfile / config
    migrations/  (PB JS migrations for collections)
    scripts/import_grace_poppy.py
  frontend/
    package.json, SvelteKit app
    src/routes/...
    src/lib/pocketbase.ts
    src/lib/components/...
    static/
  worker/
    requirements.txt
    worker.py  (poll loop)
    tts_engine.py (wraps reyna-cli tts.py + piper/kokoro)
    image_engine.py (codex + openai + gemini providers)
    asset_utils.py (resize, transcode)
  Caddyfile.snippet  (for .110 Caddy)
  docs/
    plan.md (this file)
    schema.md
```

## PB Collections JSON Proposal (draft, to convert to migration)

See code above for fields. Will generate JS migration file for:
- books, pages, image_assets, audio_assets, generation_jobs, settings

## Implementation Phases (revised)

**Phase 0 — PB + Skeleton**
- Create kids-book-studio repo, docker-compose, PB collections migration
- Frontend SvelteKit scaffold with PB SDK, login page, Library grid (lists books)
- Importer script run, verify Grace/Poppy shows in Library with 21 pages

**Phase 1 — Read-only Book + Page Editor**
- Book Overview strip with thumbs, Style Bible editor drawer
- Page Editor 3-col read-only/active asset display, history display
- No generation yet, but schema ready.

**Phase 2 — Generation Worker**
- Python worker on .102 that polls PB generation_jobs, executes edge-tts audio + optionally Codex imagegen
- Frontend Generate buttons + realtime spinner + history updates
- Image history versioning, audio history.

**Phase 3 — Review + Export**
- Alicia review flow: status + comment, PB realtime updates
- Export Tactility ZIP with correct manifest.json format (existing format compatible with Book Player app)
- Device Preview modal 320x240 autoplay
- Drag reorder pages, bulk actions.

**Phase 4 — Polish + New Book**
- New Book wizard, style presets, high-res renditions, reMarkable PDF export
- BYOK provider keys UI (settings page), local Piper/Kokoro on .110 Docker
- Direct push to ESP32 fleet optional

## Future (not now)

- Multi-clip dialogue per page
- Character sheet with ref images
- Translation tracks (keep image, different audio language)
- LLM auto-pace performance text, consistency check
- Family review with kid read-only view

## Open Risks

- Codex imagegen reliability from worker (has been flaky, needs -o file pattern from brain doc)
- PB file size for audio/images — okay, small assets
- Caddy route for new subdomain — need to add via api script pattern (see bin/caddy-api)
- Worker auth: PB service account token, not user token

## Next Step (Ready to Build)

When user says "build it", start Phase 0: scaffold repo, PB docker + migration, frontend login + library + import Grace/Poppy.
