@olegkoval/agent-skills 1.59.3 → 1.61.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 (231) hide show
  1. package/.github/copilot-instructions.md +2 -0
  2. package/.github/prompts/carousel-maker.prompt.md +102 -0
  3. package/.github/prompts/self-critique.prompt.md +167 -43
  4. package/.github/prompts/shorts-animator.prompt.md +103 -0
  5. package/.kiro/steering/carousel-maker.md +101 -0
  6. package/.kiro/steering/shorts-animator.md +102 -0
  7. package/.windsurf/rules/carousel-maker.md +100 -0
  8. package/.windsurf/rules/shorts-animator.md +101 -0
  9. package/README.md +5 -3
  10. package/adapters/claude/olko-creative/skills/carousel-maker/SKILL.md +112 -0
  11. package/adapters/claude/olko-creative/skills/carousel-maker/scripts/cm.ts +220 -0
  12. package/adapters/claude/olko-creative/skills/carousel-maker/scripts/domain/check.ts +68 -0
  13. package/adapters/claude/olko-creative/skills/carousel-maker/scripts/domain/layout.ts +75 -0
  14. package/adapters/claude/olko-creative/skills/carousel-maker/scripts/domain/schema.ts +54 -0
  15. package/adapters/claude/olko-creative/skills/carousel-maker/scripts/domain/types.ts +33 -0
  16. package/adapters/claude/olko-creative/skills/carousel-maker/scripts/edge/io.ts +87 -0
  17. package/adapters/claude/olko-creative/skills/carousel-maker/scripts/package.json +4 -0
  18. package/adapters/claude/olko-creative/skills/carousel-maker/scripts/test/domain.test.ts +99 -0
  19. package/adapters/claude/olko-creative/skills/carousel-maker/scripts/tsconfig.json +16 -0
  20. package/adapters/claude/olko-creative/skills/shorts-animator/SKILL.md +113 -0
  21. package/adapters/claude/olko-creative/skills/shorts-animator/references/kit.md +59 -0
  22. package/adapters/claude/olko-creative/skills/shorts-animator/references/looks.md +32 -0
  23. package/adapters/claude/olko-creative/skills/shorts-animator/references/writing.md +9 -0
  24. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/audio/compose.ts +290 -0
  25. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/audio/dsp.ts +193 -0
  26. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/audio/instruments.ts +185 -0
  27. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/audio/master.ts +55 -0
  28. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/audio/soundtrack.ts +119 -0
  29. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/domain/captions.ts +35 -0
  30. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/domain/check.ts +96 -0
  31. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/domain/look.ts +26 -0
  32. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/domain/patch.ts +38 -0
  33. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/domain/schema.ts +81 -0
  34. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/domain/timeline.ts +75 -0
  35. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/domain/types.ts +91 -0
  36. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/edge/chrome.ts +169 -0
  37. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/edge/doctor.ts +52 -0
  38. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/edge/media.ts +105 -0
  39. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/edge/page.ts +53 -0
  40. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/edge/probe.ts +75 -0
  41. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/edge/sound.ts +46 -0
  42. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/edge/voice.ts +183 -0
  43. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/looks/gouache.json +7 -0
  44. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/looks/ink.json +6 -0
  45. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/looks/oil.json +7 -0
  46. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/looks/pastel.json +7 -0
  47. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/looks/watercolor.json +6 -0
  48. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/package.json +4 -0
  49. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/runtime/globals.d.ts +199 -0
  50. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/runtime/kit.ts +856 -0
  51. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/runtime/paint.ts +268 -0
  52. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/runtime/stage.ts +181 -0
  53. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/runtime/tsconfig.json +12 -0
  54. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/sa.ts +262 -0
  55. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/test/domain.test.ts +107 -0
  56. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/tsconfig.json +16 -0
  57. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/tts.py +20 -0
  58. package/adapters/claude/olko-reflection/skills/self-critique/SKILL.md +167 -43
  59. package/adapters/claude/olko-reflection/skills/self-critique/scripts/claim-ledger.mjs +159 -0
  60. package/adapters/claude/olko-reflection/skills/self-critique/scripts/rule-store.mjs +193 -0
  61. package/adapters/codex/olko-creative/README.md +2 -0
  62. package/adapters/cursor/olko-creative/skills/carousel-maker/SKILL.md +112 -0
  63. package/adapters/cursor/olko-creative/skills/carousel-maker/scripts/cm.ts +220 -0
  64. package/adapters/cursor/olko-creative/skills/carousel-maker/scripts/domain/check.ts +68 -0
  65. package/adapters/cursor/olko-creative/skills/carousel-maker/scripts/domain/layout.ts +75 -0
  66. package/adapters/cursor/olko-creative/skills/carousel-maker/scripts/domain/schema.ts +54 -0
  67. package/adapters/cursor/olko-creative/skills/carousel-maker/scripts/domain/types.ts +33 -0
  68. package/adapters/cursor/olko-creative/skills/carousel-maker/scripts/edge/io.ts +87 -0
  69. package/adapters/cursor/olko-creative/skills/carousel-maker/scripts/package.json +4 -0
  70. package/adapters/cursor/olko-creative/skills/carousel-maker/scripts/test/domain.test.ts +99 -0
  71. package/adapters/cursor/olko-creative/skills/carousel-maker/scripts/tsconfig.json +16 -0
  72. package/adapters/cursor/olko-creative/skills/shorts-animator/SKILL.md +113 -0
  73. package/adapters/cursor/olko-creative/skills/shorts-animator/references/kit.md +59 -0
  74. package/adapters/cursor/olko-creative/skills/shorts-animator/references/looks.md +32 -0
  75. package/adapters/cursor/olko-creative/skills/shorts-animator/references/writing.md +9 -0
  76. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/audio/compose.ts +290 -0
  77. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/audio/dsp.ts +193 -0
  78. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/audio/instruments.ts +185 -0
  79. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/audio/master.ts +55 -0
  80. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/audio/soundtrack.ts +119 -0
  81. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/domain/captions.ts +35 -0
  82. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/domain/check.ts +96 -0
  83. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/domain/look.ts +26 -0
  84. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/domain/patch.ts +38 -0
  85. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/domain/schema.ts +81 -0
  86. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/domain/timeline.ts +75 -0
  87. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/domain/types.ts +91 -0
  88. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/edge/chrome.ts +169 -0
  89. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/edge/doctor.ts +52 -0
  90. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/edge/media.ts +105 -0
  91. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/edge/page.ts +53 -0
  92. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/edge/probe.ts +75 -0
  93. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/edge/sound.ts +46 -0
  94. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/edge/voice.ts +183 -0
  95. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/looks/gouache.json +7 -0
  96. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/looks/ink.json +6 -0
  97. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/looks/oil.json +7 -0
  98. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/looks/pastel.json +7 -0
  99. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/looks/watercolor.json +6 -0
  100. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/package.json +4 -0
  101. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/runtime/globals.d.ts +199 -0
  102. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/runtime/kit.ts +856 -0
  103. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/runtime/paint.ts +268 -0
  104. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/runtime/stage.ts +181 -0
  105. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/runtime/tsconfig.json +12 -0
  106. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/sa.ts +262 -0
  107. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/test/domain.test.ts +107 -0
  108. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/tsconfig.json +16 -0
  109. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/tts.py +20 -0
  110. package/adapters/cursor/olko-reflection/skills/self-critique/SKILL.md +167 -43
  111. package/adapters/cursor/olko-reflection/skills/self-critique/scripts/claim-ledger.mjs +159 -0
  112. package/adapters/cursor/olko-reflection/skills/self-critique/scripts/rule-store.mjs +193 -0
  113. package/adapters/grok/olko-creative/skills/carousel-maker/SKILL.md +112 -0
  114. package/adapters/grok/olko-creative/skills/carousel-maker/scripts/cm.ts +220 -0
  115. package/adapters/grok/olko-creative/skills/carousel-maker/scripts/domain/check.ts +68 -0
  116. package/adapters/grok/olko-creative/skills/carousel-maker/scripts/domain/layout.ts +75 -0
  117. package/adapters/grok/olko-creative/skills/carousel-maker/scripts/domain/schema.ts +54 -0
  118. package/adapters/grok/olko-creative/skills/carousel-maker/scripts/domain/types.ts +33 -0
  119. package/adapters/grok/olko-creative/skills/carousel-maker/scripts/edge/io.ts +87 -0
  120. package/adapters/grok/olko-creative/skills/carousel-maker/scripts/package.json +4 -0
  121. package/adapters/grok/olko-creative/skills/carousel-maker/scripts/test/domain.test.ts +99 -0
  122. package/adapters/grok/olko-creative/skills/carousel-maker/scripts/tsconfig.json +16 -0
  123. package/adapters/grok/olko-creative/skills/shorts-animator/SKILL.md +113 -0
  124. package/adapters/grok/olko-creative/skills/shorts-animator/references/kit.md +59 -0
  125. package/adapters/grok/olko-creative/skills/shorts-animator/references/looks.md +32 -0
  126. package/adapters/grok/olko-creative/skills/shorts-animator/references/writing.md +9 -0
  127. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/audio/compose.ts +290 -0
  128. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/audio/dsp.ts +193 -0
  129. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/audio/instruments.ts +185 -0
  130. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/audio/master.ts +55 -0
  131. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/audio/soundtrack.ts +119 -0
  132. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/domain/captions.ts +35 -0
  133. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/domain/check.ts +96 -0
  134. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/domain/look.ts +26 -0
  135. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/domain/patch.ts +38 -0
  136. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/domain/schema.ts +81 -0
  137. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/domain/timeline.ts +75 -0
  138. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/domain/types.ts +91 -0
  139. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/edge/chrome.ts +169 -0
  140. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/edge/doctor.ts +52 -0
  141. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/edge/media.ts +105 -0
  142. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/edge/page.ts +53 -0
  143. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/edge/probe.ts +75 -0
  144. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/edge/sound.ts +46 -0
  145. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/edge/voice.ts +183 -0
  146. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/looks/gouache.json +7 -0
  147. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/looks/ink.json +6 -0
  148. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/looks/oil.json +7 -0
  149. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/looks/pastel.json +7 -0
  150. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/looks/watercolor.json +6 -0
  151. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/package.json +4 -0
  152. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/runtime/globals.d.ts +199 -0
  153. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/runtime/kit.ts +856 -0
  154. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/runtime/paint.ts +268 -0
  155. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/runtime/stage.ts +181 -0
  156. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/runtime/tsconfig.json +12 -0
  157. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/sa.ts +262 -0
  158. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/test/domain.test.ts +107 -0
  159. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/tsconfig.json +16 -0
  160. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/tts.py +20 -0
  161. package/adapters/grok/olko-reflection/skills/self-critique/SKILL.md +167 -43
  162. package/adapters/grok/olko-reflection/skills/self-critique/scripts/claim-ledger.mjs +159 -0
  163. package/adapters/grok/olko-reflection/skills/self-critique/scripts/rule-store.mjs +193 -0
  164. package/adapters/hermes/olko-creative/README.md +2 -0
  165. package/catalog/skills.json +47 -1
  166. package/docs/superpowers/plans/2026-10-27-self-critique-v2.md +306 -0
  167. package/docs/superpowers/specs/2026-10-27-self-critique-v2-spec.md +104 -0
  168. package/package.json +1 -1
  169. package/plugins/olko-apple-kit/.claude-plugin/plugin.json +1 -1
  170. package/plugins/olko-creative/.claude-plugin/plugin.json +1 -1
  171. package/plugins/olko-creative/skills/carousel-maker/SKILL.md +111 -0
  172. package/plugins/olko-creative/skills/carousel-maker/scripts/cm.ts +220 -0
  173. package/plugins/olko-creative/skills/carousel-maker/scripts/domain/check.ts +68 -0
  174. package/plugins/olko-creative/skills/carousel-maker/scripts/domain/layout.ts +75 -0
  175. package/plugins/olko-creative/skills/carousel-maker/scripts/domain/schema.ts +54 -0
  176. package/plugins/olko-creative/skills/carousel-maker/scripts/domain/types.ts +33 -0
  177. package/plugins/olko-creative/skills/carousel-maker/scripts/edge/io.ts +87 -0
  178. package/plugins/olko-creative/skills/carousel-maker/scripts/package.json +4 -0
  179. package/plugins/olko-creative/skills/carousel-maker/scripts/test/domain.test.ts +99 -0
  180. package/plugins/olko-creative/skills/carousel-maker/scripts/tsconfig.json +16 -0
  181. package/plugins/olko-creative/skills/shorts-animator/SKILL.md +112 -0
  182. package/plugins/olko-creative/skills/shorts-animator/references/kit.md +59 -0
  183. package/plugins/olko-creative/skills/shorts-animator/references/looks.md +32 -0
  184. package/plugins/olko-creative/skills/shorts-animator/references/writing.md +9 -0
  185. package/plugins/olko-creative/skills/shorts-animator/scripts/audio/compose.ts +290 -0
  186. package/plugins/olko-creative/skills/shorts-animator/scripts/audio/dsp.ts +193 -0
  187. package/plugins/olko-creative/skills/shorts-animator/scripts/audio/instruments.ts +185 -0
  188. package/plugins/olko-creative/skills/shorts-animator/scripts/audio/master.ts +55 -0
  189. package/plugins/olko-creative/skills/shorts-animator/scripts/audio/soundtrack.ts +119 -0
  190. package/plugins/olko-creative/skills/shorts-animator/scripts/domain/captions.ts +35 -0
  191. package/plugins/olko-creative/skills/shorts-animator/scripts/domain/check.ts +96 -0
  192. package/plugins/olko-creative/skills/shorts-animator/scripts/domain/look.ts +26 -0
  193. package/plugins/olko-creative/skills/shorts-animator/scripts/domain/patch.ts +38 -0
  194. package/plugins/olko-creative/skills/shorts-animator/scripts/domain/schema.ts +81 -0
  195. package/plugins/olko-creative/skills/shorts-animator/scripts/domain/timeline.ts +75 -0
  196. package/plugins/olko-creative/skills/shorts-animator/scripts/domain/types.ts +91 -0
  197. package/plugins/olko-creative/skills/shorts-animator/scripts/edge/chrome.ts +169 -0
  198. package/plugins/olko-creative/skills/shorts-animator/scripts/edge/doctor.ts +52 -0
  199. package/plugins/olko-creative/skills/shorts-animator/scripts/edge/media.ts +105 -0
  200. package/plugins/olko-creative/skills/shorts-animator/scripts/edge/page.ts +53 -0
  201. package/plugins/olko-creative/skills/shorts-animator/scripts/edge/probe.ts +75 -0
  202. package/plugins/olko-creative/skills/shorts-animator/scripts/edge/sound.ts +46 -0
  203. package/plugins/olko-creative/skills/shorts-animator/scripts/edge/voice.ts +183 -0
  204. package/plugins/olko-creative/skills/shorts-animator/scripts/looks/gouache.json +7 -0
  205. package/plugins/olko-creative/skills/shorts-animator/scripts/looks/ink.json +6 -0
  206. package/plugins/olko-creative/skills/shorts-animator/scripts/looks/oil.json +7 -0
  207. package/plugins/olko-creative/skills/shorts-animator/scripts/looks/pastel.json +7 -0
  208. package/plugins/olko-creative/skills/shorts-animator/scripts/looks/watercolor.json +6 -0
  209. package/plugins/olko-creative/skills/shorts-animator/scripts/package.json +4 -0
  210. package/plugins/olko-creative/skills/shorts-animator/scripts/runtime/globals.d.ts +199 -0
  211. package/plugins/olko-creative/skills/shorts-animator/scripts/runtime/kit.ts +856 -0
  212. package/plugins/olko-creative/skills/shorts-animator/scripts/runtime/paint.ts +268 -0
  213. package/plugins/olko-creative/skills/shorts-animator/scripts/runtime/stage.ts +181 -0
  214. package/plugins/olko-creative/skills/shorts-animator/scripts/runtime/tsconfig.json +12 -0
  215. package/plugins/olko-creative/skills/shorts-animator/scripts/sa.ts +262 -0
  216. package/plugins/olko-creative/skills/shorts-animator/scripts/test/domain.test.ts +107 -0
  217. package/plugins/olko-creative/skills/shorts-animator/scripts/tsconfig.json +16 -0
  218. package/plugins/olko-creative/skills/shorts-animator/scripts/tts.py +20 -0
  219. package/plugins/olko-garmin-kit/.claude-plugin/plugin.json +1 -1
  220. package/plugins/olko-git-tools/.claude-plugin/plugin.json +1 -1
  221. package/plugins/olko-github-pr/.claude-plugin/plugin.json +1 -1
  222. package/plugins/olko-obsidian/.claude-plugin/plugin.json +1 -1
  223. package/plugins/olko-product/.claude-plugin/plugin.json +1 -1
  224. package/plugins/olko-reflection/.claude-plugin/plugin.json +1 -1
  225. package/plugins/olko-reflection/skills/self-critique/SKILL.md +167 -43
  226. package/plugins/olko-reflection/skills/self-critique/scripts/claim-ledger.mjs +159 -0
  227. package/plugins/olko-reflection/skills/self-critique/scripts/rule-store.mjs +193 -0
  228. package/plugins/olko-release/.claude-plugin/plugin.json +1 -1
  229. package/plugins/olko-skill-meta/.claude-plugin/plugin.json +1 -1
  230. package/plugins/olko-web-ops/.claude-plugin/plugin.json +1 -1
  231. package/scripts/lib/catalog.mjs +2 -1
@@ -0,0 +1,262 @@
1
+ #!/usr/bin/env node
2
+ /*
3
+ * Shorts Animator: a script and a look in, an animated short out.
4
+ *
5
+ * SA new <dir> [--look oil] [--format 9:16] a project: short.json, scenes.js
6
+ * SA looks the looks, moods and voices, one line each
7
+ * SA check <dir> finds what a render would get wrong, in a second, no browser
8
+ * SA voice <dir> narrates every line (cached) and prints the timing
9
+ * SA frames <dir> one painted frame per shot -> frames/sheet.jpg
10
+ * SA still <dir> <shot> [at] one shot, full size -> frames/shot-NN.jpg
11
+ * SA render <dir> the MP4: voice, score, every drawing -> <title>.mp4
12
+ * SA set <dir> key=value... change short.json from the command line (JSON values)
13
+ * SA doctor checks the machine and renders a 3-second test
14
+ *
15
+ * SA = node <skill-dir>/scripts/sa.ts
16
+ */
17
+ import fs from "node:fs";
18
+ import path from "node:path";
19
+ import { check, report } from "./domain/check.ts";
20
+ import { resolveLook, pageState } from "./domain/look.ts";
21
+ import { parseAssignment, setPath } from "./domain/patch.ts";
22
+ import { isJson, isMap, parseShort } from "./domain/schema.ts";
23
+ import { timeline } from "./domain/timeline.ts";
24
+ import type { Json, JsonMap, Result, Short, Take, Timeline } from "./domain/types.ts";
25
+ import { fail, ok } from "./domain/types.ts";
26
+ import type { Page } from "./edge/chrome.ts";
27
+ import { doctor } from "./edge/doctor.ts";
28
+ import { encoder, jpeg, mkdir, readJson, stillSheet, videoSheet, writeJson } from "./edge/media.ts";
29
+ import { pageErrors, paintPage } from "./edge/page.ts";
30
+ import { kitNames, probeScenes } from "./edge/probe.ts";
31
+ import { MOODS, soundtrack } from "./edge/sound.ts";
32
+ import { cachedTakes, narrate } from "./edge/voice.ts";
33
+
34
+ const LOOKS = path.join(import.meta.dirname, "looks");
35
+ const VOICES = "bm_george (warm British man, default), am_michael, am_adam, af_heart, af_nova, bf_emma, bf_isabella";
36
+ const NEW_SCENES = `// One function per shot. k is the drawing kit (references/kit.md).
37
+ window.SCENES = {
38
+ opening: (k) => {
39
+ k.sky({ stops: ["#f3ecd2", "#e9dfb7"] });
40
+ k.ridge({ y: 0.52, amp: 0.02, freq: 4, color: "#4f7a3a", shape: "bush" });
41
+ k.ground({ y: 0.52, color: "#8fbf3a", far: "#c3cf5a", stripes: 14 });
42
+ k.person({ x: 0.5, y: 0.7, h: 0.22, pose: "walk", facing: "front" });
43
+ },
44
+ };
45
+ `;
46
+
47
+ const unwrap = <T>(r: Result<T>): T => {
48
+ if (!r.ok) throw new Error(r.errors.join("\n "));
49
+ return r.value;
50
+ };
51
+
52
+ const slug = (s: string): string =>
53
+ s.toLowerCase().normalize("NFKD").replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "short";
54
+
55
+ const twosName = (n: number): string => (n === 1 ? "ones" : n === 2 ? "twos" : `${n}s`);
56
+
57
+ const lookFiles = (): readonly string[] => fs.readdirSync(LOOKS).filter((f) => f.endsWith(".json")).toSorted();
58
+
59
+ const namedLook = (name: string): Result<JsonMap> => {
60
+ const f = path.join(LOOKS, `${name}.json`);
61
+ if (!fs.existsSync(f)) return fail(`no look "${name}" (SA looks lists them)`);
62
+ const v = readJson(f);
63
+ return isMap(v) ? ok(v) : fail(`looks/${name}.json is not an object`);
64
+ };
65
+
66
+ const shortFile = (dir: string): string => path.join(dir, "short.json");
67
+
68
+ function rawShort(dir: string): Json {
69
+ const f = shortFile(dir);
70
+ if (!fs.existsSync(f)) throw new Error(`no short.json in ${dir} (start one with: SA new ${dir})`);
71
+ const v = readJson(f);
72
+ if (!isJson(v)) throw new Error("short.json is not JSON");
73
+ return v;
74
+ }
75
+
76
+ const load = (dir: string): Short => unwrap(parseShort(rawShort(dir)));
77
+
78
+ type Project = { readonly dir: string; readonly s: Short; readonly takes: readonly Take[]; readonly tl: Timeline };
79
+
80
+ async function project(dir: string, quiet = false): Promise<Project> {
81
+ const s = load(dir);
82
+ const takes = await narrate(dir, s, quiet);
83
+ return { dir, s, takes, tl: unwrap(timeline(s, takes)) };
84
+ }
85
+
86
+ async function withPage<T>({ dir, s, tl }: Project, use: (pg: Page) => Promise<T>): Promise<T> {
87
+ const state = pageState(s, tl, unwrap(resolveLook(s.look, namedLook)));
88
+ const pg = await paintPage(dir, state, tl.W, tl.H);
89
+ try {
90
+ const out = await use(pg);
91
+ const errs = pageErrors(pg);
92
+ if (errs.length) console.log("⚠ the page said:\n " + errs.join("\n "));
93
+ return out;
94
+ } finally {
95
+ pg.close();
96
+ }
97
+ }
98
+
99
+ const frameName = (i: number): string => `s${String(i).padStart(3, "0")}.jpg`;
100
+
101
+ /* ------------------------------------------------------------------ commands */
102
+
103
+ type Args = { readonly dir: string | undefined; readonly rest: readonly string[] };
104
+ type Command = (a: Args) => Promise<number>;
105
+
106
+ const need = (dir: string | undefined, usage: string): string => {
107
+ if (!dir) throw new Error(usage);
108
+ return dir;
109
+ };
110
+
111
+ const looks: Command = async () => {
112
+ lookFiles().forEach((f) => {
113
+ const l = readJson(path.join(LOOKS, f));
114
+ console.log(`${f.replace(".json", "").padEnd(12)} ${isMap(l) && typeof l.about === "string" ? l.about : ""}`);
115
+ });
116
+ console.log(`\nmusic moods: ${MOODS.join(", ")} (or "none", or a path to their own track)`);
117
+ console.log(`voices: ${VOICES}`);
118
+ return 0;
119
+ };
120
+
121
+ const create: Command = async ({ dir, rest }) => {
122
+ const d = need(dir, "SA new <dir>");
123
+ mkdir(d, "assets");
124
+ const opt = (k: string, dflt: string): string => {
125
+ const i = rest.indexOf(`--${k}`);
126
+ return (i >= 0 ? rest[i + 1] : undefined) ?? dflt;
127
+ };
128
+ if (!fs.existsSync(shortFile(d))) {
129
+ writeJson(shortFile(d), {
130
+ title: "Untitled short", brief: "", format: opt("format", "9:16"), look: opt("look", "oil"),
131
+ voice: { id: "bm_george", speed: 0.92 }, music: { mood: "tender" }, captions: { style: "box" },
132
+ lines: [{ say: "Every story starts somewhere.", scene: "opening", move: "push" }],
133
+ });
134
+ }
135
+ if (!fs.existsSync(path.join(d, "scenes.js"))) fs.writeFileSync(path.join(d, "scenes.js"), NEW_SCENES);
136
+ console.log(`✓ ${d}\n short.json the script, look, voice, music\n scenes.js the drawings\n assets/ anything they gave you`);
137
+ return 0;
138
+ };
139
+
140
+ const set: Command = async ({ dir, rest }) => {
141
+ const d = need(dir, "SA set <dir> key=value...");
142
+ const next = rest.map((a) => unwrap(parseAssignment(a))).reduce<Json>((doc, a) => setPath(doc, a.path, a.value), rawShort(d));
143
+ // Placeholder takes are enough to catch what only the timeline knows (an unknown format).
144
+ const s = unwrap(parseShort(next));
145
+ unwrap(timeline(s, s.lines.map(() => ({ file: null, dur: 1 }))));
146
+ writeJson(shortFile(d), next);
147
+ console.log("✓ short.json updated");
148
+ return 0;
149
+ };
150
+
151
+ const lookKeys = (): readonly string[] =>
152
+ [...new Set(lookFiles().flatMap((f) => {
153
+ const l = readJson(path.join(LOOKS, f));
154
+ return isMap(l) ? Object.keys(l) : [];
155
+ }))];
156
+
157
+ const checkCmd: Command = async ({ dir }) => {
158
+ const d = need(dir, "SA check <dir>");
159
+ const s = load(d);
160
+ const tl = unwrap(timeline(s, cachedTakes(d, s)));
161
+ const findings = check({ short: s, timeline: tl, probes: probeScenes(d, tl), kit: kitNames(), lookKeys: lookKeys() });
162
+ console.log(report(findings));
163
+ return findings.some((f) => f.level === "error") ? 1 : 0;
164
+ };
165
+
166
+ const voice: Command = async ({ dir }) => {
167
+ const { tl } = await project(need(dir, "SA voice <dir>"));
168
+ tl.beats.forEach((b, i) => console.log(`${String(i + 1).padStart(2)} ${b.start.toFixed(2).padStart(6)}s ${b.dur.toFixed(2)}s ${b.scene.padEnd(16)} ${b.say}`));
169
+ console.log(`total ${tl.duration.toFixed(1)}s, ${tl.beats.length} shots`);
170
+ return 0;
171
+ };
172
+
173
+ const frames: Command = async ({ dir }) => {
174
+ const p = await project(need(dir, "SA frames <dir>"), true);
175
+ const fdir = mkdir(p.dir, "frames");
176
+ fs.readdirSync(fdir).filter((f) => /^s\d+\.jpg$/.test(f)).forEach((f) => fs.rmSync(path.join(fdir, f)));
177
+ await withPage(p, (pg) =>
178
+ p.tl.beats.reduce<Promise<void>>(async (prev, _b, i) => {
179
+ await prev;
180
+ fs.writeFileSync(path.join(fdir, frameName(i)), jpeg(await pg.evaluate(`window.still(${i}, 0.6)`)));
181
+ }, Promise.resolve()),
182
+ );
183
+ const sheet = path.join(fdir, "sheet.jpg");
184
+ stillSheet(p.tl, path.join(fdir, "s%03d.jpg"), sheet);
185
+ console.log(`✓ ${sheet} (${p.tl.beats.length} shots, ${p.tl.duration.toFixed(1)}s)`);
186
+ return 0;
187
+ };
188
+
189
+ const still: Command = async ({ dir, rest }) => {
190
+ const p = await project(need(dir, "SA still <dir> <shot> [at]"), true);
191
+ const i = Math.max(0, parseInt(rest[0] ?? "1", 10) - 1);
192
+ const at = parseFloat(rest[1] ?? "0.6");
193
+ if (i >= p.tl.beats.length) throw new Error(`shot ${i + 1}? there are ${p.tl.beats.length}`);
194
+ const f = path.join(mkdir(p.dir, "frames"), `shot-${String(i + 1).padStart(2, "0")}.jpg`);
195
+ await withPage(p, async (pg) => fs.writeFileSync(f, jpeg(await pg.evaluate(`window.still(${i}, ${at})`))));
196
+ console.log(`✓ ${f}`);
197
+ return 0;
198
+ };
199
+
200
+ /** Next free vN: one past the highest, so deleting an old render never reuses its number. */
201
+ const nextVersion = (rdir: string): number =>
202
+ 1 + Math.max(0, ...fs.readdirSync(rdir).flatMap((f) => /^v(\d+)\.mp4$/.exec(f)?.[1] ?? []).map(Number));
203
+
204
+ async function paintAll(pg: Page, write: (b: Buffer) => Promise<void>, drawings: number): Promise<void> {
205
+ const step = async (d: number): Promise<void> => {
206
+ if (d >= drawings) return;
207
+ await write(jpeg(await pg.evaluate(`window.drawing(${d})`)));
208
+ if (d % 60 === 0) process.stdout.write(`\r· painting ${d}/${drawings} drawings`);
209
+ return step(d + 1);
210
+ };
211
+ await step(0);
212
+ process.stdout.write(`\r· painted ${drawings} drawings \n`);
213
+ }
214
+
215
+ const render: Command = async ({ dir }) => {
216
+ const t0 = Date.now();
217
+ const p = await project(need(dir, "SA render <dir>"));
218
+ const { tl, s } = p;
219
+ console.log(`· ${tl.beats.length} shots, ${tl.duration.toFixed(1)}s at ${tl.fps} fps, drawn on ${twosName(tl.twos)}`);
220
+ const audio = soundtrack(p.dir, s, tl, p.takes);
221
+ const rdir = mkdir(p.dir, "renders");
222
+ const n = nextVersion(rdir);
223
+ const outV = path.join(rdir, `v${n}.mp4`);
224
+ const enc = encoder(tl, audio, outV);
225
+ await withPage(p, (pg) => paintAll(pg, enc.write, Math.ceil((tl.duration * tl.fps) / tl.twos)));
226
+ const code = await enc.done();
227
+ if (code !== 0) throw new Error(`ffmpeg exited ${code} while encoding`);
228
+ const final = path.join(p.dir, `${slug(s.title)}.mp4`);
229
+ fs.copyFileSync(outV, final);
230
+ fs.copyFileSync(shortFile(p.dir), path.join(rdir, `v${n}.json`));
231
+ const sheet = path.join(mkdir(p.dir, "qa"), "sheet.jpg");
232
+ videoSheet(tl, final, sheet);
233
+ console.log(`✓ ${final}\n ${tl.duration.toFixed(1)}s · ${tl.W}×${tl.H} · ${((Date.now() - t0) / 1000).toFixed(0)}s to make · check: ${sheet}`);
234
+ return 0;
235
+ };
236
+
237
+ const COMMANDS: Readonly<Record<string, Command>> = {
238
+ looks, new: create, set, check: checkCmd, voice, frames, still, render,
239
+ doctor: async () => doctor(import.meta.filename),
240
+ };
241
+
242
+ const usage = (): string =>
243
+ fs.readFileSync(import.meta.filename, "utf8").split("\n").slice(2, 16).join("\n").replace(/^ \* ?/gm, "");
244
+
245
+ // stripTypeScriptTypes is stable in practice but flagged experimental; that one notice is noise to a user.
246
+ process.removeAllListeners("warning");
247
+ process.on("warning", (w) => {
248
+ if (!(w.name === "ExperimentalWarning" && /stripTypeScriptTypes/.test(w.message))) console.error(`${w.name}: ${w.message}`);
249
+ });
250
+
251
+ const [cmd = "", dirArg, ...rest] = process.argv.slice(2);
252
+ const command = COMMANDS[cmd];
253
+ if (!command) {
254
+ console.log(usage());
255
+ process.exit(cmd ? 1 : 0);
256
+ }
257
+ command({ dir: dirArg ? path.resolve(dirArg) : undefined, rest })
258
+ .then((code) => process.exit(code))
259
+ .catch((e: unknown) => {
260
+ console.error("✗ " + (e instanceof Error ? e.message : String(e)));
261
+ process.exit(1);
262
+ });
@@ -0,0 +1,107 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+ import { captionChunks, chunks, words } from "../domain/captions.ts";
4
+ import { check, report, type Facts, type Probe } from "../domain/check.ts";
5
+ import { resolveLook } from "../domain/look.ts";
6
+ import { parseAssignment, setPath } from "../domain/patch.ts";
7
+ import { parseShort } from "../domain/schema.ts";
8
+ import { timeline } from "../domain/timeline.ts";
9
+ import type { Json, Result, Short, Take, Timeline } from "../domain/types.ts";
10
+ import { fail, ok } from "../domain/types.ts";
11
+
12
+ const value = <T>(r: Result<T>): T => {
13
+ if (!r.ok) throw new Error(r.errors.join("; "));
14
+ return r.value;
15
+ };
16
+
17
+ const RAW = {
18
+ title: "test", voice: "none", music: "none",
19
+ lines: [
20
+ { say: "A bright and open morning, out on the hills.", scene: "day", move: "push" },
21
+ { say: "Night comes down over the city.", scene: "night" },
22
+ ],
23
+ };
24
+ const short = (patch: object = {}): Short => value(parseShort({ ...RAW, ...patch }));
25
+ const silent = (s: Short): readonly Take[] => s.lines.map(() => ({ file: null, dur: 2 }));
26
+ const tl = (s: Short): Timeline => value(timeline(s, silent(s)));
27
+
28
+ test("words drops the writer's emphasis and pause marks", () => {
29
+ assert.deepEqual(words("a *bright* | morning"), ["a", "bright", "morning"]);
30
+ });
31
+
32
+ test("chunks are balanced, not greedy", () => {
33
+ assert.deepEqual(chunks("one two three four five six seven", 5).map((c) => c.split(" ").length), [4, 3]);
34
+ assert.deepEqual(chunks("short line", 5), ["short line"]);
35
+ });
36
+
37
+ test("a slash in the caption forces the break", () => {
38
+ assert.deepEqual(captionChunks({ say: "Everyone, every pose.", caption: "Everyone, / every pose.", shots: [] }, 5), ["Everyone,", "every pose."]);
39
+ });
40
+
41
+ test("parseShort fills defaults and rejects a script with no lines", () => {
42
+ const s = short();
43
+ assert.equal(s.format, "9:16");
44
+ assert.equal(s.fps, 24);
45
+ assert.equal(s.voice.kind, "none");
46
+ assert.equal(parseShort({ title: "x" }).ok, false);
47
+ assert.equal(parseShort([]).ok, false);
48
+ });
49
+
50
+ test("timeline lays beats end to end and rejects an unknown format", () => {
51
+ const t = tl(short());
52
+ assert.equal(t.beats.length, 2);
53
+ assert.equal(t.W, 1080);
54
+ const [a, b] = t.beats;
55
+ assert.ok(a && b && Math.abs(a.start + a.dur - b.start) < 1e-9);
56
+ assert.ok(t.duration >= b.start + b.dur);
57
+ const bad = timeline(short({ format: "3:2" }), silent(short()));
58
+ assert.equal(bad.ok, false);
59
+ });
60
+
61
+ test("parseAssignment reads JSON values and falls back to text", () => {
62
+ assert.deepEqual(value(parseAssignment("lines.0.hold=0.8")), { path: ["lines", "0", "hold"], value: 0.8 });
63
+ assert.deepEqual(value(parseAssignment("look=watercolor")), { path: ["look"], value: "watercolor" });
64
+ assert.equal(parseAssignment("nokey").ok, false);
65
+ assert.equal(parseAssignment("a..b=1").ok, false);
66
+ });
67
+
68
+ test("setPath returns a new document and leaves the old one alone", () => {
69
+ const doc: Json = { lines: [{ say: "hi" }], music: { mood: "tender" } };
70
+ const before = JSON.stringify(doc);
71
+ const next = setPath(setPath(doc, ["lines", "0", "hold"], 1), ["music", "mood"], "night");
72
+ assert.equal(JSON.stringify(doc), before);
73
+ assert.deepEqual(next, { lines: [{ say: "hi", hold: 1 }], music: { mood: "night" } });
74
+ });
75
+
76
+ test("resolveLook merges a named base under the overrides", () => {
77
+ const named = (n: string): Result<{ readonly [k: string]: Json }> => (n === "oil" ? ok({ grain: 0.3, edge: 1 }) : fail(`no look "${n}"`));
78
+ const look = value(resolveLook({ base: "oil", grain: 0.5 }, named));
79
+ assert.equal(look.grain, 0.5);
80
+ assert.equal(look.edge, 1);
81
+ assert.equal(resolveLook("nope", named).ok, false);
82
+ });
83
+
84
+ const facts = (probes: readonly Probe[], s = short()): Facts => ({ short: s, timeline: tl(s), probes, kit: ["sky", "person"], lookKeys: ["grain"] });
85
+
86
+ test("check flags missing scenes, unknown kit calls and throwing scenes as errors", () => {
87
+ const found = check(facts([{ scene: "day", calls: [{ fn: "skyy", opts: {} }] }, { scene: "other", calls: [], error: "boom" }]));
88
+ const errors = found.filter((f) => f.level === "error").map((f) => f.msg);
89
+ assert.ok(errors.some((m) => m.includes('no scene "night"')));
90
+ assert.ok(errors.some((m) => m.includes("k.skyy is not in the kit")));
91
+ assert.ok(errors.some((m) => m.includes("throws: boom")));
92
+ });
93
+
94
+ test("check warns when a person stands in the caption band, unless captions are off", () => {
95
+ const probes: readonly Probe[] = [
96
+ { scene: "day", calls: [{ fn: "person", opts: { y: 0.82, h: 0.28 } }] },
97
+ { scene: "night", calls: [{ fn: "person", opts: { y: 0.5, h: 0.2 } }] },
98
+ ];
99
+ const band = (s: Short) => check(facts(probes, s)).filter((f) => f.msg.includes("caption line"));
100
+ assert.deepEqual(band(short()).map((f) => f.where), ['scene "day"']);
101
+ assert.deepEqual(band(short({ captions: { style: "none" } })), []);
102
+ });
103
+
104
+ test("report is one line per finding, or clean", () => {
105
+ assert.equal(report([]), "check: clean");
106
+ assert.equal(report([{ level: "error", where: "x", msg: "y" }]), "ERROR x: y");
107
+ });
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "es2023",
4
+ "lib": ["es2023"],
5
+ "module": "nodenext",
6
+ "moduleResolution": "nodenext",
7
+ "types": ["node"],
8
+ "strict": true,
9
+ "noEmit": true,
10
+ "allowImportingTsExtensions": true,
11
+ "erasableSyntaxOnly": true,
12
+ "verbatimModuleSyntax": true,
13
+ "skipLibCheck": true
14
+ },
15
+ "include": ["*.ts", "domain", "edge", "audio", "test"]
16
+ }
@@ -0,0 +1,20 @@
1
+ """The narrator: Kokoro-82M on this computer, every line of a short in one go.
2
+
3
+ python tts.py <model.onnx> <voices.bin> <voice> <speed> < jobs.json
4
+
5
+ jobs.json is [[text, out.wav], ...]. Loads the model once, writes one WAV per line,
6
+ prints one line per file. Apache-2.0 model, no account, nothing leaves the machine.
7
+ """
8
+ import json
9
+ import sys
10
+
11
+ import soundfile as sf
12
+ from kokoro_onnx import Kokoro
13
+
14
+ model, voices, voice, speed = sys.argv[1], sys.argv[2], sys.argv[3], float(sys.argv[4])
15
+ lang = {"a": "en-us", "b": "en-gb", "e": "es", "f": "fr-fr", "h": "hi", "i": "it", "j": "ja", "p": "pt-br", "z": "cmn"}.get(voice[0], "en-us")
16
+ k = Kokoro(model, voices)
17
+ for text, out in json.load(sys.stdin):
18
+ samples, rate = k.create(text, voice=voice, speed=speed, lang=lang)
19
+ sf.write(out, samples, rate)
20
+ print(out, flush=True)