request:
artifact: "one SSOT cheat sheet: SVG-ANI-BIBLE.md"
subject: "Moincraft SVG Animator v3"
outcome: "make architecture, operation, editing, audio, export, verification, release, and recovery obvious"
format: "ultra-condensed; complete; practical; precise; emojified; tables/checklists/maps; direct URLs; no filler"
safety: "publish no secrets; label optional capabilities; never invent deployment state"
proof: "facts grounded in the current source, release records, and completed verification"π¬ SVG Animator Bible
One page, one truth: a static-first storyboard studio with deterministic project/render/export behavior and an optional same-origin audio sidecar.
| π§ Identity | Value |
|---|---|
| Project directory | moincraft-images/svg-animator (relative to the workspace root) |
| Canonical schema | version: 3 |
| Stage | 1600 Γ 900 |
| Current release token | 20260803-perfection-v3 |
| SSOT | ssot.moincraft.de |
| Catalog entry | moincraft-images/index.html β relative svg-animator/ (both relative to the workspace root) |
| Public URL/origin | moincraft.de/images/svg-animator/; sidecar origin gate uses AUDIO_SERVICE_PUBLIC_ORIGIN=https://moincraft.de. |
| Static-runtime contract | exactly 6 files |
| Optional service | Python 3.11 aiohttp audio sidecar on loopback 127.0.0.1:8787; deliberately not installed in production |
π§ Active responsibility boundary
| This UI/UX workstream owns | Owned elsewhere β coordinate, do not duplicate |
|---|---|
| information architecture, recovery and project-lifecycle flows, controls, responsive shell, accessibility, focus/keyboard semantics, visual polish, operational UI feedback | rendering/processing internals, montage or audiovisual export, media-processing pipelines, direct timeline/media manipulation |
Keep the boundary explicit in every plan and handoff. UI may expose stable capabilities supplied by the media workstream, but must not independently redesign or reimplement them.
β‘ 60-second operator path
cd moincraft-images/svg-animator
npm ci
npx playwright install chromium
npm run build
npm run check
npm test
npm run test:browserSidecar setup/test (optional):
cd moincraft-images/svg-animator
python3.11 -m venv .venv-audio
.venv-audio/bin/pip install --require-hashes -r audio-service/requirements.txt
.venv-audio/bin/python -m unittest discover -s audio-service/tests
.venv-audio/bin/python audio-service/__main__.pyπ Starting the last command changes runtime state. Production uses the supplied systemd/reverse-proxy templates; do not expose port
8787directly.
πΊοΈ System map
flowchart LR U[π€ Author] --> H[index.html] H --> S[studio.js] H --> C[studio.css] S --> E[engine.js] S --> R[project-history.js] S --> A[asset-cache.js] E --> J[Project JSON v3] E --> X[Silent standalone SVG] A --> I[(IndexedDB images + audio)] S -. optional same-origin .-> P[/api/audio/*] P --> T[Edge TTS] P --> F[Freesound API] S -. fallback .-> B[Browser speechSynthesis]
π¦ Ownership: edit the owner, not a copy
| Concern | Owner | Contract |
|---|---|---|
| Data model, migration, evaluator, SVG export | engine.js | pure/deterministic project behavior |
| UI, commands, transport, provider orchestration | studio.js, index.html | zero-word icon shell + accessible names |
| Undo/redo and snapshots | project-history.js | immutable revisions; UI state is separate |
| Image/audio caching | asset-cache.js | versioned keys, quotas, deterministic eviction |
| Authored styling | studio.source.css | edit this, then generate studio.css |
| Generated styling | studio.css | runtime artifact; never hand-edit |
| Icon sprite | scripts/build-icons.mjs + locked manifest | generated into index.html; no CDN |
| Release token | marked sites + stamp/check scripts | one coherent token across URLs/imports/cache |
| Audio boundary | audio-service/ | optional, loopback, same-origin proxy only |
| Release records | deploy/ | runtime manifest, checksums, notices, licenses |
| Behavioral proof | tests/, tests/browser/ | Node, Python, and Playwright contracts |
π The six-file law
index.html
studio.css
studio.js
engine.js
project-history.js
asset-cache.jsEverything else is build source, tests, deployment support, optional sidecar, or static assets. npm run check:release rejects token drift, unmarked token sites, external scripts/links, and changes to the six-name manifest; it does not scan the working directory. The atomic manifest-driven deploy must exclude stray files such as *.bak.
𧬠Canonical project v3 β pocket model
project:
version: 3
title: string
settings:
fps: 30
snap: { enabled: boolean, mode: "frame | custom", intervalMs: "1..1000" }
defaultSceneDurationMs: "2000..12000"
audio:
preferredTtsProvider: "edge | browser"
fallbackTtsProvider: "edge | browser"
masterGain: "0..1"
defaultVoice: { provider: "edge | browser", locale, edgeVoiceId, browserVoiceURI, rate, pitch, volume }
variables: [{ id, name, type: "text | number | boolean | color", value }]
cast: [{ id, name, avatar, body: { type, accent }, voice: { provider: "inherit | edge | browser", locale, edgeVoiceId, browserVoiceURI, rate, pitch, volume } }]
media: [{ id, kind: "sfx | music", title, creator, license, canonicalUrl, attribution, source: { provider: freesound, externalId } }]
scenes:
- id: string
title: string
durationMs: integer
background: string
dialogue: string
speakerInstanceId: string|null
narration: { enabled, provider: "inherit | edge | browser", locale, edgeVoiceId, browserVoiceURI, rate, pitch, volume, gain }
instances:
- { id, castId, visible, baseTransform: { x, y, scale, flip, z }, baseMovement,
keyframes: [{ id, atMs, x, y, scale, movement }] }
audioClips: [{ id, mediaId, startMs, trimStartMs, durationMs, gain, fadeInMs, fadeOutMs, loop }]| π’ Hard limit | Value | π’ Hard limit | Value |
|---|---|---|---|
| Imported JSON | 1 MiB | Scenes | 1β12 |
| Cast | 32 | Instances / scene | 16 |
| Variables | 64 | Media records | 64 |
| Keyframes / instance | 32 | Audio clips / scene | 32 |
| Scene duration | 2,000β12,000 ms | Dialogue/TTS | 180 chars |
| Position | x 80β1520, y 360β820 | Scale | 0.5β1.75 |
| Voice rate / pitch | 0.5β2 / 0β2 | Gains | 0β1 |
π Normalization invariants
- IDs are unique inside their owning collection; dangling
castId,speakerInstanceId, andmediaIdbecome safe absence with diagnostics, never fabricated records. - Instance order is stable-sorted by finite integer
z, then canonicalized to0β¦nβ1. - Timeline values are finite integer milliseconds. At 30 fps, snap with
round(frameIndex Γ 1000 / 30)β deterministic33/34 msboundaries. - Same-time keyframe collision: last authored wins.
x/y/scaleinterpolate linearly; movement is stepped; omitted fields inherit. - Scene shortening clamps keyframes/clips, shortens crossing clips, removes zero-length clips, resolves collisions, and records one history transaction.
- Imported Freesound URLs are discarded. Numeric
externalIdregenerateshttps://freesound.org/s/<id>/. - Media stores identity + attribution onlyβnever credentials or binary audio.
- v1/v2 import migrates deterministically to v3; the pre-migration payload remains recoverable.
π§ State, undo, and variables
| State | Lives where | Undoable? |
|---|---|---|
| Project content | ProjectHistory current revision | β |
| Selected scene/instance/keyframe/clip | UI state | β |
| Open drawer/sheet, zoom, playback | UI/runtime state | β |
| Cached images/audio | IndexedDB | β |
Mutation path: command β one history transaction β normalize β new immutable revision β render. Do not mutate a history snapshot.
Typed variables: text, finite number, strict boolean, validated #RRGGBB color; names match ^[A-Za-z_][A-Za-z0-9_]{0,31}$. A bound scalar stores { variableId }; unlinking bakes its current literal. Resolve {{name}} in dialogue and supported authored fields at evaluation/export time. Rename by ID. Deletion is blocked while referenced; explicit delete + bake current values is one undoable transaction.
π§― Browser recovery map
| localStorage key | Contains | Clear rule |
|---|---|---|
moincraft.svg-animator.project.v3 | current v3 autosave | replaced only by explicit New storyboard confirmation |
moincraft.svg-animator.legacy-recovery | untouched pre-v3 source | explicit clear legacy recovery action only |
moincraft.svg-animator.import-recovery | last imported/unparseable source | retained as an exceptional recovery source |
moincraft.svg-animator.ui.v3 | non-undoable UI state | safe UI reset |
Ordinary v3 autosave resumes without an interruption modal. When exceptional legacy/import sources exist, the recovery dialog exposes one source-specific download per payload; dismissal preserves both sources. New storyboard resets only the active v3 project and leaves exceptional recovery intact.
π Cast, scenes, and timeline
- Cast owns: identity, explicit DiceBear Adventurer appearance, voice defaults.
- Instance owns: scene placement, visibility, flip, canonical layer, movement, keyframes.
- One cast entry may appear multiple times in one scene. Cast edits update every instance immediately.
- Avatar/body seam is a geometry invariant: head bottom
y=-150, neck topy=-166, deliberate16-unit overlap. Fix generator geometryβnever hide a seam with CSS. - Referenced cast/media deletion is blocked with exact usage count; destructive βremove all referencesβ is distinct and atomic.
- Voice precedence: scene override β speaker cast β project default.
inheritcontributes no provider-specific fields. - Hybrid timeline: proportional all-scene lane + selected-scene instance/movement/narration/SFX/music tracks.
- Supported editing: scrub, playhead, snap, zoom, scene duration, auto-key, x/y/scale keys, stepped movement cues, clip timing/fades/loop.
π Audio: capability, not dependency
| Provider | Credential | Seek | If unavailable |
|---|---|---|---|
| Edge TTS sidecar | none | β decoded audio | auto-fallback to browser speech |
Browser speechSynthesis | none | β reliable mid-utterance seek | visual playback continues; one status announcement |
| Freesound | server-only FREESOUND_API_TOKEN | preview/clip audio | library disabled; narration remains usable |
| ElevenLabs | not shipped | β | no fake adapter/no-op |
Exact sidecar endpoints:
| Method | Relative route | Purpose |
|---|---|---|
GET | ./api/audio/health | process readiness only |
GET | ./api/audio/capabilities | versioned Edge/Freesound availability |
GET | ./api/audio/voices?locale=β¦ | Edge voices |
POST | ./api/audio/speech | synthesize narration |
GET | ./api/audio/freesound/search | capability-gated search |
GET | ./api/audio/freesound/preview/<sound-id> | validated numeric-ID preview proxy |
π‘οΈ Sidecar security envelope
- Bind
127.0.0.1; reverse-proxy only/api/audio/*; no CORS allowance. - Reject a present
Originunless it equalsAUDIO_SERVICE_PUBLIC_ORIGIN. AUDIO_SERVICE_PUBLIC_ORIGINdefaults tohttps://moincraft.de; set the exact deployed origin before enabling the sidecar. A mismatch makes speech POSTs return403while origin-less health/capability GETs can remain green.- Speech POST requires exact JSON content type and
X-SVG-Animator-Request: 1. - Body cap
8 KiB; response cap16 MiB; Edge timeout3 s connect / 30 s total. - Defaults: synthesis concurrency
2; synthesis10/min; search30/min; capability TTL30 s. - Trust forwarded client addresses only from the loopback proxy. Never publish
FREESOUND_API_TOKEN, environment values, preview URLs, or provider credentials. healthβ upstream health. Usecapabilitiesto answer βcan Edge/Freesound work now?β
πΎ Cache keys and budgets
| Asset | Key |
|---|---|
| Background | bg:<backgroundId>:<release-token> |
| Avatar | avatar:v3:<release-token>:<sha256(canonicalAppearanceSpec)> |
| Edge audio | audio:edge:v1:<sha256(canonicalSpeechRequest)> |
| Freesound preview | audio:freesound:v1:<externalId>:<previewIdentity> |
Images retain 48 MiB / 96 entries. Audio uses 256 entries and min(128 MiB, floor(originQuota Γ 0.20)), or 64 MiB if quota is unavailable; retain β₯20% origin headroom and evict deterministic LRU entries.
π€ Deterministic standalone SVG export
Export is intentionally silent. It evaluates from an immutable project snapshot, resolves variables, preloads required visuals once, emits one root <svg>, and reports omitted narration/audio counts.
Export must be
- β self-contained: embedded visual assets; no external references
- β deterministic: identical project + assets β byte-identical output
- β
safe: no
<script>, audio, credential, or provider URL leakage - β standalone-loadable: valid SVG MIME/document in a fresh frame
- β honest: reports omitted narration and media clips
Export must never be
- β a live DOM clone
- β dependent on current UI selection/playhead/open panels
- β an audio/video/package exporter
- β allowed to silently skip a required visual asset
π₯οΈ Responsive + accessibility contract
| Width | Shell |
|---|---|
β₯1280 px | top bar + 56 px activity rail + stage + 352 px inspector + bottom timeline |
768β1279 px | activity rail; inspector right drawer; timeline bottom sheet |
<768 px | stage-first; bottom icon dock; mutually exclusive full-width sheets; no document x-overflow |
- Persistent chrome is icon-only; authored content, values, status, errors, attribution, and help may use words.
- Every control needs an accessible name and immediate hover/focus tooltip; never rely on
titlealone. - Escape cancels transport first, then closes the active dialog or sheet in the same keystroke. Focus returns to the opener.
- Keyboard flows, native/select semantics, focus visibility, reduced-motion geometry, and no horizontal overflow are release blockers.
- Golden viewports:
1600Γ1000,1024Γ768,768Γ1024,390Γ844.
β¨οΈ Keyboard map
| Key | Action |
|---|---|
Space / Escape | play-stop toggle / stop + close active overlay |
Enter / Delete | add-or-update keyframe / delete selected clip β keyframe β instance |
Ctrl/Cmd+Z / Ctrl/Cmd+Shift+Z | undo / redo |
Ctrl/Cmd+S | download project |
Home / End | selected-scene start / end |
Shift+Arrow | seek one frame |
Arrow | nudge instance 8 px, or selected keyframe one snap step |
Alt+Arrow | nudge keyframe exactly 1 ms, bypassing snap |
Activity rail Arrow/Home/End | roving focus |
Shortcuts are suppressed while typing in editable controls.
π Release runbook β stamp once
flowchart LR A[Edit owners] --> B[Unstamped preflight] B -->|all green| C[Stamp exactly once] C --> D[Final build + full matrix] D --> E[Two-build byte comparison] E --> F[Atomic six-file deploy] F --> G[Checksum + public smoke] G --> H[Keep previous release]
A. Unstamped preflight
npm ci
npx playwright install chromium
npm run build
npm run check
npm test
npm run test:browser
.venv-audio/bin/python -m unittest discover -s audio-service/testsB. One-way release action
npm run release:stamp -- <new-release-token>π Run once only after preflight. Token format: starts alphanumeric; then alphanumeric, ., _, -; max 96 characters. Do not manually edit marked release sites.
C. Final proof on stamped artifacts
- Run the complete matrix again.
- Build twice from the same tree; compare all six runtime bytes/checksums.
- Regenerate the hand-maintained ledger from the final stamped build (no npm script does this):
sha256sum index.html studio.css studio.js engine.js project-history.js asset-cache.js > deploy/runtime-checksums.sha256. - Confirm the four background checksums below are unchanged.
- Deploy only the six manifest files atomically to the existing relative route.
- Verify deployed bytes against
deploy/runtime-checksums.sha256. - Smoke the public studio route and
./api/audio/capabilitieswhen sidecar is deployed. - Retain the previous six-file release for rollback.
π§ͺ Current golden verification ledger
| Proof | Observed result |
|---|---|
| Clean dependency install | npm ci passed |
| Deterministic build | two builds; all 6 runtime files byte-identical |
| Release/syntax checks | passed |
| Node tests | 99/99 passed |
| Python sidecar tests | 14/14 passed in the retained Python 3.11 environment; a fresh hash-locked install is blocked by an aiohttp==3.14.3 wheel-hash mismatch |
| Playwright | 12/12 passed |
| Responsive matrix | 4 golden viewports; no horizontal overflow |
| Accessibility interactions | focus restoration, keyboard paths, reduced motion passed |
| Sidecar origin gate | loopback probe: foreign origin 403; configured Studio origin 200 audio/mpeg |
| Production sidecar | deliberately absent; optional capability request returns 404 and visual authoring remains usable |
| Populated export | 7 assets; first run 7 misses/7 writes; second run 7 hits; bytes identical |
| Standalone export proof | 8,653,449 bytes; root SVG; no parser errors, scripts, audio, or external refs |
Runtime SHA-256:
617e1f83bf7ca5ffd3b6a97bddfd8880d9f7504b9f41fe9c8ce10e4fccf74de2 index.html
6ab329fa377004ee0e81467c44e2f3b93f0f8e966292446b06d3206d0c085f4a studio.css
f11120720781697573230810304cf9a12ccb0235af250da521cf8766a66ff199 studio.js
f419008a61ab08bd98d52512709062c616330acc1ef3f26df3883d7e595f5b19 engine.js
ee0a63e2ed6776e5ad75c75938d8a7e2eaeee8973b97f581bb527d1dfeeb9285 project-history.js
21d24ae11c79cde4de12b5edcb2f91319b14bbf6047d1664745d6920adebbf55 asset-cache.jsProtected background SHA-256:
ade8ada726efa687d077c35da256b6bacacab6d5e297eb16a89e82ed66bfe26c bg-star-observatory.png
5c8df338ac500bfaf3868e49ba950d48082a5a2a0adba8fb35f810f74b70c543 bg-ember-inn.png
9d9e1a162dd1e056277993dfab2a7743fd7e516ed39795ce17942ed1653ed846 bg-moss-gate.png
859bb49d307d47c636047b48b2c8d03ddf70c3139e7fb88026132c16e961da92 bg-mist-harbor.pngπ Production cutover β 2026-08-04
| Release fact | Observed state |
|---|---|
| Live route | https://moincraft.de/images/svg-animator/ |
| Active release | /var/www/images/svg-animator-releases/20260803-perfection-v3-20260804032305 |
| Route pointer | /var/www/images/svg-animator is a symlink to the active release |
| Atomic operation | Linux renameat2(RENAME_EXCHANGE) swapped the staged symlink with the prior directory |
| Rollback | /var/www/images/svg-animator-releases/rollback-20260803-production-core-2 retains the prior six files and protected assets |
| Served-byte proof | all six public responses match the Runtime SHA-256 ledger above |
| Browser proof | fresh uncached desktop and mobile sessions passed New storyboard, responsive-sheet Escape/focus return, and horizontal-bound checks |
| Audio edge | no production sidecar or proxy is installed; the Studio degrades to sidecar-unavailable without blocking visual authoring |
π Failure map
| Symptom | Check first | Correct action |
|---|---|---|
| Build reports release mismatch | npm run check:release | use stamp script; never patch query strings/imports by hand |
| CSS change absent | edited studio.css? | edit studio.source.css, then npm run build:css |
| Studio works but Edge does not | ./api/audio/capabilities | inspect sidecar/proxy/provider reason; browser fallback is expected |
Speech POST returns 403 while GETs look green | browser Origin vs AUDIO_SERVICE_PUBLIC_ORIGIN | set the sidecar variable to the exact deployed origin; restart and retest POST |
| Freesound disabled | capability reason | set token server-side only, restart sidecar, recheck capability |
| Sidecar health is green but provider fails | health vs capabilities | health proves process only; use capabilities/provider response |
| Import warns about references | migration diagnostics | repair source IDs; do not fabricate referenced records |
| Cast/media will not delete | usage count | remove references or use the explicit atomic destructive command |
| Export differs | project snapshot/assets/token | eliminate UI/runtime inputs; compare required cached assets and release token |
| Export omits sound | export summary | expected: SVG is silent; use project timing for a future package/video exporter |
| Mobile page scrolls sideways | golden viewport test | keep overflow inside timeline sheet; fix document-level overflow |
| Cache pressure | quota/entry telemetry | allow deterministic LRU eviction; do not bypass headroom limits |
π Rollback
- Atomically restore the previous six runtime files.
- Verify their stored checksums.
- Smoke the relative studio route.
- Leave user storage intact: v3 preserves the legacy recovery payload, so rollback must not erase the v2 source.
- Sidecar can be disabled independently; the static studio remains functional and browser TTS may fallback.
π Dependencies, law, and direct references
| Component | Pinned/current | Reference |
|---|---|---|
| Playwright | 1.62.1 | playwright.dev |
| Tailwind CLI/CSS | 4.3.3 | tailwindcss.com |
| daisyUI | 5.7.14 | daisyui.com |
| Lucide static | 1.28.0 | lucide.dev |
| aiohttp | 3.14.3 | aiohttp docs |
| edge-tts | 7.2.8 | upstream |
| Freesound API | external capability | API docs |
| DiceBear Adventurer | pinned explicit options | style docs |
| Web Speech API | browser fallback | MDN |
| SVG | export platform | MDN |
- Exact notices:
deploy/THIRD_PARTY_NOTICES.txt. - Edge license text:
deploy/licenses/edge-tts-LICENSE.txt. - Every Freesound selection retains creator, license, canonical source URL, and formatted attribution; dependency notices do not replace per-sound attribution.
- Redistributors must preserve applicable notices and comply with each pinned dependencyβs license.
β Definition of done
- Edited the owning source, not generated/runtime copies.
- Schema v3 normalizes safely and migrations preserve recovery.
- One user action equals one history transaction.
- No credential, binary audio, arbitrary imported URL, or secret entered project/browser data.
- Static studio remains useful with the sidecar absent.
- SVG export is deterministic, self-contained, valid, silent, and honest about omissions.
- Four golden viewports, keyboard, focus, tooltips, reduced motion, and overflow pass.
- Node + Python + browser suites pass.
- Release token stamped once; six-file release contract passes.
- Two final builds are byte-identical; runtime/background checksums match.
- Static deploy is atomic; public route/capability smoke passes; prior release remains rollback-ready.
North star: deterministic core, optional capability edges, no secret leakage, no hidden mutation, no dishonest export.