Defining templates
One composition with typed fields.
A template is an ordinary project document plus a list of fields. Put {{field}} placeholders in the content, then declare each field's type.
npm install @miraiclip/templatesimport { defineTemplate } from "@miraiclip/templates";
project.dispatch({ type: "asset/add", payload: { id: "bg", kind: "video", src: "media/bg.mp4", durationUs: 6_000_000 } });
project.dispatch({ type: "track/add", payload: { id: "main", kind: "video" } });
project.dispatch({ type: "track/add", payload: { id: "titles", kind: "video" } });
project.dispatch({
type: "clip/add",
payload: { kind: "video", id: "bg-clip", trackId: "main", assetId: "bg", startUs: 0, durationUs: 6_000_000 },
});
project.dispatch({
type: "clip/add",
payload: {
kind: "text", id: "headline", trackId: "titles", startUs: 500_000, durationUs: 5_000_000,
text: "Hi {{name}}", fontFamily: "Inter", fontSizePx: 72, color: "#ffffff",
},
});
project.dispatch({
type: "clip/add",
payload: {
kind: "html", id: "badge", trackId: "titles", startUs: 1_000_000, durationUs: 4_000_000,
template: `<div style="background:{{bg}};font:700 {{size}}px sans-serif">{{label}}</div>`,
params: { bg: "{{accent}}", size: "{{size}}", label: "{{discount}}% off" },
widthPx: 600, heightPx: 200,
},
});
const template = defineTemplate({
name: "Promo",
description: "Promo card",
doc: project.toJSON(),
fields: [
{ name: "name", type: "text", maxLength: 20 },
{ name: "accent", type: "color", default: "#ff8c32" },
{ name: "size", type: "number", min: 24, max: 96, integer: true, default: 40 },
{ name: "discount", type: "number", min: 5, max: 90, integer: true },
{ name: "footage", type: "asset", assetId: "bg", required: false },
],
});The result is plain JSON. Save it to a file and load it anywhere, including the CLI.
Template shape
| Field | Required | Meaning |
|---|---|---|
version | yes | 1. defineTemplate sets it |
name | yes | Shown in summaries; the default tool name is derived from it |
doc | yes | A ProjectDocument (project.toJSON()) |
fields | yes | The fields, below |
description | no | One line about the template |
category, tags | no | For grouping templates in a library |
thumbnail | no | An image URL or a small data: URL |
Fields
type | Value | Extra checks |
|---|---|---|
text | string | maxLength, pattern (a RegExp source), enum |
number | number | min, max, integer |
boolean | true / false | none |
color | a CSS color string | Must be a non-empty string. The color itself isn't parsed |
asset | "src" or { src, durationUs? } | assetId: the asset in doc whose src it replaces |
Every field also takes:
| Option | Meaning |
|---|---|
name | The placeholder name: {{name}} |
label | Form label. Defaults to the name |
description | What the field is for. Shown in forms and tool schemas |
required | Defaults to true. A field with a default is never reported missing |
default | Used when the data leaves the field out |
Optional fields need a default
A field with required: false and no default that is left out keeps its {{placeholder}} in the output text. Give optional text fields default: "".
Where placeholders go
| Site | Example |
|---|---|
Text clip text | text: "Hi {{name}}" |
Caption word text | { text: "{{name}}", startUs, durationUs } |
HTML clip params string values | params: { bg: "{{accent}}" } |
Hydration only replaces content. It never changes timing, positions or structure, so it can't produce an invalid document.
| Rule | Detail |
|---|---|
| Whole value | When a param is exactly one placeholder ("{{size}}"), the value keeps its type: a number field puts a number in the param |
| Mixed text | "{{discount}}% off" becomes a string |
HTML template | Not a site. Its {{param}} placeholders belong to the clip's params. Bind fields through the params |
| Asset fields | Use no placeholder. They name an assetId |
Checks
defineTemplate checks the template and throws TemplateValidationError with an issues list ({ path, message }):
| Problem | Example message |
|---|---|
| A placeholder has no field | doc.clips.badge.params.bg: placeholder {{accent}} has no declared field |
| A field is never used | fields.unused: field is never used — no {{unused}} placeholder in the document |
| An asset field points at a missing asset | fields.logo: assetId "nope" is not an asset in the document |
parseTemplate(json) runs the same checks on untrusted JSON, such as a file or a request body, and returns the Template.
import { parseTemplate } from "@miraiclip/templates";
import { readFile } from "node:fs/promises";
const template = parseTemplate(JSON.parse(await readFile("promo.miraiclip-template.json", "utf8")));Authoring helpers
import { describeTemplate, extractFields, scanPlaceholders } from "@miraiclip/templates";
const doc = project.toJSON();
scanPlaceholders(doc);
// [{ name: "accent", path: "clips.badge.params.bg", whole: true },
// { name: "discount", path: "clips.badge.params.label", whole: false },
// { name: "size", path: "clips.badge.params.size", whole: true },
// { name: "name", path: "clips.headline.text", whole: false }]
extractFields(doc);
// [{ name: "accent", type: "text", required: true }, { name: "discount", type: "text", required: true }, …]| Function | Returns |
|---|---|
scanPlaceholders(doc) | Every placeholder site, with its path and whether it is the whole value |
extractFields(doc) | One required text field per placeholder. Refine types by hand. Asset fields aren't proposed |
describeTemplate(template) | A short text summary, for prompts and the CLI |
describeTemplate output for the template above, in a 1280×720 project:
template "Promo" — 1280x720 @ 30fps, 3 clips, 2 tracks
Promo card
fields:
- name (text)
- accent (color, optional, default "#ff8c32")
- size (number, optional, default 40)
- discount (number)
- footage (asset, asset bg, optional)From a finished project
An editor's "Save as template" doesn't need hand-written placeholders. suggestTemplateFields lists what could become a field. templateFromDocument turns the chosen ones into optional fields that default to their current values.
import { hydrate, suggestTemplateFields, templateFromDocument } from "@miraiclip/templates";
const candidates = suggestTemplateFields(project.toJSON());
// [{ kind: "text", name: "summer_sale", label: 'Text "Summer sale"', clipId: "title", value: "Summer sale" },
// { kind: "param", name: "bg", label: "Bg · 50% off", clipId: "card", param: "bg", value: "#112233", color: true },
// { kind: "asset", name: "video", label: 'Video "beach.mp4"', assetId: "beach", assetKind: "video", value: "media/beach.mp4" }]
const template = templateFromDocument(project.toJSON(), {
name: "Sale reel",
category: "promo",
fields: candidates.filter((c) => c.kind !== "param"), // default: all candidates
});
hydrate(template, {}); // the original contentCandidate kind | Source | Becomes |
|---|---|---|
text | A text clip | A text field; the clip's text becomes {{name}} |
param | A string param of an HTML clip (params starting with _ are skipped) | A color field when color is true, else text |
asset | A video, image or audio asset used by a clip | An asset field |
You can rename candidates (name, label) before passing them. The source document isn't modified.
Next: Data & validation.