Miraiclip SDK
Templates

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/templates
import { 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

FieldRequiredMeaning
versionyes1. defineTemplate sets it
nameyesShown in summaries; the default tool name is derived from it
docyesA ProjectDocument (project.toJSON())
fieldsyesThe fields, below
descriptionnoOne line about the template
category, tagsnoFor grouping templates in a library
thumbnailnoAn image URL or a small data: URL

Fields

typeValueExtra checks
textstringmaxLength, pattern (a RegExp source), enum
numbernumbermin, max, integer
booleantrue / falsenone
colora CSS color stringMust 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:

OptionMeaning
nameThe placeholder name: {{name}}
labelForm label. Defaults to the name
descriptionWhat the field is for. Shown in forms and tool schemas
requiredDefaults to true. A field with a default is never reported missing
defaultUsed 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

SiteExample
Text clip texttext: "Hi {{name}}"
Caption word text{ text: "{{name}}", startUs, durationUs }
HTML clip params string valuesparams: { bg: "{{accent}}" }

Hydration only replaces content. It never changes timing, positions or structure, so it can't produce an invalid document.

RuleDetail
Whole valueWhen 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 templateNot a site. Its {{param}} placeholders belong to the clip's params. Bind fields through the params
Asset fieldsUse no placeholder. They name an assetId

Checks

defineTemplate checks the template and throws TemplateValidationError with an issues list ({ path, message }):

ProblemExample message
A placeholder has no fielddoc.clips.badge.params.bg: placeholder {{accent}} has no declared field
A field is never usedfields.unused: field is never used — no {{unused}} placeholder in the document
An asset field points at a missing assetfields.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 }, …]
FunctionReturns
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 content
Candidate kindSourceBecomes
textA text clipA text field; the clip's text becomes {{name}}
paramA string param of an HTML clip (params starting with _ are skipped)A color field when color is true, else text
assetA video, image or audio asset used by a clipAn asset field

You can rename candidates (name, label) before passing them. The source document isn't modified.

Next: Data & validation.

On this page