comicforge 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,644 @@
1
+ # ComicForge Authoring Guide
2
+
3
+ This guide covers everything you need to author comics with ComicForge — for both
4
+ humans and LLMs. The [SKILL.md](SKILL.md) file is the quick-reference contract;
5
+ this document goes deeper on structure, paths, and how all the pieces fit together.
6
+
7
+ ---
8
+
9
+ ## Engine + self-contained projects
10
+
11
+ ComicForge is a content-free engine plus self-contained projects. There is **no
12
+ shared asset library** — every project owns all of its own art:
13
+
14
+ ```
15
+ comicforge/ The engine — pure code, ships no art of any kind.
16
+ examples/<name>/ A self-contained comic project: its own characters,
17
+ scenes, pixel art, and page specs. Nothing is shared.
18
+ ```
19
+
20
+ A real downstream project just installs the engine and creates the same four
21
+ asset folders; none of the example art ships with the engine. The `examples/`
22
+ directory is where we develop against the engine the same way a downstream project
23
+ would.
24
+
25
+ ### Starting a new project — `cmf init`
26
+
27
+ A project is **data only** — YAML specs + SVG / pixel art — so it needs no Python.
28
+ Install the engine globally and scaffold a project:
29
+
30
+ ```
31
+ uv tool install comicforge # puts `cmf` on your PATH; no venv per project
32
+ cmf init my-comic # scaffold the four asset dirs + a starter page
33
+ cd my-comic && cmf render pages/hello.yaml
34
+ ```
35
+
36
+ `cmf init <dir>` creates `characters/`, `scenes/`, `pixel/`, `pages/`, a renderable
37
+ `pages/hello.yaml`, a seed `pixel/heart.yaml`, a `.gitignore`, a project `README`,
38
+ and a copy of this skill under `.claude/skills/comicforge/`. It is idempotent —
39
+ existing files are left alone unless you pass `--force`. After upgrading the engine,
40
+ re-run `cmf init --force <dir>` to refresh the bundled skill.
41
+
42
+ Only reach for a `pyproject.toml` (with `comicforge` as a dependency, run via
43
+ `uv run cmf`) when you need a pinned engine version or your own render scripts.
44
+
45
+ ### `comicforge/` — the engine
46
+
47
+ The `comicforge/` package is the rendering engine. It loads assets, composes SVG,
48
+ and writes output. It contains **no concrete content** — no hardcoded characters,
49
+ no default scenes, no built-in sprites. All asset directories must be provided
50
+ explicitly (via the spec or CLI flags).
51
+
52
+ Modules:
53
+ - `library.py` — loads characters, stacks overlays, places them in panels
54
+ - `scene.py` — loads scene backgrounds, scales them to cover a panel/canvas
55
+ - `pixelart.py` — resolves pixel-art sprites from a directory or inline spec
56
+ - `render.py` — composes full pages, individual panels, and standalone scenes
57
+ - `bubbles.py` — speech, thought, and shout bubbles with word-wrap and tails
58
+ - `cli.py` — the `comicforge` command and its subcommands
59
+
60
+ ### A project — `examples/<name>/`
61
+
62
+ Each project is one directory owning everything it needs. Page specs live under
63
+ `pages/` and point at the sibling asset folders one level up:
64
+
65
+ ```
66
+ examples/pes/
67
+ characters/ Character art: an identity that owns one or more poses
68
+ tom/ flat (single pose): everything in one viewBox
69
+ base.svg
70
+ face-neutral.svg
71
+ face-happy.svg
72
+ arms-down.svg
73
+ ...
74
+ character.yaml
75
+ bara/ posed: shared faces at root + a body per pose
76
+ character.yaml
77
+ face-neutral.svg shared, re-anchored onto each pose
78
+ face-happy.svg
79
+ poses/
80
+ sit/ { pose.yaml, base.svg }
81
+ walk/ { pose.yaml, base.svg }
82
+ scenes/ Scene backgrounds: base SVG + overlay SVGs + scene.yaml
83
+ dvur/
84
+ base.svg
85
+ weather-clear.svg
86
+ weather-rain.svg
87
+ scene.yaml
88
+ pokoj/
89
+ base.svg
90
+ scene.yaml
91
+ pixel/ Pixel-art sprites: one YAML file per sprite
92
+ heart.yaml
93
+ sun.yaml
94
+ star.yaml
95
+ bone.yaml
96
+ pages/ The comic specs
97
+ slepice.yaml 2×2 comic page — Tom and Bára watch chickens
98
+ kosticka.yaml Strip with scenes, pixel art, and multiple bubble types
99
+ dvur-scene.yaml Standalone illustration — single scene filling the canvas
100
+ ```
101
+
102
+ Each spec references its project's assets via relative paths (`../characters`,
103
+ `../scenes`, `../pixel`).
104
+
105
+ ---
106
+
107
+ ## Asset loading and path resolution
108
+
109
+ The engine needs three asset directories. Each can be provided in the spec file or
110
+ via a CLI flag:
111
+
112
+ | Purpose | Spec key | CLI flag |
113
+ |---|---|---|
114
+ | Character library | `library:` | `--library` |
115
+ | Scene backgrounds | `scenes_dir:` | `--scenes` |
116
+ | Pixel-art sprites | `pixel_dir:` | `--pixel-dir` |
117
+
118
+ ### Path resolution rule
119
+
120
+ **Relative paths in the spec are resolved against the spec file's directory.**
121
+
122
+ For example, `examples/pes/pages/slepice.yaml` contains:
123
+ ```yaml
124
+ library: "../characters"
125
+ pixel_dir: "../pixel"
126
+ ```
127
+
128
+ When you run from the repo root:
129
+ ```bash
130
+ cmf render examples/pes/pages/slepice.yaml -o out.png
131
+ ```
132
+
133
+ The engine resolves `../characters` relative to `examples/pes/pages/` — arriving
134
+ at `examples/pes/characters`. This means you can always run from the repo root
135
+ regardless of where specs live.
136
+
137
+ **CLI flags are used as-is** (relative to your current working directory, or
138
+ absolute). A CLI flag overrides the spec key entirely.
139
+
140
+ ### When a directory is missing
141
+
142
+ If `library:` is absent from the spec and `--library` was not passed, the engine
143
+ raises:
144
+ ```
145
+ ValueError: library directory is required but was not provided.
146
+ Set 'library:' in the spec or pass the corresponding CLI flag.
147
+ ```
148
+
149
+ If `scenes_dir:` is absent but the spec uses a `scene:` key in a panel, the engine
150
+ raises at render time when the scene is first accessed.
151
+
152
+ `pixel_dir:` is optional — you only need it when using `{art: <name>}` references.
153
+ Inline `{grid: [...], palette: {...}}` specs work with no pixel library at all.
154
+
155
+ ---
156
+
157
+ ## Authoring a character
158
+
159
+ A character is an **identity** (name, label, …) that owns one or more **poses**.
160
+ A pose is a body drawn in its own viewBox; expressions (`face`, …) can be *shared*
161
+ across poses, authored once. There are two on-disk shapes.
162
+
163
+ ### Single-pose (flat)
164
+
165
+ The classic layout — everything in one viewBox:
166
+
167
+ ```
168
+ <project>/characters/<name>/
169
+ base.svg The body drawn in a fixed local viewBox
170
+ <slot>-<variant>.svg Overlays in the same viewBox (one per variant)
171
+ character.yaml Manifest: name, label, viewbox, slots, default
172
+ ```
173
+
174
+ ```yaml
175
+ # character.yaml
176
+ name: tom # internal ID (must match directory name)
177
+ label: Tom # display name for the UI / manifest
178
+ viewbox: [200, 320] # local canvas size [width, height]
179
+ default:
180
+ face: neutral # default variant for each slot (used when omitted in spec)
181
+ arms: down
182
+ slots:
183
+ arms: [down, wave, point, crossed, hips, thumbsup]
184
+ face: [neutral, happy, surprised, sad, angry, laugh, wink]
185
+ ```
186
+
187
+ All SVG files (base + overlays) share **the same viewBox**. The engine strips each
188
+ to its inner markup and stacks base → overlays in the slot order from
189
+ `character.yaml`.
190
+
191
+ ### Multiple poses
192
+
193
+ When the **body** changes between poses (legs differ sit vs walk), each pose gets
194
+ its own `base.svg`. Shared overlays (faces) live at the character root and are
195
+ re-registered onto each pose via an **anchor** — a reference point (typically the
196
+ head centre) they are drawn around.
197
+
198
+ ```
199
+ <project>/characters/<name>/
200
+ character.yaml Identity + SHARED slots + default + pose list
201
+ <slot>-<variant>.svg SHARED overlays, drawn around the canonical anchor
202
+ poses/
203
+ <pose>/
204
+ pose.yaml viewbox, anchor, optional pose-specific slots/default
205
+ base.svg this pose's body
206
+ <slot>-<variant>.svg optional pose-specific overlays
207
+ ```
208
+
209
+ ```yaml
210
+ # character.yaml
211
+ name: bara
212
+ label: Bára
213
+ anchor: [120, 72] # canonical point the shared overlays are drawn around
214
+ slots: {face: [neutral, happy]} # SHARED across poses, re-anchored per pose
215
+ default: {pose: sit, face: neutral}
216
+ poses: [sit, walk]
217
+ ```
218
+
219
+ ```yaml
220
+ # poses/walk/pose.yaml
221
+ viewbox: [240, 180]
222
+ anchor: [132, 64] # where THIS pose's head-centre lands
223
+ slots: {arms: [trot]} # OPTIONAL — pose-specific slots + overlays
224
+ default: {arms: trot}
225
+ ```
226
+
227
+ For each shared overlay the engine emits a `translate(pose.anchor − character.anchor)`
228
+ so one `face-happy.svg` re-registers onto every pose. **Translate-only**: keep the
229
+ head the same *size* across poses; only its position changes. A flat character is
230
+ just the posed model with one implicit pose at anchor `[0, 0]`.
231
+
232
+ ### Composition and placement
233
+
234
+ `Character.compose_inner(selection, pose)` assembles, in draw order:
235
+ 1. The chosen pose's `base.svg`
236
+ 2. That pose's pose-specific overlays (in `pose.yaml` slot order)
237
+ 3. The shared overlays (in `character.yaml` slot order), each wrapped in the
238
+ anchor-translate
239
+
240
+ `Character.place(selection, cx, cy, height, flip, pose)` wraps the result in a `<g
241
+ transform>` that scales it to `height` px tall (using the pose's viewBox) and
242
+ centres it at `(cx, cy)`. Omit `pose` to use `default.pose`.
243
+
244
+ ### Adding a pose or expression
245
+
246
+ - **New expression (shared):** draw it around the canonical anchor, save as
247
+ `<name>/<slot>-<variant>.svg`, add the variant to `slots` in `character.yaml`.
248
+ - **New pose:** add `poses/<pose>/` with its own `base.svg` + `pose.yaml`
249
+ (set its `anchor`), and list `<pose>` under `poses:`.
250
+ - **Flat character, new overlay:** save `<name>/<slot>-<variant>.svg` in the shared
251
+ viewBox and add it to `slots`.
252
+
253
+ No code changes needed.
254
+
255
+ ---
256
+
257
+ ## Authoring a scene
258
+
259
+ A scene is a full-panel illustrated background. It follows the same model as a
260
+ character: `base.svg` + optional overlay files + `scene.yaml`.
261
+
262
+ ### Directory layout
263
+
264
+ ```
265
+ <project>/scenes/<name>/
266
+ base.svg The background drawn in a fixed local viewBox
267
+ <slot>-<variant>.svg Optional: weather effects, props, etc.
268
+ scene.yaml Manifest
269
+ ```
270
+
271
+ ### scene.yaml
272
+
273
+ ```yaml
274
+ name: dvur
275
+ label: Dvůr
276
+ viewbox: [320, 200]
277
+ default:
278
+ weather: clear
279
+ slots:
280
+ weather: [clear, rain]
281
+ ```
282
+
283
+ A scene with no slots has only `name`, `label`, and `viewbox`.
284
+
285
+ ### Cover-scaling behavior
286
+
287
+ Scenes are rendered with `cover` scaling: the scene is scaled so its **smaller
288
+ dimension fits** and the other dimension may overflow, always filling the full panel
289
+ with no empty borders. The scene is centred within the panel.
290
+
291
+ ### Using a scene in a spec
292
+
293
+ ```yaml
294
+ # In a panel:
295
+ scene: dvur # default slots
296
+ scene: {name: dvur, weather: rain} # pick a slot variant
297
+
298
+ # In a standalone scene spec:
299
+ type: scene
300
+ scene:
301
+ name: dvur
302
+ weather: clear
303
+ scale: 3 # px per scene unit (canvas = viewbox × scale)
304
+ ```
305
+
306
+ Confirm available scenes and slots with:
307
+ ```bash
308
+ cmf scenes --scenes examples/pes/scenes
309
+ ```
310
+
311
+ ---
312
+
313
+ ## Authoring pixel art
314
+
315
+ Pixel art is defined as a grid of characters mapped to colors. It can come from
316
+ the library or be defined inline in the spec.
317
+
318
+ ### Project sprites (`<project>/pixel/<name>.yaml`)
319
+
320
+ Each file contains `grid` (list of equal-length strings) and `palette` (map of
321
+ character to hex color):
322
+
323
+ ```yaml
324
+ # examples/pes/pixel/heart.yaml
325
+ palette:
326
+ R: "#e8556d"
327
+ r: "#b83e54"
328
+ grid:
329
+ - ".RR..RR."
330
+ - "RRRRRRRR"
331
+ - "RRRRRRRR"
332
+ - "RRRRRRRR"
333
+ - ".RRRRRR."
334
+ - "..RRRR.."
335
+ - "...RR..."
336
+ ```
337
+
338
+ The transparent-cell convention: `.` (period) or ` ` (space) = transparent.
339
+ Any other character maps to a color in `palette`.
340
+
341
+ ### Inline sprites
342
+
343
+ Define the grid and palette directly in the panel spec:
344
+
345
+ ```yaml
346
+ pixel:
347
+ - grid: ["....", ".RR.", ".RR.", "...."]
348
+ palette: {R: "#e8556d"}
349
+ x: 0.5
350
+ y: 0.5
351
+ scale: 0.3
352
+ ```
353
+
354
+ Inline sprites require no library. The `pixel_dir:` spec key is only needed when
355
+ using `{art: <name>}` references.
356
+
357
+ ### Using pixel art in a spec
358
+
359
+ ```yaml
360
+ # From the project's pixel/ dir (needs pixel_dir: in the spec):
361
+ pixel_dir: "../pixel"
362
+ # ...
363
+ pixel:
364
+ - art: heart
365
+ x: 0.8
366
+ y: 0.25
367
+ scale: 0.18
368
+
369
+ # Inline (no pixel_dir needed):
370
+ pixel:
371
+ - grid: ["XX", "XX"]
372
+ palette: {X: "#ff0000"}
373
+ x: 0.5
374
+ y: 0.5
375
+ scale: 0.2
376
+ ```
377
+
378
+ Coordinates: `x`/`y` are panel fractions (0–1); `scale` = sprite height as a
379
+ fraction of panel height. Multiple pixel items can appear in one panel.
380
+
381
+ ---
382
+
383
+ ## CLI reference
384
+
385
+ Run all commands from the repo root with `cmf <cmd>`.
386
+
387
+ **Default output.** `render`, `scene`, `panel`, and `character` accept `-o`, but
388
+ when you omit it they write a timestamped file into the gitignored `output/` dir
389
+ (e.g. `output/slepice-20260614-191613.png`). Successive renders accumulate so you
390
+ can compare how a page or character evolved; pass `-o` to pin an exact path.
391
+
392
+ ### `init` — scaffold a new project
393
+
394
+ ```bash
395
+ cmf init <dir> [--force]
396
+ ```
397
+
398
+ Creates a data-only project at `<dir>`: the `characters/`, `scenes/`, `pixel/`,
399
+ `pages/` asset dirs, a renderable `pages/hello.yaml`, a seed `pixel/heart.yaml`, a
400
+ `.gitignore`, a `README`, and a copy of this skill under `.claude/skills/comicforge/`.
401
+ Idempotent — existing files are kept unless `--force` is given.
402
+
403
+ ### `render` — render a comic page
404
+
405
+ ```bash
406
+ cmf render <spec.yaml> [-o <output>] # default: output/<spec>-<timestamp>.png
407
+ [--library <dir>] # override spec's library: key
408
+ [--scenes <dir>] # override spec's scenes_dir: key
409
+ [--pixel-dir <dir>] # override spec's pixel_dir: key
410
+ ```
411
+
412
+ Output format is determined by the file extension: `.svg`, `.png`, or `.pdf`.
413
+
414
+ ```bash
415
+ cmf render examples/pes/pages/slepice.yaml -o slepice.png
416
+ cmf render examples/pes/pages/slepice.yaml -o slepice.pdf
417
+ ```
418
+
419
+ ### `validate` — check a spec without rendering
420
+
421
+ ```bash
422
+ cmf validate <spec.yaml>
423
+ [--library <dir>] # override spec's library: key
424
+ [--scenes <dir>] # override spec's scenes_dir: key
425
+ [--pixel-dir <dir>] # override spec's pixel_dir: key
426
+ ```
427
+
428
+ Statically checks a page or `scene` spec against the libraries it points at and
429
+ reports **every** problem at once — no output is drawn. It catches things
430
+ `render` doesn't: `render` stops at the first unrenderable item and *silently
431
+ ignores* keys it doesn't recognise, so a typo like `fcae: happy` or `post: walk`
432
+ renders a default-faced, default-posed actor with no error. `validate` flags:
433
+
434
+ - characters / scenes / pixel sprites missing from the library
435
+ - poses an actor asks for that the character lacks
436
+ - slot variants that don't exist for the chosen character+pose / scene
437
+ - actor / scene keys that aren't reserved and aren't a real slot (likely typos)
438
+ - bubble `speaker` naming no actor in the panel; unknown bubble `kind`
439
+ - structural holes (a page with no `rows`, a row with no `panels`, a bubble with
440
+ no `text`); an unknown `type:`, or a `type:` that contradicts the structure
441
+ (a `scene` spec carrying `rows`)
442
+
443
+ Exits `0` when the spec is sound, `1` (with a bulleted problem list) otherwise —
444
+ so it works in a pre-render check or CI.
445
+
446
+ ```bash
447
+ cmf validate examples/pes/pages/slepice.yaml # -> "...: ok"
448
+ ```
449
+
450
+ ### `scene` — render a standalone illustration
451
+
452
+ ```bash
453
+ cmf scene <spec.yaml> [-o <output>] # default: output/<spec>-<timestamp>.png
454
+ [--library <dir>]
455
+ [--scenes <dir>]
456
+ [--pixel-dir <dir>]
457
+ ```
458
+
459
+ ```bash
460
+ cmf scene examples/pes/pages/dvur-scene.yaml -o dvur.png
461
+ ```
462
+
463
+ A scene spec should declare `type: scene` (page specs default to `type: page`).
464
+ `render` and `scene` each reject the wrong spec type with a clear message
465
+ pointing at the other command, rather than crashing.
466
+
467
+ ### `panel` — render individual panels for review
468
+
469
+ ```bash
470
+ cmf panel <spec.yaml> [-o <output>] # default: output/<spec>-r<R>c<C>-<ts>.png
471
+ [--row 0] [--col 0] # which panel (0-indexed)
472
+ [--all] # render every panel into a directory
473
+ # (default: output/<spec>-panels-<ts>/)
474
+ [--scale 0.5] # size vs full-page (default 0.5 = low res)
475
+ [--library <dir>]
476
+ [--scenes <dir>]
477
+ [--pixel-dir <dir>]
478
+ ```
479
+
480
+ ```bash
481
+ cmf panel examples/pes/pages/slepice.yaml -o panel.png --row 0 --col 1
482
+ cmf panel examples/pes/pages/kosticka.yaml -o panels/ --all
483
+ ```
484
+
485
+ ### `character` — render one character on its own
486
+
487
+ Quick visual test of a single character — no page, no panel, just the composed
488
+ character cropped to its pose on a plain canvas.
489
+
490
+ ```bash
491
+ cmf character <name> [selection ...] --library <dir>
492
+ [-o <output>] # default: output/<name>-<timestamp>.png
493
+ [--pose <pose>] # which pose (default: the character's default)
494
+ [--scale 2.0] # px per viewBox unit
495
+ [--bg "#ffffff"] # canvas background colour (white by default)
496
+ [--flip] # mirror horizontally
497
+ [--thumb-px 320] # body width of the small companion PNG (0 to skip)
498
+ ```
499
+
500
+ Each `selection` token is either a bare name — matched first against the
501
+ character's **poses**, then against the variants of any **slot** — or an explicit
502
+ `key=value` (`pose=walk`, `face=happy`). Unknown tokens error with the available
503
+ poses and slots listed.
504
+
505
+ Writes **two files**: the full render at `-o`, and a smaller `<stem>.small.png`
506
+ beside it (body ≈ `--thumb-px` wide) — read the small one to spend fewer tokens.
507
+
508
+ ```bash
509
+ cmf character bara sit happy --library examples/pes/characters -o bara.png
510
+ cmf character bara pose=walk face=neutral --library examples/pes/characters
511
+ cmf character tom --library examples/pes/characters # default pose -> tom.png
512
+ ```
513
+
514
+ ### `characters` — list a project's characters
515
+
516
+ ```bash
517
+ cmf characters --library <dir>
518
+ ```
519
+
520
+ The `--library` flag is **required** (no default). Prints a JSON manifest of every
521
+ character, its slots, available variants, and defaults.
522
+
523
+ ```bash
524
+ cmf characters --library examples/pes/characters
525
+ ```
526
+
527
+ ### `scenes` — list a project's scenes
528
+
529
+ ```bash
530
+ cmf scenes --scenes <dir>
531
+ ```
532
+
533
+ The `--scenes` flag is **required** (no default). Prints a JSON manifest of every
534
+ scene and its slots.
535
+
536
+ ```bash
537
+ cmf scenes --scenes examples/pes/scenes
538
+ ```
539
+
540
+ ### `inspire` — generate reference images from a theme + descriptions
541
+
542
+ Paints **inspiration** art (a reference to author SVG from), *not* shipped assets.
543
+ Reads a project theme (`theme.yaml`) and a list of descriptions (`references.yaml`),
544
+ composes one prompt per item, and calls the Replicate API.
545
+
546
+ ```bash
547
+ cmf inspire <references.yaml> -o <out_dir>
548
+ [--theme <theme.yaml>] # default: theme.yaml beside the spec
549
+ [--only id1,id2] # generate only these ids
550
+ [--force] # regenerate even if a .png already exists
551
+ [--dry-run] # compose prompts only — no API call, no token
552
+ [--review] # also write a review.html grid of images + prompts
553
+ ```
554
+
555
+ Each item writes `<out_dir>/<id>.png` + `<id>.prompt.txt`. `theme.yaml` and the
556
+ output dir default to siblings of the references spec.
557
+
558
+ **Theme** (`<project>/theme.yaml`) — applied to every image:
559
+
560
+ | key | meaning |
561
+ |---|---|
562
+ | `style` | the look, in words (prepended to every prompt) |
563
+ | `palette` | list of hex colors fed to the model as a color scale |
564
+ | `mood` | one-line mood/tone |
565
+ | `negative` | what to avoid; default discourages text/marks |
566
+ | `aspect_ratio` | e.g. `"1:1"` (default) |
567
+ | `model` | Replicate model id (default `google/imagen-3`) |
568
+
569
+ **References** (`<project>/references.yaml`) — a `items:` list (or a bare list).
570
+ Each entry needs an id (`id`/`name`) and a description (`prompt`/`description`/`desc`).
571
+
572
+ Live generation requires the optional extra and a token:
573
+
574
+ ```bash
575
+ pip install "comicforge[inspire]" # adds replicate + python-dotenv
576
+ export REPLICATE_API_TOKEN=... # or put it in a .env beside the spec
577
+ cmf inspire examples/pes/references.yaml --review
578
+ cmf inspire examples/pes/references.yaml --dry-run # no token needed
579
+ ```
580
+
581
+ The composed prompt is `style` → `Subject: <prompt>` → palette → mood → negative,
582
+ joined by blank lines (preview it with `--dry-run` and the `.prompt.txt` sidecar).
583
+ Do **not** auto-vectorize the result — hand/LLM-author the SVG using it as a guide.
584
+
585
+ ---
586
+
587
+ ## Spec reference
588
+
589
+ For the full spec grammar, see [SKILL.md](SKILL.md). Key points:
590
+
591
+ - **Page-level keys**: `title`, `page` (A4/A5/letter/[w,h]), `px_per_mm`,
592
+ `margin_mm`, `gutter_mm`, `library`, `scenes_dir`, `pixel_dir`
593
+ - **Rows and panels**: `rows[].height` (relative weight), `rows[].panels[].width`
594
+ (relative weight), panel keys: `bg`, `scene`, `actors`, `pixel`, `bubbles`
595
+ - **Actor keys**: `char`, `pose` (optional; defaults to the character's default
596
+ pose), per-slot variant keys (`face`, `arms`, etc.), `x`, `y`, `scale`, `flip`
597
+ - **Bubble keys**: `text`, `kind` (speech/thought/shout), `speaker`, optional
598
+ `x`/`y`/`to`/`max_chars`/`fs`
599
+ - **Pixel keys**: `art` (library name) or `grid`+`palette`, plus `x`/`y`/`scale`
600
+
601
+ ---
602
+
603
+ ## Python API
604
+
605
+ ```python
606
+ from comicforge import render_spec, render_scene, load_spec, PixelLibrary
607
+
608
+ # Render a full comic page
609
+ render_spec("examples/pes/pages/slepice.yaml", "slepice.png")
610
+
611
+ # Render a standalone scene
612
+ render_scene("examples/pes/pages/dvur-scene.yaml", "dvur.png")
613
+
614
+ # Load a spec dict for inspection
615
+ spec = load_spec("examples/pes/pages/slepice.yaml")
616
+
617
+ # Pass asset dirs explicitly (overrides spec keys)
618
+ from comicforge.library import Library
619
+ from comicforge.scene import SceneLibrary
620
+ lib = Library("examples/pes/characters")
621
+ scn = SceneLibrary("examples/pes/scenes")
622
+ px = PixelLibrary("examples/pes/pixel")
623
+ render_spec("examples/pes/pages/kosticka.yaml", "out.png",
624
+ library=lib, scenes=scn, pixel_library=px)
625
+ ```
626
+
627
+ ---
628
+
629
+ ## Demo pages at a glance
630
+
631
+ All three pages live in the `examples/pes/` project.
632
+
633
+ | Page | Spec | Description |
634
+ |---|---|---|
635
+ | `slepice` | `examples/pes/pages/slepice.yaml` | 2×2 page — Tom tells Bára to watch the chickens. No scenes. Pixel: sun, bone, heart. |
636
+ | `kosticka` | `examples/pes/pages/kosticka.yaml` | Comic strip — scenes (`pokoj`, `dvur`), pixel art, speech/thought/shout bubbles. |
637
+ | `dvur-scene` | `examples/pes/pages/dvur-scene.yaml` | Standalone illustration — farmyard, Tom and Bára. Render with `comicforge scene`. |
638
+
639
+ Render all demos from the repo root:
640
+ ```bash
641
+ cmf render examples/pes/pages/slepice.yaml -o slepice.png
642
+ cmf render examples/pes/pages/kosticka.yaml -o kosticka.png
643
+ cmf scene examples/pes/pages/dvur-scene.yaml -o dvur.png
644
+ ```