feat(compose): add atelier (bespoke) composition mode + doctrine
Introduce a hand-authored, project-local Remotion render path that bypasses the cut-schema and the stock scene-type registry, for hero/bespoke videos that must look distinct from one another. - video_compose: composition_mode="atelier" (or renderer_family="bespoke") routes to _render_via_atelier, which renders a project-local entry under remotion-composer/projects/<slug>/ with an optional per-project public_dir (skips copying the bloated shared public/). No cut-schema, no stock registry. - skills/meta/bespoke-composition.md: routing skill — art direction (visual-style) -> motion principles (Disney 12) -> engine mechanics (remotion-best-practices + stock components read only as a mechanics codex) -> atelier render. Doctrine: reuse engine knowledge, never creative components. - AGENT_GUIDE: "Composition Authoring Mode" (templated vs atelier); default atelier for hero work; scene-type catalog reframed as a mechanics codex. - animation-runtime-selector + INDEX: authoring-mode-first pointers. - base_tool.run_command: decode subprocess output as UTF-8/replace (Windows cp1252 crashed the reader thread on Remotion's Unicode progress output). - .gitignore: remotion-composer/projects/ (throwaway bespoke compositions).
This commit is contained in:
@@ -81,6 +81,9 @@ node_modules/
|
||||
|
||||
# Remotion build output and copied project assets (regenerable at render time)
|
||||
remotion-composer/out/
|
||||
# Project-local bespoke ("atelier") compositions — hand-authored per video,
|
||||
# throwaway and regenerable. Keeps the shared src/ free of one-off scenes.
|
||||
remotion-composer/projects/
|
||||
remotion-composer/public/*
|
||||
# But keep demo props (shipped with the project for zero-key demos)
|
||||
!remotion-composer/public/demo-props/
|
||||
|
||||
@@ -130,6 +130,15 @@ Exception: if only one runtime is available on the machine, the agent proceeds w
|
||||
|
||||
This rule applies to every pipeline that invokes `video_compose` — not just Wave 1. A pipeline's director skill may recommend a runtime, but that recommendation is input to the conversation with the user, not a decision.
|
||||
|
||||
### Composition Authoring Mode — Templated vs Atelier
|
||||
|
||||
Orthogonal to *runtime* is *authoring mode*: **how** the composition is built. Present it as its own proposal decision and log it in `decision_log` (`category: "composition_mode"`).
|
||||
|
||||
- **Templated** — assemble the stock `cut.type` scene-types (`text_card`, `stat_card`, `bar_chart`, …) into the `Explainer`/`CinematicRenderer` compositions. Fast, cheap, reliable — and the reason most videos look alike. Right for batch output, localization variants, quick drafts, and low-stakes internal clips.
|
||||
- **Atelier** — **hand-author the composition from scratch**: bespoke scenes, a one-off theme, and motion written for this piece, rendered via `composition_mode: "atelier"` (see `video_compose` → `_render_via_atelier`). No reusable creative components; a fresh visual language every time.
|
||||
|
||||
**Default to atelier for hero work** — marketing, launches, brand pieces, any single-deliverable explainer that must impress. The deciding rule: *reuse engine knowledge, never creative components.* In atelier mode the stock scene-type catalog, `hyperframes-registry` blocks, fixtures, and finished components are **off-limits** — they are frozen looks that reintroduce sameness. Before building, route through **`skills/meta/bespoke-composition.md`**, which sequences: art direction (`visual-style`) → motion principles (Disney 12 via `framer-motion`/`lottie-bodymovin`) → engine mechanics (`remotion-best-practices` + the stock components read *only as a mechanics codex*) → render via the atelier path. Close with a **distinctness review**: *could this be any other product's video? does it reuse a look I've made before?* — the inverse of "does it match the reference." Atelier costs more tokens and iteration than templated; say so at proposal so the user opts in knowingly.
|
||||
|
||||
### Escalate Blockers Explicitly
|
||||
|
||||
When a blocker occurs, the agent must surface it immediately using this structure:
|
||||
@@ -396,6 +405,8 @@ For these requests:
|
||||
See `remotion-composer/SCENE_TYPES.md` for the authoritative list and their cut schemas. Current scene types usable via `cut.type`:
|
||||
`text_card`, `stat_card`, `callout`, `comparison`, `hero_title`, `terminal_scene`, `anime_scene`, `bar_chart`, `line_chart`, `pie_chart`, `kpi_grid`, `progress_bar`. Overlay types include `section_title`, `stat_reveal`, `hero_title`, `provider_chip`.
|
||||
|
||||
These stock scene-types are the **templated** path — fast and reliable, but they are why videos look alike. For **hero work, prefer atelier mode** (hand-authored composition) over this catalog; read those types as a *mechanics codex*, not a menu to assemble. See "Composition Authoring Mode" above and `skills/meta/bespoke-composition.md`.
|
||||
|
||||
**When Remotion is NOT available** and `render_runtime="remotion"` was NOT locked, `video_compose` may use FFmpeg Ken Burns motion on still images. This still works but produces less engaging visuals. Mention this tradeoff in the proposal. When `render_runtime="remotion"` IS locked and Remotion is unavailable, that's a blocker — escalate, don't silently swap.
|
||||
|
||||
When `render_runtime="hyperframes"` is locked and HyperFrames is unavailable (Node < 22, missing `ffmpeg`/`npx`, or `hyperframes doctor` reports issues), that's also a blocker. Do not substitute Remotion or FFmpeg without user approval + a logged `render_runtime_selection` decision.
|
||||
|
||||
@@ -280,6 +280,8 @@ Cross-cutting skills that apply to all pipelines:
|
||||
| Reviewer | `meta/reviewer.md` | Self-review protocol after every stage |
|
||||
| Checkpoint Protocol | `meta/checkpoint-protocol.md` | When/how to checkpoint and request human approval |
|
||||
| Skill Creator | `meta/skill-creator.md` | Dynamically create new skills during pipeline runs |
|
||||
| Animation Runtime Selector | `meta/animation-runtime-selector.md` | Choose render runtime + animation library per scene |
|
||||
| Bespoke Composition (Atelier) | `meta/bespoke-composition.md` | Hand-author a composition from scratch (hero work) — no stock scene-types; routes art-direction → motion principles → engine mechanics → atelier render |
|
||||
|
||||
## Style Playbooks
|
||||
|
||||
|
||||
@@ -7,6 +7,13 @@ Meta-skill that answers two questions:
|
||||
|
||||
Read this before authoring any animated component or composition, and whenever you're choosing `render_runtime` at proposal time. It routes you to the right Layer 3 skill so you don't waste time hand-rolling what a plugin already solves.
|
||||
|
||||
> **Authoring mode comes first.** Before runtime or library, decide *how* the composition is
|
||||
> built: **templated** (assemble stock `cut.type` scenes) vs **atelier** (hand-author from
|
||||
> scratch). Default to atelier for hero work and follow `skills/meta/bespoke-composition.md`.
|
||||
> The routing below applies within either mode — but in atelier mode the stock scene-types and
|
||||
> registry blocks are off-limits; you write your own. "Does a stock cut-type fit?" is **not** a
|
||||
> valid shortcut for a hero piece. See `AGENT_GUIDE.md` → "Composition Authoring Mode".
|
||||
|
||||
## When to use this skill
|
||||
|
||||
Apply when:
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
# Bespoke Composition (Atelier Mode)
|
||||
|
||||
Meta-skill for **hand-authoring a composition from scratch** instead of assembling stock
|
||||
scene-types. This is the "hand-stitched every time" path: for hero pieces, every pixel of the
|
||||
look is written fresh so no two videos share a visual language.
|
||||
|
||||
Read this whenever you've chosen **atelier mode** for a piece (see "When to use"). It does not
|
||||
hand you components — it routes you to the *principles, engine mechanics, and tool wiring* you
|
||||
need so that what you build is correct, and distinct.
|
||||
|
||||
> The single rule that governs everything below: **reuse engine knowledge, never creative
|
||||
> components.** How Remotion resolves an asset is engine knowledge — reuse it freely. How a
|
||||
> previous video looked is a creative decision — never reuse it.
|
||||
|
||||
## When to use this skill (authoring mode is a proposal decision)
|
||||
|
||||
OpenMontage now separates three orthogonal axes, all locked at proposal:
|
||||
|
||||
- `renderer_family` — creative grammar
|
||||
- `render_runtime` — technical engine (remotion / hyperframes / ffmpeg)
|
||||
- **`composition_mode`** — **templated** (assemble stock `cut.type` scenes) **vs. atelier** (hand-author)
|
||||
|
||||
Pick **atelier** by default for: marketing, launches, explainers that must impress, brand
|
||||
pieces, anything single-deliverable where quality is the point. Pick **templated** for: batch
|
||||
output, localization variants, quick drafts, low-stakes internal clips — places where reliable
|
||||
sameness is fine and bespoke cost is unjustified. Present the choice to the user at proposal and
|
||||
log it in `decision_log` (`category: "composition_mode"`), the same way you present runtime.
|
||||
|
||||
If atelier is chosen, the stock scene-type catalog, the `hyperframes-registry` blocks, fixtures,
|
||||
and any finished component are **off-limits** — they are frozen looks and reintroduce sameness.
|
||||
|
||||
## The construction route
|
||||
|
||||
Author in this order. Each step routes you to existing knowledge — do not skip the first one.
|
||||
|
||||
### 1. Commit to an art direction *for this subject* — the divergence engine
|
||||
Before writing any component, decide a visual language that fits **this** topic and no other.
|
||||
Use the **`visual-style`** Layer 3 skill (CREATE mode) to lock: palette, type personality,
|
||||
motion character, layout system, and **one signature device** unique to this piece. Difference
|
||||
between videos is guaranteed here — not by withholding components, but by forcing a fresh
|
||||
direction each time. Write it down (a short `art-direction.md` in the project) and build to it.
|
||||
|
||||
Ask yourself: *what visual metaphor belongs to this subject that I have not used before?* If the
|
||||
answer resembles a past piece, you haven't found the direction yet.
|
||||
|
||||
### 2. Decide the motion language — principles, not presets
|
||||
Reach for **principle** skills, never finished animations:
|
||||
- **`framer-motion`** and **`lottie-bodymovin`** — Disney's 12 principles (anticipation, staging,
|
||||
follow-through, slow-in/out, arc, timing, exaggeration, appeal). Runtime-agnostic; apply the
|
||||
*principles* in your own Remotion `spring()`/`interpolate()` code.
|
||||
- The HyperFrames `references/motion-principles.md` — easing as emotion, timing as weight.
|
||||
|
||||
### 3. Reach for a richer vocabulary only when the concept demands it
|
||||
Most scenes are Remotion primitives. Escalate when the *idea* needs it, not by default:
|
||||
`gsap-*` (kinetic typography via SplitText, shape morph via MorphSVG, curved motion via
|
||||
MotionPath, line-draw via DrawSVG, custom easing), `threejs-*` (3D), `d3-viz` (data-driven
|
||||
custom charts — build the chart by hand; do **not** drop in the stock `bar_chart`/`line_chart`),
|
||||
`manim-*` (math), `canvas-procedural-animation` (particles/weather).
|
||||
|
||||
### 4. Get the engine mechanics right — the gotcha codex
|
||||
This is the only place you "reuse": the engine's solved problems. These are facts about how
|
||||
Remotion works, not looks. Study `.agents/skills/remotion-best-practices` (19 rule files:
|
||||
timing, transitions, text-animations, transparent video, fonts, audio, sequencing, measuring
|
||||
text). You may also read the stock components in `remotion-composer/src/components/` **as a
|
||||
mechanics codex — to learn idioms, never to import or imitate a look.**
|
||||
|
||||
Recurring mechanics that bite if you don't know them:
|
||||
- **Determinism**: no `Math.random()` / `Date.now()` per frame — use Remotion `random(seed)` or a
|
||||
seeded helper, or particles/easing flicker across the render.
|
||||
- **Per-scene duration**: `useVideoConfig().durationInFrames` returns the *composition* length, not
|
||||
your scene's. Drive scene-local timing from a passed `durationInFrames`/`Sequence`, not the global.
|
||||
- **Asset paths**: URLs and `staticFile()` (public/) work everywhere; **`<Audio>` rejects `file://`**
|
||||
(only `<OffthreadVideo>`/`<Img>` accept absolute `file://`). Put audio/video in a per-project
|
||||
public dir and reference via `staticFile`. Mirror the `resolveAsset` helper.
|
||||
- **GSAP-in-Remotion**: use a `paused` timeline and `.seek(frame/fps)` — never `requestAnimationFrame`
|
||||
— so frames render deterministically.
|
||||
- **Fonts**: `loadFont()` from `@remotion/google-fonts/<Name>` at module scope, once.
|
||||
|
||||
### 5. Render through the atelier path (project-local, throwaway)
|
||||
Bespoke scenes are **throwaway and project-local** — they never enter the shared `src/` registry.
|
||||
|
||||
- Author under `remotion-composer/projects/<slug>/` (gitignored). It needs its own Remotion entry
|
||||
(`index.tsx` + a `Root` registering only this composition) so it reuses the composer's
|
||||
`node_modules` and stays out of the global `Root.tsx`. The entry MUST live under
|
||||
`remotion-composer/` for the bundler to resolve `remotion`.
|
||||
- Keep media in a small per-project public dir and pass it as `public_dir` so renders don't copy
|
||||
the bloated shared `public/`.
|
||||
- Render via `video_compose` `operation="render"` with:
|
||||
|
||||
```json
|
||||
edit_decisions = {
|
||||
"render_runtime": "remotion",
|
||||
"composition_mode": "atelier",
|
||||
"bespoke": {
|
||||
"entry": "remotion-composer/projects/<slug>/index.tsx",
|
||||
"composition_id": "<id registered in that entry's Root>",
|
||||
"props_path": "<absolute path to props.json>",
|
||||
"public_dir": "<absolute path to the project's public dir>",
|
||||
"scale": 0.5, // 0.5 for a fast draft; drop for the 1080p final
|
||||
"crf": 18, // crisp final
|
||||
"concurrency": 8
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
No `asset_manifest` or `cuts` are required in atelier mode — the composition owns its own assets.
|
||||
|
||||
## Guardrails so this doesn't backfire
|
||||
|
||||
- **Distinctness review (replaces conformance review).** Before final render, ask: *could this be
|
||||
any other product's video? Does it reuse a look I've made before?* If yes, the art direction
|
||||
failed — return to step 1. This is the inverse of "does it match the reference."
|
||||
- **No silent fallback to stock.** "Keep it simple" applies to *mechanics* (a 10-line spring is
|
||||
fine), never to *design* (simple ≠ reaching for `text_card`). If you catch yourself adding a
|
||||
stock `cut.type` to a hero piece, stop.
|
||||
- **Cost honesty.** Atelier costs more agent tokens and iteration than templated. Say so at proposal
|
||||
so the user opts in knowingly. Quality varies more without a stock baseline — mitigate with strong
|
||||
principle skills (above) and the distinctness review, not by reintroducing reuse.
|
||||
- **Checkpoint cadence.** Follow `skills/meta/checkpoint-protocol.md`: present script + scene plan
|
||||
for approval BEFORE generating assets, then a footage/asset checkpoint, then a first-render
|
||||
checkpoint. Do not batch-generate ahead of sign-off.
|
||||
|
||||
## Worked precedent (for the *workflow*, not the look)
|
||||
|
||||
The first atelier piece was the Phantom Reach explainer (`projects/phantom-reach-explainer/`):
|
||||
Playwright-captured app footage with a PII-blur layer → per-sentence TTS stitched with silence
|
||||
beats → free Pixabay music → hand-authored Remotion scenes (custom intro, score-ring, agentic
|
||||
flow, CTA) on a one-off violet theme. Study its **process** (capture → timing → props builder →
|
||||
bespoke scenes → render). **Do not reproduce its visual language** — the next piece must look
|
||||
nothing like it. That is the whole point.
|
||||
|
||||
See also: `skills/meta/animation-runtime-selector.md` (runtime + library routing),
|
||||
`AGENT_GUIDE.md` → "Composition Authoring Mode".
|
||||
@@ -333,6 +333,12 @@ class BaseTool(ABC):
|
||||
resolved_cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
# Force UTF-8 decoding. The default uses the OS locale (cp1252 on
|
||||
# Windows), which raises UnicodeDecodeError on a subprocess that
|
||||
# emits Unicode/emoji (e.g. Remotion's progress output), killing the
|
||||
# reader thread and potentially swallowing the real error text.
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
timeout=timeout,
|
||||
cwd=cwd,
|
||||
check=True,
|
||||
|
||||
@@ -15,6 +15,12 @@ Routing is driven by `edit_decisions.render_runtime` (locked at proposal):
|
||||
- `ffmpeg` → FFmpeg concat/trim. Used only for simple video cuts without
|
||||
composition, or when the approved path explicitly names FFmpeg.
|
||||
|
||||
Authoring mode is orthogonal to runtime. Setting
|
||||
`edit_decisions.composition_mode = "atelier"` (or `renderer_family="bespoke"`)
|
||||
routes to a hand-authored, project-local Remotion composition that BYPASSES the
|
||||
cut-schema and the stock scene-type registry entirely — the "hand-stitched
|
||||
every time" path for hero/bespoke pieces. See `_render_via_atelier`.
|
||||
|
||||
Silent runtime swaps are forbidden by governance. If the chosen runtime is
|
||||
unavailable or fails, this tool surfaces a structured blocker and waits for
|
||||
the agent to re-ask the user rather than substituting a different engine.
|
||||
@@ -667,6 +673,134 @@ class VideoCompose(BaseTool):
|
||||
)
|
||||
return comp
|
||||
|
||||
def _render_via_atelier(
|
||||
self,
|
||||
inputs: dict[str, Any],
|
||||
edit_decisions: dict[str, Any],
|
||||
) -> ToolResult:
|
||||
"""Render a hand-authored, project-local Remotion composition ("atelier" mode).
|
||||
|
||||
Unlike the cut-schema path, atelier mode does NOT route through the
|
||||
stock Explainer/CinematicRenderer compositions, the cut.type scene
|
||||
registry, or RENDERER_FAMILY_MAP. The agent hand-authors a bespoke
|
||||
composition — its own scenes, theme, and motion — and points this
|
||||
renderer at the project-local entry. This is the deliberate
|
||||
"hand-stitched every time" path: zero reusable creative components,
|
||||
a fresh visual language per video.
|
||||
|
||||
Contract — edit_decisions["bespoke"] = {
|
||||
"entry": <path to the project-local Remotion entry .tsx;
|
||||
MUST live under remotion-composer/ so the
|
||||
Remotion bundler can resolve node_modules.
|
||||
Convention: remotion-composer/projects/<slug>/index.tsx>,
|
||||
"composition_id": <id registered in that entry's Root>,
|
||||
"props_path": <optional absolute path to a props JSON (--props)>,
|
||||
"public_dir": <optional path to a SMALL per-project public dir,
|
||||
avoids copying the bloated shared public/>,
|
||||
"scale": <optional float, e.g. 0.5 for a fast draft>,
|
||||
"crf": <optional int, e.g. 18 for a crisp final>,
|
||||
"concurrency": <optional int>,
|
||||
}
|
||||
"""
|
||||
bespoke = edit_decisions.get("bespoke") or {}
|
||||
entry = bespoke.get("entry")
|
||||
comp_id = bespoke.get("composition_id")
|
||||
if not entry or not comp_id:
|
||||
return ToolResult(
|
||||
success=False,
|
||||
error=(
|
||||
"atelier mode requires edit_decisions.bespoke.entry (path to the "
|
||||
"project-local Remotion entry .tsx) and edit_decisions.bespoke."
|
||||
"composition_id (the id registered in that entry's Root)."
|
||||
),
|
||||
)
|
||||
|
||||
composer_dir = Path(__file__).resolve().parent.parent.parent / "remotion-composer"
|
||||
if not composer_dir.exists() or not (composer_dir / "node_modules").exists():
|
||||
return ToolResult(
|
||||
success=False,
|
||||
error=(
|
||||
f"remotion-composer or its node_modules is missing at {composer_dir}. "
|
||||
f"Run `cd remotion-composer && npm install` first."
|
||||
),
|
||||
)
|
||||
|
||||
entry_path = Path(entry)
|
||||
if not entry_path.is_absolute():
|
||||
# Resolve relative to repo root first, then to the composer dir.
|
||||
repo_root = composer_dir.parent
|
||||
cand = (repo_root / entry).resolve()
|
||||
entry_path = cand if cand.exists() else (composer_dir / entry).resolve()
|
||||
entry_path = entry_path.resolve()
|
||||
if not entry_path.exists():
|
||||
return ToolResult(success=False, error=f"atelier entry not found: {entry_path}")
|
||||
|
||||
# The entry must live under remotion-composer/ so Remotion's bundler can
|
||||
# resolve `remotion` and friends from node_modules. Project-local
|
||||
# bespoke compositions therefore live at remotion-composer/projects/<slug>/.
|
||||
try:
|
||||
entry_path.relative_to(composer_dir)
|
||||
except ValueError:
|
||||
return ToolResult(
|
||||
success=False,
|
||||
error=(
|
||||
f"atelier entry {entry_path} must live under {composer_dir} so the "
|
||||
f"Remotion bundler can resolve node_modules. Place bespoke "
|
||||
f"compositions under remotion-composer/projects/<slug>/ (gitignored)."
|
||||
),
|
||||
)
|
||||
|
||||
output_path = Path(inputs.get("output_path", "renders/output.mp4")).resolve()
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
cmd = ["npx", "remotion", "render", str(entry_path), str(comp_id), str(output_path)]
|
||||
|
||||
props_path = bespoke.get("props_path")
|
||||
if props_path:
|
||||
pp = Path(props_path).resolve()
|
||||
if not pp.exists():
|
||||
return ToolResult(success=False, error=f"atelier props_path not found: {pp}")
|
||||
# Equals form is required for cross-platform path parsing (see _remotion_render).
|
||||
cmd.append(f"--props={pp}")
|
||||
|
||||
public_dir = bespoke.get("public_dir")
|
||||
if public_dir:
|
||||
pd = Path(public_dir).resolve()
|
||||
if pd.exists():
|
||||
cmd.append(f"--public-dir={pd}")
|
||||
|
||||
if bespoke.get("scale"):
|
||||
cmd.append(f"--scale={bespoke['scale']}")
|
||||
if bespoke.get("crf") is not None:
|
||||
cmd.append(f"--crf={bespoke['crf']}")
|
||||
if bespoke.get("concurrency"):
|
||||
cmd.append(f"--concurrency={bespoke['concurrency']}")
|
||||
|
||||
try:
|
||||
# Run from inside the composer dir so npx resolves the local
|
||||
# remotion binary (mirrors _remotion_render).
|
||||
self.run_command(cmd, timeout=1800, cwd=composer_dir)
|
||||
except Exception as e:
|
||||
return ToolResult(success=False, error=f"Atelier (bespoke) Remotion render failed: {e}")
|
||||
|
||||
if not output_path.exists():
|
||||
return ToolResult(
|
||||
success=False,
|
||||
error=f"Atelier render completed but output file missing: {output_path}",
|
||||
)
|
||||
|
||||
return ToolResult(
|
||||
success=True,
|
||||
data={
|
||||
"operation": "render",
|
||||
"composition_mode": "atelier",
|
||||
"entry": str(entry_path),
|
||||
"composition_id": comp_id,
|
||||
"output": str(output_path),
|
||||
},
|
||||
artifacts=[str(output_path)],
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _build_theme_from_playbook(
|
||||
playbook_name: str | None,
|
||||
@@ -937,6 +1071,19 @@ class VideoCompose(BaseTool):
|
||||
asset_manifest = inputs.get("asset_manifest")
|
||||
if not edit_decisions:
|
||||
return ToolResult(success=False, error="edit_decisions required for render")
|
||||
|
||||
# --- Atelier (bespoke) mode -------------------------------------
|
||||
# Hand-authored, project-local Remotion composition. Deliberately
|
||||
# bypasses the cut-schema, the stock scene-type registry, and the
|
||||
# RENDERER_FAMILY_MAP. This is the "hand-stitched every time" path:
|
||||
# the agent writes a fresh composition (its own scenes, theme, motion)
|
||||
# under remotion-composer/projects/<slug>/ and points this renderer at
|
||||
# it. No reusable creative components; a new visual language per video.
|
||||
# Triggered by composition_mode="atelier" (or renderer_family="bespoke").
|
||||
if (edit_decisions.get("composition_mode") == "atelier"
|
||||
or edit_decisions.get("renderer_family") == "bespoke"):
|
||||
return self._render_via_atelier(inputs, edit_decisions)
|
||||
|
||||
if not asset_manifest:
|
||||
return ToolResult(success=False, error="asset_manifest required for render")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user