@docyrus/animaguru 0.18.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.
- package/README.md +63 -1
- package/dist/animaguru.js +581 -268
- package/package.json +1 -1
- package/skills/animaguru/SKILL.md +16 -0
- package/skills/animaguru/references/sharing.md +4 -1
- package/skills/animaguru-branching-story/SKILL.md +72 -0
- package/skills/animaguru-general-video/SKILL.md +1 -0
- package/skills/animaguru-general-video/references/branching-story.md +13 -0
- package/skills/animaguru-quiz/SKILL.md +80 -0
- package/skills/animaguru-video/SKILL.md +16 -2
- package/skills/animaguru-video/references/short-form-ad.md +1 -0
- package/templates/video/AGENTS.md +2 -0
- package/templates/video/README.md +8 -0
- package/templates/video/WORKSPACE-AGENTS.md +3 -1
- package/templates/video/index.html +1 -1
- package/templates/video/scripts/build.mjs +4 -1
- package/templates/video/scripts/export-mp4.mjs +5 -0
- package/templates/video/scripts/html-export.mjs +12 -4
- package/templates/video/scripts/project-context.mjs +20 -3
- package/templates/video/scripts/quiz.d.mts +31 -0
- package/templates/video/scripts/quiz.mjs +126 -0
- package/templates/video/scripts/review.mjs +9 -1
- package/templates/video/scripts/story.d.mts +36 -0
- package/templates/video/scripts/story.mjs +139 -0
- package/templates/video/scripts/workspace-plugin.mjs +9 -4
- package/templates/video/shared/locales/en/player.json +30 -1
- package/templates/video/src/kit/index.ts +1 -0
- package/templates/video/src/kit/story.ts +19 -0
- package/templates/video/src/main.ts +6 -1
- package/templates/video/src/player-types.ts +6 -0
- package/templates/video/src/player.ts +232 -21
- package/templates/video/src/quiz.ts +315 -0
- package/templates/video/src/story.ts +112 -0
- package/templates/video/src/style.css +93 -1
package/package.json
CHANGED
|
@@ -94,6 +94,22 @@ Run tooling from the workspace root. `npm run dev` opens a hub with separate ani
|
|
|
94
94
|
|
|
95
95
|
For hosting, run `animaguru submit animations/NAME --yes`, or `animaguru submit --animation NAME --yes` at the root. Existing sharing and spending authorization rules apply. Multiple films require an explicit selection; do not silently submit the first folder. Put playback narration in the film's `public/assets/`, configure its public-relative master path in `film.config.json`, and retain original requests/receipts in `production/`. HTML5 export rejects a configured soundtrack missing from its package. Manual playback is audible; autoplay and capture start muted.
|
|
96
96
|
|
|
97
|
+
## Frame checks
|
|
98
|
+
|
|
99
|
+
CLI 0.19.0 adds `animaguru review check [DIRECTORY] [--animation NAME] --yes`. After `npm run review`, it sends the film's review frames to AnimaGuru's frame checker (Cloudflare's Clef-omni) and returns each frame's probability of an empty frame, cut-off or overlapping text, broken letters and hard-to-read text, plus a polish score; flagged frames come first and the full answers go to `production/review/check.json`. It charges a fraction of a credit per frame (`--dry-run` for the most). `--source frames` checks up to 12 full-size frames from `npm run frames -- --full`; `--questions FILE` adds up to 16 questions of your own. Open the flagged frames first. See the `animaguru-video` skill.
|
|
100
|
+
|
|
101
|
+
## Reference videos
|
|
102
|
+
|
|
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
|
+
|
|
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
|
+
|
|
97
113
|
## Motion library
|
|
98
114
|
|
|
99
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.
|
|
@@ -4,13 +4,14 @@ Read from the `animaguru` skill only when the user asks to host, share or upload
|
|
|
4
4
|
|
|
5
5
|
## Submit a finished animation to AnimaGuru
|
|
6
6
|
|
|
7
|
-
`submit` hosts a finished animation, separate from paid asset generation and `files export`. In a scaffolded video project, `animaguru submit --yes` builds its portable HTML5 ZIP and uploads it privately. Use `--file` for an existing AnimaGuru HTML5 export or MP4; only the export is uploaded, never the source workspace. `--dry-run` inspects the file, metadata and audience locally without signing in or uploading.
|
|
7
|
+
`submit` hosts a finished animation, separate from paid asset generation and `files export`. In a scaffolded video project, `animaguru submit --yes` builds its portable HTML5 ZIP and uploads it privately. Use `--file` for an existing AnimaGuru HTML5 export or MP4; only the export is uploaded, never the source workspace. `--dry-run` inspects the file, metadata and audience locally without signing in or uploading. The video is kept in the selected workspace's library, or a synced project's workspace; pass `--workspace personal` or `--workspace TEAM_ID` when the user names the library to use.
|
|
8
8
|
|
|
9
9
|
```sh
|
|
10
10
|
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,18 @@ 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
|
+
|
|
57
|
+
### A reference video to remake
|
|
58
|
+
|
|
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.
|
|
60
|
+
|
|
49
61
|
## Production contract
|
|
50
62
|
|
|
51
63
|
- Save the interview and creative decisions in `production/brief.md`; update `animaguru.project.json` with interview completion and the returned remote project ID. Do not store credentials there.
|
|
@@ -94,9 +106,9 @@ Drive scenes from `cueSheet(timing)` and captions from `joinCaptions(timing.capt
|
|
|
94
106
|
|
|
95
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.
|
|
96
108
|
|
|
97
|
-
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.
|
|
98
110
|
|
|
99
|
-
For MP4, 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.
|
|
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.
|
|
100
112
|
|
|
101
113
|
`npm run dev` starts the preview. The capture API and file map are documented in the generated project README. Publishing a website, package or campaign is separate from creating the local video.
|
|
102
114
|
|
|
@@ -108,6 +120,8 @@ Use `src/studies/timing.ts` for analytic easing, measured stroke reveals and cue
|
|
|
108
120
|
|
|
109
121
|
`npm run review -- --headless` collects actual text bounds every 0.2 s, representative frames and repeated cold-seek comparisons into `production/review/` and prints one line per finding (without `--headless`, open the printed URL). Besides seek, frame-bound, reading-time and cue checks, it flags overlapping text, text outside `safeArea`, text below `minTextSize` (default 2.2% of the shorter side) and film text under a burned-in caption, with time ranges. Fix those before looking at frames. Optional cues with `audioTime` are checked to an output-frame tolerance. Reading-time notes are design guidance, not a substitute for watching/listening; partial type reveals and occlusion still need visual review. Do not report a check as passed when metadata or media was unavailable.
|
|
110
122
|
|
|
123
|
+
Then run `animaguru review check --animation NAME --yes`. It sends the review frames, with the film's title, format and brief, to AnimaGuru's frame checker (Cloudflare's Clef-omni) and returns each frame's probability of an empty frame, text or a logo cut off by the edge, overlapping text, broken letters and text too hard to read on a phone, plus a polish score from 0 (broken) to 3 (polished). Flagged frames are listed first and the full answers are saved to `production/review/check.json`. It charges a fraction of a credit per frame; `--dry-run` returns the most it can cost. Open the flagged frames before the others. `--source frames` checks the full-size frames of `npm run frames -- --full` instead (up to 12), and `--questions FILE` adds up to 16 questions of your own, such as whether the logo is visible on the end card. The checker estimates probabilities: it does not judge motion, timing or taste, so still watch the film before calling it finished.
|
|
124
|
+
|
|
111
125
|
For visual review, run `npm run frames -- --animation NAME --at 1.3,4.9,…` (or `--every 3`, `--count 12`; `--no-captions`, `--full` for full-size JPEGs). It saves labelled contact sheets of up to 12 frames to `production/review/frames/` and prints the layout notes for those times. Look at one sheet instead of many single frames, and re-run only the times you changed.
|
|
112
126
|
|
|
113
127
|
## Sharing a finished film
|
|
@@ -49,6 +49,7 @@ A read at about 2 words per second, sped up 1.12×, fits 85–95 words into 45 s
|
|
|
49
49
|
## 7. Review in two or three rounds
|
|
50
50
|
|
|
51
51
|
- `npm run review -- --animation NAME --headless`, then fix every overlap, safe-area, small-text and caption line it prints.
|
|
52
|
+
- `animaguru review check --animation NAME --yes` checks the review frames for an empty hook frame, cut-off or overlapping text and broken letters. Open the frames it flags first.
|
|
52
53
|
- `npm run frames -- --animation NAME --at TIMES`, with each beat's midpoint and each transition, gives one contact sheet per 12 frames. Fix what it shows, then re-run only the changed times.
|
|
53
54
|
|
|
54
55
|
## 8. Export
|
|
@@ -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.
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
This root owns shared branding, installed skills, dependencies, the PixiJS player and export tools. Each film lives in `animations/NAME/`. Open this root in Codex or Claude Code.
|
|
4
4
|
|
|
5
|
+
- For a branching story, where viewers choose what happens next, run `animaguru create animation NAME --story` and read the installed `animaguru-branching-story` skill: `story.json` beside the film's `film.config.json` links its segments, and it plays as HTML5 only.
|
|
6
|
+
- For a quiz (an educational explainer or lesson whose film pauses for questions, with a final quiz and a score), run `animaguru create animation NAME --quiz` and read the installed `animaguru-quiz` skill: `quiz.json` beside `film.config.json` holds the questions, and the film plays as HTML5 only, never MP4.
|
|
5
7
|
- For a new film, run `animaguru create animation NAME` here; infer a useful name from the request when none was supplied. Reuse an existing folder when the user is revising that film. If several films could match the request, clarify which one before editing it.
|
|
6
8
|
- Read `branding/brand.json`, branding files/guides in `branding/`, and any relevant branding files the user placed at the root. If the kit is still the scaffold placeholder or incomplete, read `.agents/skills/animaguru-brand-kit/SKILL.md` (or `.claude/skills/animaguru-brand-kit/SKILL.md` in Claude Code). Use an official website or brand guide in the first prompt; if none was supplied, ask for one before researching the brand. Populate `branding/` and record sources in `branding/brand-guide.md`. Every animation inherits these shared settings. Keep guides local; exports include registered web assets, rather than the whole branding folder.
|
|
7
9
|
- Read the installed `animaguru-video` skill and the selected film's `animaguru.project.json`. After establishing the shared brand source, first ask for **Explainer**, **Advertisement**, or **General Video**; then ask for a visual style using `animaguru.templates.json` and https://animaguru.com/animation-styles; then run the compact creative interview. Reuse explicit choices and supplied/completed answers. Save the brief inside that film's `production/` folder. Purpose and visual style are separate decisions.
|
|
@@ -14,7 +16,7 @@ Run commands from the workspace root:
|
|
|
14
16
|
- `npm run dev`: the workspace hub and separate `/animations/NAME/` players.
|
|
15
17
|
- `npm run build -- --animation NAME`: typecheck and build that film.
|
|
16
18
|
- `npm run export:html -- --animation NAME`: portable HTML5 ZIP with local media and common branding.
|
|
17
|
-
- `npm run export:mp4 -- --animation NAME
|
|
19
|
+
- `npm run export:mp4 -- --animation NAME` (not for a branching story or a quiz film): verified MP4 without burned-in captions, plus `.srt`/`.vtt` subtitle files from the captions track (`--burn-captions` draws them into the picture instead, as `NAME-LANG-captioned.mp4`), and a `.jpg` thumbnail of the `poster` frame set in film.config.json. Add `--headless` to capture in local headless Chrome; otherwise open the printed capture page.
|
|
18
20
|
- `npm run review -- --animation NAME --headless`: frame, text, layout (overlap, safe area, text size), seeking and soundtrack checks, one line per finding.
|
|
19
21
|
- `npm run frames -- --animation NAME --at 1.2,4.8`: labelled contact sheets in `production/review/frames/` plus layout notes for those times.
|
|
20
22
|
- `npm run audio:vo|audio:music|audio:mix -- --animation NAME`: cut and place the voiceover (`timing.json`), fit the music bed, and master the soundtrack.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<html lang="en">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
|
6
6
|
<meta name="theme-color" content="#f5f3ed" />
|
|
7
7
|
<link rel="icon" type="image/svg+xml" href="./brand/logo-mark.svg" />
|
|
8
8
|
<title>AnimaGuru · Motion studio</title>
|
|
@@ -16,7 +16,10 @@ if (context.workspace) {
|
|
|
16
16
|
const cache = path.join(root, ".animaguru/cache", context.name);
|
|
17
17
|
await mkdir(cache, { recursive: true });
|
|
18
18
|
const imported = (file) => JSON.stringify(file.split(path.sep).join("/"));
|
|
19
|
-
|
|
19
|
+
// A branching story's story.json and a quiz's quiz.json are checked against the player's types too.
|
|
20
|
+
const story = context.story ? `import story from ${imported(path.join(context.projectRoot,"story.json"))};\n` : "";
|
|
21
|
+
const quiz = context.quiz ? `import quiz from ${imported(path.join(context.projectRoot,"quiz.json"))};\n` : "";
|
|
22
|
+
await writeFile(path.join(cache, "contract.ts"), `import config from ${imported(path.join(context.projectRoot,"film.config.json"))};\nimport {Film,chapters} from ${imported(path.join(context.projectRoot,"src/film.ts"))};\n${story}${quiz}import type {PlayerOptions} from ${imported(path.join(root,"runtime/player-types.ts"))};\nconst player: PlayerOptions = {config,Film,chapters${context.story ? ",story" : ""}${context.quiz ? ",quiz" : ""}};\nvoid player;\n`);
|
|
20
23
|
await writeFile(path.join(cache, "tsconfig.json"), JSON.stringify({ extends: path.join(root, "tsconfig.json"), include: [path.join(root,"runtime/**/*.ts"),path.join(context.projectRoot,"src/**/*.ts"),path.join(cache,"contract.ts")] }));
|
|
21
24
|
typecheckArguments = ["--project", path.join(cache, "tsconfig.json"), "--noEmit"];
|
|
22
25
|
}
|
|
@@ -12,6 +12,9 @@ import { workspacePlugin } from "./workspace-plugin.mjs";
|
|
|
12
12
|
import { subtitleCues, toSrt, toVtt } from "./subtitles.mjs";
|
|
13
13
|
import { captureHeadless, takeHeadlessFlag } from "./headless-browser.mjs";
|
|
14
14
|
|
|
15
|
+
// The cloud Animator makes no MP4s: once the user applies the draft, they export it in their own browser.
|
|
16
|
+
if (process.env.ANIMAGURU_CLOUD_WORKSPACE) throw new Error("MP4 export is not available in the cloud Animator. After the user applies the draft, they export the MP4 in their browser from the draft page.");
|
|
17
|
+
|
|
15
18
|
const root = fileURLToPath(new URL("../", import.meta.url));
|
|
16
19
|
const selection = projectArguments(process.argv.slice(2));
|
|
17
20
|
const args = selection.rest;
|
|
@@ -27,6 +30,8 @@ for (let index = 0; index < args.length; index++) {
|
|
|
27
30
|
}
|
|
28
31
|
if (!Number.isInteger(port) || port < 1024 || port > 65535) throw new Error("Export port must be 1024–65535.");
|
|
29
32
|
const context = await projectContext(root, selection.animation);
|
|
33
|
+
if (context.story) throw new Error("This film is a branching story (story.json): it plays as HTML5 only, because an MP4 has one path. Deliver it with npm run export:html or animaguru submit.");
|
|
34
|
+
if (context.quiz) throw new Error("This film has a quiz (quiz.json): it plays as HTML5 only, because its questions wait for a viewer's answers. Deliver it with npm run export:html or animaguru submit.");
|
|
30
35
|
const config = context.config;
|
|
31
36
|
const { width, height, fps, duration, language } = config;
|
|
32
37
|
if (![width, height].every((n) => Number.isInteger(n) && n >= 2 && n <= 4096 && n % 2 === 0) || ![24, 25, 30, 60].includes(fps) || !Number.isFinite(duration) || duration <= 0 || duration > 600 || Math.abs(duration * fps - Math.round(duration * fps)) > 0.000001) throw new Error("Use even dimensions up to 4096px, 24/25/30/60 fps, and a frame-aligned duration up to 600s in film.config.json.");
|
|
@@ -4,6 +4,8 @@ import { createHash } from "node:crypto";
|
|
|
4
4
|
import { zipSync, strToU8 } from "fflate";
|
|
5
5
|
import { projectContext, publicFile } from "./project-context.mjs";
|
|
6
6
|
import { workspacePlugin } from "./workspace-plugin.mjs";
|
|
7
|
+
import { storySummary, validateStory } from "./story.mjs";
|
|
8
|
+
import { quizSummary, validateQuiz } from "./quiz.mjs";
|
|
7
9
|
|
|
8
10
|
const escapeHtml = (value) => String(value).replace(/[&<>"']/g, (character) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[character]);
|
|
9
11
|
|
|
@@ -13,9 +15,15 @@ export function validateHtmlConfig(config) {
|
|
|
13
15
|
if (typeof config.title !== "string" || typeof config.language !== "string") throw new Error("Set the film title and language in film.config.json.");
|
|
14
16
|
}
|
|
15
17
|
|
|
16
|
-
/**
|
|
17
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Only the built website is packaged, never the project, skills, receipts, or dependencies. A branching story's manifest
|
|
20
|
+
* says so ("story"), so a host plays it as an interactive film and never offers an MP4 of it; a quiz's ("quiz") counts
|
|
21
|
+
* its questions, and a quiz film has no MP4 either.
|
|
22
|
+
*/
|
|
23
|
+
export async function packageHtmlDirectory(directory, config, story = null, quiz = null) {
|
|
18
24
|
validateHtmlConfig(config);
|
|
25
|
+
if (story) validateStory(story, config);
|
|
26
|
+
if (quiz) validateQuiz(quiz, config, story);
|
|
19
27
|
const files = {};
|
|
20
28
|
const manifest = [];
|
|
21
29
|
async function collect(relative = "") {
|
|
@@ -39,7 +47,7 @@ export async function packageHtmlDirectory(directory, config) {
|
|
|
39
47
|
const embed = iframe.replace(`aspect-ratio:${config.width} / ${config.height}`, `height:100%`);
|
|
40
48
|
files["embed-example.html"] = strToU8(`<!doctype html>\n<html lang="${escapeHtml(config.language)}"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${title}</title></head><body style="margin:0;background:#f5f3ed"><div style="position:relative;width:100%;height:calc(100vw * ${config.height} / ${config.width} + 112px);max-height:100vh">${embed}</div></body></html>\n`);
|
|
41
49
|
files["README.txt"] = strToU8(`AnimaGuru HTML5 animation: ${config.title}\n\nUpload this entire folder to any static HTTP(S) host. Open index.html for the animation or embed-example.html for a working iframe example. ES modules and local media need an HTTP(S) server; do not open index.html with file://.\n\nEmbed the hosted animation:\n${iframe.replace('src="./index.html"', 'src="https://your-site.example/animation/index.html"')}\n\nAllow extra iframe height for controls, or add ?controls=0&autoplay=1&loop=1 for a canvas-only embed. Playback starts muted; viewers can unmute with the sound button. Autoplay respects reduced-motion preferences (controls remain available in that case).\n\nThis archive contains the compiled PixiJS animation and local media. It needs no Node.js, AnimaGuru account, API, or MP4 conversion on the host. Keep all files together; relative paths support nested hosting folders. External assets explicitly added by an author still require their external sources.\n`);
|
|
42
|
-
files["manifest.json"] = strToU8(JSON.stringify({ format: "spriteguru-html5", version: 1, title: config.title, width: config.width, height: config.height, duration: config.duration, language: config.language, audio: config.audio ?? null, files: manifest }, null, 2) + "\n");
|
|
50
|
+
files["manifest.json"] = strToU8(JSON.stringify({ format: "spriteguru-html5", version: 1, title: config.title, width: config.width, height: config.height, duration: config.duration, language: config.language, audio: config.audio ?? null, ...(story ? { story: storySummary(story) } : {}), ...(quiz ? { quiz: quizSummary(quiz) } : {}), files: manifest }, null, 2) + "\n");
|
|
43
51
|
return { filename: `${config.slug}-html5.zip`, bytes: zipSync(files), files: Object.keys(files).length };
|
|
44
52
|
}
|
|
45
53
|
|
|
@@ -54,6 +62,6 @@ export async function buildHtmlExport(root, { animation } = {}) {
|
|
|
54
62
|
try {
|
|
55
63
|
const { build } = await import("vite");
|
|
56
64
|
await build({ root, configFile: false, base: "./", publicDir: context.workspace ? false : undefined, plugins: context.workspace ? [workspacePlugin(root, { animation: context.name, mode: "embed" })] : [], mode: "embed", logLevel: "silent", build: { outDir: temporary, emptyOutDir: true, sourcemap: false } });
|
|
57
|
-
return await packageHtmlDirectory(temporary, config);
|
|
65
|
+
return await packageHtmlDirectory(temporary, config, context.story, context.quiz);
|
|
58
66
|
} finally { await rm(temporary, { recursive: true, force: true }); }
|
|
59
67
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { access, readFile, readdir, realpath, stat } from "node:fs/promises";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { STORY_FILE, validateStory } from "./story.mjs";
|
|
5
|
+
import { QUIZ_FILE, validateQuiz } from "./quiz.mjs";
|
|
4
6
|
|
|
5
7
|
export const animationName = (value) => typeof value === "string" && /^[a-z0-9][a-z0-9-]{0,79}$/.test(value);
|
|
6
8
|
const extensions = new Set(["png", "jpg", "jpeg", "webp", "svg", "gif", "woff", "woff2", "ttf", "otf", "mp3", "wav", "m4a", "ogg", "mp4", "webm"]);
|
|
@@ -18,7 +20,9 @@ export async function listAnimations(root) {
|
|
|
18
20
|
if (!entry.isDirectory() || !animationName(entry.name)) continue;
|
|
19
21
|
const config = await optionalJson(path.join(root, "animations", entry.name, "film.config.json"));
|
|
20
22
|
const project = await optionalJson(path.join(root, "animations", entry.name, "animaguru.project.json"));
|
|
21
|
-
|
|
23
|
+
const exists = (file) => access(path.join(root, "animations", entry.name, file)).then(() => true, () => false);
|
|
24
|
+
const [story, quiz] = await Promise.all([exists(STORY_FILE), exists(QUIZ_FILE)]);
|
|
25
|
+
if (config && project) result.push({ name: entry.name, title: config.title, duration: config.duration, category: project.category, template: project.template, story, quiz });
|
|
22
26
|
}
|
|
23
27
|
return result.sort((a, b) => a.name.localeCompare(b.name));
|
|
24
28
|
}
|
|
@@ -62,7 +66,9 @@ export async function projectContext(root, name) {
|
|
|
62
66
|
const workspace = await workspaceInfo(root);
|
|
63
67
|
if (!workspace) {
|
|
64
68
|
if (name) throw new Error("--animation selects a project inside an AnimaGuru workspace.");
|
|
65
|
-
|
|
69
|
+
const config = JSON.parse(await readFile(path.join(root, "film.config.json"), "utf8"));
|
|
70
|
+
const story = await projectStory(root, config);
|
|
71
|
+
return { workspace: false, root, projectRoot: root, publicRoot: path.join(root, "public"), output: path.join(root, "production/exports"), route: "/", config, story, quiz: await projectQuiz(root, config, story), branding: null, brandFiles: [] };
|
|
66
72
|
}
|
|
67
73
|
if (!name) {
|
|
68
74
|
const animations = await listAnimations(root);
|
|
@@ -77,7 +83,18 @@ export async function projectContext(root, name) {
|
|
|
77
83
|
await access(path.join(projectRoot, "animaguru.project.json"));
|
|
78
84
|
const config = JSON.parse(await readFile(path.join(projectRoot, "film.config.json"), "utf8"));
|
|
79
85
|
const brand = await readBranding(root);
|
|
80
|
-
|
|
86
|
+
const story = await projectStory(projectRoot, config);
|
|
87
|
+
return { workspace: true, name, root, projectRoot, publicRoot: path.join(projectRoot, "public"), output: path.join(projectRoot, "production/exports"), route: `/animations/${name}/`, config, story, quiz: await projectQuiz(projectRoot, config, story), branding: brand.branding, brandFiles: brand.files };
|
|
88
|
+
}
|
|
89
|
+
/** The film's story.json, checked, when it is a branching story; null for a film with one path. */
|
|
90
|
+
async function projectStory(projectRoot, config) {
|
|
91
|
+
const story = await optionalJson(path.join(projectRoot, STORY_FILE));
|
|
92
|
+
return story === null ? null : validateStory(story, config);
|
|
93
|
+
}
|
|
94
|
+
/** The film's quiz.json, checked against its duration and story, when it asks questions; null otherwise. */
|
|
95
|
+
async function projectQuiz(projectRoot, config, story) {
|
|
96
|
+
const quiz = await optionalJson(path.join(projectRoot, QUIZ_FILE));
|
|
97
|
+
return quiz === null ? null : validateQuiz(quiz, config, story);
|
|
81
98
|
}
|
|
82
99
|
export async function publicFile(context, relative) {
|
|
83
100
|
if (typeof relative !== "string" || !relative || relative.startsWith("/") || relative.includes("\\") || relative.split("/").some((segment) => !segment || segment === "." || segment === "..")) throw new Error("Use a local media path relative to the animation's public folder.");
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { StorySegment } from "./story.mjs";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* One question: 2–6 options and the right one (answer, an index from 0), or for "select all that apply" the right
|
|
5
|
+
* ones as indices in increasing order. The explanation shows after the viewer answers.
|
|
6
|
+
*/
|
|
7
|
+
export type QuizQuestion = { id?: string; prompt: string; options: string[]; answer: number | number[]; explanation?: string };
|
|
8
|
+
/**
|
|
9
|
+
* A pause in the film for one question or a quick round of up to five. at: the second of the film's timeline it
|
|
10
|
+
* pauses at; review: where Watch again goes back to (the previous checkpoint, or the start, by default).
|
|
11
|
+
*/
|
|
12
|
+
export type QuizCheckpoint = { id: string; at: number; title?: string; review?: number; questions: QuizQuestion[] };
|
|
13
|
+
/** The quiz after the film's end; pass is the percentage of right answers, over every question asked, that passes. */
|
|
14
|
+
export type QuizFinal = { title?: string; pass?: number; questions: QuizQuestion[] };
|
|
15
|
+
export type QuizUiKey = "check" | "final" | "progress" | "selectAll" | "checkAnswer" | "correct" | "incorrect" | "answer" | "continue" | "next" | "seeResults" | "watchAgain" | "score" | "passed" | "notPassed" | "perfect" | "retake" | "restart" | "complete" | "skipped";
|
|
16
|
+
export type Quiz = {
|
|
17
|
+
version: number;
|
|
18
|
+
checkpoints?: QuizCheckpoint[];
|
|
19
|
+
final?: QuizFinal;
|
|
20
|
+
/** The cards' colors; otherwise the brand's. */
|
|
21
|
+
theme?: { accent?: string; surface?: string; text?: string };
|
|
22
|
+
/** The player's own words on the cards, for a quiz in another language. */
|
|
23
|
+
ui?: Partial<Record<QuizUiKey, string>>;
|
|
24
|
+
};
|
|
25
|
+
export type QuizSummary = { checkpoints: number; questions: number; final: number; pass: number | null };
|
|
26
|
+
export const QUIZ_FILE: string;
|
|
27
|
+
export const QUIZ_UI_KEYS: QuizUiKey[];
|
|
28
|
+
export function validateQuiz(quiz: unknown, config?: { duration: number }, story?: { segments: StorySegment[] } | null): Quiz;
|
|
29
|
+
export function checkpointSegment(checkpoint: QuizCheckpoint, segments: StorySegment[]): StorySegment | null;
|
|
30
|
+
export function quizSummary(quiz: Quiz): QuizSummary;
|
|
31
|
+
export function quizNotes(quiz: Quiz): string[];
|