motionrender 0.2.3 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -2951,25 +2951,23 @@ function inferChatTask(messages2, options3) {
2951
2951
  return "compose";
2952
2952
  }
2953
2953
  function createProvider(config = {}) {
2954
- const id2 = (config.provider || process.env.OVE_AI_PROVIDER || (process.env.FREELLMAPI_BASE_URL ? "freellmapi" : "") || (process.env.NVIDIA_API_KEY ? "nvidia" : process.env.GROQ_API_KEY ? "groq" : process.env.ANTHROPIC_API_KEY ? "anthropic" : "")).toLowerCase();
2954
+ const id2 = (config.provider || process.env.OVE_AI_PROVIDER || (process.env.FREELLMAPI_BASE_URL || process.env.FREELLMAPI_API_KEY ? "freellmapi" : "") || (process.env.NVIDIA_API_KEY ? "nvidia" : process.env.GROQ_API_KEY ? "groq" : process.env.ANTHROPIC_API_KEY ? "anthropic" : "") || "freellmapi").toLowerCase();
2955
2955
  switch (id2) {
2956
+ case "mock":
2957
+ return new MockProvider();
2956
2958
  case "freellmapi":
2957
2959
  case "openai":
2958
- return attachGroqAudio(new FreeLlmApiProvider(config), config);
2960
+ return new FreeLlmApiProvider(config);
2961
+ case "groq":
2962
+ return new GroqProvider(config);
2959
2963
  case "nvidia":
2960
2964
  case "nim":
2961
2965
  return attachGroqAudio(new NvidiaProvider(config), config);
2962
2966
  case "anthropic":
2963
2967
  case "claude":
2964
2968
  return attachGroqAudio(new AnthropicProvider(config), config);
2965
- case "groq":
2966
- return new GroqProvider(config);
2967
- case "mock":
2968
- return new MockProvider();
2969
- case "":
2970
- throw new Error("No AI provider chosen: pass --provider freellmapi|nvidia|groq|anthropic|mock, or set FREELLMAPI_BASE_URL, NVIDIA_API_KEY, GROQ_API_KEY or ANTHROPIC_API_KEY");
2971
2969
  default:
2972
- throw new Error(`Unknown AI provider "${id2}"; use freellmapi, nvidia, groq, anthropic or mock`);
2970
+ return new FreeLlmApiProvider(config);
2973
2971
  }
2974
2972
  }
2975
2973
  function describeProvider(provider) {
@@ -3259,6 +3257,65 @@ var init_providers = __esm({
3259
3257
  if (!text) throw new Error("FreeLLMAPI chat returned no text. Check active models and upstream keys in your FreeLLMAPI dashboard.");
3260
3258
  return text;
3261
3259
  }
3260
+ async speak(text, options3 = {}) {
3261
+ const response = await request(`${this.baseUrl}/audio/speech`, {
3262
+ method: "POST",
3263
+ signal: timeoutSignal(options3.signal),
3264
+ headers: jsonHeaders(this.headers()),
3265
+ body: JSON.stringify({
3266
+ model: "auto",
3267
+ input: text,
3268
+ voice: options3.voice ?? "alloy"
3269
+ })
3270
+ }, "FreeLLMAPI speech");
3271
+ return { data: new Uint8Array(await response.arrayBuffer()), extension: "mp3" };
3272
+ }
3273
+ async transcribe(audio, filename, options3 = {}) {
3274
+ const form = new FormData();
3275
+ form.append("file", new Blob([audio]), filename || "audio.mp3");
3276
+ form.append("model", "auto");
3277
+ form.append("response_format", "verbose_json");
3278
+ if (options3.language) form.append("language", options3.language);
3279
+ const response = await request(`${this.baseUrl}/audio/transcriptions`, {
3280
+ method: "POST",
3281
+ signal: timeoutSignal(options3.signal),
3282
+ headers: this.headers(),
3283
+ body: form
3284
+ }, "FreeLLMAPI transcription");
3285
+ const data = await response.json();
3286
+ return {
3287
+ text: String(data?.text ?? ""),
3288
+ words: Array.isArray(data?.words) ? data.words.map((w) => ({
3289
+ text: String(w.word ?? w.text ?? ""),
3290
+ start: Number(w.start) || 0,
3291
+ end: Number(w.end) || 0
3292
+ })) : [],
3293
+ cues: Array.isArray(data?.segments) ? data.segments.map((s) => ({
3294
+ text: String(s.text ?? ""),
3295
+ start: Number(s.start) || 0,
3296
+ end: Number(s.end) || 0
3297
+ })) : []
3298
+ };
3299
+ }
3300
+ async image(prompt, options3 = {}) {
3301
+ const size = options3.size ?? "1024x1024";
3302
+ const response = await request(`${this.baseUrl}/images/generations`, {
3303
+ method: "POST",
3304
+ signal: timeoutSignal(options3.signal),
3305
+ headers: jsonHeaders(this.headers()),
3306
+ body: JSON.stringify({
3307
+ model: "auto",
3308
+ prompt,
3309
+ n: 1,
3310
+ size,
3311
+ response_format: "b64_json"
3312
+ })
3313
+ }, "FreeLLMAPI image");
3314
+ const data = await response.json();
3315
+ const b64 = data?.data?.[0]?.b64_json;
3316
+ if (!b64) throw new Error("FreeLLMAPI did not return image data");
3317
+ return { data: Buffer.from(b64, "base64"), extension: "png" };
3318
+ }
3262
3319
  };
3263
3320
  attachGroqAudio = (provider, config) => {
3264
3321
  if (provider.speak || provider.transcribe || !process.env.GROQ_API_KEY) return provider;
@@ -4430,6 +4487,13 @@ var init_template = __esm({
4430
4487
  };
4431
4488
  templatesFor = (templates, size) => templates.filter((template) => sameShape(template, size));
4432
4489
  systemText = `You fill in ready-made video templates. Reply with JSON only: {"template": "<id>", "theme": "<theme id, only for templates that take a theme>", "values": {"<variable>": <value>, ...}}.
4490
+ Choose the template that best matches the story structure and topic of the brief (stories and stats -> story-tall; features and cards -> showcase-tall; tips and steps -> social-reel).
4491
+ For templates that take a theme, ALWAYS choose the theme that best matches the mood, industry, and visual tone of the brief:
4492
+ - "midnight": modern tech, SaaS, B2B, cloud, finance
4493
+ - "neon": gaming, nightlife, social apps, crypto, cutting-edge AI
4494
+ - "espresso": artisan craft, food & beverage, coffee, lifestyle, podcasts, warmth
4495
+ - "paper": clean editorial, education, publishing, thought leadership, light minimalist
4496
+ - "signal": high-impact, fitness, sports, breaking news, urgency
4433
4497
  Picture variables (image, image2\u2026) take the id of one supplied picture, or "" for none; never invent ids. Examples only show the length and kind of text wanted: never reuse their wording (including labels like INTRODUCING); use the brief's own names, numbers and details, and prefer a concrete line over a generic one such as "Ready to start?". Rules: write the words viewers will read, never a description of the video; do not invent dates, prices or claims that are not in the brief; use only the listed variable names; write real copy for the brief, about as long as each example (never much longer); "color" values are hex like "#f4d58d"; list values are arrays of objects with the listed fields.
4434
4498
  If no template suits the brief, reply {"template": "none"}.`;
4435
4499
  }
@@ -4514,7 +4578,7 @@ function narrationScene(composition, options3) {
4514
4578
  return next;
4515
4579
  }
4516
4580
  async function narrate(provider, options3) {
4517
- if (!provider.speak) throw new Error(`The ${provider.id} provider cannot make speech; use --provider groq (or add a speak() adapter)`);
4581
+ if (!provider.speak) throw new Error(`The ${provider.id} provider cannot make speech (or add a speak() adapter)`);
4518
4582
  const brief = options3.brief?.trim();
4519
4583
  let text = options3.text?.trim() ?? "";
4520
4584
  if (!text && brief) {
@@ -4561,7 +4625,7 @@ async function narrate(provider, options3) {
4561
4625
  return { audio: audioFile, captions: captionsFile, durationSeconds, words: transcript.words.length, composition, text };
4562
4626
  }
4563
4627
  async function captionAudio(provider, options3) {
4564
- if (!provider.transcribe) throw new Error(`The ${provider.id} provider cannot transcribe; use --provider groq (or add a transcribe() adapter)`);
4628
+ if (!provider.transcribe) throw new Error(`The ${provider.id} provider cannot transcribe (or add a transcribe() adapter)`);
4565
4629
  const audioFile = resolve6(options3.audio);
4566
4630
  const data = await readFile8(audioFile);
4567
4631
  options3.onProgress?.("Transcribing\u2026");
@@ -4812,7 +4876,7 @@ function editComposition(options3) {
4812
4876
  return composition;
4813
4877
  }
4814
4878
  async function editVideo(provider, options3) {
4815
- if (!provider.transcribe) throw new Error(`The ${provider.id} provider cannot transcribe; use --provider groq (or add a transcribe() adapter)`);
4879
+ if (!provider.transcribe) throw new Error(`The ${provider.id} provider cannot transcribe (or add a transcribe() adapter)`);
4816
4880
  const clip2 = resolve7(options3.clip);
4817
4881
  const outDir = resolve7(options3.outDir);
4818
4882
  const width = options3.width ?? 1080, height = options3.height ?? 1920, fps = options3.fps ?? 30;
@@ -4920,7 +4984,7 @@ function shortsCandidatePlan(candidate, cut, maxSilence, keepFillers) {
4920
4984
  return cut ? planCuts(slice, { maxSilence, keepFillers, padding: 0.2 }) : planCuts(slice, { maxSilence: Number.MAX_SAFE_INTEGER, keepFillers: true, padding: 0.2 });
4921
4985
  }
4922
4986
  async function shortsFrom(provider, options3) {
4923
- if (!provider.transcribe) throw new Error(`The ${provider.id} provider cannot transcribe; use --provider groq (or add a transcribe() adapter)`);
4987
+ if (!provider.transcribe) throw new Error(`The ${provider.id} provider cannot transcribe (or add a transcribe() adapter)`);
4924
4988
  const clip2 = resolve8(options3.clip);
4925
4989
  const outDir = resolve8(options3.outDir);
4926
4990
  const width = options3.width ?? 1080, height = options3.height ?? 1920, fps = options3.fps ?? 30;
@@ -5048,7 +5112,7 @@ function imageSize(size) {
5048
5112
  return sizes[size.toLowerCase()] ?? size;
5049
5113
  }
5050
5114
  async function generateImage(provider, options3) {
5051
- if (!provider.image) throw new Error(`The ${provider.id} provider cannot generate images; use --provider mock (Groq has no image model; or add an image() adapter)`);
5115
+ if (!provider.image) throw new Error(`The ${provider.id} provider cannot generate images (or add an image() adapter)`);
5052
5116
  const generated = await provider.image(options3.prompt, { size: imageSize(options3.size), signal: options3.signal });
5053
5117
  const file2 = resolve9(/\.(png|jpe?g|webp)$/i.test(options3.out) ? options3.out : `${options3.out}.${generated.extension}`);
5054
5118
  await mkdir12(dirname9(file2), { recursive: true });
@@ -5970,17 +6034,31 @@ var init_plan = __esm({
5970
6034
  init_registry();
5971
6035
  paths = ["template", "json", "react"];
5972
6036
  allowedPaths = (options3) => (options3.paths ?? paths).filter((path) => path !== "react" || options3.allowReact);
5973
- systemText2 = (options3) => `You are the production planner for Open Video Engine. Decide how the brief below should be produced, before anything is generated.
6037
+ systemText2 = (options3) => `You are the creative production planner for Open Video Engine. Decide how the brief below should be produced, before anything is generated.
5974
6038
  Reply with ONE JSON object and nothing else:
5975
6039
  {
5976
6040
  "path": "template" | "json" | "react",
5977
- "template": "<template id>", // only with "template": the one template that suits the brief
6041
+ "template": "<template id>", // only with "template": the one template that best suits the brief's structure and topic
5978
6042
  "theme": "<theme id>", // optional, only with a template that takes a theme
5979
6043
  "idea": "<what the video says, in one line>",
5980
6044
  "beats": [{ "name": "Hook", "onScreen": "what is on screen, concretely", "seconds": 3, "motion": "how it moves in" }],
5981
6045
  "reasons": ["why this path suits this brief"]
5982
6046
  }
5983
- Templates are the primary path: choose "template" whenever one listed template suits the brief's kind and aspect ratio, even when its length differs \u2014 a template's length is its own, and a length difference is not a reason to leave the template route. Choose "json" only when no template fits the kind and aspect: a bespoke composition with scene layers, camera moves and kinetic type. Choose "react" only when allowed: it writes React code for bespoke high-end motion graphics. Every beat needs a concrete picture, not a mood.
6047
+ Templates provide proven layouts, but pick the template that specifically fits the brief's narrative style:
6048
+ - Testimonials, customer stories, quotes, metrics, or personal founder stories: choose "story-tall" or "quote-card".
6049
+ - Multi-feature tours, app UI cards, feature lists, and product showcases: choose "showcase-tall", "product-wide" or "product-card".
6050
+ - Quick tips, 3-step processes, how-to guides, and numbered reels: choose "social-reel".
6051
+ - Company keynotes, presentations, and product launch films: choose "keynote-wide", "launch-wide" or "case-study".
6052
+ - Choose "json" when the brief requires a bespoke kinetic video with custom motion graphics, camera moves, and scene architecture.
6053
+ - Choose "react" only when allowed: it writes React code for bespoke high-end motion graphics.
6054
+
6055
+ Themes: For templates that take a theme, ALWAYS choose the theme matching the tone and brand of the brief:
6056
+ - "midnight": modern tech, SaaS, B2B, cloud, finance (deep slate navy, sky blue).
6057
+ - "neon": gaming, nightlife, social apps, crypto, cutting-edge AI (dark violet, hot magenta, electric lime).
6058
+ - "espresso": artisan craft, food & beverage, coffee, lifestyle, podcasts, warmth (dark espresso, amber gold).
6059
+ - "paper": clean editorial, education, publishing, thought leadership (clean light aesthetic, deep ink).
6060
+ - "signal": high-impact, fitness, sports, breaking news, urgency (bold monochrome with alert red).
6061
+ Every beat needs a concrete picture, not a mood.
5984
6062
  Reply with "path" set to one of: ${allowedPaths(options3).join(" | ")}.
5985
6063
  Available capabilities:
5986
6064
  ${describeCapabilities(options3.registry, options3)}`;
@@ -6115,7 +6193,7 @@ async function cliRegistry() {
6115
6193
  async function composeSmart(provider, options3, mode = {}) {
6116
6194
  const needsFull = mode.full || !!options3.fields?.length || (options3.assets ?? []).some((asset) => asset.type !== "image");
6117
6195
  let plan;
6118
- if (!mode.noPlan) {
6196
+ if (!mode.noPlan && !mode.full && !mode.template) {
6119
6197
  const registry = mode.registry ?? await cliRegistry();
6120
6198
  try {
6121
6199
  plan = await planBuild(provider, { ...options3, registry, maxAttempts: Math.min(options3.maxAttempts ?? 2, 2) });
@@ -6123,13 +6201,16 @@ async function composeSmart(provider, options3, mode = {}) {
6123
6201
  options3.onProgress?.(`Planning skipped (${error.message.split("\n")[0]}); using the built-in routing.`);
6124
6202
  }
6125
6203
  }
6126
- const templatePath = plan ? plan.path === "template" : !needsFull;
6204
+ const templatePath = (plan ? plan.path === "template" : !needsFull) || Boolean(mode.template);
6127
6205
  if (templatePath && !needsFull) {
6128
6206
  try {
6129
6207
  const choices = await templateChoices();
6130
- const picked = plan?.template ? choices.filter((choice) => choice.id === plan.template) : choices;
6131
- const themes = plan?.theme ? (await listThemes()).filter((theme) => theme.id === plan.theme) : await listThemes();
6132
- const filled2 = await composeFromTemplate(provider, picked.length ? picked : choices, { ...options3, themes });
6208
+ const targetTemplateId = mode.template ?? plan?.template;
6209
+ const picked = targetTemplateId ? choices.filter((choice) => choice.id === targetTemplateId) : choices;
6210
+ const targetThemeId = mode.theme ?? plan?.theme;
6211
+ const allThemes = await listThemes();
6212
+ const themes = targetThemeId ? allThemes.filter((theme) => theme.id === targetThemeId) : allThemes;
6213
+ const filled2 = await composeFromTemplate(provider, picked.length ? picked : choices, { ...options3, themes: themes.length ? themes : allThemes });
6133
6214
  if (filled2) {
6134
6215
  const wanted = options3.durationSeconds;
6135
6216
  const current = filled2.composition.durationFrames / filled2.composition.fps;
@@ -7188,7 +7269,9 @@ function createHandlers(env) {
7188
7269
  maxAttempts: integerIn(input.maxAttempts, 3, "maxAttempts", 1, 10),
7189
7270
  onProgress: (message2) => progress(message2)
7190
7271
  };
7191
- const result = await capabilities.composeSmart(ai, options3, { full });
7272
+ const theme = typeof input.theme === "string" && input.theme.trim() ? input.theme.trim() : void 0;
7273
+ const template = typeof input.template === "string" && input.template.trim() ? input.template.trim() : void 0;
7274
+ const result = await capabilities.composeSmart(ai, options3, { full, theme, template });
7192
7275
  let composition = result.composition;
7193
7276
  let narration;
7194
7277
  if (options3.narration) {
@@ -130,6 +130,12 @@ export declare class FreeLlmApiProvider implements AiProvider {
130
130
  private headers;
131
131
  resolveModel(task: ChatTask, explicitModel?: string, signal?: AbortSignal): Promise<string>;
132
132
  chat(messages: ChatMessage[], options?: ChatOptions): Promise<string>;
133
+ speak(text: string, options?: SpeakOptions): Promise<AudioResult>;
134
+ transcribe(audio: Uint8Array, filename: string, options?: {
135
+ language?: string;
136
+ signal?: AbortSignal;
137
+ }): Promise<Transcript>;
138
+ image(prompt: string, options?: ImageOptions): Promise<ImageResult>;
133
139
  }
134
140
  export declare class MockProvider implements AiProvider {
135
141
  id: string;
@@ -56,6 +56,8 @@ export declare function composeSmart(provider: AiProvider, options: ComposeOptio
56
56
  full?: boolean;
57
57
  noPlan?: boolean;
58
58
  registry?: Registry;
59
+ theme?: string;
60
+ template?: string;
59
61
  }): Promise<ComposeResult & {
60
62
  template?: string;
61
63
  source?: Record<string, unknown>;
@@ -30,6 +30,9 @@ export interface JobPaths {
30
30
  export type JobCapabilities = {
31
31
  composeSmart(provider: AiProvider, options: ComposeOptions, mode: {
32
32
  full?: boolean;
33
+ noPlan?: boolean;
34
+ theme?: string;
35
+ template?: string;
33
36
  }): Promise<ComposeResult & {
34
37
  template?: string;
35
38
  source?: Record<string, unknown>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "motionrender",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "description": "JSON-first, frame-deterministic motion graphics video engine: render compositions to MP4, WebM, ProRes, GIF or PNG frames from Node or the command line.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -77,4 +77,4 @@
77
77
  "optional": true
78
78
  }
79
79
  }
80
- }
80
+ }