@docyrus/animaguru 0.6.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 (198) hide show
  1. package/NOTICE.md +256 -0
  2. package/README.md +241 -0
  3. package/dist/animaguru.js +27338 -0
  4. package/package.json +37 -0
  5. package/skills/animaguru/SKILL.md +115 -0
  6. package/skills/animaguru-advertising/SKILL.md +25 -0
  7. package/skills/animaguru-advertising/references/brand-bumper.md +11 -0
  8. package/skills/animaguru-advertising/references/comparison.md +11 -0
  9. package/skills/animaguru-advertising/references/creator-testimonial.md +11 -0
  10. package/skills/animaguru-advertising/references/editorial-advertorial.md +11 -0
  11. package/skills/animaguru-advertising/references/game-reward.md +11 -0
  12. package/skills/animaguru-advertising/references/offer-promotion.md +11 -0
  13. package/skills/animaguru-advertising/references/product-launch.md +11 -0
  14. package/skills/animaguru-advertising/references/product-proof.md +11 -0
  15. package/skills/animaguru-analog-film-tape/SKILL.md +29 -0
  16. package/skills/animaguru-analog-film-tape/references/recipe.md +23 -0
  17. package/skills/animaguru-animated-data-visualization/SKILL.md +39 -0
  18. package/skills/animaguru-animated-data-visualization/references/data-contract.md +16 -0
  19. package/skills/animaguru-animated-data-visualization/references/variants.md +17 -0
  20. package/skills/animaguru-art-deco-motion/SKILL.md +29 -0
  21. package/skills/animaguru-art-deco-motion/references/recipe.md +22 -0
  22. package/skills/animaguru-ascii-crt/SKILL.md +29 -0
  23. package/skills/animaguru-ascii-crt/references/recipe.md +22 -0
  24. package/skills/animaguru-blueprint-drafting/SKILL.md +29 -0
  25. package/skills/animaguru-blueprint-drafting/references/recipe.md +22 -0
  26. package/skills/animaguru-cartoon-character-animation/SKILL.md +35 -0
  27. package/skills/animaguru-cartoon-character-animation/references/variants.md +17 -0
  28. package/skills/animaguru-cutout-animation/SKILL.md +33 -0
  29. package/skills/animaguru-cutout-animation/references/production-recipe.md +37 -0
  30. package/skills/animaguru-cutout-animation/references/variants.md +17 -0
  31. package/skills/animaguru-cutout-animation/references/visual-style.md +34 -0
  32. package/skills/animaguru-dark-tech-stage/SKILL.md +29 -0
  33. package/skills/animaguru-dark-tech-stage/references/recipe.md +23 -0
  34. package/skills/animaguru-diagram-formula-motion/SKILL.md +36 -0
  35. package/skills/animaguru-diagram-formula-motion/references/variants.md +17 -0
  36. package/skills/animaguru-engraved-relief-print/SKILL.md +29 -0
  37. package/skills/animaguru-engraved-relief-print/references/recipe.md +24 -0
  38. package/skills/animaguru-flat-vector-motion/SKILL.md +35 -0
  39. package/skills/animaguru-flat-vector-motion/references/variants.md +17 -0
  40. package/skills/animaguru-general-video/SKILL.md +20 -0
  41. package/skills/animaguru-general-video/references/ambient-loop.md +11 -0
  42. package/skills/animaguru-general-video/references/music-lyrics.md +11 -0
  43. package/skills/animaguru-general-video/references/short-film.md +11 -0
  44. package/skills/animaguru-general-video/references/titles-event.md +11 -0
  45. package/skills/animaguru-general-video/references/visual-essay.md +11 -0
  46. package/skills/animaguru-generative-light-signal/SKILL.md +29 -0
  47. package/skills/animaguru-generative-light-signal/references/recipe.md +23 -0
  48. package/skills/animaguru-glass-mosaic/SKILL.md +29 -0
  49. package/skills/animaguru-glass-mosaic/references/recipe.md +23 -0
  50. package/skills/animaguru-halftone-editorial/SKILL.md +29 -0
  51. package/skills/animaguru-halftone-editorial/references/recipe.md +23 -0
  52. package/skills/animaguru-holographic-scan/SKILL.md +29 -0
  53. package/skills/animaguru-holographic-scan/references/recipe.md +22 -0
  54. package/skills/animaguru-interface-motion/SKILL.md +36 -0
  55. package/skills/animaguru-interface-motion/references/screen-mapping.md +16 -0
  56. package/skills/animaguru-interface-motion/references/variants.md +17 -0
  57. package/skills/animaguru-isometric-worlds/SKILL.md +35 -0
  58. package/skills/animaguru-isometric-worlds/references/variants.md +17 -0
  59. package/skills/animaguru-kinetic-typography/SKILL.md +35 -0
  60. package/skills/animaguru-kinetic-typography/references/variants.md +17 -0
  61. package/skills/animaguru-miniature-diorama/SKILL.md +29 -0
  62. package/skills/animaguru-miniature-diorama/references/recipe.md +23 -0
  63. package/skills/animaguru-painterly-depth-animation/SKILL.md +35 -0
  64. package/skills/animaguru-painterly-depth-animation/references/variants.md +17 -0
  65. package/skills/animaguru-paper-popup/SKILL.md +29 -0
  66. package/skills/animaguru-paper-popup/references/recipe.md +22 -0
  67. package/skills/animaguru-photo-footage-editorial/SKILL.md +29 -0
  68. package/skills/animaguru-photo-footage-editorial/references/recipe.md +23 -0
  69. package/skills/animaguru-pixel-art-animation/SKILL.md +36 -0
  70. package/skills/animaguru-pixel-art-animation/references/variants.md +17 -0
  71. package/skills/animaguru-portal-zoom/SKILL.md +29 -0
  72. package/skills/animaguru-portal-zoom/references/recipe.md +21 -0
  73. package/skills/animaguru-risograph-print/SKILL.md +29 -0
  74. package/skills/animaguru-risograph-print/references/recipe.md +23 -0
  75. package/skills/animaguru-sand-drawing/SKILL.md +29 -0
  76. package/skills/animaguru-sand-drawing/references/recipe.md +21 -0
  77. package/skills/animaguru-shadow-theatre/SKILL.md +29 -0
  78. package/skills/animaguru-shadow-theatre/references/recipe.md +23 -0
  79. package/skills/animaguru-silkscreen-poster/SKILL.md +29 -0
  80. package/skills/animaguru-silkscreen-poster/references/recipe.md +22 -0
  81. package/skills/animaguru-stop-motion-objects/SKILL.md +29 -0
  82. package/skills/animaguru-stop-motion-objects/references/recipe.md +23 -0
  83. package/skills/animaguru-studio-product-reveal/SKILL.md +29 -0
  84. package/skills/animaguru-studio-product-reveal/references/recipe.md +23 -0
  85. package/skills/animaguru-video/SKILL.md +79 -0
  86. package/skills/animaguru-whiteboard-line-drawing/SKILL.md +34 -0
  87. package/skills/animaguru-whiteboard-line-drawing/references/variants.md +17 -0
  88. package/templates/video/AGENTS.md +34 -0
  89. package/templates/video/CLAUDE.md +19 -0
  90. package/templates/video/README.md +101 -0
  91. package/templates/video/VIDEO-STYLE-NOTICES.txt +54 -0
  92. package/templates/video/film.config.json +10 -0
  93. package/templates/video/gitignore +11 -0
  94. package/templates/video/index.html +14 -0
  95. package/templates/video/package.json +28 -0
  96. package/templates/video/public/brand/logo-mark.svg +5 -0
  97. package/templates/video/scripts/build.mjs +12 -0
  98. package/templates/video/scripts/export-html.mjs +12 -0
  99. package/templates/video/scripts/export-mp4.mjs +209 -0
  100. package/templates/video/scripts/html-export-plugin.mjs +35 -0
  101. package/templates/video/scripts/html-export.mjs +54 -0
  102. package/templates/video/scripts/install-example.mjs +32 -0
  103. package/templates/video/scripts/review-analysis.mjs +29 -0
  104. package/templates/video/scripts/review-audio.mjs +24 -0
  105. package/templates/video/scripts/review.mjs +37 -0
  106. package/templates/video/scripts/starter-film.sha256 +1 -0
  107. package/templates/video/shared/locales/en/export.json +10 -0
  108. package/templates/video/shared/locales/en/film.json +42 -0
  109. package/templates/video/shared/locales/en/player.json +31 -0
  110. package/templates/video/shared/locales/en/style-studies.json +168 -0
  111. package/templates/video/shared/locales/en/timeline.json +33 -0
  112. package/templates/video/src/assets.ts +4 -0
  113. package/templates/video/src/download.ts +27 -0
  114. package/templates/video/src/exporter.ts +70 -0
  115. package/templates/video/src/film-resources.ts +29 -0
  116. package/templates/video/src/film.ts +132 -0
  117. package/templates/video/src/main.ts +219 -0
  118. package/templates/video/src/resource-timeline.ts +193 -0
  119. package/templates/video/src/review.ts +50 -0
  120. package/templates/video/src/studies/film.ts +26 -0
  121. package/templates/video/src/studies/media.ts +46 -0
  122. package/templates/video/src/studies/model.ts +60 -0
  123. package/templates/video/src/studies/pixi-renderer.ts +121 -0
  124. package/templates/video/src/studies/scenes.ts +410 -0
  125. package/templates/video/src/studies/timing.ts +53 -0
  126. package/templates/video/src/studies/variants.ts +39 -0
  127. package/templates/video/src/style.css +111 -0
  128. package/templates/video/src/timeline-model.ts +55 -0
  129. package/templates/video/style-source-provenance.json +11 -0
  130. package/templates/video/tsconfig.json +15 -0
  131. package/templates/video/vite.config.mjs +9 -0
  132. package/vendor/pixijs-skills/LICENSE +21 -0
  133. package/vendor/pixijs-skills/provenance.json +79 -0
  134. package/vendor/pixijs-skills/skills/pixijs/SKILL.md +66 -0
  135. package/vendor/pixijs-skills/skills/pixijs/references/index.md +136 -0
  136. package/vendor/pixijs-skills/skills/pixijs-accessibility/SKILL.md +224 -0
  137. package/vendor/pixijs-skills/skills/pixijs-application/SKILL.md +293 -0
  138. package/vendor/pixijs-skills/skills/pixijs-application/references/application-options.md +314 -0
  139. package/vendor/pixijs-skills/skills/pixijs-assets/SKILL.md +303 -0
  140. package/vendor/pixijs-skills/skills/pixijs-assets/references/background.md +137 -0
  141. package/vendor/pixijs-skills/skills/pixijs-assets/references/bundles.md +122 -0
  142. package/vendor/pixijs-skills/skills/pixijs-assets/references/caching.md +143 -0
  143. package/vendor/pixijs-skills/skills/pixijs-assets/references/compressed-textures.md +120 -0
  144. package/vendor/pixijs-skills/skills/pixijs-assets/references/fonts.md +205 -0
  145. package/vendor/pixijs-skills/skills/pixijs-assets/references/gif.md +171 -0
  146. package/vendor/pixijs-skills/skills/pixijs-assets/references/manifests.md +175 -0
  147. package/vendor/pixijs-skills/skills/pixijs-assets/references/progress.md +142 -0
  148. package/vendor/pixijs-skills/skills/pixijs-assets/references/resolution.md +142 -0
  149. package/vendor/pixijs-skills/skills/pixijs-assets/references/spritesheet.md +119 -0
  150. package/vendor/pixijs-skills/skills/pixijs-assets/references/svg.md +132 -0
  151. package/vendor/pixijs-skills/skills/pixijs-assets/references/video.md +158 -0
  152. package/vendor/pixijs-skills/skills/pixijs-blend-modes/SKILL.md +215 -0
  153. package/vendor/pixijs-skills/skills/pixijs-color/SKILL.md +299 -0
  154. package/vendor/pixijs-skills/skills/pixijs-core-concepts/SKILL.md +132 -0
  155. package/vendor/pixijs-skills/skills/pixijs-core-concepts/references/render-loop.md +216 -0
  156. package/vendor/pixijs-skills/skills/pixijs-core-concepts/references/renderers.md +287 -0
  157. package/vendor/pixijs-skills/skills/pixijs-create/SKILL.md +276 -0
  158. package/vendor/pixijs-skills/skills/pixijs-custom-rendering/SKILL.md +405 -0
  159. package/vendor/pixijs-skills/skills/pixijs-custom-rendering/references/advanced-gpu.md +244 -0
  160. package/vendor/pixijs-skills/skills/pixijs-custom-rendering/references/uniform-types.md +94 -0
  161. package/vendor/pixijs-skills/skills/pixijs-environments/SKILL.md +243 -0
  162. package/vendor/pixijs-skills/skills/pixijs-events/SKILL.md +387 -0
  163. package/vendor/pixijs-skills/skills/pixijs-filters/SKILL.md +300 -0
  164. package/vendor/pixijs-skills/skills/pixijs-html-source/SKILL.md +228 -0
  165. package/vendor/pixijs-skills/skills/pixijs-math/SKILL.md +489 -0
  166. package/vendor/pixijs-skills/skills/pixijs-migration-v8/SKILL.md +415 -0
  167. package/vendor/pixijs-skills/skills/pixijs-performance/SKILL.md +489 -0
  168. package/vendor/pixijs-skills/skills/pixijs-scene-container/SKILL.md +345 -0
  169. package/vendor/pixijs-skills/skills/pixijs-scene-core-concepts/SKILL.md +243 -0
  170. package/vendor/pixijs-skills/skills/pixijs-scene-core-concepts/references/constructor-options.md +155 -0
  171. package/vendor/pixijs-skills/skills/pixijs-scene-core-concepts/references/container-hierarchy.md +232 -0
  172. package/vendor/pixijs-skills/skills/pixijs-scene-core-concepts/references/layers.md +145 -0
  173. package/vendor/pixijs-skills/skills/pixijs-scene-core-concepts/references/masking.md +129 -0
  174. package/vendor/pixijs-skills/skills/pixijs-scene-core-concepts/references/render-groups.md +140 -0
  175. package/vendor/pixijs-skills/skills/pixijs-scene-core-concepts/references/scene-management.md +169 -0
  176. package/vendor/pixijs-skills/skills/pixijs-scene-core-concepts/references/transforms.md +198 -0
  177. package/vendor/pixijs-skills/skills/pixijs-scene-dom-container/SKILL.md +193 -0
  178. package/vendor/pixijs-skills/skills/pixijs-scene-gif/SKILL.md +233 -0
  179. package/vendor/pixijs-skills/skills/pixijs-scene-graphics/SKILL.md +576 -0
  180. package/vendor/pixijs-skills/skills/pixijs-scene-mesh/SKILL.md +141 -0
  181. package/vendor/pixijs-skills/skills/pixijs-scene-mesh/references/mesh-perspective.md +189 -0
  182. package/vendor/pixijs-skills/skills/pixijs-scene-mesh/references/mesh-plane.md +159 -0
  183. package/vendor/pixijs-skills/skills/pixijs-scene-mesh/references/mesh-rope.md +176 -0
  184. package/vendor/pixijs-skills/skills/pixijs-scene-mesh/references/mesh-simple.md +144 -0
  185. package/vendor/pixijs-skills/skills/pixijs-scene-mesh/references/mesh.md +213 -0
  186. package/vendor/pixijs-skills/skills/pixijs-scene-particle-container/SKILL.md +323 -0
  187. package/vendor/pixijs-skills/skills/pixijs-scene-sprite/SKILL.md +120 -0
  188. package/vendor/pixijs-skills/skills/pixijs-scene-sprite/references/animated-sprite.md +262 -0
  189. package/vendor/pixijs-skills/skills/pixijs-scene-sprite/references/nineslice-sprite.md +202 -0
  190. package/vendor/pixijs-skills/skills/pixijs-scene-sprite/references/sprite.md +181 -0
  191. package/vendor/pixijs-skills/skills/pixijs-scene-sprite/references/tiling-sprite.md +216 -0
  192. package/vendor/pixijs-skills/skills/pixijs-scene-text/SKILL.md +136 -0
  193. package/vendor/pixijs-skills/skills/pixijs-scene-text/references/bitmap-text.md +232 -0
  194. package/vendor/pixijs-skills/skills/pixijs-scene-text/references/html-text.md +192 -0
  195. package/vendor/pixijs-skills/skills/pixijs-scene-text/references/split-bitmap-text.md +187 -0
  196. package/vendor/pixijs-skills/skills/pixijs-scene-text/references/split-text.md +168 -0
  197. package/vendor/pixijs-skills/skills/pixijs-scene-text/references/text.md +311 -0
  198. package/vendor/pixijs-skills/skills/pixijs-ticker/SKILL.md +292 -0
@@ -0,0 +1,34 @@
1
+ # AnimaGuru video project
2
+
3
+ This is a video production workspace generated by `animaguru create`. The user should be able to describe a film in one prompt and collaborate with you to create it.
4
+
5
+ 1. Read `.agents/skills/animaguru-video/SKILL.md`, `animaguru.project.json`, and `animaguru.templates.json`. New workspaces have no selected category or style.
6
+ 2. **On the first creative message, ask for the category first:** offer **Explainer**, **Advertisement** and **General Video**, then wait. Save the answer and advance `setupStage` to `style`. Explicit valid choices already in the user's message count as answers.
7
+ 3. **Then ask for the visual style:** offer the installed options for that category, share [the animation-style gallery](https://animaguru.com/animation-styles) or open it in the internal browser when available, and wait for the user's choice. Use `animaguru.templates.json` as the source of available choices; a shortlist can help with many options, but keep the full gallery available. Do not automatically select a style. Save its template ID and skill, and advance `setupStage` to `brief`.
8
+ 4. Read the chosen style's skill and run the `animaguru-video` quick interview, including its visual questions. Save the brief to `production/brief.md`, mark the interview complete and set `setupStage` to `ready`. Reuse supplied answers and completed briefs for revisions. Honor an explicit request to skip the brief interview.
9
+ 5. Read the local AnimaGuru skill for generation and the PixiJS router for rendering. Use available image-generation tools when helpful. Respect the user's selected models and credit budget; creating this folder does not authorize paid jobs.
10
+
11
+ Selections and setup progress are recorded in `animaguru.project.json`. Available choices are in `animaguru.templates.json`; all style skills are installed for both agents. Update the player label in `shared/locales/en/player.json` after selection. Explainer, Advertisement and General Video describe purpose; visual templates describe appearance and motion. Establish the audience, subject, tone, narrative and outcome through the brief.
12
+
13
+ ## Files and commands
14
+
15
+ - `film.config.json`: title, output slug, dimensions, fps, duration, language and optional audio path relative to `public/`.
16
+ - `src/film.ts`: replace the demo with the actual deterministic scene timeline. `src/main.ts`: AnimaGuru player and capture API. `src/exporter.ts` and `scripts/export-mp4.mjs`: capture transport and verified encoding.
17
+ - 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
+ - `shared/locales/`: all user-facing copy. `public/assets/`: local media. `production/`: brief, sources, storyboard, narration, requests and receipts.
19
+ - Use `publicAsset("assets/file.ext")` from `src/assets.ts` for public media. Root-absolute paths break an HTML5 export hosted in a subfolder. `npm run export:html` produces a portable static ZIP with an iframe example; the download menu also supports this format. `npm run build` includes a ready-made HTML5 download for static hosting.
20
+ - `npm run dev`: preview. `npm run build`: TypeScript check and production build. `npm run export:mp4`: local MP4 capture; open its printed URL and wait for completion.
21
+
22
+ Use strict TypeScript, two-space indentation and double quotes. Preserve the `window.animaguru` async seeking/frame API. Keep scene content and captions in the canvas. Load local assets and await video decoding for capture. Set the final duration and soundtrack in the shared config. Do not embed credentials or reuse another client's media.
23
+
24
+ Before delivery, run the build and review desktop/mobile playback, captions, all chapter boundaries, audio sync and representative frames. Export MP4 when requested; publishing to external services is separate work. Keep generation receipts so a retry cannot accidentally duplicate paid work.
25
+
26
+ ## Purpose compatibility and optional workflows
27
+
28
+ Accept schema 2 projects and saved `advertorial` as Advertisement with editorial intent. Preserve their completed brief and selected style; do not restart setup. New schema 3 projects also save an optional `workflow` ID. Read the installed advertising/general-video skill and the applicable workflow reference; infer the workflow from the request or fold missing choices into the same quick interview. A custom brief can leave workflow null.
29
+
30
+ Original 2.5D study recipes are in `src/studies/`. In an untouched starter, `npm run example -- STYLE_ID` installs one; it refuses to overwrite an authored film. Import the recipe directly for an existing production. `npm run review` checks actual visible text bounds and seek consistency, then saves frames and a report. Keep the HTML5-first delivery and use the client's identity in the film. Physically rendered 3D remains deferred.
31
+
32
+ ## Submit to AnimaGuru
33
+
34
+ When asked to host or share the finished film, read the local AnimaGuru skill's submission section. `animaguru submit --yes` builds the portable HTML5 export and uploads it privately; `--file` accepts an existing HTML5 ZIP or MP4. `--share "reviewer@example.com"` selects verified AnimaGuru accounts, while `--visibility public` publishes to https://animaguru.com/videos and allows watching, downloading and HTML5 embedding. Include a cover with `--poster public/assets/cover.png` when available. Do not infer public sharing from a request to create or export a film. The CLI returns the hosted watch URL and video ID. Owners can change details and sharing from that page.
@@ -0,0 +1,19 @@
1
+ # AnimaGuru video project
2
+
3
+ Read `AGENTS.md` for the shared production workflow and project layout. Claude Code skills are installed in `.claude/skills/` as ordinary local files; the matching `.agents/skills/` copies support Codex.
4
+
5
+ Start with `.claude/skills/animaguru-video/SKILL.md`, `animaguru.project.json` and `animaguru.templates.json`. New workspaces have no selected category or visual style.
6
+
7
+ On the first creative message, **ask for the category first**, offering **Explainer**, **Advertisement** and **General Video**, and wait. **Then ask for the visual style**, showing the installed options and sharing [the animation-style gallery](https://animaguru.com/animation-styles), or opening it in the internal browser when available. Use `animaguru.templates.json` for available choices, and wait for the style selection too. A shortlist can help with many options; keep the full gallery available. Honor valid choices already supplied in the message.
8
+
9
+ Save each choice and advance `setupStage` from `category` to `style` to `brief`. Read the selected style's skill, then run the brief interview and save it to `production/brief.md`. Mark `interview` complete and `setupStage` ready when finished. Update the player's localized category/style label. Derive story and audience from the brief. Reuse saved choices and completed briefs on later edits; honor an explicit request to skip the brief interview.
10
+
11
+ The installed `animaguru` skill explains paid asset generation; `pixijs` routes to the rendering skills. Do not assume a Codex-only image tool exists in this environment; AnimaGuru can generate the media. Both agents use the same project files, local player and MP4 exporter.
12
+
13
+ ## Purpose compatibility and optional workflows
14
+
15
+ Accept schema 2 projects and saved `advertorial` as Advertisement with editorial intent. Preserve their completed brief and selected style; do not restart setup. New schema 3 projects also save an optional `workflow` ID. Read the installed advertising/general-video skill and the applicable workflow reference; infer the workflow from the request or fold missing choices into the same quick interview. A custom brief can leave workflow null.
16
+
17
+ Original 2.5D study recipes are in `src/studies/`. In an untouched starter, `npm run example -- STYLE_ID` installs one; it refuses to overwrite an authored film. Import the recipe directly for an existing production. `npm run review` checks actual visible text bounds and seek consistency, then saves frames and a report. Keep the HTML5-first delivery and use the client's identity in the film. Physically rendered 3D remains deferred.
18
+
19
+ For requested hosting or sharing, use `animaguru submit --yes` in this workspace (private by default). `--share "reviewer@example.com"` shares with selected verified AnimaGuru accounts; `--visibility public` publishes to https://animaguru.com/videos. `--file` accepts an existing HTML5 ZIP or MP4. Read the local AnimaGuru skill's submission section for audience selection and upload recovery. Creation and local export do not imply publication.
@@ -0,0 +1,101 @@
1
+ # Your AnimaGuru motion studio
2
+
3
+ Open this folder in Codex or Claude Code and describe your video. The agent first asks **Explainer, Advertisement or General Video**, then offers the available visual styles, then gathers the creative brief. Each choice is saved in `animaguru.project.json`. Watch the [animation-style gallery](https://animaguru.com/animation-styles) to help choose; your agent can share the link or open it in the internal browser.
4
+
5
+ Example prompt:
6
+
7
+ > Create an animated video about [subject]. Help me choose its category and visual style, then interview me about the audience, message, format and motion direction before building it.
8
+
9
+ **Explainer**, **Advertisement** and **General Video** describe the project's purpose. The installed visual styles define appearance and movement and are available for every purpose. Browse `animaguru.templates.json` or the gallery to compare cutout, typography, vector, line drawing, isometric, data, diagram, interface, cartoon, pixel and painterly treatments. The brief determines audience, tone and story structure.
10
+
11
+ ## Preview
12
+
13
+ Requires Node.js 24+. Dependencies are installed by `animaguru create NAME`. If installation was interrupted, run `npm install` inside this folder to finish it.
14
+
15
+ ```sh
16
+ npm run dev
17
+ ```
18
+
19
+ Open the local URL printed by Vite (normally http://127.0.0.1:5195). The starter contains a playable 24-second graphic demo with three chapters. It does not include generated people, paid assets or a soundtrack. Your agent replaces it with the real film. Run `npm run build` to check types and produce `dist/`; `npm run preview` serves that build.
20
+
21
+ The player supports play/pause, timeline seeking, captions, fullscreen and a transcript. Its download icon opens **Save frame** (PNG) and **Export HTML5** (ZIP). The adjacent icon toggles fullscreen. Space plays/pauses, arrows seek five seconds, M controls sound, C captions and F fullscreen. The shortcuts leave focused controls alone. Playback pauses when the tab is hidden.
22
+
23
+ The resource timeline spans the player width below the video and grows to show every row, without internal vertical scrolling. Each row shows a resource or text layer; clips use the film's actual start/end times. The playhead follows playback and seeking. Select a clip to inspect its timing, text and source filename; click an empty lane or drag the ruler to scrub. Focus the ruler and use Left/Right for one frame, Shift+Left/Right for one second, or Home/End. Zoom from 100–800%, use Fit to see the whole film, and scroll horizontally when zoomed in. Caption and audio rows reflect the player's visibility/mute state. The timeline is a resource/timing inspector; clip selection changes the playhead, not the authored start/end times.
24
+
25
+ Keep `src/film-resources.ts` synchronized with `src/film.ts` as the film changes. `Film.resources` supplies the timeline and `window.animaguru.resources`. Add tracks for every imported image, video, audio stem or new graphic layer, using real cue times and public-relative `source` paths. Reused resources can have multiple clips on one row. The starter groups repeated text roles into rows and includes a soundtrack row only when `film.config.json.audio` is set. Authoring panels are omitted from HTML5 embeds and MP4 captures.
26
+
27
+ ## Project files
28
+
29
+ - `animaguru.project.json`: category, template, setup stage, gallery URL, interview state and remote project ID. Category and template start unset.
30
+ - `animaguru.templates.json`: installed category/style options and skill IDs for the agent-led setup.
31
+ - `film.config.json`: dimensions, fps, duration, language, output slug and soundtrack. Set `audio` to a path relative to `public/`, such as `assets/film-en.wav`. Keep it `null` for a silent film. Use full-length, already mixed audio; short audio is padded with silence.
32
+ - `src/film.ts`: deterministic PixiJS demo scenes. Replace them with the storyboard.
33
+ - `src/film-resources.ts`: resource tracks and cue metadata; `src/resource-timeline.ts`: the timeline interface.
34
+ - `src/main.ts` / `src/style.css`: AnimaGuru player. Its surrounding branding is separate from the film's client identity.
35
+ - `src/assets.ts`: use `publicAsset("assets/file.png")` for images, videos, audio and fonts so they load correctly when embedded under a nested URL.
36
+ - `shared/locales/en/`: film and player copy. Extend this when adding languages.
37
+ - `public/assets/`: local images, videos, fonts and audio. `production/`: the brief, source records, generation receipts and review artifacts.
38
+ - `.agents/skills/` / `.claude/skills/`: AnimaGuru, video workflow, selected template, and the official PixiJS skills. Ordinary copies work without symlink support. If editing a skill, keep the two copies aligned.
39
+
40
+ ## Export and embed HTML5
41
+
42
+ Choose **Export HTML5** from the player's download menu, or run:
43
+
44
+ ```sh
45
+ npm run export:html
46
+ ```
47
+
48
+ This builds a portable ZIP in `production/exports/`. Unzip it and upload the entire folder to a static HTTP(S) host. The package contains the compiled PixiJS animation, local media, a file-hash manifest, instructions and `embed-example.html`. It uses the live animation rather than converting it to MP4. No Node.js, AnimaGuru account or API is needed on the hosting server. Browser ES modules require HTTP(S); opening the files directly with `file://` is not supported.
49
+
50
+ The exported `index.html` shows only the film and playback controls. Embed it with an iframe pointing to your hosted folder:
51
+
52
+ ```html
53
+ <iframe src="https://your-site.example/animation/index.html"
54
+ title="My animation" style="width:100%;aspect-ratio:16/9;border:0"
55
+ allow="autoplay; fullscreen" allowfullscreen loading="lazy"></iframe>
56
+ ```
57
+
58
+ Allow extra iframe height for controls if needed. `?autoplay=1&loop=1&controls=0` creates a muted, looping, canvas-only embed. Autoplay respects reduced-motion preferences; controls stay available when autoplay is suppressed. The normal embed includes play/pause, seek, sound, captions and fullscreen. The authoring page and download menu are omitted.
59
+
60
+ `npm run build` also puts a ready-made HTML5 ZIP in `dist/downloads/`, so the download menu works on a deployed static player. Rebuild after editing the film. During local development, the menu builds a fresh ZIP in a separate Node process. Both routes package the built website and public media, excluding source files, skills and generation receipts. Keep required media local and use `publicAsset()`; externally hosted media added by an author will still need those remote sources.
61
+
62
+ ## Export MP4
63
+
64
+ ```sh
65
+ npm run export:mp4
66
+ # If the export port is occupied:
67
+ npm run export:mp4 -- --port 5197
68
+ ```
69
+
70
+ Open the local capture URL printed by the command and keep it open until complete. The script runs a separate loopback Vite server (default port 5196), streams high-quality JPEG frames from the Pixi canvas to the bundled FFmpeg binary, and mixes the configured soundtrack. The initial starter exports with a silent AAC track. Exporting local media does not call AnimaGuru or spend credits. FFmpeg/FFprobe binaries are supplied through platform-specific npm dependencies; installation needs a supported platform and network access.
71
+
72
+ Completed H.264/AAC MP4s and JSON verification reports appear in `production/exports/`. Existing files are preserved. Frame retries are deduplicated. Ctrl+C removes only this session's unfinished output. The verification checks duration, exact frame count, dimensions, pixel format, codecs and audio language tag. The exporter supports even dimensions up to 4096 pixels, 24/25/30/60 fps and films up to ten minutes. Set duration to an exact number of frames.
73
+
74
+ ## Capture API
75
+
76
+ Wait for `window.animaguru.ready` or the `animaguru:ready` event, then:
77
+
78
+ ```js
79
+ await window.animaguru.seek(12);
80
+ await window.animaguru.renderFrame(360, 30);
81
+ window.animaguru.setCaptions(false);
82
+ window.animaguru.canvas.toDataURL("image/png");
83
+ ```
84
+
85
+ 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.
86
+
87
+ ## Credits and provenance
88
+
89
+ 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.
90
+
91
+ The installed style skills cover visual treatment and motion technique across subjects and audiences. Project stories, client assets and claims come from the brief. PixiJS skills are bundled from [the official collection](https://github.com/pixijs/pixijs-skills) under MIT; see `PIXIJS-SKILLS-LICENSE.txt` and `production/pixijs-skills-provenance.json`.
92
+
93
+ ## Original studies and production review
94
+
95
+ Browse `animaguru.templates.json` for all 31 styles and purpose workflows. The expanded 20 styles include a runnable original study; use `npm run example -- blueprint-drafting` in an untouched starter. The command backs up the starter, refuses to replace an authored film, and connects the study to the normal player, timeline and HTML5/MP4 exporters. To compose studies in an existing film, import their scene and renderer instead.
96
+
97
+ `npm run review` starts a local browser review session. Open the printed URL; it saves representative JPEG frames, actual text bounds, reading-hold notes and repeated-seek comparisons in `production/review/`. The numerical report complements frame inspection and full-speed listening. Optional media trim/time mapping and cue helpers are in `src/studies/`. All procedural example art is original; source inspirations and MIT notices are recorded in `VIDEO-STYLE-NOTICES.txt`.
98
+
99
+ ## Share on AnimaGuru
100
+
101
+ After signing in with `animaguru auth login`, run `animaguru submit --yes` here to build and host the finished HTML5 animation privately. Add `--share "reviewer@example.com"` to invite selected verified AnimaGuru accounts, or `--visibility public` to list it at [Videos](https://animaguru.com/videos). `--poster public/assets/cover.png` adds a cover. Already exported an MP4? Pass `--file production/exports/film.mp4`. The CLI returns your hosted player URL; public HTML5 animations can be embedded from that page. Owners manage details and access there. Only the exported player/media are uploaded.
@@ -0,0 +1,54 @@
1
+ Video style sources and notices
2
+
3
+ The executable studies and preview art are original AnimaGuru procedural 2D/2.5D work. Material/motion guidance was adapted from the pinned repositories below. No external creator video, sample music or demo font is bundled. Third-party demo assets retain their own licenses.
4
+
5
+ lemo-opuscar
6
+ MIT License
7
+
8
+ Copyright (c) 2026 LemoLab
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Third-party assets in the demos (sample libraries, fonts, music, voices) keep their own
29
+ licences; see each demo's CREDITS. You are responsible for the materials you use in your
30
+ own films.
31
+
32
+
33
+ awesome-opus5-5-videos
34
+ MIT License
35
+
36
+ Copyright (c) 2026 yihui-dev
37
+
38
+ Permission is hereby granted, free of charge, to any person obtaining a copy
39
+ of this software and associated documentation files (the "Software"), to deal
40
+ in the Software without restriction, including without limitation the rights
41
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
42
+ copies of the Software, and to permit persons to whom the Software is
43
+ furnished to do so, subject to the following conditions:
44
+
45
+ The above copyright notice and this permission notice shall be included in all
46
+ copies or substantial portions of the Software.
47
+
48
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
49
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
50
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
51
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
52
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
53
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
54
+ SOFTWARE.
@@ -0,0 +1,10 @@
1
+ {
2
+ "title": "A story starts here",
3
+ "slug": "my-animaguru-film",
4
+ "width": 1920,
5
+ "height": 1080,
6
+ "fps": 30,
7
+ "duration": 24,
8
+ "language": "en",
9
+ "audio": null
10
+ }
@@ -0,0 +1,11 @@
1
+ node_modules/
2
+ dist/
3
+ production/exports/
4
+ production/frames/
5
+ .env
6
+ .env.*
7
+ !.env.example
8
+ *credentials*.json
9
+ *secrets*.json
10
+ *.log
11
+ .DS_Store
@@ -0,0 +1,14 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <meta name="theme-color" content="#f5f3ed" />
7
+ <link rel="icon" type="image/svg+xml" href="/brand/logo-mark.svg" />
8
+ <title>AnimaGuru · Motion studio</title>
9
+ </head>
10
+ <body>
11
+ <div id="app"></div>
12
+ <script type="module" src="/src/main.ts"></script>
13
+ </body>
14
+ </html>
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "animaguru-video",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "engines": {
7
+ "node": ">=24"
8
+ },
9
+ "scripts": {
10
+ "dev": "vite --host 127.0.0.1 --port 5195",
11
+ "build": "tsc --noEmit && node scripts/build.mjs",
12
+ "preview": "vite preview --host 127.0.0.1 --port 5195",
13
+ "export:mp4": "node scripts/export-mp4.mjs",
14
+ "export:html": "node scripts/export-html.mjs",
15
+ "example": "node scripts/install-example.mjs",
16
+ "review": "node scripts/review.mjs"
17
+ },
18
+ "dependencies": {
19
+ "pixi.js": "8.22.0"
20
+ },
21
+ "devDependencies": {
22
+ "@ffmpeg-installer/ffmpeg": "1.1.0",
23
+ "@ffprobe-installer/ffprobe": "2.1.2",
24
+ "fflate": "0.8.3",
25
+ "typescript": "5.9.3",
26
+ "vite": "8.3.3"
27
+ }
28
+ }
@@ -0,0 +1,5 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96" role="img" aria-label="AnimaGuru">
2
+ <rect x="3" y="3" width="90" height="90" rx="22" fill="#F5F6F8"/>
3
+ <path d="M19 72 40 22h16l21 50H60l-4-10H39l-4 10H19Zm25-23h7l-3.5-9L44 49Z" fill="#15181D"/>
4
+ <path d="m59 40 16 9-16 9V40Z" fill="#2C6BD0"/>
5
+ </svg>
@@ -0,0 +1,12 @@
1
+ import { build } from "vite";
2
+ import { mkdir, writeFile } from "node:fs/promises";
3
+ import { fileURLToPath } from "node:url";
4
+ import path from "node:path";
5
+ import { buildHtmlExport } from "./html-export.mjs";
6
+
7
+ const root = fileURLToPath(new URL("../", import.meta.url));
8
+ await build({ root });
9
+ const html = await buildHtmlExport(root);
10
+ await mkdir(path.join(root, "dist/downloads"), { recursive: true });
11
+ await writeFile(path.join(root, "dist/downloads", html.filename), html.bytes);
12
+ console.log(`HTML5 download ready: dist/downloads/${html.filename}`);
@@ -0,0 +1,12 @@
1
+ import { writeFile } from "node:fs/promises";
2
+ import { fileURLToPath } from "node:url";
3
+ import { randomUUID } from "node:crypto";
4
+ import path from "node:path";
5
+ import { buildHtmlExport } from "./html-export.mjs";
6
+
7
+ const root = fileURLToPath(new URL("../", import.meta.url));
8
+ const result = await buildHtmlExport(root);
9
+ const destination = path.join(root, "production/exports", result.filename.replace(/\.zip$/, `-${randomUUID()}.zip`));
10
+ await writeFile(destination, result.bytes, { flag: "wx" });
11
+ const metadata = { path: destination, filename: result.filename, bytes: result.bytes.length, files: result.files };
12
+ console.log(process.argv.includes("--json") ? JSON.stringify(metadata) : `Saved HTML5 animation: ${destination}\nUnzip, upload the folder to a static host, and use embed-example.html for iframe markup.`);
@@ -0,0 +1,209 @@
1
+ import { createServer } from "vite";
2
+ import { spawn, execFile } from "node:child_process";
3
+ import { promisify } from "node:util";
4
+ import { mkdir, access, rename, rm, stat, writeFile, readFile, realpath } from "node:fs/promises";
5
+ import { createHash, randomUUID } from "node:crypto";
6
+ import { fileURLToPath } from "node:url";
7
+ import path from "node:path";
8
+ import ffmpeg from "@ffmpeg-installer/ffmpeg";
9
+ import ffprobe from "@ffprobe-installer/ffprobe";
10
+
11
+ const root = fileURLToPath(new URL("../", import.meta.url));
12
+ const args = process.argv.slice(2);
13
+ if (args.length && (args.length !== 2 || args[0] !== "--port" || !/^\d+$/.test(args[1]))) throw new Error("Usage: npm run export:mp4 -- [--port 5196]");
14
+ const port = Number(args[1] ?? 5196);
15
+ if (!Number.isInteger(port) || port < 1024 || port > 65535) throw new Error("Export port must be 1024–65535.");
16
+ const config = JSON.parse(await readFile(path.join(root, "film.config.json"), "utf8"));
17
+ const { width, height, fps, duration, language } = config;
18
+ 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.");
19
+ if (typeof language !== "string" || !/^[a-z]{2,3}(?:-[A-Za-z]{2,4})?$/.test(language)) throw new Error("Set a language code in film.config.json.");
20
+ if (typeof config.slug !== "string" || !/^[a-z0-9][a-z0-9-]{0,79}$/.test(config.slug)) throw new Error("Use a lowercase, hyphenated film slug (1–80 characters).");
21
+ const languageTag = ({ en: "eng", tr: "tur", de: "deu", fr: "fra", es: "spa" })[language.split("-")[0]] ?? "und";
22
+ let audioSource;
23
+ if (config.audio !== null) {
24
+ if (typeof config.audio !== "string" || !config.audio) throw new Error("audio must be null or a local path relative to public/.");
25
+ const publicRoot = await realpath(path.join(root, "public"));
26
+ audioSource = await realpath(path.resolve(publicRoot, config.audio));
27
+ if (!audioSource.startsWith(publicRoot + path.sep)) throw new Error("The soundtrack must be inside public/.");
28
+ }
29
+ const outputDirectory = path.join(root, "production/exports");
30
+ await mkdir(outputDirectory, { recursive: true });
31
+ let destination = path.join(outputDirectory, `${config.slug}-${language}.mp4`);
32
+ for (const candidate of [destination, destination.replace(/\.mp4$/, ".partial.mp4")]) {
33
+ try {
34
+ await access(candidate);
35
+ destination = path.join(outputDirectory, `${config.slug}-${language}-${randomUUID()}.mp4`);
36
+ break;
37
+ } catch (error) { if (error.code !== "ENOENT") throw error; }
38
+ }
39
+ const partial = destination.replace(/\.mp4$/, ".partial.mp4");
40
+ const token = randomUUID();
41
+ let encoder;
42
+ let encoderDone;
43
+ let encoderError;
44
+ let encoderLog = "";
45
+ let settings;
46
+ let received = 0;
47
+ let lastFrameHash;
48
+ let frameQueue = Promise.resolve();
49
+ let encoding = false;
50
+ let finished = false;
51
+ let aborted = false;
52
+ let ownsPartial = false;
53
+ let server;
54
+ let lastActivity = Date.now();
55
+ const run = promisify(execFile);
56
+
57
+ async function readBody(request, limit = 24 * 1024 * 1024) {
58
+ let bytes = 0;
59
+ const parts = [];
60
+ for await (const part of request) {
61
+ bytes += part.length;
62
+ if (bytes > limit) throw new Error("Capture request is too large.");
63
+ parts.push(part);
64
+ }
65
+ return Buffer.concat(parts);
66
+ }
67
+
68
+ function jpegDimensions(bytes) {
69
+ if (bytes.length < 4 || bytes[0] !== 0xff || bytes[1] !== 0xd8) return null;
70
+ let cursor = 2;
71
+ while (cursor + 4 < bytes.length) {
72
+ if (bytes[cursor] !== 0xff) return null;
73
+ const marker = bytes[cursor + 1];
74
+ const size = bytes.readUInt16BE(cursor + 2);
75
+ if (size < 2 || cursor + 2 + size > bytes.length) return null;
76
+ if ([0xc0, 0xc1, 0xc2].includes(marker) && size >= 8) return { height: bytes.readUInt16BE(cursor + 5), width: bytes.readUInt16BE(cursor + 7) };
77
+ cursor += 2 + size;
78
+ }
79
+ return null;
80
+ }
81
+
82
+ async function cleanup(error) {
83
+ if (aborted || finished) return;
84
+ aborted = true;
85
+ console.error(`Export stopped: ${error.message ?? error}`);
86
+ if (encoder && encoder.exitCode === null) {
87
+ encoder.stdin.destroy();
88
+ encoder.kill("SIGTERM");
89
+ const forceStop = setTimeout(() => encoder.kill("SIGKILL"), 2000);
90
+ await encoderDone.catch(() => {});
91
+ clearTimeout(forceStop);
92
+ }
93
+ if (ownsPartial) await rm(partial, { force: true });
94
+ clearInterval(idleCheck);
95
+ await server?.close();
96
+ process.exitCode = 1;
97
+ }
98
+
99
+ async function route(request, response, url) {
100
+ if (request.method !== "POST" || request.headers["x-animaguru-export"] !== token || request.headers.origin !== `http://127.0.0.1:${port}`) {
101
+ response.writeHead(403).end("This export session accepts only its own local capture page."); return;
102
+ }
103
+ lastActivity = Date.now();
104
+ const send = (value) => response.writeHead(200, { "Content-Type": "application/json" }).end(JSON.stringify(value));
105
+ if (url.pathname === "/__export/start") {
106
+ if (encoder) throw new Error("Export has already started. Keep the original capture tab open.");
107
+ settings = JSON.parse((await readBody(request, 2048)).toString());
108
+ if (settings.language !== language) throw new Error(`Expected ${language} text and captions. Refusing to pair a different language with the audio.`);
109
+ if (settings.width !== width || settings.height !== height || settings.fps !== fps || settings.duration !== duration) throw new Error("The player settings differ from film.config.json. Restart the export after changing the configuration.");
110
+ settings.frames = Math.round(settings.duration * fps);
111
+ await writeFile(partial, "", { flag: "wx" });
112
+ ownsPartial = true;
113
+ encoder = spawn(ffmpeg.path, [
114
+ "-hide_banner", "-loglevel", "warning", "-y",
115
+ "-f", "image2pipe", "-framerate", String(fps), "-vcodec", "mjpeg", "-i", "pipe:0",
116
+ ...(audioSource ? ["-i", audioSource] : ["-f", "lavfi", "-i", "anullsrc=r=48000:cl=stereo"]),
117
+ "-map", "0:v:0", "-map", "1:a:0",
118
+ "-c:v", "libx264", "-preset", "medium", "-crf", "18",
119
+ "-vf", `scale=${width}:${height}:in_color_matrix=bt601:out_color_matrix=bt709:in_range=full:out_range=tv,format=yuv420p`,
120
+ "-color_primaries", "bt709", "-color_trc", "bt709", "-colorspace", "bt709",
121
+ "-af", "apad", "-c:a", "aac", "-b:a", "192k", "-ar", "48000",
122
+ "-metadata:s:a:0", `language=${languageTag}`, "-metadata:s:v:0", `language=${languageTag}`,
123
+ "-t", String(settings.duration), "-movflags", "+faststart", partial,
124
+ ], { stdio: ["pipe", "ignore", "pipe"] });
125
+ encoder.stderr.on("data", (chunk) => { encoderLog = (encoderLog + chunk.toString()).slice(-8000); });
126
+ encoder.stdin.on("error", (error) => { encoderError = error; });
127
+ encoderDone = new Promise((resolve, reject) => {
128
+ encoder.once("error", reject);
129
+ encoder.once("close", (code) => code === 0 ? resolve() : reject(new Error(`FFmpeg exited ${code}: ${encoderLog}`)));
130
+ });
131
+ encoderDone.catch((error) => { encoderError = error; });
132
+ console.log(`Rendering ${language.toUpperCase()}: ${settings.frames} frames (${settings.duration}s), ${width} × ${height} / ${fps} fps.`);
133
+ send({ fps, frames: settings.frames }); return;
134
+ }
135
+ if (url.pathname === "/__export/frame") {
136
+ if (!encoder || encoding || finished || aborted) throw new Error("No active frame capture.");
137
+ if (encoderError) throw encoderError;
138
+ const index = Number(url.searchParams.get("index"));
139
+ if (!Number.isInteger(index) || index < 0 || index >= settings.frames) throw new Error("Invalid frame index.");
140
+ const image = await readBody(request);
141
+ const dimensions = jpegDimensions(image);
142
+ if (request.headers["content-type"] !== "image/jpeg" || dimensions?.width !== width || dimensions?.height !== height) throw new Error("Expected a complete JPEG frame matching film.config.json.");
143
+ // A lost response can cause the client to resend a frame already encoded.
144
+ // Serialize retries with in-flight writes and acknowledge identical repeats.
145
+ const operation = frameQueue.then(async () => {
146
+ if (encoderError) throw encoderError;
147
+ if (aborted) throw new Error("Export has stopped.");
148
+ const hash = createHash("sha256").update(image).digest("hex");
149
+ if (index === received - 1 && hash === lastFrameHash) return;
150
+ if (index !== received) throw new Error(`Expected frame ${received}, received ${index}.`);
151
+ await new Promise((resolve, reject) => encoder.stdin.write(image, (error) => error ? reject(error) : resolve()));
152
+ lastFrameHash = hash;
153
+ received++;
154
+ if (received % 90 === 0 || received === settings.frames) console.log(`${Math.round(received / settings.frames * 100)}% — ${received}/${settings.frames} frames`);
155
+ });
156
+ frameQueue = operation.catch(() => {});
157
+ await operation;
158
+ send({ received }); return;
159
+ }
160
+ if (url.pathname === "/__export/finish") {
161
+ if (!encoder || encoding || received !== settings.frames) throw new Error("The capture is incomplete.");
162
+ encoding = true;
163
+ encoder.stdin.end();
164
+ await encoderDone;
165
+ console.log("Checking the encoded video and soundtrack…");
166
+ const { stdout } = await run(ffprobe.path, ["-v", "error", "-count_frames", "-show_streams", "-show_format", "-of", "json", partial], { maxBuffer: 1024 * 1024 });
167
+ const probe = JSON.parse(stdout);
168
+ const video = probe.streams.find((stream) => stream.codec_type === "video");
169
+ const audio = probe.streams.find((stream) => stream.codec_type === "audio");
170
+ if (!video || video.codec_name !== "h264" || video.width !== width || video.height !== height || Number(video.nb_read_frames) !== settings.frames || video.pix_fmt !== "yuv420p" || !audio || audio.codec_name !== "aac" || audio.tags?.language !== languageTag || Math.abs(Number(probe.format.duration) - settings.duration) > 0.15) throw new Error("The encoded file did not pass duration, frame count, dimensions, language, or codec verification.");
171
+ await rename(partial, destination);
172
+ await writeFile(destination.replace(/\.mp4$/, ".json"), JSON.stringify({ file: path.basename(destination), audio: config.audio, settings, verification: probe }, null, 2) + "\n");
173
+ const bytes = (await stat(destination)).size;
174
+ finished = true;
175
+ clearInterval(idleCheck);
176
+ console.log(`Saved: ${destination}\nSize: ${(bytes / 1024 / 1024).toFixed(1)} MiB\nVerified: ${settings.frames} frames, ${settings.duration}s, H.264 video and AAC audio.`);
177
+ response.once("finish", () => { void server.close(); });
178
+ send({ path: destination, bytes }); return;
179
+ }
180
+ if (url.pathname === "/__export/abort") {
181
+ const message = JSON.parse((await readBody(request, 4096)).toString());
182
+ send({ aborted: true });
183
+ void cleanup(new Error(message.error || "Capture cancelled.")); return;
184
+ }
185
+ response.writeHead(404).end("Unknown capture operation.");
186
+ }
187
+
188
+ server = await createServer({
189
+ configFile: false, root, clearScreen: false,
190
+ server: { host: "127.0.0.1", port, strictPort: true, fs: { allow: [root] } },
191
+ plugins: [{ name: "animaguru-mp4-export", configureServer(vite) {
192
+ vite.middlewares.use((request, response, next) => {
193
+ const url = new URL(request.url ?? "/", `http://127.0.0.1:${port}`);
194
+ if (!url.pathname.startsWith("/__export/")) return next();
195
+ void route(request, response, url).catch((error) => {
196
+ if (request.aborted || response.destroyed) return;
197
+ if (!response.headersSent) response.writeHead(500).end(error.message);
198
+ void cleanup(error);
199
+ });
200
+ });
201
+ } }],
202
+ });
203
+ const idleCheck = setInterval(() => {
204
+ if (Date.now() - lastActivity > 10 * 60 * 1000) void cleanup(new Error("No capture activity for ten minutes."));
205
+ }, 30000);
206
+ process.once("SIGINT", () => { void cleanup(new Error("Cancelled.")); });
207
+ process.once("SIGTERM", () => { void cleanup(new Error("Cancelled.")); });
208
+ try { await server.listen(); } catch (error) { await cleanup(error); throw error; }
209
+ console.log(`Open this local export page and keep it open until complete:\nhttp://127.0.0.1:${port}/?capture=1&export=${token}\nOutput: ${destination}\nThe preview can stay open. This capture runs locally without generation credits.`);
@@ -0,0 +1,35 @@
1
+ import { execFile } from "node:child_process";
2
+ import { createReadStream } from "node:fs";
3
+ import { promisify } from "node:util";
4
+ import path from "node:path";
5
+
6
+ export function acceptsHtmlExport(request) {
7
+ const origin = request.headers.origin;
8
+ return request.method === "POST" && request.headers["x-animaguru-export"] === "html5"
9
+ && typeof origin === "string" && (origin === `http://${request.headers.host}` || origin === `https://${request.headers.host}`);
10
+ }
11
+
12
+ export function htmlExportPlugin(root) {
13
+ let pending;
14
+ return { name: "spriteguru-html5-export", configureServer(server) {
15
+ server.middlewares.use((request, response, next) => {
16
+ if (request.url?.split("?")[0] !== "/__animaguru/html") return next();
17
+ if (!acceptsHtmlExport(request)) { response.writeHead(403).end("Export must be requested from this player."); return; }
18
+ // A separate process keeps Vite's production build from changing the dev server's NODE_ENV.
19
+ pending ??= promisify(execFile)(process.execPath, [path.join(root, "scripts/export-html.mjs"), "--json"], {
20
+ cwd: root, env: { ...process.env, NODE_ENV: "production" }, timeout: 180000, maxBuffer: 1024 * 1024,
21
+ }).then(({ stdout }) => JSON.parse(stdout)).finally(() => { pending = undefined; });
22
+ void pending.then((result) => {
23
+ if (response.destroyed) return;
24
+ response.writeHead(200, { "Content-Type": "application/zip", "Content-Disposition": `attachment; filename="${result.filename}"`, "Content-Length": result.bytes, "Cache-Control": "no-store" });
25
+ const stream = createReadStream(result.path);
26
+ stream.on("error", () => response.destroy());
27
+ response.once("close", () => stream.destroy());
28
+ stream.pipe(response);
29
+ }).catch((error) => {
30
+ server.config.logger.error(`HTML5 export failed: ${error.message}`);
31
+ if (!response.headersSent && !response.destroyed) response.writeHead(500, { "Content-Type": "text/plain" }).end("HTML5 export failed. Check the terminal, fix the build, and try again.");
32
+ });
33
+ });
34
+ } };
35
+ }
@@ -0,0 +1,54 @@
1
+ import { mkdtemp, mkdir, readFile, readdir, rm } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { createHash } from "node:crypto";
4
+ import { zipSync, strToU8 } from "fflate";
5
+
6
+ const escapeHtml = (value) => String(value).replace(/[&<>"']/g, (character) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[character]);
7
+
8
+ export function validateHtmlConfig(config) {
9
+ if (!config || typeof config.slug !== "string" || !/^[a-z0-9][a-z0-9-]{0,79}$/.test(config.slug)) throw new Error("Use a lowercase, hyphenated film slug in film.config.json.");
10
+ if (![config.width, config.height].every((value) => Number.isInteger(value) && value > 0 && value <= 4096) || !Number.isFinite(config.duration) || config.duration <= 0) throw new Error("Set valid film dimensions and duration in film.config.json.");
11
+ if (typeof config.title !== "string" || typeof config.language !== "string") throw new Error("Set the film title and language in film.config.json.");
12
+ }
13
+
14
+ /** Only the built website is packaged, never the project, skills, receipts, or dependencies. */
15
+ export async function packageHtmlDirectory(directory, config) {
16
+ validateHtmlConfig(config);
17
+ const files = {};
18
+ const manifest = [];
19
+ async function collect(relative = "") {
20
+ for (const entry of await readdir(path.join(directory, relative), { withFileTypes: true })) {
21
+ const name = path.posix.join(relative, entry.name);
22
+ if (entry.isSymbolicLink()) throw new Error(`HTML5 export cannot include a symlink: ${name}`);
23
+ if (entry.isDirectory()) await collect(name);
24
+ else if (entry.isFile()) {
25
+ const data = new Uint8Array(await readFile(path.join(directory, name)));
26
+ files[name] = [data, { level: /\.(?:mp4|webm|wav|m4a|mp3|png|jpe?g|webp|woff2?|zip)$/i.test(name) ? 0 : 6 }];
27
+ manifest.push({ path: name, bytes: data.length, sha256: createHash("sha256").update(data).digest("hex") });
28
+ }
29
+ }
30
+ }
31
+ await collect();
32
+ if (!files["index.html"]) throw new Error("The HTML5 build did not produce index.html.");
33
+ const title = escapeHtml(config.title);
34
+ const iframe = `<iframe src="./index.html" title="${title}" style="width:100%;aspect-ratio:${config.width} / ${config.height};border:0" allow="autoplay; fullscreen" allowfullscreen loading="lazy"></iframe>`;
35
+ // Extra height keeps the embedded controls separate from the film's exact aspect ratio.
36
+ const embed = iframe.replace(`aspect-ratio:${config.width} / ${config.height}`, `height:100%`);
37
+ 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`);
38
+ 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`);
39
+ 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, files: manifest }, null, 2) + "\n");
40
+ return { filename: `${config.slug}-html5.zip`, bytes: zipSync(files), files: Object.keys(files).length };
41
+ }
42
+
43
+ export async function buildHtmlExport(root) {
44
+ const config = JSON.parse(await readFile(path.join(root, "film.config.json"), "utf8"));
45
+ validateHtmlConfig(config);
46
+ const exports = path.join(root, "production/exports");
47
+ await mkdir(exports, { recursive: true });
48
+ const temporary = await mkdtemp(path.join(exports, ".html5-build-"));
49
+ try {
50
+ const { build } = await import("vite");
51
+ await build({ root, configFile: false, base: "./", mode: "embed", logLevel: "silent", build: { outDir: temporary, emptyOutDir: true, sourcemap: false } });
52
+ return await packageHtmlDirectory(temporary, config);
53
+ } finally { await rm(temporary, { recursive: true, force: true }); }
54
+ }