@docyrus/animaguru 0.19.0 → 0.20.0

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.
Files changed (32) hide show
  1. package/README.md +36 -0
  2. package/dist/animaguru.js +101 -10
  3. package/package.json +1 -1
  4. package/skills/animaguru/SKILL.md +8 -0
  5. package/skills/animaguru/references/sharing.md +3 -0
  6. package/skills/animaguru-branching-story/SKILL.md +72 -0
  7. package/skills/animaguru-general-video/SKILL.md +1 -0
  8. package/skills/animaguru-general-video/references/branching-story.md +13 -0
  9. package/skills/animaguru-quiz/SKILL.md +80 -0
  10. package/skills/animaguru-video/SKILL.md +9 -1
  11. package/templates/video/AGENTS.md +2 -0
  12. package/templates/video/README.md +8 -0
  13. package/templates/video/WORKSPACE-AGENTS.md +3 -1
  14. package/templates/video/scripts/build.mjs +4 -1
  15. package/templates/video/scripts/export-mp4.mjs +2 -0
  16. package/templates/video/scripts/html-export.mjs +12 -4
  17. package/templates/video/scripts/project-context.mjs +20 -3
  18. package/templates/video/scripts/quiz.d.mts +31 -0
  19. package/templates/video/scripts/quiz.mjs +126 -0
  20. package/templates/video/scripts/review.mjs +9 -1
  21. package/templates/video/scripts/story.d.mts +36 -0
  22. package/templates/video/scripts/story.mjs +139 -0
  23. package/templates/video/scripts/workspace-plugin.mjs +9 -4
  24. package/templates/video/shared/locales/en/player.json +29 -1
  25. package/templates/video/src/kit/index.ts +1 -0
  26. package/templates/video/src/kit/story.ts +19 -0
  27. package/templates/video/src/main.ts +6 -1
  28. package/templates/video/src/player-types.ts +6 -0
  29. package/templates/video/src/player.ts +193 -17
  30. package/templates/video/src/quiz.ts +315 -0
  31. package/templates/video/src/story.ts +112 -0
  32. package/templates/video/src/style.css +87 -0
package/README.md CHANGED
@@ -404,6 +404,32 @@ animaguru reference get va_… --wait
404
404
 
405
405
  The video is a local MP4, MOV or WebM file up to 100 MB and ten minutes, or an HTTPS link to the video file itself. Pages on YouTube, Instagram or TikTok cannot be fetched; download the video you have the rights to use and pass the file. An analysis takes several minutes: about eight for a 6-second clip, because Glimpse inspects the frames with code over many steps. Inside a video project it is saved to `production/references/NAME.json`, elsewhere to `NAME.analysis.json` (or `--out FILE`). It is charged what it costs, about 22 to 36 credits for a 6-second clip in testing and more for a longer video, after holding 200 credits (300 with `--effort medium`, 500 with `xhigh`). MCP clients use `analyze_video` with a link and poll `get_video_analysis`.
406
406
 
407
+ ## Branching stories
408
+
409
+ CLI 0.20.0 makes branching stories: films whose viewers choose what happens next and reach one of several endings. A `story.json` beside the film's `film.config.json` lays its segments end to end on the timeline and says where each leads: to the viewer's choices, on to another segment, or to an ending.
410
+
411
+ ```sh
412
+ animaguru create animation lantern --story # a story.json over the starter film: one choice, two endings
413
+ npm run dev # play it; ?segment=ID starts at a segment
414
+ npm run review -- --animation lantern --headless # every segment, plus the story's playthroughs
415
+ animaguru submit --animation lantern --yes # uploads it as an interactive HTML5 film
416
+ ```
417
+
418
+ The player shows the choices over the picture, with an optional timer that takes a default, and an ending panel with Play again and Back to the last choice. A branching story plays as HTML5 only: `npm run export:mp4` refuses it, and the cloud Animator offers no MP4 of it. The `animaguru-branching-story` skill has the format.
419
+
420
+ ## Quizzes
421
+
422
+ CLI 0.20.0 also makes quiz films, for educational explainers, lessons and training: a `quiz.json` beside `film.config.json` pauses the film at checkpoints to ask one question or a quick round (for example after every scene), and asks a final quiz after its end, then shows the viewer's score. Questions have one right answer (multiple choice or true/false) or several (select all that apply), an optional explanation, and a pass mark for the final quiz; a wrong answer at a checkpoint offers Watch that part again.
423
+
424
+ ```sh
425
+ animaguru create animation moon-lesson --quiz # a quiz.json over the starter film: a checkpoint and a final quiz
426
+ npm run dev # play it and answer
427
+ npm run review -- --animation moon-lesson --headless # the film under the cards, plus the quiz's questions and notes
428
+ animaguru submit --animation moon-lesson --yes # uploads it; the video page plays the quiz
429
+ ```
430
+
431
+ The questions are HTML cards over the picture, so they play in HTML5: locally, in the exported ZIP and on AnimaGuru's video pages. A quiz film plays as HTML5 only, because its questions wait for a viewer's answers: `npm run export:mp4` refuses it, and the cloud Animator offers no MP4 of it. A quiz also works inside a branching story (`--story --quiz`). The `animaguru-quiz` skill has the format and how to write good questions.
432
+
407
433
  ## Motion library
408
434
 
409
435
  CLI 0.14.0 adds `animaguru motion`. Search ready-made motion design examples (PixiJS scenes built on the video film kit) by describing the motion, then install one into a film. Search uses natural language and keywords together; it needs `animaguru auth login` and spends no credits.
@@ -486,6 +512,16 @@ To request public gallery publication, upload privately, open the video in My vi
486
512
 
487
513
  `videos list --scope mine|shared|public` lists films. Owners can edit metadata, change visibility and replace recipients on the website, or use `videos share VIDEO_ID --visibility private --share "alex@example.com" --yes`. `videos delete VIDEO_ID --yes` removes the film and files. Making a film private or removing a recipient revokes access to existing playback links.
488
514
 
515
+ CLI 0.20.0 lists and moves videos between libraries. `videos list` shows the selected workspace's library, the one `submit` uploads to; `--workspace personal` or `--workspace TEAM_ID` lists another. `videos move VIDEO_ID… --workspace TEAM_ID --yes` moves your own videos into a team you belong to, or back with `--workspace personal`:
516
+
517
+ ```sh
518
+ animaguru videos list --workspace personal
519
+ animaguru videos move vid_… vid_… --workspace team_… --yes
520
+ animaguru videos list --workspace team_…
521
+ ```
522
+
523
+ A moved video keeps its ID, watch and embed links, subtitles, recipients and review, and everyone in the team can watch it. The new library must have room for it. A video made from a project's animation stays with that project: transfer the project to the team on the website first, then move the video. Videos move one at a time; if one cannot move, the command stops, prints the IDs it moved and the ones it didn't, and exits with an error. Moving a video into the library it is already in changes nothing, so the same command can be run again.
524
+
489
525
  Uploads are divided into SHA-256 verified 8 MiB parts. Interrupted submissions print their video ID: inspect with `videos get VIDEO_ID`, retry a fully uploaded film with `videos complete VIDEO_ID`, or delete the unfinished draft before resubmitting. Completion is idempotent. HTML5 ZIPs allow 64 MiB compressed / 96 MiB expanded with 500 entries and 32 MiB per asset; MP4s allow 512 MiB; PNG/JPEG/WebP covers allow 2 MiB. Uploads are available to verified accounts with a 2 GiB video library allowance. Hosting does not run paid generation jobs.
490
526
 
491
527
  ## Upload directly to YouTube
package/dist/animaguru.js CHANGED
@@ -19761,7 +19761,7 @@ var require_ignore2 = __commonJS({
19761
19761
  // cli/package.json
19762
19762
  var package_default = {
19763
19763
  name: "@docyrus/animaguru",
19764
- version: "0.19.0",
19764
+ version: "0.20.0",
19765
19765
  description: "Agent-friendly AnimaGuru CLI built with CrustJS",
19766
19766
  type: "module",
19767
19767
  bin: {
@@ -46078,6 +46078,10 @@ var animation_styles_default = {
46078
46078
  "visual-essay": {
46079
46079
  name: "Visual Essay & Podcast",
46080
46080
  description: "Add accurate graphics and chapters to source text or recorded speech."
46081
+ },
46082
+ "branching-story": {
46083
+ name: "Branching Story",
46084
+ description: "Let viewers choose what happens next and reach one of several endings; plays as an interactive HTML5 film."
46081
46085
  }
46082
46086
  },
46083
46087
  artMotionLink: "Explore Art Motion styles \u2192"
@@ -46134,7 +46138,8 @@ var VIDEO_WORKFLOW_IDS = [
46134
46138
  "music-lyrics",
46135
46139
  "titles-event",
46136
46140
  "ambient-loop",
46137
- "visual-essay"
46141
+ "visual-essay",
46142
+ "branching-story"
46138
46143
  ];
46139
46144
  var VIDEO_WORKFLOWS2 = VIDEO_WORKFLOW_IDS.map((id2, index2) => ({
46140
46145
  id: id2,
@@ -46200,6 +46205,37 @@ async function installDependencies(directory, options) {
46200
46205
  child.once("close", (code) => code === 0 ? resolveInstall() : reject(new Error(`npm install exited with code ${code}.`)));
46201
46206
  });
46202
46207
  }
46208
+ var STARTER_STORY = {
46209
+ version: 1,
46210
+ start: "opening",
46211
+ segments: [
46212
+ {
46213
+ id: "opening",
46214
+ label: "The spark",
46215
+ start: 0,
46216
+ end: 8,
46217
+ prompt: "Where should the story go?",
46218
+ choiceAt: 5,
46219
+ choices: [{ label: "Show the wonder", to: "wonder" }, { label: "Make the invitation", to: "invitation" }]
46220
+ },
46221
+ { id: "wonder", label: "The possibility", start: 8, end: 16, ending: "A world worth exploring" },
46222
+ { id: "invitation", label: "The invitation", start: 16, end: 24, ending: "Your next story starts here" }
46223
+ ]
46224
+ };
46225
+ var writeStarterStory = (directory) => writeFile6(join6(directory, "story.json"), JSON.stringify(STARTER_STORY, null, 2) + "\n", { flag: "wx" });
46226
+ function starterQuiz(story = false) {
46227
+ return {
46228
+ version: 1,
46229
+ checkpoints: [{ id: "spark", at: story ? 4.5 : 7.5, title: "Quick check", questions: [
46230
+ { prompt: "Where does every great story begin?", options: ["With a big budget", "With a little spark", "With the ending"], answer: 1, explanation: "The opening scene says it: every great story begins with a little spark." }
46231
+ ] }],
46232
+ final: { title: "Quiz", pass: 50, questions: [
46233
+ { prompt: "What does a quiz film do at a checkpoint?", options: ["Skips to the end", "Pauses and asks a question", "Plays faster"], answer: 1, explanation: "A checkpoint pauses the film on its own frame, and the film goes on once the viewer answers." },
46234
+ { prompt: "Which of these can a question have?", options: ["One right answer", "Several right answers", "A countdown timer"], answer: [0, 1], explanation: "A question has one right answer, or several when it asks to select all that apply. Questions wait for the viewer." }
46235
+ ] }
46236
+ };
46237
+ }
46238
+ var writeStarterQuiz = (directory, story = false) => writeFile6(join6(directory, "quiz.json"), JSON.stringify(starterQuiz(story), null, 2) + "\n", { flag: "wx" });
46203
46239
  async function createVideoProject(options, installer = installDependencies) {
46204
46240
  if (!options.directory.trim()) throw new Error("Choose a new project folder, for example: animaguru create my-video");
46205
46241
  const directory = resolve5(options.directory);
@@ -46223,7 +46259,7 @@ async function createVideoProject(options, installer = installDependencies) {
46223
46259
  const manifest = JSON.parse(await readFile4(packagePath, "utf8"));
46224
46260
  manifest.name = basename2(directory).toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 100) || "animaguru-video";
46225
46261
  await writeFile6(packagePath, JSON.stringify(manifest, null, 2) + "\n");
46226
- const skills = [.../* @__PURE__ */ new Set(["animaguru", "animaguru-video", "animaguru-brand-kit", "animaguru-cutcraft", "animaguru-art-motion", ...VIDEO_PURPOSE_SKILLS, ...VIDEO_TEMPLATES.map((template) => template.skill)])];
46262
+ const skills = [.../* @__PURE__ */ new Set(["animaguru", "animaguru-video", "animaguru-brand-kit", "animaguru-cutcraft", "animaguru-art-motion", "animaguru-branching-story", "animaguru-quiz", ...VIDEO_PURPOSE_SKILLS, ...VIDEO_TEMPLATES.map((template) => template.skill)])];
46227
46263
  const pixiSkillsRoot = join6(packageRoot, "vendor/pixijs-skills/skills");
46228
46264
  const pixiSkills = (await readdir2(pixiSkillsRoot, { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
46229
46265
  for (const agent of [".agents", ".claude"]) {
@@ -46247,6 +46283,8 @@ async function createVideoProject(options, installer = installDependencies) {
46247
46283
  animaguruProjectId: null
46248
46284
  }, null, 2) + "\n");
46249
46285
  await writeFile6(join6(directory, "animaguru.templates.json"), JSON.stringify(listTemplates(), null, 2) + "\n");
46286
+ if (options.story) await writeStarterStory(directory);
46287
+ if (options.quiz) await writeStarterQuiz(directory, options.story);
46250
46288
  if (options.install !== false) {
46251
46289
  options.log?.("Installing PixiJS, Vite, TypeScript, and the MP4 tools\u2026");
46252
46290
  try {
@@ -46369,7 +46407,11 @@ async function createAnimation(options) {
46369
46407
  await mkdir6(join6(directory, "public/assets"), { recursive: true });
46370
46408
  await mkdir6(join6(directory, "production"), { recursive: true });
46371
46409
  await writeFile6(join6(directory, "animaguru.project.json"), JSON.stringify({ schemaVersion: 4, cliVersion: package_default.version, workspace: "../..", category: null, template: null, workflow: null, skill: null, setupStage: "category", galleryUrl: ANIMATION_STYLE_GALLERY_URL, interview: "pending", animaguruProjectId: null, branding: "inherit" }, null, 2) + "\n");
46372
- options.log?.(`Animation ready: ${directory}. It uses the workspace's branding, player and export tools.`);
46410
+ if (options.story) await writeStarterStory(directory);
46411
+ if (options.quiz) await writeStarterQuiz(directory, options.story);
46412
+ const kind = options.story && options.quiz ? "Branching story with a quiz" : options.story ? "Branching story" : options.quiz ? "Quiz film" : "Animation";
46413
+ options.log?.(`${kind} ready: ${directory}. It uses the workspace's branding, player and export tools.`);
46414
+ const skills = [...options.story ? ["animaguru-branching-story"] : [], ...options.quiz ? ["animaguru-quiz"] : []];
46373
46415
  return {
46374
46416
  type: "animation",
46375
46417
  path: directory,
@@ -46377,7 +46419,9 @@ async function createAnimation(options) {
46377
46419
  name: options.name,
46378
46420
  category: null,
46379
46421
  template: null,
46380
- nextSteps: [`Describe the film in Codex or Claude Code, working in animations/${options.name}.`, `Run npm run dev to preview, or npm run export:html -- --animation ${options.name}.`]
46422
+ ...options.story ? { story: "story.json" } : {},
46423
+ ...options.quiz ? { quiz: "quiz.json" } : {},
46424
+ nextSteps: skills.length ? [`Describe the ${options.story ? "story" : "film"} in Codex or Claude Code, working in animations/${options.name}; the agent follows the ${skills.join(" and ")} skill${skills.length > 1 ? "s" : ""} and replaces the starter ${[...options.story ? ["story.json"] : [], ...options.quiz ? ["quiz.json"] : []].join(" and ")}.`, `Run npm run dev to play it; ${options.story ? "a branching story" : "a quiz film"} plays as HTML5 only, delivered with npm run export:html -- --animation ${options.name} or animaguru submit.`] : [`Describe the film in Codex or Claude Code, working in animations/${options.name}.`, `Run npm run dev to preview, or npm run export:html -- --animation ${options.name}.`]
46381
46425
  };
46382
46426
  }
46383
46427
 
@@ -46703,9 +46747,10 @@ var videoDetails = external_exports.object({
46703
46747
  agreement: videoAgreementAcceptance.optional()
46704
46748
  }).refine((value) => value.visibility !== "public" || !value.recipients.length, "Public videos do not use a recipient list.");
46705
46749
  var videoAnimationName = external_exports.string().regex(/^[a-z0-9][a-z0-9-]{0,79}$/, "Use the animation's folder name.");
46750
+ var videoWorkspace = external_exports.union([external_exports.literal("me"), external_exports.string().regex(/^team_[a-f0-9]{32}$/)]);
46706
46751
  var videoSubmission = videoDetails.and(external_exports.object({
46707
46752
  /** The workspace that keeps the video; a project's video goes to the project's workspace. */
46708
- workspace: external_exports.union([external_exports.literal("me"), external_exports.string().regex(/^team_[a-f0-9]{32}$/)]).optional(),
46753
+ workspace: videoWorkspace.optional(),
46709
46754
  project: external_exports.string().regex(/^prj_[A-Za-z0-9]{8,40}$/).optional(),
46710
46755
  animation: videoAnimationName.optional(),
46711
46756
  format: external_exports.enum(["html5", "mp4"]),
@@ -46714,6 +46759,7 @@ var videoSubmission = videoDetails.and(external_exports.object({
46714
46759
  height: external_exports.number().int().min(1).max(4096).default(1080),
46715
46760
  duration: external_exports.number().positive().max(7200).optional()
46716
46761
  })).refine((value) => value.bytes <= VIDEO_MAX_BYTES[value.format], "HTML5 ZIPs support up to 64 MiB; MP4s support up to 512 MiB.").refine((value) => !value.animation || value.project, "Name the project of the animation.");
46762
+ var videoMove = external_exports.object({ workspace: videoWorkspace }).strict();
46717
46763
  function videoPosterType(bytes) {
46718
46764
  if (bytes.length >= 8 && bytes.slice(0, 8).every((b, i2) => b === [137, 80, 78, 71, 13, 10, 26, 10][i2])) return "image/png";
46719
46765
  if (bytes.length >= 3 && bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255) return "image/jpeg";
@@ -47820,6 +47866,31 @@ async function changeVideoSubtitles(client, id2, options) {
47820
47866
  parseSubtitles(content.toString("utf8"));
47821
47867
  return client.request(path + language.data, { method: "PUT", body: subtitlesBody(content) });
47822
47868
  }
47869
+ async function listVideos(client, options) {
47870
+ const scope = options.scope ?? "mine";
47871
+ const page = options.page ?? 1;
47872
+ if (options.workspace !== void 0 && scope !== "mine") throw new Error("--workspace chooses a library; use it with --scope mine.");
47873
+ if (scope === "public") return client.request(`/api/videos?page=${page}`, { authenticated: false });
47874
+ if (scope !== "mine") return client.request(`/api/v1/videos?scope=${scope}&page=${page}`);
47875
+ const workspace3 = options.workspace === void 0 ? await selectedWorkspace(client.origin) : libraryWorkspace(options.workspace);
47876
+ return client.request(`/api/v1/videos?scope=mine&page=${page}${workspace3 === "me" ? "" : `&workspace=${encodeURIComponent(workspace3)}`}`);
47877
+ }
47878
+ async function moveVideos(client, ids, options) {
47879
+ if (!options.yes) throw new Error("Pass --yes to authorize moving these videos. Everyone in a team can watch the videos in its library.");
47880
+ if (!ids.length) throw new Error("Name the videos to move with their IDs from animaguru videos list.");
47881
+ if (options.workspace === void 0) throw new Error("Pass --workspace personal or a team ID from animaguru workspaces list.");
47882
+ const workspace3 = libraryWorkspace(options.workspace);
47883
+ const moved = [];
47884
+ for (const [index2, id2] of ids.entries()) {
47885
+ try {
47886
+ const { video } = await client.request(`/api/v1/videos/${encodeURIComponent(id2)}/workspace`, { method: "PATCH", body: { workspace: workspace3 } });
47887
+ moved.push({ id: video.id, title: video.title, workspaceId: video.workspaceId });
47888
+ } catch (error62) {
47889
+ return { moved, error: `${id2} was not moved: ${error62 instanceof Error ? error62.message : String(error62)}`, notMoved: ids.slice(index2) };
47890
+ }
47891
+ }
47892
+ return { moved };
47893
+ }
47823
47894
 
47824
47895
  // cli/src/assets.ts
47825
47896
  import { mkdir as mkdir8, readFile as readFile9, stat as stat4, writeFile as writeFile8 } from "node:fs/promises";
@@ -49902,10 +49973,17 @@ function createCli() {
49902
49973
  { name: "out", type: "string", description: "Write the analysis to this file instead" },
49903
49974
  { name: "wait", type: "boolean", description: "Wait until the analysis finishes" },
49904
49975
  urlFlag
49905
- ).action(async ({ args, flags, stdout, stderr, signal }) => out(stdout, await referenceGet(new Client(flags.url, signal), args.id, { ...flags, log: stderr })))))).command("create", (creation) => creation.args({ name: "name", type: "string", description: "Legacy standalone project folder; prefer create workspace NAME" }).action(async ({ args, stdout, stderr, signal }) => {
49976
+ ).action(async ({ args, flags, stdout, stderr, signal }) => out(stdout, await referenceGet(new Client(flags.url, signal), args.id, { ...flags, log: stderr })))))).command("create", (creation) => creation.args({ name: "name", type: "string", description: "Legacy standalone project folder; prefer create workspace NAME" }).flags(
49977
+ { name: "story", type: "boolean", description: "Start a branching story: viewers choose its path, and it plays as HTML5 only" },
49978
+ { name: "quiz", type: "boolean", description: "Start a quiz film: questions pause the film at checkpoints and a final quiz follows it (HTML5 only, never MP4)" }
49979
+ ).action(async ({ args, flags, stdout, stderr, signal }) => {
49906
49980
  if (!args.name) throw new Error("Use animaguru create workspace NAME, or animaguru create animation NAME inside a workspace.");
49907
- out(stdout, await createVideoProject({ directory: args.name, log: stderr, signal }));
49908
- }).add(defineCommand("workspace", { description: "Create a shared-brand workspace with one player, dependencies, skills and export toolchain" }, (command) => command.args({ name: "name", type: "string", required: true }).flags({ name: "skip-install", type: "boolean", default: false, description: "Create source files without installing dependencies" }).action(async ({ args, flags, stdout, stderr, signal }) => out(stdout, await createWorkspace({ directory: args.name, install: !flags["skip-install"], log: stderr, signal }))))).add(defineCommand("animation", { description: "Add an animation to a workspace without copying shared runtime, dependencies or skills" }, (command) => command.args({ name: "name", type: "string", required: true }).flags({ name: "workspace", type: "string", description: "Workspace folder; otherwise finds it from the current directory" }).action(async ({ args, flags, stdout, stderr, signal }) => out(stdout, await createAnimation({ name: args.name, workspace: flags.workspace, log: stderr, signal })))))).add(defineCommand("submit", { description: "Upload a finished HTML5 animation or MP4 to AnimaGuru; private unless --visibility public" }, (command) => command.args({ name: "directory", type: "string", default: ".", description: "Video project folder; defaults to current folder" }).flags(
49981
+ out(stdout, await createVideoProject({ directory: args.name, story: flags.story, quiz: flags.quiz, log: stderr, signal }));
49982
+ }).add(defineCommand("workspace", { description: "Create a shared-brand workspace with one player, dependencies, skills and export toolchain" }, (command) => command.args({ name: "name", type: "string", required: true }).flags({ name: "skip-install", type: "boolean", default: false, description: "Create source files without installing dependencies" }).action(async ({ args, flags, stdout, stderr, signal }) => out(stdout, await createWorkspace({ directory: args.name, install: !flags["skip-install"], log: stderr, signal }))))).add(defineCommand("animation", { description: "Add an animation to a workspace without copying shared runtime, dependencies or skills" }, (command) => command.args({ name: "name", type: "string", required: true }).flags(
49983
+ { name: "workspace", type: "string", description: "Workspace folder; otherwise finds it from the current directory" },
49984
+ { name: "story", type: "boolean", description: "Start a branching story: viewers choose its path at choices, and it plays as HTML5 only (never MP4)" },
49985
+ { name: "quiz", type: "boolean", description: "Start a quiz film (an educational explainer): checkpoints pause the film to ask questions, a final quiz and score follow its end; it plays as HTML5 only (never MP4)" }
49986
+ ).action(async ({ args, flags, stdout, stderr, signal }) => out(stdout, await createAnimation({ name: args.name, workspace: flags.workspace, story: flags.story, quiz: flags.quiz, log: stderr, signal })))))).add(defineCommand("submit", { description: "Upload a finished HTML5 animation or MP4 to AnimaGuru; private unless --visibility public" }, (command) => command.args({ name: "directory", type: "string", default: ".", description: "Video project folder; defaults to current folder" }).flags(
49909
49987
  { name: "url", type: "string", default: "https://animaguru.com", env: { name: "ANIMAGURU_URL" } },
49910
49988
  { name: "animation", type: "string", description: "Animation name when submitting from a multi-animation workspace" },
49911
49989
  { name: "workspace", type: "string", description: "Video library: personal or a team ID; defaults to the selected workspace, or a synced project's" },
@@ -49919,11 +49997,24 @@ function createCli() {
49919
49997
  { name: "subtitle-language", type: "string", description: "Subtitle language code such as en or tr; defaults to the MP4's export receipt or film.config.json" },
49920
49998
  { name: "yes", type: "boolean", description: "Authorize upload and chosen sharing" },
49921
49999
  { name: "dry-run", type: "boolean", description: "Inspect locally without uploading" }
49922
- ).action(async ({ args, flags, stdout, stderr, signal }) => out(stdout, await submitVideo(new Client(flags.url, signal), { ...flags, directory: args.directory, log: stderr, signal }))))).command("videos", (videos) => videos.add(defineCommand("list", { description: "List your videos, privately shared videos, or the public gallery" }, (command) => command.flags({ name: "url", type: "string", default: "https://animaguru.com", env: { name: "ANIMAGURU_URL" } }, { name: "scope", type: "string", choices: ["mine", "shared", "public"], default: "mine" }, { name: "page", type: "number", default: 1 }).action(async ({ flags, stdout, signal }) => out(stdout, await new Client(flags.url, signal).request(flags.scope === "public" ? `/api/videos?page=${flags.page}` : `/api/v1/videos?scope=${flags.scope}&page=${flags.page}`, { authenticated: flags.scope !== "public" }))))).add(defineCommand("get", { description: "Get a video's details or the state of your unfinished upload" }, (command) => command.args({ name: "id", type: "string", required: true }).flags({ name: "url", type: "string", default: "https://animaguru.com", env: { name: "ANIMAGURU_URL" } }).action(async ({ args, flags, stdout, signal }) => out(stdout, await new Client(flags.url, signal).request(`/api/v1/videos/${encodeURIComponent(args.id)}`))))).add(defineCommand("complete", { description: "Retry completion of an already uploaded animation" }, (command) => command.args({ name: "id", type: "string", required: true }).flags({ name: "url", type: "string", default: "https://animaguru.com", env: { name: "ANIMAGURU_URL" } }).action(async ({ args, flags, stdout, signal }) => out(stdout, await new Client(flags.url, signal).request(`/api/v1/videos/${encodeURIComponent(args.id)}/complete`, { method: "POST", timeout: 3e5 }))))).add(defineCommand("share", { description: "Replace a video's visibility and recipient list" }, (command) => command.args({ name: "id", type: "string", required: true }).flags({ name: "url", type: "string", default: "https://animaguru.com", env: { name: "ANIMAGURU_URL" } }, { name: "visibility", type: "string", required: true, choices: ["private", "public"] }, { name: "share", type: "string" }, { name: "yes", type: "boolean", required: true }).action(async ({ args, flags, stdout, signal }) => {
50000
+ ).action(async ({ args, flags, stdout, stderr, signal }) => out(stdout, await submitVideo(new Client(flags.url, signal), { ...flags, directory: args.directory, log: stderr, signal }))))).command("videos", (videos) => videos.add(defineCommand("list", { description: "List a video library (the selected workspace's by default), privately shared videos, or the public gallery" }, (command) => command.flags(
50001
+ { name: "url", type: "string", default: "https://animaguru.com", env: { name: "ANIMAGURU_URL" } },
50002
+ { name: "scope", type: "string", choices: ["mine", "shared", "public"], default: "mine" },
50003
+ { name: "page", type: "number", default: 1 },
50004
+ { name: "workspace", type: "string", description: "The library to list with --scope mine: 'personal' or a team ID from workspaces list" }
50005
+ ).action(async ({ flags, stdout, signal }) => out(stdout, await listVideos(new Client(flags.url, signal), flags))))).add(defineCommand("get", { description: "Get a video's details or the state of your unfinished upload" }, (command) => command.args({ name: "id", type: "string", required: true }).flags({ name: "url", type: "string", default: "https://animaguru.com", env: { name: "ANIMAGURU_URL" } }).action(async ({ args, flags, stdout, signal }) => out(stdout, await new Client(flags.url, signal).request(`/api/v1/videos/${encodeURIComponent(args.id)}`))))).add(defineCommand("complete", { description: "Retry completion of an already uploaded animation" }, (command) => command.args({ name: "id", type: "string", required: true }).flags({ name: "url", type: "string", default: "https://animaguru.com", env: { name: "ANIMAGURU_URL" } }).action(async ({ args, flags, stdout, signal }) => out(stdout, await new Client(flags.url, signal).request(`/api/v1/videos/${encodeURIComponent(args.id)}/complete`, { method: "POST", timeout: 3e5 }))))).add(defineCommand("share", { description: "Replace a video's visibility and recipient list" }, (command) => command.args({ name: "id", type: "string", required: true }).flags({ name: "url", type: "string", default: "https://animaguru.com", env: { name: "ANIMAGURU_URL" } }, { name: "visibility", type: "string", required: true, choices: ["private", "public"] }, { name: "share", type: "string" }, { name: "yes", type: "boolean", required: true }).action(async ({ args, flags, stdout, signal }) => {
49923
50006
  if (!flags.yes) throw new Error("Pass --yes to authorize changing who can see this video.");
49924
50007
  const client = new Client(flags.url, signal);
49925
50008
  const { video } = await client.request(`/api/videos/${encodeURIComponent(args.id)}`);
49926
50009
  out(stdout, await client.request(`/api/v1/videos/${encodeURIComponent(args.id)}`, { method: "PATCH", body: videoDetails.parse({ ...video, visibility: flags.visibility, recipients: sharingEmails(flags.share) }) }));
50010
+ }))).add(defineCommand("move", { description: "Move your videos to another library, Personal or a team you belong to; their links, subtitles and sharing stay" }, (command) => command.args({ name: "ids", type: "string", variadic: true, description: "Video IDs from videos list" }).flags(
50011
+ { name: "url", type: "string", default: "https://animaguru.com", env: { name: "ANIMAGURU_URL" } },
50012
+ { name: "workspace", type: "string", required: true, description: "'personal' or a team ID from workspaces list" },
50013
+ { name: "yes", type: "boolean", required: true }
50014
+ ).action(async ({ args, flags, stdout, signal }) => {
50015
+ const result = await moveVideos(new Client(flags.url, signal), args.ids ?? [], flags);
50016
+ out(stdout, result);
50017
+ if (result.error) throw new Error(result.error);
49927
50018
  }))).add(defineCommand("subtitles", { description: "Add or replace an MP4's subtitles in one language, or remove a language" }, (command) => command.args({ name: "id", type: "string", required: true }).flags(
49928
50019
  { name: "url", type: "string", default: "https://animaguru.com", env: { name: "ANIMAGURU_URL" } },
49929
50020
  { name: "file", type: "string", description: ".srt or .vtt subtitle file" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@docyrus/animaguru",
3
- "version": "0.19.0",
3
+ "version": "0.20.0",
4
4
  "description": "Agent-friendly AnimaGuru CLI built with CrustJS",
5
5
  "type": "module",
6
6
  "bin": {
@@ -102,6 +102,14 @@ CLI 0.19.0 adds `animaguru review check [DIRECTORY] [--animation NAME] --yes`. A
102
102
 
103
103
  CLI 0.19.0 adds `animaguru reference analyze VIDEO --yes` for a video someone shares to have the same kind of video made for their own brand or story. VIDEO is a local MP4, MOV or WebM file (up to 100 MB and ten minutes) or an HTTPS link to the video file itself; YouTube, Instagram and TikTok pages cannot be fetched, so ask for the file. Elorian's Glimpse watches and listens to it and returns a remake brief: format, story structure, style, palette, typography, motion and transitions, audio with a transcript, timed scenes, the original brand's elements to replace, and what makes it work. It takes several minutes (about eight for a 6-second clip), saves the analysis to `production/references/NAME.json` in a video project (`--out FILE` elsewhere) and is charged what it costs: about 22 to 36 credits for a 6-second clip in testing, more for a longer video (`--dry-run` shows the hold, 200 credits at the default low effort). Tell the user the cost before running it. `--focus TEXT` says what matters most; `--effort low|medium|xhigh` trades cost for depth; `--no-wait` returns the ID for `animaguru reference get ID --wait`. MCP clients use `analyze_video` with a link and poll `get_video_analysis`. Remake from the brief with the user's own brand kit and story: keep the structure, pacing and techniques, never the original's brand, claims, people or footage. See the `animaguru-video` skill.
104
104
 
105
+ ## Branching stories
106
+
107
+ CLI 0.20.0 adds branching stories: films whose viewers choose what happens next at choices and reach one of several endings. `animaguru create animation NAME --story` starts one with a `story.json` beside `film.config.json`; the story's segments lie end to end on the film's timeline and `story.json` says where each leads (choices, `next` or an ending). The player shows the choices over the picture, and review, frames and frame checks cover every segment. A branching story plays as HTML5 only: `npm run export:mp4` refuses it, `animaguru submit` uploads it as an interactive film, and AnimaGuru's video page plays it with its choices. Read the `animaguru-branching-story` skill before writing one.
108
+
109
+ ## Quizzes
110
+
111
+ CLI 0.20.0 also adds quiz films for educational explainers, lessons and training: `animaguru create animation NAME --quiz` starts one with a `quiz.json` beside `film.config.json`. Checkpoints pause the film at chosen seconds to ask one question or a quick round (for example after every scene), a final quiz follows its end, and the viewer sees their score; the designer decides where the questions go. The questions are HTML cards over the picture that wait for the viewer's answers, so a quiz film plays as HTML5 only, locally, in the exported ZIP and on AnimaGuru's video pages: `npm run export:mp4` refuses it. A quiz also works inside a branching story. Read the `animaguru-quiz` skill before writing one.
112
+
105
113
  ## Motion library
106
114
 
107
115
  `animaguru motion search DESCRIPTION [--category ID] [--labels a,b] [--code]` finds ready-made PixiJS motion examples (kinetic typography, app UI and more) built on the video film kit; `animaguru motion get ID --write src/scenes` installs one into a film. See the `animaguru-video` skill. Searching is free.
@@ -11,6 +11,7 @@ animaguru submit --share "reviewer@example.com" --yes
11
11
  animaguru submit --visibility public --poster public/assets/cover.png --yes
12
12
  animaguru submit --file production/exports/film.mp4 --title "My film" --yes
13
13
  animaguru videos list --scope mine
14
+ animaguru videos list --workspace team_…
14
15
  animaguru videos list --scope shared
15
16
  animaguru videos get VIDEO_ID
16
17
  ```
@@ -19,6 +20,8 @@ Submissions default to private. `--share` is a comma-separated list of verified
19
20
 
20
21
  The JSON result returns the video ID and watch URL. Owners manage details and recipients on that page, or run `animaguru videos share VIDEO_ID --visibility private --share "reviewer@example.com" --yes`; this replaces the entire recipient list. Public sharing uses `--visibility public` without `--share`. Removing a recipient or making a public film private blocks previous playback URLs immediately. `animaguru videos delete VIDEO_ID --yes` removes the hosted files.
21
22
 
23
+ `videos list` shows the selected workspace's library; add `--workspace personal` or `--workspace TEAM_ID` for another (CLI 0.20.0). When the user asks to move videos between their personal library and a team, run `animaguru videos move VIDEO_ID… --workspace TEAM_ID --yes` (or `--workspace personal`). Moving keeps each video's ID, links, subtitles, recipients and review, and every team member can watch it, so move only videos the user names. A video made from a project's animation stays with its project: the user transfers the project to the team on the website first. If one video can't be moved, the command stops and reports `moved`, `error` and `notMoved`; fix the cause, then rerun it with the same IDs, since a video already in the target library is left as it is.
24
+
22
25
  For an MP4, `submit` also uploads `FILM.vtt` or `FILM.srt` from beside it as subtitles, which the AnimaGuru player shows over the picture (CLI 0.10.0). The dry run lists the file and language; use `--subtitles FILE`, `--subtitles none` or `--subtitle-language CODE` when needed. Upload the clean MP4 with its subtitles rather than a `-captioned.mp4`, unless the user wants captions in the picture. `animaguru videos subtitles VIDEO_ID --file FILE --yes` adds or replaces one language on an uploaded MP4 and `--remove CODE --yes` removes one. New subtitles on a public video send it back for staff review.
23
26
 
24
27
  Uploads use checksum-verified parts. After an interrupted submission, inspect the printed ID first. If all parts arrived, `animaguru videos complete VIDEO_ID` safely retries finalization and returns the existing result when already complete. Otherwise cancel that draft with `videos delete --yes` before submitting again. Do not silently delete a potentially successful publication after a network timeout.
@@ -0,0 +1,72 @@
1
+ ---
2
+ name: animaguru-branching-story
3
+ description: Design and build a branching (interactive, choose-your-path) story film in an AnimaGuru video workspace, where viewers pick what happens next at choices and reach one of several endings. Plays as HTML5 only, never MP4. Use when the user asks for an interactive, branching, choose-your-own-adventure or multiple-ending film, ad, explainer or lesson.
4
+ ---
5
+
6
+ # AnimaGuru branching story
7
+
8
+ A branching story is an ordinary AnimaGuru film whose timeline holds every segment of every path, laid end to end, plus a `story.json` beside `film.config.json` that says where each segment goes. The player plays one segment at a time: at a segment with choices it shows the prompt and buttons over the picture and continues into the segment the viewer picks; a segment with `next` flows on by itself; an ending shows its title with Play again and Back to the last choice. Review, frames, the frame checker and capture still see the whole timeline as one film, so every segment of every path is checked.
9
+
10
+ It plays as HTML5 only, locally, in an exported ZIP and on AnimaGuru's video pages. An MP4 has one path, so `npm run export:mp4` refuses a film with `story.json`, the cloud Animator offers no MP4 for it, and you should not promise one. Read sibling `animaguru-video` first for the setup, interview, kit, audio and review workflow; this skill adds what changes.
11
+
12
+ ## Start
13
+
14
+ - In a workspace: `animaguru create animation NAME --story`. It writes a starter `story.json` over the starter film's three scenes (an opening with a choice and two endings), so `npm run dev` shows the mechanics at once. Replace both with the real story.
15
+ - An existing film becomes a branching story by adding `story.json`; a film without one is linear and unchanged.
16
+ - To ask questions on the way or at an ending (an interactive lesson), add a `quiz.json` too, following sibling `animaguru-quiz`; `--story --quiz` starts both.
17
+ - Category and style selection work as for any film. A story told for its own sake is General Video with workflow `branching-story`; an interactive explainer, lesson or ad keeps its own category and records the branching in `production/brief.md`.
18
+
19
+ ## The interview, for a story
20
+
21
+ Fold these into the one compact interview round; reuse what the user already said:
22
+
23
+ 1. **Shape:** how many choices on the way and how many endings (2–3 choices and 2–4 endings suit a 1–3 minute playthrough). Whether paths rejoin (a diamond: choices that change a scene and meet again), loop back ("try again"), or split for good.
24
+ 2. **What a choice means:** the viewer steers a character, picks a topic to learn, or chooses a product path. Each option should lead somewhere visibly different; a choice that changes nothing feels broken.
25
+ 3. **Timing:** whether choices wait for the viewer (default) or run a timer (`timeout`, 8–12 s) that takes a default, as in a lean-back film.
26
+ 4. **Language** of the prompts, buttons and endings; the panels' own words (The end, Play again, Back to the last choice) are translated through `ui` in `story.json`.
27
+
28
+ Write the graph into `production/brief.md` as a short outline: each segment's id, what happens, and where it leads.
29
+
30
+ ## story.json
31
+
32
+ ```json
33
+ {
34
+ "version": 1,
35
+ "start": "opening",
36
+ "segments": [
37
+ { "id": "opening", "label": "The fork", "start": 0, "end": 12, "prompt": "Which way does Mo go?", "choiceAt": 8,
38
+ "choices": [ { "label": "Into the forest", "to": "forest" }, { "label": "Along the river", "to": "river" } ] },
39
+ { "id": "forest", "label": "The forest", "start": 12, "end": 22, "next": "home" },
40
+ { "id": "river", "label": "The river", "start": 22, "end": 32, "prompt": "Mo hears a splash.", "timeout": 10, "default": 0,
41
+ "choices": [ { "label": "Look closer", "to": "home" }, { "label": "Run back", "to": "opening" } ] },
42
+ { "id": "home", "label": "Home", "start": 32, "end": 40, "ending": "Home before dark" }
43
+ ],
44
+ "ui": { "theEnd": "The end", "playAgain": "Play again", "back": "Back to the last choice", "choose": "What happens next?" },
45
+ "theme": { "accent": "#2c6bd0", "surface": "#fffdfa", "text": "#16232a" }
46
+ }
47
+ ```
48
+
49
+ - Each segment ends in exactly one way: `choices` (2–4, each a `label` of up to 60 characters and a target `to`), `next` (another segment) or `ending` (the ending's title).
50
+ - `start` and `end` are seconds of the film's timeline. Segments lie end to end without gaps or overlaps: the first starts at 0 and the last ends at `film.config.json`'s `duration`. The order on the timeline is yours; keep each path's segments in reading order where you can, which makes review easier to follow.
51
+ - `choiceAt` (seconds of the timeline, within the segment) is when the choices appear; by default at the segment's end. The segment keeps playing under them; a viewer who chooses early sees the segment finish, then the chosen segment. At the segment's end the player holds its last frame until a choice.
52
+ - `timeout` (3–60 s) counts from when the choices appear, while the film plays and while it holds; `default` (an index, the first by default) is taken when it runs out. Leave `timeout` out to wait for the viewer.
53
+ - `prompt` (up to 120 characters) shows above the buttons. `label` names the segment on the timeline and in the transcript. `ui` and `theme` are optional; the theme defaults to the brand's primary and text colors.
54
+ - Every segment must be reachable from `start`, and every segment must still be able to reach an ending, so no path traps the viewer. `npm run build`, `npm run dev`, review and export check the graph and name the first problem.
55
+
56
+ ## Scenes for a story
57
+
58
+ - Drive scenes from `story.json`, so a re-timed segment moves its scenes: `import story from "../story.json"` and `const beat = storyBeats(story)` from the kit; `beat("forest").start`, `.end`, `.choiceAt` and `.length`. Pass `storyChapters(story)` as the film's `chapters` (or chapters with the same times), so the transcript and review cues follow the segments.
59
+ - **A segment must stand on its own at both ends.** It may be entered from several segments and left for several, so it starts from a clean picture (no object carried over from whichever segment came before on the timeline) and finishes in a picture that holds well as a still. Show each segment's scene only within its own `[start, end)`.
60
+ - **Design the choice window.** From `choiceAt` to the end, keep motion calm and loop-friendly (breathing, drifting, idle character motion) so the held last frame looks intended, and keep the bottom third of the frame free of text and captions: the prompt and buttons sit there over a dark gradient. Put the question's subject in the upper two thirds.
61
+ - An ending segment builds to a final image with room for the centered ending panel (title and buttons) over a dimmed picture; the film's own end card is optional.
62
+ - Set `poster` in `film.config.json` to a strong frame of the opening segment.
63
+
64
+ ## Sound
65
+
66
+ `config.audio` stays one mixed soundtrack over the whole timeline. The player moves it to each segment's start when the story jumps and pauses it while a choice waits, so each segment's sound must begin and end cleanly at the segment's boundaries: no narration line or musical phrase across a boundary, a short fade at each segment's end, and an ending that resolves. For narration, list the VO segments in `production/audio/vo.json` in timeline order, one or more per story segment, and use `gapBefore` to leave each story segment's choice window or quiet ending before the next one's first line: `npm run audio:vo` places them in sequence, so lay out `story.json` from the result (the kit's `cueSheet(timing).segment(id)` gives each VO segment's start and end) rather than the other way round. For music, cut a bed per segment, or use a calm loopable bed, rather than one long track whose phrases run across jumps. Silent stories with burned-in captions work well too.
67
+
68
+ ## Review and delivery
69
+
70
+ - `npm run dev` plays the story; `?segment=ID` starts at a segment, and in the console `animaguru.story.goTo("ID")`, `.choose(1)`, `.back()` and `.restart()` drive it. Play every path to each ending at desktop and phone sizes, try keyboard choice (Tab, or 1–4), a timeout, Back and Play again.
71
+ - `npm run review -- --headless` checks text, seeking and frames over the whole timeline, then prints the story's segments, choices, endings and shortest and longest playthrough, plus notes such as a timeout too short to read. `npm run frames` and `animaguru review check` work as for any film; choose times inside each segment.
72
+ - Deliver with `npm run export:html -- --animation NAME` or `animaguru submit --yes` (private by default). The HTML5 manifest records the story, so AnimaGuru's video page plays it interactively and labels it as an interactive story; its seek bar covers the segment playing. Say plainly that it has no MP4.
@@ -14,6 +14,7 @@ Infer a suitable workflow from the request or clarify it in that interview. Do n
14
14
  - **Titles & Event Motion** (`titles-event`): Present names, credits or event identity in a composed sequence. Read [the workflow](references/titles-event.md).
15
15
  - **Ambient & Seamless Loop** (`ambient-loop`): Build a mood or display loop with continuous motion at its boundary. Read [the workflow](references/ambient-loop.md).
16
16
  - **Visual Essay & Podcast** (`visual-essay`): Add accurate graphics and chapters to source text or recorded speech. Read [the workflow](references/visual-essay.md).
17
+ - **Branching Story** (`branching-story`): Let viewers choose what happens next and reach one of several endings; plays as an interactive HTML5 film, never MP4. Read [the workflow](references/branching-story.md) and sibling `animaguru-branching-story`.
17
18
 
18
19
  Develop an original sequence from the user's sources, audience, tone, duration and desired outcome. Avoid importing fixed scene counts, mandatory emotional arcs, default audiences or a compulsory CTA. Respect supplied narration/music preferences. Discover AnimaGuru generation models and costs through the local main skill; this purpose selection does not authorize paid jobs.
19
20
 
@@ -0,0 +1,13 @@
1
+ # Branching Story
2
+
3
+ Let viewers choose what happens next and reach one of several endings; plays as an interactive HTML5 film.
4
+
5
+ Read sibling `animaguru-branching-story` now: it is the contract for `story.json`, scenes that stand on their own at both ends, choice windows, sound across jumps, review of every path and HTML5-only delivery. Start a new film with `animaguru create animation NAME --story`.
6
+
7
+ Fold this missing information into the shared quick interview: **How many choices and endings, what does each choice change, do choices wait for the viewer or run a timer, and in which language?** Reuse supplied answers. Outline the graph (each segment, what happens and where it leads) in `production/brief.md` before building scenes.
8
+
9
+ - Give every choice a visible consequence within a few seconds of the next segment; options that lead to the same picture feel broken.
10
+ - Keep each playthrough short (often 1–3 minutes) and the endings distinct; one ending may loop back to a choice ("try again") if the brief invites it.
11
+ - Write prompts and button labels as the story's voice, short enough to read in the choice window.
12
+
13
+ Completion: every segment reachable and every path ending, each path played through in the browser, the review notes addressed, and the film delivered as HTML5 with no MP4 promised. Use `animaguru-video` for the common production, budget and review contract.
@@ -0,0 +1,80 @@
1
+ ---
2
+ name: animaguru-quiz
3
+ description: Add a quiz to an AnimaGuru film in a video workspace (an educational explainer, lesson, training or onboarding film): questions that pause the film at checkpoints (one question or a quick round, for example after every scene), a final quiz after its end, and the viewer's score. Plays as HTML5 only, never MP4. Use when the user asks for a quiz, questions, knowledge checks, a test, an assessment or an interactive lesson or explainer.
4
+ ---
5
+
6
+ # AnimaGuru quiz
7
+
8
+ A quiz film is an ordinary AnimaGuru film plus a `quiz.json` beside `film.config.json`. The player pauses the film at each **checkpoint** to ask one question or a quick round, shows whether the answer was right with the explanation, and continues; after the film's end it asks the **final quiz** and shows the viewer's score, with Retake the quiz and Watch again. A wrong answer at a checkpoint offers Watch that part again, which replays that part and asks again. The film works with or without a final quiz, with or without checkpoints, and inside a branching story (`story.json`) too.
9
+
10
+ The cards are HTML over the picture (buttons a viewer can tap, tab to, or press 1–6 for), so the questions play in HTML5: locally, in an exported ZIP and on AnimaGuru's video pages. A quiz film plays as HTML5 only, because its questions wait for a viewer's answers: `npm run export:mp4` refuses a film with `quiz.json`, the cloud Animator offers no MP4 for it, and you should not promise one. Capture, review and frames see the film without the cards. Read sibling `animaguru-video` first for the setup, interview, kit, audio and review workflow; this skill adds what changes.
11
+
12
+ ## Start
13
+
14
+ - In a workspace: `animaguru create animation NAME --quiz` writes a starter `quiz.json` (one checkpoint in the starter film's first scene and a short final quiz), so `npm run dev` shows the cards at once. Replace it with the real quiz. `--story --quiz` starts a branching story with a quiz.
15
+ - An existing film gets a quiz by adding `quiz.json`; deleting it makes the film plain again.
16
+ - An educational film is usually the **Explainer** category; record the quiz plan in `production/brief.md`.
17
+
18
+ ## The interview: the designer decides where the questions go
19
+
20
+ Fold these into the one compact interview round, and reuse what the user already said ("one question after every scene" answers the first two):
21
+
22
+ 1. **Where:** a question or quick round after every scene, at chosen moments, only a final quiz at the end, or both. Offer "one question after each scene plus a 3–5 question final quiz" as the default for a lesson.
23
+ 2. **How many:** per checkpoint (1 is usual, 2–3 for a quick round; at most 5) and in the final quiz (3–8 is usual; at most 20). Mid-film rounds of more than three questions interrupt the film; move them to the final quiz.
24
+ 3. **Kinds of question:** one right answer (multiple choice, 2–6 options), true or false (two options), or select all that apply. Ask whether to explain each answer (recommended).
25
+ 4. **Scoring:** a pass mark (`pass`, a percentage) or just a score; and whether a wrong answer should offer Watch that part again (always on at checkpoints) and from where (`review`).
26
+ 5. **Audience and language:** the learners' age or level sets the wording; questions in another language need the card's own words translated through `ui`.
27
+
28
+ Base every question on what the film itself shows or says, so a viewer who watched can answer it. Check facts the same way as the film's claims; never quiz on something the film does not teach.
29
+
30
+ ## quiz.json
31
+
32
+ ```json
33
+ {
34
+ "version": 1,
35
+ "checkpoints": [
36
+ { "id": "light", "at": 15, "title": "Quick check", "review": 3.5, "questions": [
37
+ { "prompt": "Where does moonlight come from?", "options": ["The Moon makes its own light", "Sunlight bouncing off the Moon", "Light from cities on Earth"],
38
+ "answer": 1, "explanation": "The Moon has no light of its own; we see sunlight that bounces off it." } ] },
39
+ { "id": "orbit", "at": 30.5, "questions": [
40
+ { "prompt": "About how long is one cycle of phases?", "options": ["One day", "One week", "About 29.5 days", "One year"], "answer": 2 } ] }
41
+ ],
42
+ "final": { "title": "Moon quiz", "pass": 70, "questions": [
43
+ { "prompt": "Which of these are phases of the Moon?", "options": ["Crescent", "Gibbous", "Eclipse", "First quarter"], "answer": [0, 1, 3],
44
+ "explanation": "An eclipse is a different event, when a shadow falls on the Moon or the Moon blocks the Sun." },
45
+ { "prompt": "True or false: at new moon, the Moon's sunlit side faces away from us.", "options": ["True", "False"], "answer": 0 } ] },
46
+ "theme": { "accent": "#b4560f", "surface": "#fffaf0", "text": "#1a2238" },
47
+ "ui": { "check": "Quick check", "final": "Quiz" }
48
+ }
49
+ ```
50
+
51
+ - **A question:** `prompt` (up to 160 characters; keep it under about 110 for phones), `options` (2–6, each up to 100 characters, no two alike), `answer` (the right option's index, 0 for the first; for select all that apply, the right indices in increasing order, never every option), optional `explanation` (up to 280 characters, shown after the answer) and optional `id` (lowercase, hyphenated; stable ids keep reports comparable after edits). The card adds "Select all that apply." itself; do not repeat it in the prompt.
52
+ - **A checkpoint:** `id`, `at` (the second of the film's timeline it pauses at, from 0.5 to the duration), `questions` (1–5), optional `title` (the card's eyebrow, "Quick check" by default) and optional `review` (the second Watch that part again goes back to; by default the previous checkpoint, or the start). List checkpoints in time order, at least 2 s apart.
53
+ - **The final quiz:** `questions` (1–20), optional `title` ("Quiz" by default) and `pass` (1–100: the percentage of right answers that passes). The score counts every question on the viewer's path, checkpoints included; a checkpoint skipped by seeking counts as not answered.
54
+ - **In a branching story:** each checkpoint's `at` must lie inside one segment (a checkpoint at a segment's end belongs to that segment) and, in a segment with choices, no later than its `choiceAt`; `review` stays in that segment. The score covers the checkpoints of the segments the viewer played, and the final quiz plays at the first ending they reach, before the ending's card.
55
+ - `theme` colors the cards (accent for buttons and highlights, surface for the card, text); otherwise the brand's primary color on a light card. `ui` translates the player's words on the cards: `check`, `final`, `progress` ("Question {n} of {total}"), `selectAll`, `checkAnswer`, `correct`, `incorrect`, `answer` ("The answer: {answer}."), `continue`, `next`, `seeResults`, `watchAgain`, `score` ("You got {correct} of {total} right"), `passed`, `notPassed` ("{pass}% passes…"), `perfect`, `retake`, `restart`, `complete` and `skipped` ("{count} not answered"); keep the `{…}` placeholders.
56
+ - `npm run build`, `npm run dev`, review and export check the file (and, in a story, against its segments) and name the first problem.
57
+
58
+ ## Writing good questions
59
+
60
+ - One idea per question, in the film's words and the learners' language. Ask about understanding (why, which, what happens next), not trivia from a single frame.
61
+ - Distractors are plausible mistakes a learner really makes (for the Moon: "Earth's shadow covers it"), similar in length and tone to the right answer. Avoid "all of the above", "none of the above", double negatives and trick wording.
62
+ - Vary the position of the right answer; review warns when it is always the same option.
63
+ - An explanation teaches: why the right answer is right and, briefly, why the tempting wrong one is wrong. The card already shows the right option when the viewer misses it.
64
+
65
+ ## Scenes for a quiz
66
+
67
+ - **Put each checkpoint on a held frame.** The film stops on the frame at `at` and the card covers its middle over a dimmed picture, so end each scene with a settled, intentional picture: put `at` on that hold, 0.3–1 s before the scene's next transition, after the narration and captions have finished. Never pause mid-sentence or mid-movement.
68
+ - The question's subject should be what the scene just showed; the viewer can still see the dimmed picture around the card.
69
+ - The final quiz opens over the film's last frame, so end the film on a calm summary or title image rather than a cut to black.
70
+ - Add a `Quiz` track to `src/film-resources.ts` with a short clip at each checkpoint, so the authoring timeline shows where the film stops.
71
+
72
+ ## Sound
73
+
74
+ The film's soundtrack pauses while a card is open and continues with the film. End each narration line and musical phrase before a checkpoint; a phrase cut by the pause sounds broken. Watch that part again replays the soundtrack from `review`.
75
+
76
+ ## Review and delivery
77
+
78
+ - `npm run dev` plays the quiz. In the console, `animaguru.quiz.pick(1)`, `.check()`, `.next()`, `.retake()` and `.reset()` drive the cards and `animaguru.quiz.score` reads the score; `animaguru.state.quiz` says whether a card is open or asking. The page also receives an `animaguru:quiz` window event for each answer and when the quiz is complete. Play the whole film, answer right and wrong, try Watch that part again, Retake the quiz and Watch again, at desktop and phone sizes.
79
+ - `npm run review -- --headless` checks the film under the cards, then prints the quiz's checkpoints, questions and pass mark, plus notes (an answer always in the same position, questions without explanations, checkpoints close together, long prompts). Check the frame at every checkpoint with `npm run frames -- --at …`.
80
+ - Deliver with `npm run export:html -- --animation NAME` or `animaguru submit --yes` (private by default); AnimaGuru's video page plays the quiz and labels the video with its number of questions. Say plainly that it has no MP4; a user who also wants an MP4 of the film needs a copy without `quiz.json`, which plays straight through with no questions.
@@ -46,6 +46,14 @@ Cover the missing essentials, grouping related questions:
46
46
 
47
47
  Save the answers in `production/brief.md`. For a short vertical social ad (Reels, Shorts, TikTok, about 15–60 s), read `references/short-form-ad.md` next: it is the end-to-end command sequence, so the CLI, catalog and older films need no exploration. Develop the story, sequence and pacing from that brief and the selected category. The visual template must not impose a family audience, emotional arc, fixed scene sequence, film length, narration tone or CTA. Once the brief is established, proceed with routine creative decisions without repeated approvals.
48
48
 
49
+ ### A branching story
50
+
51
+ When the user wants viewers to choose what happens next (an interactive, branching or multiple-ending film, ad, explainer or lesson), read sibling `animaguru-branching-story` before planning: the story's segments lie end to end on the film's timeline and `story.json` links them, and the result plays as HTML5 only, never MP4. Start one with `animaguru create animation NAME --story`.
52
+
53
+ ### A quiz
54
+
55
+ When the user wants questions in the film (an educational explainer or lesson with knowledge checks, a question after every scene, a final quiz, a test or a score), read sibling `animaguru-quiz` before planning: `quiz.json` pauses the film at checkpoints and asks a final quiz after its end. The user decides where the questions go and how many; fold those choices into the interview. Like a branching story, it plays as HTML5 only, never MP4. Start one with `animaguru create animation NAME --quiz`.
56
+
49
57
  ### A reference video to remake
50
58
 
51
59
  When the user shares a video and asks for the same kind of film for their own brand or story, analyse it first: `animaguru reference analyze FILE_OR_LINK --animation NAME --yes` (a local MP4, MOV or WebM file, or an HTTPS link to the video file; for a YouTube, Instagram or TikTok page, ask for the file). It takes several minutes and about 22 to 36 credits for a short clip, more for a longer one, so tell the user before running it; it saves `production/references/NAME.json`. Read its scenes, format, story, style, audio and `brand_elements`, and let it answer the interview questions it covers (length, format, pacing, structure, visual and motion direction), so ask only what it cannot: the user's brand (their saved brand kit), product, offer, story and claims. Pick the installed style closest to the reference's look. Rebuild its structure, pacing, layout and motion techniques with the user's own brand, copy and assets; never reuse the original's logo, footage, characters, music, voice or claims. Note in `production/brief.md` which reference the film follows.
@@ -98,7 +106,7 @@ Drive scenes from `cueSheet(timing)` and captions from `joinCaptions(timing.capt
98
106
 
99
107
  Run `npm run build`. Review the actual browser at desktop and mobile sizes, play all chapters, check seeking/replay/captions/audio, inspect representative full-resolution frames and listen to the narration. Check text fit and cutout edges, especially after localization. Do not claim checks that were not performed.
100
108
 
101
- Deliver the playable HTML film first unless the user asked for MP4 too. For an embeddable HTML5 deliverable, use the download menu or `npm run export:html`. Unzip and test the static package over HTTP(S), including its iframe example; it should play without the authoring workspace or an AnimaGuru account. `npm run build` includes the same download in the static player. Preserve embed mode and hide authoring controls there.
109
+ Deliver the playable HTML film first unless the user asked for MP4 too. A branching story (`story.json`) and a quiz film (`quiz.json`) are HTML5 only. For an embeddable HTML5 deliverable, use the download menu or `npm run export:html`. Unzip and test the static package over HTTP(S), including its iframe example; it should play without the authoring workspace or an AnimaGuru account. `npm run build` includes the same download in the static player. Preserve embed mode and hide authoring controls there.
102
110
 
103
111
  For MP4 on the user's own computer, run `npm run export:mp4 -- --headless`; it captures in a local headless Chrome (installed Chrome or Edge, `PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH`, or `npx playwright install chromium`). Without `--headless`, open the printed local capture URL in a browser and keep it open until completion. The script verifies H.264/AAC, dimensions, duration and exact frame count and writes a JSON report next to the MP4. Captions stay out of the picture; the `captions` resource track becomes `.srt` (YouTube, desktop players) and `.vtt` (HTML5) subtitle files with the MP4's name, so keep that track's clips equal to the spoken lines. For social feeds that need open captions, add `-- --burn-captions`; it saves `SLUG-LANGUAGE-captioned.mp4` with captions in the picture and no subtitle files. Set `"poster"` in `film.config.json` to the seconds of the film's most representative frame (title, product or end card, never a fade or a blank moment); the export saves it beside the MP4 as a 1280-pixel JPEG thumbnail, which `animaguru youtube upload` and video pages can use. Exporting existing local assets spends no generation credits. If browser control is unavailable, give the user the printed URL and explain the one manual step. The cloud Animator makes no MP4s and `export:mp4` refuses to run there: the user applies the draft and exports the MP4 in their browser.
104
112
 
@@ -13,6 +13,8 @@ Selections and setup progress are recorded in `animaguru.project.json`. Availabl
13
13
  ## Files and commands
14
14
 
15
15
  - `film.config.json`: title, output slug, dimensions, fps, duration, language and optional audio path relative to `public/`.
16
+ - `story.json` (optional): makes the film a branching story whose viewers choose its path; it plays as HTML5 only. Read `.agents/skills/animaguru-branching-story/SKILL.md` before writing one.
17
+ - `quiz.json` (optional): pauses the film at checkpoints to ask questions and follows its end with a final quiz and the viewer's score; it plays as HTML5 only, never MP4. Read `.agents/skills/animaguru-quiz/SKILL.md` before writing one.
16
18
  - `src/film.ts`: replace the demo with the actual deterministic scene timeline. `src/main.ts`: film entry; `src/player.ts`: reusable AnimaGuru player and capture API. `src/exporter.ts` and `scripts/export-mp4.mjs`: capture transport and verified encoding.
17
19
  - Keep `src/film-resources.ts` aligned with the real film: add every image/video/audio/graphic resource and text layer, its actual cue times and public-relative source path. `Film.resources` powers the full-width timeline below the player. Do not leave demo resources in a finished film or invent audio tracks when none exist. The timeline belongs to the authoring player, outside embed/capture mode.
18
20
  - `shared/locales/`: all user-facing copy. `public/assets/`: local media. `production/`: brief, sources, storyboard, narration, requests and receipts.
@@ -86,6 +86,14 @@ window.animaguru.canvas.toDataURL("image/png");
86
86
 
87
87
  Call frame rendering sequentially and await it before reading pixels. When adding video textures, await decoded frames before returning. `?capture=1&t=12` hides the player controls for review. The MP4 captures only the canvas; keep all film content and captions there. The demo uses system Arial; bundle appropriately licensed fonts for consistent final typography across machines.
88
88
 
89
+ ## Branching stories
90
+
91
+ A `story.json` beside `film.config.json` makes the film a branching story: its segments lie end to end on the timeline, and the player plays one at a time, shows the viewer's choices over the picture and continues into the chosen segment. `?segment=ID` starts at a segment. `window.animaguru.story` has the graph, the `path` taken, and `choose(index)`, `back()`, `restart()` and `goTo(id)`; `window.animaguru.state.story` names the segment playing, its start and end, and whether choices are waiting or the story has ended. Capture (`?capture=1`) and review see the whole timeline as one film. A branching story plays as HTML5 only: `npm run export:mp4` refuses it. The `animaguru-branching-story` skill has the format.
92
+
93
+ ## Quizzes
94
+
95
+ A `quiz.json` beside `film.config.json` adds a quiz: each checkpoint pauses the film at its second to ask one question or a quick round, a final quiz follows the film's end, and the viewer sees their score with Retake the quiz and Watch again. A wrong answer at a checkpoint offers Watch that part again. `window.animaguru.quiz` has the questions, `pick(index)`, `check()`, `next()`, `retake()`, `reset()` and the `score`; `window.animaguru.state.quiz` says whether a card is open and asking; the page receives an `animaguru:quiz` window event for each answer and when the quiz is complete. Capture and review see the film without the cards. A quiz film plays as HTML5 only: `npm run export:mp4` refuses it. It works in a branching story too. The `animaguru-quiz` skill has the format.
96
+
89
97
  ## Credits and provenance
90
98
 
91
99
  Scaffolding requires no AnimaGuru account and performs no paid generation. Your agent will use `animaguru auth login`, the installed AnimaGuru skill and your chosen credit budget when media is needed. Keep requests and receipts; inspect existing jobs after uncertain outcomes before resubmitting.