Miraiclip SDK
Use cases

Personalized video

One video per customer from your data.

One video per customer: a greeting with their name, a card with their plan and usage stats, background music and a credits line. The data comes from a CSV export of your customer table.

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

1. Build the base composition

TimeClipContent
0–3 shello (text)Hi {{name}}, here's your year
3–8 sstats (html)Data card: name, plan, videos made, hours saved
8–10 scredits (text)Credit line for the music
0–10 sbed (audio)Stock music, 1 s fade-out
// build-template.ts (Node)
import { writeFileSync } from "node:fs";
import { createProject, creditsFor, licenseReport } from "@miraiclip/core";
import { defineTemplate } from "@miraiclip/templates";

const CARD = `
<div style="width:1200px;height:640px;box-sizing:border-box;padding:64px;border-radius:32px;
  background:#0f172a;color:#fff;font-family:sans-serif;display:flex;flex-direction:column;gap:32px">
  <div style="font-size:40px;opacity:.7">{{name}} · {{plan}} plan</div>
  <div style="display:flex;gap:96px">
    <div><div style="font-size:120px;font-weight:700">{{videos}}</div><div style="font-size:36px">videos made</div></div>
    <div><div style="font-size:120px;font-weight:700">{{hours}}</div><div style="font-size:36px">hours saved</div></div>
  </div>
</div>`;

const project = createProject({ width: 1920, height: 1080, fps: 30 });

// Stock music with its license (importAudio from @miraiclip/audio-sources sets these for you)
project.dispatch({
  type: "asset/add",
  payload: {
    id: "music", kind: "audio", src: "media/calm-piano.mp3", durationUs: 30_000_000,
    name: "Calm Piano",
    source: { provider: "freesound", id: "123456", url: "https://freesound.org/s/123456/" },
    license: { id: "CC-BY-4.0", commercial: true, attributionRequired: true },
    attribution: "“Calm Piano” by Jane Doe (CC BY 4.0)",
  },
});

project.dispatch({ type: "track/add", payload: { id: "titles", kind: "video" } });
project.dispatch({ type: "track/add", payload: { id: "sound", kind: "audio" } });

project.dispatch({
  type: "clip/add",
  payload: {
    kind: "text", id: "hello", trackId: "titles", startUs: 0, durationUs: 3_000_000,
    text: "Hi {{name}}, here's your year", fontFamily: "sans-serif", fontSizePx: 88, color: "#ffffff",
  },
});
project.dispatch({
  type: "clip/add",
  payload: {
    kind: "html", id: "stats", trackId: "titles", startUs: 3_000_000, durationUs: 5_000_000,
    template: CARD, widthPx: 1200, heightPx: 640,
    params: { name: "{{name}}", plan: "{{plan}}", videos: "{{videos}}", hours: "{{hours}}" },
  },
});
project.dispatch({
  type: "clip/add",
  payload: {
    kind: "audio", id: "bed", trackId: "sound", assetId: "music",
    startUs: 0, durationUs: 10_000_000, fadeOutUs: 1_000_000,
  },
});

// End card with the credit lines the licenses ask for
const credits = creditsFor(project.toJSON());
project.dispatch({
  type: "clip/add",
  payload: {
    kind: "text", id: "credits", trackId: "titles", startUs: 8_000_000, durationUs: 2_000_000,
    text: `Music: ${credits.join("\n")}`, fontFamily: "sans-serif", fontSizePx: 36, color: "#cbd5e1",
  },
});

const issues = licenseReport(project.toJSON(), { commercial: true });
if (issues.length > 0) throw new Error(issues.map((i) => i.message).join("\n"));

const template = defineTemplate({
  name: "year-in-review",
  doc: project.toJSON(),
  fields: [
    { name: "name", type: "text", maxLength: 40 },
    { name: "plan", type: "text", enum: ["Starter", "Pro", "Business"] },
    { name: "videos", type: "number", integer: true, min: 0 },
    { name: "hours", type: "number", min: 0 },
  ],
});

writeFileSync("year-in-review.json", JSON.stringify(template, null, 2));

Stock audio and credits

Check the license before you render for customers. licenseReport(doc, { commercial: true }) reports used assets that are non-commercial (non-commercial), came from a provider without a recorded license (unknown-license), or require attribution but have no credit line (missing-attribution). creditsFor(doc) returns the credit lines to show; here it returns ["“Calm Piano” by Jane Doe (CC BY 4.0)"], which goes into the credits clip. More in Audio.

2. Declare the fields

FieldTypeRuleBinds to
nametextmaxLength: 40hello text and stats param
plantextenum: Starter, Pro, Businessstats param
videosnumberinteger, min: 0stats param
hoursnumbermin: 0stats param

A param whose whole value is one placeholder ("{{videos}}") gets the typed value: videos: 214, a number. Inside longer text ("Hi {{name}}, …") the value is inserted as a string. Numbers print as-is (1204, 37.5). For 1,204 or 37.5 h, format in your data prep and make the field text. See Defining templates.

3. The data

id,name,plan,videos,hours
c_1001,Ada,Pro,214,37.5
c_1002,Grace,Starter,12,2
c_1003,Linus,Enterprise,8,1.5
c_1004,Margaret,Business,1204.5,310

rowsFromCsv converts each declared field's cells to its type. Columns that aren't fields, like id, stay strings; split them off before hydrating, because every key in a row must be a field.

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

export const template = parseTemplate(JSON.parse(readFileSync("year-in-review.json", "utf8")));

// rowsFromCsv coerces declared fields (videos, hours) to numbers; other columns stay strings
export const customers = rowsFromCsv(template, readFileSync("customers.csv", "utf8")).map(({ id, ...data }) => ({
  id: String(id),
  data,
}));
// [{ id: "c_1001", data: { name: "Ada", plan: "Pro", videos: 214, hours: 37.5 } }, …]

4. Hydrate and validate

// check.ts (Node)
import { tryHydrate } from "@miraiclip/templates";
import { customers, template } from "./customers";

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

Output for the CSV above:

c_1003 [ { path: 'data.plan', message: 'must be one of: Starter, Pro, Business' } ]
c_1004 [ { path: 'data.videos', message: 'expected an integer' } ]

Fix the data, or let those rows fail and render the rest. More in Data & validation.

5. Preview one customer

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

const template = parseTemplate(await (await fetch("/templates/year-in-review.json")).json());
const project = createProject(hydrate(template, { name: "Ada", plan: "Pro", videos: 214, hours: 37.5 }));

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

timeUs: 5_000_000 lands on the data card. To hear the music, mount a player instead: Preview.

6. Render every customer

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/year-in-review.json")).json());
const customers: { id: string; data: TemplateData }[] = await (await fetch("/api/year-in-review")).json();

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

/api/year-in-review stands for your backend returning the rows from step 3. download is the helper from Export UI.

On a server

// render.ts (Node)
import { renderTemplateBatch } from "@miraiclip/templates/render";
import { customers, template } from "./customers";

const { failed } = await renderTemplateBatch(template, customers.map((c) => c.data), {
  out: (_data, index) => `out/${customers[index].id}.mp4`,
  format: "mp4",
  quality: "high",
  concurrency: 2,
  onProgress: ({ rowsDone, totalRows, row }) =>
    console.log(`${rowsDone}/${totalRows}`, customers[row.index].id, row.ok ? "ok" : row.error),
});

if (failed.length > 0) process.exitCode = 1;

Invalid rows are reported with row.issues and row.error (invalid data: data.plan: must be one of: …); the other rows still render. See Batch rendering.

From the CLI

The CLI passes every CSV column to the template, so the id column would fail every row as an unknown field. Drop it and name files by row:

npx miraiclip-templates render year-in-review.json \
  --data customers-no-id.csv --out "out/{index}-{name}.mp4" --quality high

Output naming

ApproachFile
out function with your customer idout/c_1001.mp4
"out/{index}-{name}.mp4"out/0-Ada.mp4

Two customers with the same name map to the same {name} path; include {index} or an id. Rules: Output paths.

Variations

ToDo this
Show a customer photoAn image asset used as <img src="asset:avatar"> in the card, and an asset field for it
Format numbersFormat in data prep (Intl.NumberFormat) and declare the field as text
Change the accent per planA color field set from the plan in data prep
Generated voiceover per customerGenerate audio per row with @miraiclip/audio-sources, store it, and pass its URL to an asset field as { src, durationUs }. See Audio
Your own musicA plain upload (no source, license or attribution) adds no credit line and no license issue
Drop the credits clipOnly when creditsFor(doc) is empty

On this page