@a-company/atelier 0.36.0 → 0.38.0

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.
Files changed (99) hide show
  1. package/dist/{chunk-JPZ4F4PW.js → chunk-3ARBOSWY.js} +64 -5
  2. package/dist/chunk-3ARBOSWY.js.map +1 -0
  3. package/dist/cli.js +11469 -413
  4. package/dist/cli.js.map +1 -1
  5. package/dist/{dist-M67UZGFQ.js → dist-3YQK6PI6.js} +2 -2
  6. package/dist/index.cjs +3193 -227
  7. package/dist/index.cjs.map +1 -1
  8. package/dist/index.d.cts +701 -8
  9. package/dist/index.d.ts +701 -8
  10. package/dist/index.js +7237 -72
  11. package/dist/index.js.map +1 -1
  12. package/dist/mcp.js +2898 -507
  13. package/dist/mcp.js.map +1 -1
  14. package/package.json +14 -9
  15. package/src/web/inline-app.ts +55 -4
  16. package/src/web/timeline-state-types.ts +28 -0
  17. package/src/web/timeline-view.test.ts +99 -0
  18. package/src/web/timeline-view.ts +339 -0
  19. package/src/web/workspace-app.ts +3146 -0
  20. package/templates/workspace/.claude/agents/atelier-iris.md +75 -0
  21. package/templates/workspace/.claude/agents/atelier-lux.md +67 -0
  22. package/templates/workspace/.claude/agents/atelier-quill.md +61 -0
  23. package/templates/workspace/.gitignore +30 -0
  24. package/templates/workspace/.paradigm/personas/_shared/cascade-merge.md +172 -0
  25. package/templates/workspace/CLAUDE.md +93 -0
  26. package/templates/workspace/README.md +75 -0
  27. package/templates/workspace/SETUP.md +127 -0
  28. package/templates/workspace/_brand/.atelier-brand.yaml +34 -0
  29. package/templates/workspace/_brand/DESIGN.md +56 -0
  30. package/templates/workspace/_brand/SCRIPT.md +41 -0
  31. package/templates/workspace/_brand/STORYBOARD.md +33 -0
  32. package/templates/workspace/_packs/README.md +54 -0
  33. package/templates/workspace/projects/README.md +49 -0
  34. package/templates/workspace/workspace.atelier +22 -0
  35. package/university/content/notes/N-atel-001-first-render.md +114 -0
  36. package/university/content/notes/N-atel-001-install-and-launch.md +84 -0
  37. package/university/content/notes/N-atel-001-what-is-atelier.md +51 -0
  38. package/university/content/notes/N-atel-101-easings.md +97 -0
  39. package/university/content/notes/N-atel-101-layers.md +106 -0
  40. package/university/content/notes/N-atel-101-states-and-deltas.md +94 -0
  41. package/university/content/notes/N-atel-101-the-atelier-format.md +72 -0
  42. package/university/content/notes/N-atel-201-authoring-tools.md +141 -0
  43. package/university/content/notes/N-atel-201-mcp-overview.md +86 -0
  44. package/university/content/notes/N-atel-201-patterns.md +108 -0
  45. package/university/content/notes/N-atel-201-visual-and-effects.md +125 -0
  46. package/university/content/notes/N-atel-301-composition-and-overlays.md +141 -0
  47. package/university/content/notes/N-atel-301-effects.md +136 -0
  48. package/university/content/notes/N-atel-301-images-and-video.md +126 -0
  49. package/university/content/notes/N-atel-301-shapes-and-text.md +118 -0
  50. package/university/content/notes/N-atel-401-hierarchical-states.md +71 -0
  51. package/university/content/notes/N-atel-401-motion-deep-dive.md +106 -0
  52. package/university/content/notes/N-atel-401-presets-and-templates.md +98 -0
  53. package/university/content/notes/N-atel-401-transitions.md +94 -0
  54. package/university/content/notes/N-atel-501-detected-vs-user-edited.md +76 -0
  55. package/university/content/notes/N-atel-501-layer-tag-isolation.md +62 -0
  56. package/university/content/notes/N-atel-501-silence-trim.md +98 -0
  57. package/university/content/notes/N-atel-501-transcribe-and-captions.md +98 -0
  58. package/university/content/notes/N-atel-601-carousel.md +71 -0
  59. package/university/content/notes/N-atel-601-overlay-rules.md +96 -0
  60. package/university/content/notes/N-atel-601-recipe-tools-and-apply.md +84 -0
  61. package/university/content/notes/N-atel-601-studio-recipe.md +103 -0
  62. package/university/content/notes/N-atel-701-choosing-output.md +68 -0
  63. package/university/content/notes/N-atel-701-png-and-frames.md +84 -0
  64. package/university/content/notes/N-atel-701-vector.md +85 -0
  65. package/university/content/notes/N-atel-701-video.md +88 -0
  66. package/university/content/notes/N-atel-801-editing-surface.md +69 -0
  67. package/university/content/notes/N-atel-801-live-bridge.md +84 -0
  68. package/university/content/notes/N-atel-801-studio-app.md +72 -0
  69. package/university/content/notes/N-atel-801-symbiotic-loop.md +56 -0
  70. package/university/content/paths/LP-atel-001.yaml +21 -0
  71. package/university/content/paths/LP-atel-101.yaml +22 -0
  72. package/university/content/paths/LP-atel-201.yaml +23 -0
  73. package/university/content/paths/LP-atel-301.yaml +22 -0
  74. package/university/content/paths/LP-atel-401.yaml +22 -0
  75. package/university/content/paths/LP-atel-501.yaml +22 -0
  76. package/university/content/paths/LP-atel-601.yaml +22 -0
  77. package/university/content/paths/LP-atel-701.yaml +22 -0
  78. package/university/content/paths/LP-atel-801.yaml +22 -0
  79. package/university/content/quizzes/Q-atel-001-orientation.yaml +66 -0
  80. package/university/content/quizzes/Q-atel-101-document-model.yaml +66 -0
  81. package/university/content/quizzes/Q-atel-201-mcp-authoring.yaml +66 -0
  82. package/university/content/quizzes/Q-atel-301-visual-system.yaml +66 -0
  83. package/university/content/quizzes/Q-atel-401-state-machines.yaml +66 -0
  84. package/university/content/quizzes/Q-atel-501-video-pipeline.yaml +66 -0
  85. package/university/content/quizzes/Q-atel-601-recipes.yaml +66 -0
  86. package/university/content/quizzes/Q-atel-701-export.yaml +66 -0
  87. package/university/content/quizzes/Q-atel-801-studio-loop.yaml +66 -0
  88. package/university/index.yaml +720 -0
  89. package/university/pack.yaml +21 -0
  90. package/dist/chunk-5QQESXI6.js +0 -4432
  91. package/dist/chunk-5QQESXI6.js.map +0 -1
  92. package/dist/chunk-JPZ4F4PW.js.map +0 -1
  93. package/dist/cli.cjs +0 -6313
  94. package/dist/cli.cjs.map +0 -1
  95. package/dist/cli.d.cts +0 -1
  96. package/dist/cli.d.ts +0 -1
  97. package/dist/mcp.cjs +0 -5462
  98. package/dist/mcp.cjs.map +0 -1
  99. /package/dist/{dist-M67UZGFQ.js.map → dist-3YQK6PI6.js.map} +0 -0
@@ -0,0 +1,98 @@
1
+ ---
2
+ id: N-atel-501-silence-trim
3
+ title: Silence trimming with parametric cuts
4
+ type: note
5
+ track: ATEL-501
6
+ author: atelier
7
+ created: '2026-05-20'
8
+ updated: '2026-05-20'
9
+ tags:
10
+ - course
11
+ - atel-501
12
+ - video
13
+ - trim
14
+ difficulty: intermediate
15
+ estimatedMinutes: 6
16
+ prerequisites:
17
+ - N-atel-101-layers
18
+ summary: How `atelier trim` turns ffmpeg silencedetect output into parametric video cuts — the immutable-boundary + tunable-padding model, the `--tighten`/`--loosen`/`--cut` overrides, and the `cuts.json` artifact.
19
+ ---
20
+
21
+ ## What `atelier trim` does
22
+
23
+ ```
24
+ atelier trim ./my-video-project
25
+ ```
26
+
27
+ A VideoProject is a folder: a source clip, a `project.atelier` composition, and the pipeline artifacts (`cuts.json`, `transcript.json`). `atelier trim` reads the source, finds the silence, and rewrites the `silence-trim` tagged layers in `project.atelier` so the timeline plays only the parts where someone is talking.
28
+
29
+ It does this in a fixed pipeline:
30
+
31
+ 1. Probe that ffmpeg has the `silencedetect` filter.
32
+ 2. `ffprobe` the source duration.
33
+ 3. Run `silencedetect` → silence intervals.
34
+ 4. Invert silence into the gaps between it — the **speech intervals**.
35
+ 5. Build a fresh `CutEntry[]` with default (or recipe) padding.
36
+ 6. Merge with the existing `cuts.json` to preserve user padding (unless `--reset`).
37
+ 7. Apply `--tighten` / `--loosen` / `--cut` overrides.
38
+ 8. Resolve overlaps at silence midpoints.
39
+ 9. Clamp boundaries to `[0, duration]`.
40
+ 10. Write `cuts.json` and rewrite the `silence-trim` layers.
41
+
42
+ ## The detect step
43
+
44
+ `silencedetect` is a threshold detector: anything quieter than `--noise` for at least `--min-silence` seconds is silence.
45
+
46
+ | Flag | Default | Meaning |
47
+ |---|---|---|
48
+ | `--noise <dB>` | `-30dB` | Below this level counts as silence |
49
+ | `--min-silence <seconds>` | `0.35` | Shortest gap that registers as silence |
50
+
51
+ Atelier never stores the silence directly — it inverts it. Leading silence (before the first detected gap) and trailing silence (after the last) are handled so the very start and end of a clip aren't accidentally cut.
52
+
53
+ ## Immutable boundary, tunable padding
54
+
55
+ This is the core idea of the cut model. A `CutEntry` is four numbers:
56
+
57
+ ```json
58
+ { "rawStart": 4.21, "rawEnd": 9.83, "paddingPre": 0.08, "paddingPost": 0.12 }
59
+ ```
60
+
61
+ - **`rawStart` / `rawEnd`** are the *detected* speech boundaries. They are the machine's findings and you don't edit them by hand.
62
+ - **`paddingPre` / `paddingPost`** are *yours*. They widen (or tighten) the cut around the raw boundary so a clip doesn't snap shut on the first or last syllable.
63
+
64
+ The effective cut is `[rawStart - paddingPre, rawEnd + paddingPost]`. Defaults are deliberately asymmetric — `paddingPre` is `0.08s`, `paddingPost` is `0.12s` — because a hair more room after a phrase reads more natural than room before it.
65
+
66
+ Why separate the two? Because re-running detection regenerates `rawStart`/`rawEnd` from the audio, but your padding is an editorial decision that should survive. That separation is the `~detected-vs-user-edited` aspect, and note 3 covers exactly how the padding gets re-attached after a re-detect.
67
+
68
+ ## The three padding overrides
69
+
70
+ You tune padding without ever touching a raw boundary:
71
+
72
+ | Flag | Effect |
73
+ |---|---|
74
+ | `--tighten <ms>` | Reduce padding on **every** cut by N milliseconds |
75
+ | `--loosen <ms>` | Increase padding on **every** cut by N milliseconds |
76
+ | `--cut <index>` | Scope `--pad-pre` / `--pad-post` to **one** cut |
77
+
78
+ `--tighten` and `--loosen` are in **milliseconds** — they're converted to seconds (`ms / 1000`) and added to current padding, which is why they compose with whatever per-cut overrides already exist instead of clobbering them. Padding floors at zero; you can't tighten a cut into a negative span.
79
+
80
+ ```
81
+ atelier trim ./proj --loosen 120 # everything breathes a bit more
82
+ atelier trim ./proj --cut 3 --pad-post 0.4 # cut #3 lingers; the rest unchanged
83
+ ```
84
+
85
+ `--pad-pre` / `--pad-post` without `--cut` set the *default* padding applied to freshly-detected cuts.
86
+
87
+ ## Overlap and clamp
88
+
89
+ After overrides, two adjacent cuts can have padding that overruns the silence between them. `resolveOverlaps` splits the gap at its **midpoint** — neither cut wins, each takes its half. Then `clampBoundaries` pulls the first cut's `paddingPre` and the last cut's `paddingPost` back inside `[0, duration]` so nothing reaches past the clip.
90
+
91
+ ## `cuts.json` is the source of truth
92
+
93
+ ```
94
+ atelier trim ./proj --dry-run # print computed cuts, write nothing
95
+ atelier trim ./proj --json # emit the result as JSON for piping
96
+ ```
97
+
98
+ `cuts.json` (version `1.1`) is the durable artifact. The `silence-trim` layers in `project.atelier` are *derived* from it — a re-run drops and rebuilds them. Edit padding through the CLI, not by hand-editing the layers, and your intent persists across every re-detect.
@@ -0,0 +1,98 @@
1
+ ---
2
+ id: N-atel-501-transcribe-and-captions
3
+ title: Transcription, transcript editing, and captions
4
+ type: note
5
+ track: ATEL-501
6
+ author: atelier
7
+ created: '2026-05-20'
8
+ updated: '2026-05-20'
9
+ tags:
10
+ - course
11
+ - atel-501
12
+ - video
13
+ - captions
14
+ difficulty: intermediate
15
+ estimatedMinutes: 7
16
+ prerequisites:
17
+ - N-atel-101-layers
18
+ summary: '`atelier transcribe` (Whisper), the `atelier transcript` edit family (fix/add/delete/merge/split/list), and `atelier captions regenerate` — how spoken audio becomes word-level caption layers you can correct by hand.'
19
+ ---
20
+
21
+ ## Transcribe
22
+
23
+ ```
24
+ atelier transcribe ./my-video-project
25
+ ```
26
+
27
+ This runs Whisper over the source clip, writes `transcript.json`, and rewrites the `caption` tagged `TextVisual` layers in `project.atelier`. The pipeline:
28
+
29
+ 1. Probe a Whisper backend.
30
+ 2. Run Whisper → raw transcript JSON.
31
+ 3. Parse into the `VideoTranscript` shape (segments → words).
32
+ 4. Merge with the existing transcript to preserve your edits (unless `--reset`).
33
+ 5. Write `transcript.json`.
34
+ 6. Unless `--no-captions`: rebuild the caption layers.
35
+
36
+ ### Backend probe order
37
+
38
+ The probe checks, in order:
39
+
40
+ 1. **`whisper-cpp`** — if `whisper-cli` is on your PATH (`brew install whisper-cpp`).
41
+ 2. **`openai-api`** — if `OPENAI_API_KEY` is set. Reserved for an opt-in path; it currently throws "not yet implemented." Install whisper.cpp for local transcription.
42
+ 3. **`none`** — nothing available; the command throws with install guidance.
43
+
44
+ | Flag | Default | Meaning |
45
+ |---|---|---|
46
+ | `--model <name>` | `base.en` | `tiny`/`base`/`small`/`medium`/`large-v3` (and `.en` variants) |
47
+ | `--language <code>` | autodetect | BCP-47 hint; omit to autodetect |
48
+ | `--reset` | off | Discard existing edits; full fresh transcript |
49
+ | `--no-captions` | off | Write `transcript.json` only; skip caption layers |
50
+
51
+ whisper.cpp is run with `--word-thold 0.01` so it emits word-level timestamps. When only segment-level boundaries come back, word timing is synthesized by an even split across the segment — captions still get per-word timing to animate against.
52
+
53
+ ## The transcript edit family
54
+
55
+ Whisper mishears. `atelier transcript` is the family of subcommands that correct it without re-running the model. Words are addressed by a **global index** across all segments — start with `list`:
56
+
57
+ ```
58
+ atelier transcript list ./proj # every word, its index, start time, and flags
59
+ ```
60
+
61
+ | Subcommand | What it does |
62
+ |---|---|
63
+ | `fix` | Correct text — `--replace 'wrong=right'` (batch) or `--word <idx> --text '...'` (single) |
64
+ | `add` | Insert a user-added word — `--after-word <idx> --text '...'` (`--duration` default `0.15s`) |
65
+ | `delete` | Hide a word — `--word <idx>` (kept in transcript, dropped from captions) |
66
+ | `merge` | Join words `i` and `i+1` — `--word <i>` |
67
+ | `split` | Split one word at a fraction — `--word <idx> --at <0–1>` |
68
+
69
+ ```
70
+ atelier transcript fix ./proj --replace 'atelyer=atelier' --replace 'lottie=Lottie'
71
+ atelier transcript fix ./proj --word 42 --text 'parametric'
72
+ atelier transcript delete ./proj --word 7
73
+ ```
74
+
75
+ Two behaviors worth burning in:
76
+
77
+ - **`delete` doesn't delete.** It sets `hidden` on the word. The word stays in `transcript.json` (so a re-transcribe can re-recognize it) but is skipped when captions are built. This is a soft, reversible cut.
78
+ - **`split --at` must be strictly between 0 and 1.** Exactly `0` or `1` is rejected — there's nothing to split off. Timing prorates across the split point; text defaults to slicing at the character midpoint, overridable with `--first` / `--second`.
79
+
80
+ Every edit subcommand regenerates the caption layers after writing the transcript, unless you pass `--no-regenerate`.
81
+
82
+ ## Captions: phrases and word-level timing
83
+
84
+ Captions aren't one layer per word — words are grouped into **phrases**, one `caption` tagged layer each. A new phrase starts whenever any of these is true:
85
+
86
+ - the phrase reaches the max-words ceiling (default 5),
87
+ - the current word ends in `. ! ? , ; :`, or
88
+ - the gap to the next word exceeds the pause threshold (default `0.4s`).
89
+
90
+ Hidden words are skipped during grouping. Each phrase layer is a `TextVisual` placed in the lower third (`yRatio` default `0.85`) with `opacity: 0`, and gets a fade-in / fade-out delta pair keyed to its start and end times.
91
+
92
+ ## Regenerate without re-running Whisper
93
+
94
+ ```
95
+ atelier captions regenerate ./proj
96
+ ```
97
+
98
+ When you only change *styling* — font, size, position, phrase grouping — there's no reason to transcribe again. `captions regenerate` re-derives the caption layers from the current `transcript.json`. Its only flag is `--recipe <name>`, which applies a Studio Recipe's `caption_style` + `caption_grouping`. Whisper stays untouched; your transcript edits stay intact.
@@ -0,0 +1,71 @@
1
+ ---
2
+ id: N-atel-601-carousel
3
+ title: The carousel driver — a folder of images into posts
4
+ type: note
5
+ track: ATEL-601
6
+ author: atelier
7
+ created: '2026-05-20'
8
+ updated: '2026-05-20'
9
+ tags:
10
+ - course
11
+ - atel-601
12
+ - carousel
13
+ - batch
14
+ difficulty: intermediate
15
+ estimatedMinutes: 5
16
+ prerequisites:
17
+ - N-atel-301-composition-and-overlays
18
+ summary: atelier carousel takes a recipe plus a folder of images and composes one PNG per image, threading currentIndex/totalCount into the page-number overlay so each post reads "i/N". The folder → posts → destination workflow, the 1-based index, fit-to-canvas composition, and zero-padded sortable output names.
19
+ ---
20
+
21
+ ## The workflow: folder into posts
22
+
23
+ A carousel is the multi-image-post format — a swipeable deck where each frame is its own image with consistent branding and a "where am I in the deck" indicator. The `atelier carousel` command is the batch driver for exactly that:
24
+
25
+ ```
26
+ atelier carousel <recipe> --inputs <glob> --out-dir <dir>
27
+ ```
28
+
29
+ The shape of the workflow is **folder → posts → destination.** Point `--inputs` at a folder of images, hand it a recipe, name a `--out-dir`, and it composes one recipe-overlaid PNG per image. Short forms `-i` / `-d` exist; `--width` / `--height` set the canvas (default 1080×1080), and `-f` / `--frame` pick the frame to render.
30
+
31
+ ```
32
+ atelier carousel my-style --inputs ./shots --out-dir ./posts
33
+ atelier carousel my-style -i "shots/*.png" -d ./posts --width 1080 --height 1350
34
+ ```
35
+
36
+ `--inputs` accepts a directory (every image inside), a single file, or a single-segment `*`-glob in the final path component (`shots/*.png`). Multi-segment and recursive globs are out of scope — point it at a directory instead. Accepted extensions are `.png`, `.jpg`, `.jpeg`, `.webp`. The matched list is sorted lexicographically so ordering is stable and predictable.
37
+
38
+ ## Per-image composition
39
+
40
+ For each image, `composeCarouselFrameDoc` builds a minimal single-frame document: a canvas-sized doc with a background and one fit-to-canvas `ImageVisual` layer (anchored centered at `{0.5, 0.5}`). The image's natural dimensions aren't known until node-canvas decodes the file, so the bounds start at the canvas extent and are re-fitted (`refitImageBounds`) once the real dimensions are available at render time.
41
+
42
+ Then the recipe's `overlay_rules` are applied on top of that base doc — the handle (if any) and, crucially, the page-number.
43
+
44
+ ## currentIndex / totalCount threaded into page-number
45
+
46
+ This is what makes the carousel a carousel. The driver loops over the sorted inputs and, for image `n`, applies the recipe with a **1-based** index:
47
+
48
+ ```
49
+ const index = n + 1; // 1-based
50
+ applyRecipeToOverlay(baseDoc, recipe, { currentIndex: index, totalCount: total });
51
+ ```
52
+
53
+ `totalCount` is the count of matched images. Because both context values are present, the page-number layer *is* emitted (unlike a single-frame `apply-recipe`), and `renderPageNumberFormat` substitutes them into the format string. A recipe whose `page_number.format` is `"{current:02d}/{total:02d}"` produces `01/05`, `02/05`, … across a five-image deck. This is the one place page-numbers come fully alive — the carousel batch is the natural carrier of the `i/N` context.
54
+
55
+ ## Output names: zero-padded and sortable
56
+
57
+ Each composed PNG is written to `--out-dir` (created if missing) with a zero-padded, sortable name (`carouselFileName`):
58
+
59
+ ```
60
+ 03-myphoto.png # prefix = index, zero-padded to max(2, digits-in-N)
61
+ ```
62
+
63
+ The prefix width is `Math.max(2, String(total).length)` — always at least two digits, widening when the deck has 100+ images — so the files sort in deck order in any file browser or upload picker, then carry the original stem and a `.png` extension. The page-number *content* and the filename *prefix* both derive from the same 1-based index, so what the viewer sees and how the files sort stay in agreement.
64
+
65
+ ## Where it sits in the pipeline
66
+
67
+ The carousel driver renders through the same shared export-image path the rest of the CLI uses, so it inherits node-canvas's font and image handling. It needs node-canvas available; if the optional native renderer is missing, it fails with a clear `CanvasUnavailableError` rather than a cryptic crash.
68
+
69
+ The carousel is the culmination of the recipe story: one authored style, one folder of raw images, and a complete branded, numbered, swipeable deck out the other end — every overlay placed by the same `overlay_rules` translator and the same layer-tag isolation invariant that protects every other Atelier pipeline.
70
+
71
+ That closes ATEL-601. You can now author a Studio Recipe, apply it through the CLI and through MCP, attach anchored handle and page-number overlays, and batch a folder of images into a finished carousel.
@@ -0,0 +1,96 @@
1
+ ---
2
+ id: N-atel-601-overlay-rules
3
+ title: Overlay rules — anchored handle and page-number presets
4
+ type: note
5
+ track: ATEL-601
6
+ author: atelier
7
+ created: '2026-05-20'
8
+ updated: '2026-05-20'
9
+ tags:
10
+ - course
11
+ - atel-601
12
+ - overlays
13
+ - recipe
14
+ difficulty: intermediate
15
+ estimatedMinutes: 6
16
+ prerequisites:
17
+ - N-atel-301-composition-and-overlays
18
+ summary: overlay_rules (Phase 1.5) declares two anchored text presets — handle and page_number. Page-number formats support {current}/{total} and zero-padded {current:02d}/{total:02d}. Anchors map to four corners via frame + anchorPoint. Overlays live in the overlay tag namespace and the translator builds a brand-new document (~copy-on-write-doc).
19
+ ---
20
+
21
+ ## The overlay_rules block
22
+
23
+ `overlay_rules` was promoted from the reserved Phase 3 set into first-class **Phase 1.5**. It declares anchored text overlays that mark a composition. There are two sub-blocks, both optional:
24
+
25
+ ```yaml
26
+ overlay_rules:
27
+ handle:
28
+ text: "@username"
29
+ anchor: "bottom-left"
30
+ margin: 32
31
+ style:
32
+ font_family: "Inter"
33
+ font_size: 36
34
+ font_weight: "bold"
35
+ color: "#FFFFFF"
36
+ page_number:
37
+ format: "{current:02d}/{total:02d}"
38
+ anchor: "bottom-right"
39
+ margin: 32
40
+ style:
41
+ font_family: "Inter"
42
+ font_size: 18
43
+ font_weight: "normal"
44
+ color: "#9CA3AF"
45
+ ```
46
+
47
+ The translator that turns these rules into layers is `applyRecipeToOverlay` (in `packages/cli/src/lib/recipe.ts`). It builds at most one `handle` layer and one `page_number` layer, both `TextVisual`.
48
+
49
+ ## handle — a static marker
50
+
51
+ A `handle` is a literal text overlay: a social handle, a credit, a watermark. Its `OverlayHandleRule` shape is `{ text, anchor, margin?, style? }`. The `text` is rendered verbatim — template variables are reserved for later, so today `text` is exactly the string that appears.
52
+
53
+ The built layer always gets the stable id **`overlay-handle`** and `tags: ["overlay"]`. The handle is *unconditional*: if `overlay_rules.handle` is present, the handle layer is always produced, on single frames and carousel pages alike.
54
+
55
+ ## page_number — a parameterized template
56
+
57
+ A `page_number` is a template, not a literal. Its `OverlayPageNumberRule` shape is `{ format, anchor, margin?, style? }`, and `format` is a string with placeholders:
58
+
59
+ ```
60
+ "{current}/{total}" -> "3/5"
61
+ "{current:02d}/{total:02d}" -> "03/05"
62
+ "Page {current} of {total}" -> "Page 3 of 5"
63
+ ```
64
+
65
+ Two placeholder forms are recognized: bare `{current}` / `{total}`, and the zero-padded `{current:0Nd}` / `{total:0Nd}` form (the `N` is the pad width, so `{current:02d}` pads to two digits). The schema (`^valid-recipe` gate) enforces the syntax at validate time and requires **at least one** of `{current}` / `{total}` to appear — a format with neither is rejected. Render-time substitution happens in `renderPageNumberFormat`, which is a regex replace over those two placeholder shapes.
66
+
67
+ The built layer gets the stable id **`overlay-page-number`** and `tags: ["overlay"]`.
68
+
69
+ **Page-number needs carousel context.** `applyRecipeToOverlay` only emits the page-number layer when both `currentIndex` and `totalCount` are passed in its context argument. On a single-frame apply (no carousel), the context is absent, so the layer is skipped with a one-shot warning — `01/01` on a standalone post is misleading, so skipping is by design. The handle is unaffected. The carousel driver (next note) is what threads `currentIndex`/`totalCount` in.
70
+
71
+ ## The four-corner anchor math
72
+
73
+ `anchor` is one of `top-left | top-right | bottom-left | bottom-right` (the `OverlayAnchor` type). The translator's `anchorToFrame` maps each corner to a `frame` position plus an `anchorPoint`, with `margin` (default 24px) insetting from the anchored edges:
74
+
75
+ | `anchor` | `frame` | `anchorPoint` |
76
+ |---|---|---|
77
+ | `top-left` | `{ x: margin, y: margin }` | `{ x: 0, y: 0 }` |
78
+ | `top-right` | `{ x: canvas.width - margin, y: margin }` | `{ x: 1, y: 0 }` |
79
+ | `bottom-left` | `{ x: margin, y: canvas.height - margin }` | `{ x: 0, y: 1 }` |
80
+ | `bottom-right` | `{ x: canvas.width - margin, y: canvas.height - margin }` | `{ x: 1, y: 1 }` |
81
+
82
+ The pairing of `frame` and `anchorPoint` is what makes the corner stick. A `bottom-right` overlay anchors its own bottom-right corner (`anchorPoint {1, 1}`) at the canvas's bottom-right-minus-margin point — so the text grows inward, away from the edge, regardless of how long it is. The anchor also doubles as the rotation/scale pivot if you later animate the overlay.
83
+
84
+ The `style` block is a partial `OverlayTextStyle` (`font_family`, `font_size`, `font_weight`, `color`) and merges over a runtime baseline of `Inter / 24px / 600 / #F5F5F7` — so an overlay with no `style` still renders legibly.
85
+
86
+ ## The overlay tag namespace and ~copy-on-write-doc
87
+
88
+ Two invariants govern how overlays touch a document.
89
+
90
+ **Layer-tag isolation (`~layer-tag-isolation`).** Every overlay layer carries `tags: ["overlay"]`. Before appending the freshly-derived overlays, `applyRecipeToOverlay` filters out *every* existing `overlay`-tagged layer: `doc.layers.filter((l) => !(l.tags ?? []).includes("overlay"))`. Re-applying a recipe drops the prior overlay set and re-adds — idempotent, repeatable. User-authored (untagged) layers, caption-pipeline layers (`tags: ["caption"]`), and silence-trim layers are never touched. The stable ids (`overlay-handle`, `overlay-page-number`) mean a re-apply replaces in place rather than stacking duplicates.
91
+
92
+ **Copy-on-write (`~copy-on-write-doc`).** The translator never mutates the input. It constructs a *new* document — spreads the doc, swaps in a new layers array — and returns it: `return { ...doc, layers: [...preserved, ...overlayLayers] }`. The input document is left untouched. This guarantees a concurrent reader (notably the studio's live WebSocket bridge) can never observe a half-mutated document mid-update.
93
+
94
+ ## One translator, two homes
95
+
96
+ `applyRecipeToOverlay` exists in two places: the canonical version in `packages/cli/src/lib/recipe.ts` and a deliberate duplicate in `packages/mcp/src/tools/recipes.ts`. The MCP package does *not* depend on the CLI package — importing it would risk a cli↔mcp dependency cycle — so the overlay translator (and `renderPageNumberFormat`) are re-implemented on top of the shared schema package. The duplication is intentional and documented in the MCP file; both implementations honor the same two invariants above.
@@ -0,0 +1,84 @@
1
+ ---
2
+ id: N-atel-601-recipe-tools-and-apply
3
+ title: Recipe tooling — CLI verbs, MCP tools, and resolution
4
+ type: note
5
+ track: ATEL-601
6
+ author: atelier
7
+ created: '2026-05-20'
8
+ updated: '2026-05-20'
9
+ tags:
10
+ - course
11
+ - atel-601
12
+ - cli
13
+ - mcp
14
+ difficulty: intermediate
15
+ estimatedMinutes: 6
16
+ prerequisites:
17
+ - N-atel-301-composition-and-overlays
18
+ summary: The recipe surface — CLI verbs (recipe new/validate/show, apply-recipe, the --recipe flag on trim/transcribe/captions regenerate) and MCP tools (atelier_recipe_list/get/validate/save/apply). save refuses invalid recipes; apply wires only overlay_rules. The two-tier resolution chain and CLI-wins precedence.
19
+ ---
20
+
21
+ ## CLI: the recipe family
22
+
23
+ ```
24
+ atelier recipe new <name> scaffold a starter YAML with every default filled in
25
+ atelier recipe validate <path> validate against the schema (^valid-recipe gate)
26
+ atelier recipe show <path> print a recipe; --with-defaults overlays code defaults
27
+ ```
28
+
29
+ **`recipe new`** scaffolds. It writes a starter `.recipe.yaml` (into `./.atelier/recipes/` by default, or `--dir`) with every Phase 1 field present and commented, so you learn the shape by editing. It refuses to overwrite an existing file, and it guards the footgun where `recipe new foo.recipe.yaml` would otherwise produce `foo.recipe.yaml.recipe.yaml`. Note: `new` only writes a template — it does *not* validate, because the scaffold is always valid by construction.
30
+
31
+ **`recipe validate`** runs the recipe through `validateRecipe` and reports `PASS`/`FAIL`. It also surfaces forward-compat warnings for reserved Phase 3 fields. Add `--json` for machine-readable output.
32
+
33
+ **`recipe show`** prints the recipe back as YAML. With `--with-defaults` it overlays the code defaults onto every omitted field, so you see the fully-resolved effective recipe.
34
+
35
+ ## CLI: applying a recipe
36
+
37
+ A recipe touches a project two ways.
38
+
39
+ **The `--recipe <name>` flag** on the individual pipelines:
40
+
41
+ ```
42
+ atelier trim <project> --recipe my-style silence_policy as the baseline
43
+ atelier transcribe <project> --recipe my-style caption_style + caption_grouping
44
+ atelier captions regenerate <project> --recipe my-style caption_style + caption_grouping
45
+ ```
46
+
47
+ Each flag pulls the relevant section from the recipe and uses it as the baseline for that pipeline. (The caption verb is `captions regenerate`, not a bare `captions`.)
48
+
49
+ **The `apply-recipe` convenience verb** runs trim and transcribe against the same recipe in one shot:
50
+
51
+ ```
52
+ atelier apply-recipe <project> <recipe>
53
+ ```
54
+
55
+ In order: `atelier trim`, then `atelier transcribe`, then — if the recipe has `overlay_rules` — the overlay translator rewrites the project's `overlay`-tagged layers. Flags: `--reset` (destructive — discards existing user padding and transcript edits on both pipelines), `--no-trim`, `--no-transcribe`. Because `apply-recipe` is a single-frame apply with no carousel context, the page-number layer is silently skipped; the handle is applied if present.
56
+
57
+ ## MCP: the conversational surface
58
+
59
+ Five tools let an agent view, edit, and apply recipes without shelling out:
60
+
61
+ ```
62
+ atelier_recipe_list enumerate discoverable recipes (project + user library)
63
+ atelier_recipe_get load a recipe; returns BOTH the parsed object and its YAML
64
+ atelier_recipe_validate validate a path OR inline YAML text
65
+ atelier_recipe_save write a recipe — refuses to write if invalid
66
+ atelier_recipe_apply apply overlay_rules to an in-store document
67
+ ```
68
+
69
+ Two behaviors are worth internalizing.
70
+
71
+ **`atelier_recipe_save` validates before writing.** It runs `validateRecipe` first and *refuses* to write an invalid recipe, returning the validation issues instead. This is the deliberate contrast with `recipe new`: `new` scaffolds an always-valid template; `save` is how an agent persists an *edited* recipe, and it guards the write so a malformed edit never lands on disk. It creates parent directories as needed and accepts either a recipe object or raw YAML text.
72
+
73
+ **`atelier_recipe_apply` only applies `overlay_rules`.** This is the key agent mental-model point. The apply tool wires up *only* the handle + page-number overlays onto an in-store document (the result is stored back with `source: "llm"`). Silence-trim, transcribe, and caption application stay **CLI-only** — there is no MCP path for them. If the recipe requests a `page_number` but `currentIndex`/`totalCount` aren't passed, that layer is skipped and a warning is returned (same rule as the CLI translator).
74
+
75
+ ## Resolution chain and precedence
76
+
77
+ A bare recipe name resolves through a two-tier chain (`resolveRecipePath` in `packages/cli/src/lib/recipe.ts`), checking these extensions at each tier — `.recipe.yaml`, `.recipe.json`, `.yaml`, `.yml`, `.json`:
78
+
79
+ 1. **Project-local:** `<projectDir>/.atelier/recipes/<name>.<ext>`
80
+ 2. **User library:** `~/.atelier/recipes/<name>.<ext>`
81
+
82
+ The project directory is searched first, so a project can shadow a personal recipe of the same name. A path that is absolute or contains a slash skips resolution entirely and loads literally. `recipe new`, `recipe validate`, and `recipe show` all share this chain, so a name authored by `new` is found by `validate` and `show`.
83
+
84
+ **CLI-wins precedence.** When a recipe is combined with explicit CLI flags, the CLI flag wins. In `applyRecipeToTrimOptions`, each field is `cliOptions.x ?? policy.x` — the recipe only fills in what the CLI left unspecified. So `atelier trim <project> --recipe my-style --noise -40dB` uses the recipe's silence policy as the baseline but honors the `-40dB` you typed. The recipe is a baseline; the command line is an override.
@@ -0,0 +1,103 @@
1
+ ---
2
+ id: N-atel-601-studio-recipe
3
+ title: The Studio Recipe — style as a reusable preset
4
+ type: note
5
+ track: ATEL-601
6
+ author: atelier
7
+ created: '2026-05-20'
8
+ updated: '2026-05-20'
9
+ tags:
10
+ - course
11
+ - atel-601
12
+ - recipe
13
+ - presets
14
+ difficulty: intermediate
15
+ estimatedMinutes: 5
16
+ prerequisites:
17
+ - N-atel-301-composition-and-overlays
18
+ summary: A Studio Recipe is a reusable bundle of style defaults — silence policy, caption style, phrase grouping — authored once and applied to many projects. Phase 1 ships three implemented sections; partial recipes fall back to code defaults; six Phase 3 fields parse as opaque values for forward-compat.
19
+ ---
20
+
21
+ ## The value play
22
+
23
+ A Studio Recipe is *your style, saved as a preset.* You spend an afternoon dialing in how your shorts look — how aggressively silence gets trimmed, what font and size the captions render at, how words group into phrases — and then you never tune it by hand again. You write those decisions into one YAML file and apply it to the next project, and the one after that.
24
+
25
+ That's the whole pitch: **a recipe makes a look reproducible.** The same defaults, every project, until you decide to change them in one place.
26
+
27
+ The type lives in `packages/types/src/recipe.ts` as `StudioRecipe`. The top of the file is metadata:
28
+
29
+ ```yaml
30
+ version: "1.0" # recipe format version — current is "1.0"
31
+ name: "my-shorts-style"
32
+ description: "Punchy captions, tight silence trim"
33
+ author: "@me"
34
+ tags: [shorts, vertical]
35
+ ```
36
+
37
+ ## The three Phase 1 sections
38
+
39
+ Phase 1 implements three sections, each consumed by a different pipeline:
40
+
41
+ ```yaml
42
+ silence_policy: # consumed by `atelier trim`
43
+ noise: "-30dB"
44
+ min_silence: 0.35
45
+ default_padding_pre: 0.08
46
+ default_padding_post: 0.12
47
+ match_tolerance: 0.5
48
+
49
+ caption_style: # consumed by `atelier transcribe` / `captions regenerate`
50
+ font_family: "Inter"
51
+ font_size: 84
52
+ font_weight: "bold" # normal | bold | numeric 100..900
53
+ text_align: "center" # left | center | right
54
+ color: "#FFFFFF"
55
+ y_ratio: 0.85 # 0=top, 1=bottom
56
+ width_ratio: 0.9
57
+ fade_seconds: 0.05
58
+
59
+ caption_grouping: # consumed by `atelier transcribe` phrase grouping
60
+ max_words: 5
61
+ pause_gap: 0.4
62
+ ```
63
+
64
+ **`silence_policy`** is the silence-trim baseline. Every field maps one-to-one onto an `atelier trim` option — `noise` is the `silencedetect` threshold, `min_silence` is the minimum gap that registers as a cut, the two `default_padding_*` values are the leading/trailing pad on new cuts, and `match_tolerance` is the timing window that preserves your hand-tuned padding when you re-run the trim (the detected-vs-user-edited invariant from ATEL-501).
65
+
66
+ **`caption_style`** is the caption layer's visual presentation — font, color, vertical position as a ratio of canvas height, width as a ratio of canvas width, fade duration. The recipe is the canonical source for caption styling; the CLI doesn't expose per-invocation caption-style flags.
67
+
68
+ **`caption_grouping`** controls how transcribed words clump into on-screen phrases: at most `max_words` per phrase, with a forced break whenever the gap between words exceeds `pause_gap` seconds.
69
+
70
+ ## Partial recipes fall back to code defaults
71
+
72
+ Every section — and every field within a section — is optional. A recipe that only sets caption color is completely valid:
73
+
74
+ ```yaml
75
+ version: "1.0"
76
+ name: "just-the-color"
77
+ caption_style:
78
+ color: "#FFD60A"
79
+ ```
80
+
81
+ Omitted fields fall back to the code defaults baked into `renderRecipeWithDefaults` (in `packages/cli/src/lib/recipe.ts`). Those defaults are the same values the scaffold writes: `noise: -30dB`, `min_silence: 0.35`, caption `font_size: 84`, `max_words: 5`, and so on. The merge is shallow-per-section: your recipe's section overlays the defaults section, so setting one caption field doesn't wipe out the rest.
82
+
83
+ To see the effective values — your recipe overlaid on the defaults — run `atelier recipe show <name> --with-defaults`. It prints the fully-resolved recipe so there's no guessing about what an omitted field will become.
84
+
85
+ ## Reserved Phase 3 fields parse as opaque values
86
+
87
+ Five fields are declared in the type but not yet implemented:
88
+
89
+ ```
90
+ caption_highlight # per-word highlight color animation
91
+ transition_kit # transition defaults between clips
92
+ palette # brand colors / font set
93
+ audio_policy # ducking, music lane
94
+ aspect_targets # multi-aspect render targets
95
+ ```
96
+
97
+ These are **reserved for Phase 3** (extraction-of-style-from-a-reference-video). They parse cleanly as opaque values — the schema accepts them as `unknown` — so a recipe that includes them today stays forward-compatible. Phase 1 simply ignores their contents.
98
+
99
+ The one caveat: `atelier recipe validate` emits a forward-compat *warning* (not an error) when a reserved field is present, so you're never caught off-guard that a section you authored isn't doing anything yet. (`overlay_rules`, covered in the next note, was promoted out of this reserved set into first-class Phase 1.5 — it does *not* warn.)
100
+
101
+ ## Why a flat preset and not a template engine
102
+
103
+ A recipe is deliberately *data, not behavior.* It carries no logic, no conditionals, no per-layer targeting — just a flat bag of style values that three pipelines read. That keeps recipes diffable, shareable, and safe to validate before they ever touch a document. The richer machinery (templates, presets, variables from ATEL-201) is what the pipelines use internally; the recipe is the human-facing dial.
@@ -0,0 +1,68 @@
1
+ ---
2
+ id: N-atel-701-choosing-output
3
+ title: Choosing an output — a decision guide by destination
4
+ type: note
5
+ track: ATEL-701
6
+ author: atelier
7
+ created: '2026-05-20'
8
+ updated: '2026-05-20'
9
+ tags:
10
+ - course
11
+ - atel-701
12
+ - delivery
13
+ - decision-guide
14
+ difficulty: intermediate
15
+ estimatedMinutes: 6
16
+ prerequisites:
17
+ - N-atel-101-the-atelier-format
18
+ summary: One decision guide across every exporter — pick the format by destination (social post, web embed, motion-design handoff, preview), hit the right aspect target, and know when to use the CLI versus the studio Export button.
19
+ ---
20
+
21
+ ## Start from the destination, not the format
22
+
23
+ The mistake is asking "what can Atelier export?" The right question is "where is this going?" — and the destination picks the exporter for you.
24
+
25
+ | Destination | Format | Command / surface |
26
+ | --- | --- | --- |
27
+ | Social image post | PNG | `atelier export-image` (or `atelier carousel` for a set) |
28
+ | Open Graph / thumbnail / poster | PNG | `atelier export-image --width …` |
29
+ | Web logo / icon that must scale | SVG | `atelier export-svg` |
30
+ | Motion-design handoff (runtime / AE) | Lottie JSON | `atelier export-lottie` |
31
+ | Social / web video | MP4 | `atelier render` or studio Export |
32
+ | README / chat loop | GIF | `atelier render -f gif` |
33
+ | Web video, modern browsers, small files | WebM | studio Export (WebCodecs) |
34
+ | Frame inspection / debugging | JSON | `atelier still` (default format) |
35
+
36
+ ## The four common destinations, decided
37
+
38
+ **Social post.** A still image, fixed pixel size, often square or portrait. → **PNG** via `atelier export-image`. Set your aspect with `--width`/`--height` (or design the canvas to the target). For a whole *set* of posts, see the carousel note below — don't loop the single-frame command.
39
+
40
+ **Web embed.** Two cases. If it's a logo, icon, or any artwork that has to stay crisp at arbitrary sizes → **SVG** via `atelier export-svg`, inlined into markup (drop the `--xml-declaration`). If it's video → **MP4** for reach, **WebM** (studio) when the audience is modern browsers and file size matters.
41
+
42
+ **Motion-design handoff.** You're giving the animation to a design or front-end team to drop into a product → **Lottie** via `atelier export-lottie`. It's the whole document as vector animation, tiny, and playable in every major runtime. Watch the stderr warnings: a clean export means the Lottie matches; a warning means a feature dropped and you should preview it in a player.
43
+
44
+ **Preview.** You just want to see the current frame. → `atelier still` for the resolved-frame JSON (what the renderer sees), or `atelier still --format png` / `atelier export-image` for a quick pixel look. No FFmpeg, no canvas build — both raster previews run on a plain install.
45
+
46
+ ## Aspect targets
47
+
48
+ Design the document canvas to the destination's aspect when you can — it's lossless. Common targets:
49
+
50
+ - **1:1 (square)** — 1080×1080. Feed posts, carousels.
51
+ - **4:5 (portrait)** — 1080×1350. Maximizes feed real estate.
52
+ - **9:16 (vertical)** — 1080×1920. Stories, Reels, Shorts, TikTok.
53
+ - **16:9 (landscape)** — 1920×1080. YouTube, web hero video, presentations.
54
+
55
+ On a still export you can also retarget at export time: `atelier export-image hero.atelier --out card.png --width 1200` derives the height to preserve aspect. Override **both** `--width` and `--height` only when you deliberately want a non-aspect-preserving size — it will squash otherwise. For MP4, remember H.264 needs **even** dimensions; an odd canvas is rejected with the next even size suggested.
56
+
57
+ ## CLI or the studio Export button?
58
+
59
+ Same artwork, two ways to get a file out:
60
+
61
+ - **The studio Export button** encodes in the browser via WebCodecs — no FFmpeg, no node-canvas, nothing to install. Best for one-off exports while you're editing, and the only path that reaches WebM. Interactive, per-document.
62
+ - **The CLI** (`export-image`, `export-svg`, `export-lottie`, `render`) is scriptable and batchable — wire it into a build, a script, a CI job. Raster and vector exports run on a plain install; **video (`atelier render`) additionally needs FFmpeg on `$PATH` and the `canvas` package** with its cairo/pango stack (see N-atel-701-video).
63
+
64
+ Rule of thumb: editing and exporting one thing → studio. Automating or batching → CLI.
65
+
66
+ ## Batch image delivery — see ATEL-601
67
+
68
+ When the job is *many* images at once — a folder of shots composed into a uniform set of posts with consistent overlays — that's `atelier carousel`, covered in **ATEL-601 (Studio Recipes)**. It drives the same single-frame raster path across every input and applies a recipe's handle + page-number overlays per image. This note doesn't re-teach it; reach for ATEL-601 when you need batch delivery rather than a single export.