---
name: template-registry
description: |
  Video-template registry skill. Stores every video-template definition and lists the available templates (templateId / name / aspect ratio / style tags).

  Use this skill as soon as the user mentions any of these intents:
  - View available templates / list every template

  Note: DSL→TemplateBinding is no longer a separate exposed step — once prepare_video_assets receives a template_id it builds the binding internally.
triggers:
  - View available templates / list every template
---

# Video Template Registry & Binding Skill

Stores every video-template definition, loads a template by **template-id**, and produces a **TemplateBinding** for a **Video DSL**, mapping each scene to a template slot.

## Core concepts

- **Template Manifest**: template metadata describing supported aspect ratios, durations, styles, and scene patterns.
- **TemplateBinding**: the binding result — maps each DSL scene to a template slot + Remotion Composition.
- **Template registry**: `registry.json` shipped by the `@ab-templates/metadata` package (housed in the template-library repo).

## Template registry

Template metadata is authored in the standalone **template-library** monorepo, published to ab-api, and served to the CLI over HTTP. At runtime template-registry loads the registry from a single source — the ab-api endpoint (see the env table below) — which returns every template definition (full slotMapping, compositions, etc.) including the caller's private/tenant templates.

> The `template-library/packages/metadata/registry.json` file below is the authoring layout in the monorepo. It is **not** read at runtime anymore; only monorepo-only maintenance tooling (`check_contracts.py`, `sync_registry.py`) touches it directly.

```
template-library/packages/
├── templates/src/
│   ├── html-slide/        # Knowledge-board template
│   │   ├── Composition.tsx
│   │   └── template.json
│   ├── image-slide/       # Basic image + narration template (includes the tech variant)
│   │   └── ...
│   ├── picture-book-en/   # English picture-book video template
│   │   └── ...
│   └── screen-walkthrough/ # Screen-recording walkthrough template
│       └── ...
└── metadata/
    └── registry.json      # Aggregated registry (every template.json merged)
```

## Agent behavior: image URL handling

**DSL AssetRef entries must use HTTPS URLs. Local file paths are forbidden.**

HTTPS URLs serve two phases:
1. **Resolve phase** (`--resolve-only`): the agent previews already-generated images via the HTTPS URL and asks the user to confirm.
2. **Render phase** (`--render-plan`): `render_video.py` automatically downloads each image from its HTTPS URL into `output/public-assets/` before launching Remotion, then exposes it through `--public-dir` so Remotion loads everything locally with no external network.

- **Forbidden**: writing local paths like `./image_1.png` or `/tmp/image_1.png` into the DSL (the resolve phase cannot preview them and the script cannot reuse them).
- **Forbidden**: using WebFetch against internal image domains (blocked by security policy).
- **Allowed**: writing raw HTTPS URLs directly into the DSL AssetRef.
- **Allowed**: downloading an image locally with `curl` when you need to read the content, but the DSL still carries the original HTTPS URL.

## Authentication & environment

The skill itself only reads the registry; whether a token is needed depends on the configured source.

| Env var | Description | Default |
|---------|-------------|---------|
| `VIDEO_TEMPLATE_REGISTRY_URL` | ab-api endpoint returning the registry (the single source of truth). | derived from `MM_API_BASE_URL` |
| `MM_API_BASE_URL` | ab-api base URL the CLI talks to. When `VIDEO_TEMPLATE_REGISTRY_URL` is unset, the registry endpoint is derived as `<base>/remotionTemplate/registry`. | `https://api.remixmate.ai/api` |
| `PRIV_TOKEN` | Sent as `X-Priv-Token` when hitting the registry endpoint. Falls back to `~/.config/remixmate/credentials.json`. | unset |
| `VIDEO_TEMPLATE_REGISTRY_HTTP_METHOD` | `POST` (default) or `GET`. POST shape matches ab-api `{code,msg,data}`. | `POST` |

Resolution (single source — see `scripts/registry_loader.py` for the canonical implementation):
the registry is loaded **only** from the ab-api HTTP endpoint — explicit `VIDEO_TEMPLATE_REGISTRY_URL`, otherwise `<MM_API_BASE_URL>/remotionTemplate/registry` (default `https://api.remixmate.ai/api/...`). There is no local-file / monorepo / PREFER_LOCAL fallback: those multi-source paths were removed to avoid registry skew (private/multi-tenant templates only exist on ab-api). On a transient HTTP failure the loader degrades to the on-disk cache of the same URL; with no cache it fails loud. A standalone install (Codex / `npm i -g`) just needs `MM_API_BASE_URL` (or `VIDEO_TEMPLATE_REGISTRY_URL`) pointed at your ab-api plus a valid `PRIV_TOKEN`.

## Steps

> This skill is a Python skill of the remixmate CLI (`entry.type: python` → `scripts/list_templates.py`). The agent tool name `template_registry` is the only entry; locally reproduce via `remixmate template-registry --list-templates`. The list command delegates to `scripts/registry_loader.py` — the same loader (with caching + stable/beta gating) that `render-video` and `gen-script` import in-process, so there is a single registry-reading implementation.
>
> The Python binding logic that maps DSL → TemplateBinding lives in `scripts/match_template.py` but is **not exposed as a CLI** — it is only consumed as a Python library by `render-video`'s `render_video.py` via `import match_template`.

1. **List available templates**: run `remixmate template-registry --list-templates` to view the templates in the registry along with their supported aspect ratios / style tags, and decide which `templateId` to pick.
2. **Read that template's contract**: run `remixmate template-registry --template-id <id> --json-output` for the full definition (llmHint, `customPayloadSchema` with the legal `slideId` values, `slotMapping`, `variants`). The list view only carries a 200-char llmHint preview, which is not enough to author against.
3. **Write the DSL**: when generating the Video DSL, put the chosen `templateId` into `meta.templateId` (the canonical location). Use `meta.templateVariant` / `renderHints.templateVariant` to explicitly select a variant. The legacy `renderHints.templatePreference[0]` is still tolerated by `match_template.py` and `dsl_validator._pick_template_id` during transition, but new authors should write `meta.templateId`.
4. **Produce the TemplateBinding**: there is no standalone CLI for DSL → TemplateBinding; `prepare_video_assets` calls `match_template.build_binding(template, dsl)` inline during the asset-resolution pipeline and embeds the binding into the RenderPlan it hands to the renderer — no separate `*.binding.json` file is written.

> Design trade-off: collapsing the binding step into the asset-prep pipeline (no CLI, no on-disk artifact) avoids binding files drifting between the agent, the database, and the file system; any hand-edited `.binding.json` would never be consumed by the renderer anyway. For local debugging you can still `import match_template.build_binding` from Python.

### List available templates

```bash
remixmate template-registry --list-templates
```

### Sync the registry cache (optional, used for offline / LLM prompt)

```bash
python3 <SkillDir>/scripts/sync_registry.py
```

`sync_registry.py` and `check_contracts.py` are **maintenance helpers** for template-library, not skill entry points. Keep invoking them directly via `python3`.

### Verify template / Remotion contract consistency

```bash
python3 <SkillDir>/scripts/check_contracts.py
```

### Build a binding locally (debug only, not part of the render pipeline)

```python
import json, sys
sys.path.insert(0, "<SkillDir>/scripts")
from match_template import build_binding, load_full_template, load_registry

dsl = json.load(open("my-video.dsl.json"))
template = next(t for t in load_registry() if t["templateId"] == "image-slide")
binding = build_binding(load_full_template(template), dsl)
print(json.dumps(binding, ensure_ascii=False, indent=2))
```

## Common CLI flags

| Flag | Description | Default |
|------|-------------|---------|
| `--list-templates` | List every available template as a **summary** (templateId / name / description / aspect ratios / language / status / styleTags / variantIds / the `capabilities` keys that drive authoring — `payloadStyle` / `needsNarration` / `durationStrategy` / `narrationDriver` — and llmHint truncated to 200 chars). | — |
| `--template-id <id>` | Print that template's **full definition** — llmHint in full, `customPayloadSchema`, `slotMapping`, `compositions`, `variants`. Repeatable. | — |
| `--full` | List mode: emit full definitions instead of summaries. Refuses when the result would exceed 60 K characters. | off |
| `--filter-tag` | Keep only templates whose `styleTags` match this substring (case-insensitive). | none |
| `--filter-aspect` | Keep only templates declaring this aspect ratio (e.g. `9:16`). | none |
| `--filter-language` | Keep only templates whose `contentLanguage` includes this code (`zh`/`en`); language-agnostic templates always show. | none |
| `--include-beta` | Also show `status: beta` templates (same effect as `ENABLE_BETA_TEMPLATES=1`). | off |
| `--json-output` | Emit `{ "templates": [...] }` instead of the table — summaries, or full definitions under `--template-id` / `--full`. | off |

### List vs. detail

A registry row is 4–17 KB of JSON, so dumping every full definition at once overflows an LLM tool result (the caller sees "exceeds maximum allowed tokens" instead of the contract it asked for). The list verb therefore returns summaries — enough to *choose* a template — and `--template-id` returns the one definition you need to *author* for it:

```bash
remixmate template-registry --list-templates                         # choose
remixmate template-registry --template-id html-slide --json-output   # then read its contract
```

This mirrors the list/detail split described in `scripts/registry_loader.py` (P1.2), realized CLI-side so it holds even while the backend still serves one merged registry payload.

## Props extraction rules

`match_template.py` converts each DSL scene into a `props` dict for the renderer. Extraction runs through **two paths** (highest priority first). Earlier versions also had two fallback layers that did "implicit pass-through of fields named in `requiredProps ∪ optionalProps`"; those were removed in P1.3 because the priority conflict between the allowlist and propExtractors confused authors. Only the two most explicit paths remain:

### Extraction paths

1. **`slotMapping[purpose].propExtractors` (explicit extraction)**
   - Looks like `"titleText": { "from": "textLayers", "role": "headline" }`.
   - Two forms are supported: `{ from: "textLayers", role: "..." }` matches a textLayer by role; `{ from: "a.b.c" }` reads a value from the scene by dotted path.
2. **`customPayload` full-field pass-through (no allowlist check)**
   - First `scene.customPayload.templateData.*` is injected; then `scene.customPayload.*` (except `templateData` itself). The latter overrides same-named values from the former, but neither can override fields already produced by propExtractors.
   - This is the channel for templates such as html-slide that define their own sub-schema and need to pass `slideId` / `items` / `highlightMap`, etc.
   - `scene.templateData` (without the `customPayload` wrapper) is also recognized as a `templateData` source — equivalent shorthand.
   - **Note**: `requiredProps` / `optionalProps` are currently **just template-schema documentation**; they trigger no implicit pass-through. A field reaches `props` either through a propExtractor or through `customPayload`.

### Which form to use

| Field type | Form | Example |
|---|---|---|
| Comes from the standard DSL shape (textLayers, audio.narration.assetRef) and needs **renaming / re-routing** | declare in `propExtractors` | `titleText ← textLayers[role=headline]` |
| Comes from a scene-level custom field whose name matches a prop directly | put it in `customPayload.<propName>` | `customPayload.slideId`, `customPayload.background`, `customPayload.bullets` |
| Structured data internal to the template (arrays, sub-objects, etc.) | use `customPayload.templateData.*` | `templateData.concepts`, `templateData.items` (shared by feature-grid / timeline / column-compare) |

**Common pitfall**: writing the same field in both `propExtractors` and `customPayload` — when they collide, `propExtractors` wins. Keep each field in exactly one place. `build_binding` now **detects this**: any prop name that is both declared in `propExtractors` and hit by `customPayload` pass-through (top-level, or `templateData` when the extractor reads from elsewhere) is recorded on the binding row as `dualChannelProps` and printed as a stderr warning; `test-template-pipeline.py` surfaces it as a non-fatal authoring warning. A field placed in its extractor's own declared source (e.g. extractor `from:"templateData.slideId"` + value in `templateData.slideId`) is **not** flagged — that is the legitimate source, not a double-write.

> Historical issue: an early html-slide `point` slot declared `slideId` as `propExtractors.slideId={from:"templateData.slideId"}` and also marked it as `requiredProps`. But every existing DSL wrote `slideId` at `customPayload.slideId` (top-level, not inside templateData), so the propExtractor returned nothing and rendering fell back to the generic layout. The convention now is unified: **slideId flows through customPayload top-level pass-through**, propExtractors no longer declares it. New templates must follow the same convention.

## Narration for multi-card scenes (html-slide template)

When `html-slide` selects `demo-concept-overview` / `feature-grid` / `timeline` / `column-compare` (all "many-card sequential highlight" slides), you **must** use structured narration to avoid highlight/audio drift:

```jsonc
"audio": {
  "narration": {
    "intro": "Hermes Agent has five core capabilities.",   // optional: while this line plays, no card is highlighted
    "items": [                                              // one line per card
      "Persistent memory remembers everything across sessions.",
      "More than forty built-in tools cover search, files, image generation, and more.",
      "Plugs into Telegram, WeChat, and other channels.",
      "Built-in task scheduling.",
      "Extends to unlimited capabilities via the MCP protocol."
    ],
    "outro": "",                                            // optional
    "assetRef": "narration-overview"
  }
}
```

Rules:
- `items.length` must equal `customPayload.templateData.<concepts|items>.length` (demo-concept-overview uses `concepts`; the other three slides use `items`), otherwise `dsl_validator` rejects it.
- Do not write `templateData.highlightMap`; `render_video.py` builds it automatically from the per-line timestamps that TTS returns.
- If the author explicitly provides a `highlightMap`, the system keeps it as-is.
- The legacy flat `narration.text` form still works but the author then has to keep `highlightMap` in sync with the subtitle segmentation (error-prone, not recommended).
- `narration.languageBoost` (optional, also accepted at `global.narration`) pins the TTS language hint (`auto` / `Chinese` / `English` / …). Leave it unset unless auto-detection gets it wrong — the service applies its own default, and a scene-level value overrides the global one, exactly like `speed`.

## Image sources (picture-book-en and every other template)

Image assets support two source forms:

### AI-generated image

```jsonc
{
  "assetId": "img-page1",
  "type": "image",
  "source": "gen-image",
  "status": "planned",
  "payload": {
    "prompt": "Children's picture book illustration...",
    "ratio": "1:1"
  }
}
```

### User-supplied link (use an existing image directly)

```jsonc
{
  "assetId": "img-page1",
  "type": "image",
  "source": "existing",
  "status": "ready",
  "url": "https://cdn.example.com/my-image.jpg"
}
```

**Rules:**
- `source: "existing"` + `status: "ready"` + top-level `url` → resolver uses the URL as-is, no generation API call.
- `source: "gen-image"` + `status: "planned"` + `payload.prompt` → resolver calls the gen-image skill.
- Local paths (`./image.png`, `/tmp/...`) are forbidden; HTTPS URLs only.
- Both forms can be mixed inside the same DSL — each asset decides independently.

## Picture-book narration rule (picture-book-en template)

With `picture-book-en`, **narration reads English only, never Chinese**:

- `audio.narration.text` and the corresponding `gen-voice` asset's `payload.text` **must contain only the English source** (ASCII letters + standard English punctuation); any CJK character or Chinese punctuation is forbidden.
- The Chinese translation appears only on screen via `textLayers[role=subheadline].content` (mapped to `chineseTitle` / `chineseText`); it never enters TTS.
- Layout is fixed:
  - **Cover**: top half is the title area (English `englishTitle` on top in a large round-bold font; Chinese `chineseTitle` below in a smaller font); bottom half is the cover image / video.
  - **Content page**: top half is the text area (English `englishText` on top in a large round-bold font; Chinese `chineseText` below); bottom half is the illustration.
- `textLayers` role mapping is mandatory: `role=headline` carries the English text, `role=subheadline` carries the Chinese text.

## Error handling

- **No template specified**: the DSL must set `meta.templateId` (or, for legacy DSLs only, `renderHints.templatePreference[0]`); otherwise `prepare_video_assets` refuses to build a binding and prints the list of available templates.
- **Malformed DSL**: validate first with `gen-script --validate` (delegates to `video_dsl.runtime.dsl_validator.validate_structural`).
- **Empty template registry**: confirm the ab-api registry endpoint (`VIDEO_TEMPLATE_REGISTRY_URL`, or the one derived from `MM_API_BASE_URL`) is reachable and that `PRIV_TOKEN` is valid.
