Miraiclip SDK
Use cases

Catalog to product video

Product feeds into on-brand video ads.

A product feed (title, price, image URL, brand color) becomes one short video per product. The template is a product card; each row swaps the photo, the text and the color.

npm install @miraiclip/core @miraiclip/renderer @miraiclip/templates
npm install @miraiclip/server-export # only for server rendering

1. Build the base composition

The card is an html clip. It draws the product photo with src="asset:product", so CSS (object-fit: contain) fits any photo into the same box.

// build-template.ts (Node)
import { writeFileSync } from "node:fs";
import { createProject } from "@miraiclip/core";
import { defineTemplate } from "@miraiclip/templates";

const CARD = `
<div style="width:1080px;height:1080px;box-sizing:border-box;padding:80px;background:{{brand}};
  display:flex;flex-direction:column;align-items:center;gap:40px;font-family:sans-serif;color:#fff">
  <img src="asset:product" style="width:920px;height:640px;object-fit:contain">
  <div style="font-size:64px;font-weight:700;text-align:center">{{title}}</div>
  <div style="font-size:56px">{{price}}</div>
</div>`;

const project = createProject({ width: 1080, height: 1080, fps: 30 });
project.dispatch({ type: "asset/add", payload: { id: "product", kind: "image", src: "media/placeholder.png" } });
project.dispatch({ type: "track/add", payload: { id: "card", kind: "video" } });
project.dispatch({
  type: "clip/add",
  payload: {
    kind: "html", id: "card", trackId: "card", startUs: 0, durationUs: 5_000_000,
    template: CARD,
    params: { brand: "{{brand}}", title: "{{title}}", price: "{{price}}" },
  },
});

const template = defineTemplate({
  name: "product-card",
  category: "catalog",
  doc: project.toJSON(),
  fields: [
    { name: "title", type: "text", maxLength: 60 },
    { name: "price", type: "text", description: "Formatted price, e.g. $129.00" },
    { name: "brand", type: "color", default: "#111827" },
    { name: "image", type: "asset", assetId: "product", description: "Product photo URL or path" },
  ],
});

writeFileSync("product-card.json", JSON.stringify(template, null, 2));

An image clip would also work, but it draws the image at its own pixel size, so photos of different sizes would show at different sizes. The html clip keeps the layout fixed.

2. Declare the fields

FieldTypeRuleBinds to
titletextmaxLength: 60card html param
pricetextalready formattedcard html param
brandcolordefault #111827card html param (background)
imageassetrequiredreplaces the product asset's src

Fields are required unless they have a default or required: false. Param values are HTML-escaped, so a title like Tom & Jerry renders as text. See Defining templates.

3. Prepare the data

Templates have no formatters: a value is inserted as given. Format prices (currency, separators, locale) before hydrating, and keep ids like the SKU outside the row, since every key in a row must be a declared field.

[
  { "sku": "LMP-01", "title": "Brass desk lamp", "price": 129, "currency": "USD", "image": "https://cdn.example.com/lmp-01.jpg", "brandColor": "#1f6feb" },
  { "sku": "MUG-07", "title": "Stoneware mug", "price": 18.5, "currency": "EUR", "image": "https://cdn.example.com/mug-07.jpg", "brandColor": "#c2410c" },
  { "sku": "CHR-12", "title": "Oak lounge chair", "price": 1299, "currency": "USD", "image": "media/chr-12.jpg", "brandColor": "#15803d" }
]
// items.ts (Node)
import { readFileSync } from "node:fs";
import type { TemplateData } from "@miraiclip/templates";

interface Product {
  sku: string;
  title: string;
  price: number;
  currency: string;
  image: string;
  brandColor: string;
}

const products: Product[] = JSON.parse(readFileSync("products.json", "utf8"));

export const items = products.map((p) => ({
  sku: p.sku,
  data: {
    title: p.title,
    price: new Intl.NumberFormat("en-US", { style: "currency", currency: p.currency }).format(p.price),
    image: p.image,
    brand: p.brandColor,
  } satisfies TemplateData,
}));
// [{ sku: "LMP-01", data: { title: "Brass desk lamp", price: "$129.00", image: "https://cdn.example.com/lmp-01.jpg", brand: "#1f6feb" } },
//  { sku: "MUG-07", data: { …, price: "€18.50", … } },
//  { sku: "CHR-12", data: { …, price: "$1,299.00", image: "media/chr-12.jpg", … } }]

The same rows as CSV, for the CLI. Quote cells that contain commas; an empty cell uses the default.

title,price,brand,image
Brass desk lamp,$129.00,#1f6feb,https://cdn.example.com/lmp-01.jpg
Stoneware mug,€18.50,,https://cdn.example.com/mug-07.jpg
Oak lounge chair,"$1,299.00",#15803d,media/chr-12.jpg

4. Hydrate and validate

// check.ts (Node)
import { readFileSync } from "node:fs";
import { parseTemplate, tryHydrate } from "@miraiclip/templates";
import { items } from "./items";

const template = parseTemplate(JSON.parse(readFileSync("product-card.json", "utf8")));

for (const { sku, data } of items) {
  const result = tryHydrate(template, data);
  if (!result.ok) console.error(sku, result.issues);
}

A row without an image fails with { path: "data.image", message: "required field is missing" }. Validation checks the values, not the media: a broken image URL fails at render time, in that row only.

5. Preview one product

import { createProject } from "@miraiclip/core";
import { renderProjectStill } from "@miraiclip/renderer";
import { hydrate, parseTemplate } from "@miraiclip/templates";

const template = parseTemplate(await (await fetch("/templates/product-card.json")).json());
const project = createProject(
  hydrate(template, {
    title: "Brass desk lamp",
    price: "$129.00",
    image: "https://cdn.example.com/lmp-01.jpg",
  }),
);

const png = await renderProjectStill(project, { timeUs: 1_000_000, width: 540, height: 540 });
document.querySelector("img")!.src = URL.createObjectURL(png);

The page fetches the image, so a remote image host must allow cross-origin requests (CORS). For playback, see Preview.

6. Render the catalog

In the browser

import { createProject } from "@miraiclip/core";
import { exportProject } from "@miraiclip/renderer";
import { hydrate, parseTemplate, type TemplateData } from "@miraiclip/templates";

declare function download(bytes: Uint8Array, filename: string): void; // see Export UI

const template = parseTemplate(await (await fetch("/templates/product-card.json")).json());
const items: { sku: string; data: TemplateData }[] = await (await fetch("/api/catalog-items")).json();

for (const { sku, data } of items) {
  const project = createProject(hydrate(template, data));
  download(await exportProject(project, { format: "mp4" }), `${sku}.mp4`);
}

/api/catalog-items stands for your backend returning the rows from step 3. download is the helper from Export UI.

On a server

// render.ts (Node)
import { readFileSync } from "node:fs";
import { parseTemplate } from "@miraiclip/templates";
import { renderTemplateBatch } from "@miraiclip/templates/render";
import { items } from "./items";

const template = parseTemplate(JSON.parse(readFileSync("product-card.json", "utf8")));

const { rows, failed } = await renderTemplateBatch(template, items.map((item) => item.data), {
  out: (_data, index) => `out/${items[index].sku}.mp4`,
  format: "mp4",
  concurrency: 2,
});

console.log(`${rows.length - failed.length}/${rows.length} rendered`);
for (const row of failed) console.error(items[row.index].sku, row.error);
Image valueResolved from
https://… or data: URLFetched as-is
A relative path (media/chr-12.jpg)assetsDir (default: the working directory)

Every asset in the document must resolve, including ones a row doesn't swap. See Batch rendering.

From the CLI

npx miraiclip-templates render product-card.json --data products.csv --out "out/{index}.mp4" --concurrency 2

The CLI names files from row values or {index}. To name by SKU, use renderTemplateBatch with an out function as above. Naming rules: Output paths.

Variations

ToDo this
Show a sale priceAdd salePrice (text) and format it in step 3
Animate the cardAdd keyframes or an animation preset to the card clip before defineTemplate. See Keyframes
Add a brand logoA second asset field and an <img src="asset:logo"> in the card
Product video instead of a photoA video asset and clip; pass { src, durationUs } per row. For media shorter than the clip, run fitClipsToMedia on the hydrated document before exporting (renderTemplateBatch hydrates internally and doesn't apply it). See Asset fields
Square and verticalOne template per size, as in Ad variants
Localized pricesPass the locale to Intl.NumberFormat per market

On this page