@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,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", "test"]
16
+ }
@@ -0,0 +1,113 @@
1
+ ---
2
+ name: shorts-animator
3
+ description: Use when someone wants a narrated, painted or hand-drawn animated short (TikTok, Reels, YouTube Shorts) from a script or topic and a look described in words. Every frame is drawn in code and GPU-painted, voiced locally, captioned and scored into an MP4; no image models or stock.
4
+ license: MIT
5
+ allowed-tools: Bash, Read, Write, Edit
6
+ compatibility: Codex, Claude Code, Hermes Agent, Cursor, GitHub Copilot, Windsurf, Kiro, and other Agent Skills compatible tools. Needs Node 22.18+, ffmpeg and Chrome; the narrator installs Python and its voice model on first use.
7
+ metadata:
8
+ targets: [_source-only]
9
+ author: Oleg Koval
10
+ tags:
11
+ - video
12
+ - animation
13
+ - shorts
14
+ - narration
15
+ - canvas
16
+ ---
17
+ <!-- Generated by scripts/build-adapters.sh. Do not edit directly. -->
18
+
19
+ # Shorts Animator
20
+
21
+ A script and a look in, an animated short out. You write the lines and draw every shot as canvas code; the
22
+ tool paints it on the GPU on twos, narrates it with a local Kokoro voice, captions it, scores it and
23
+ encodes the MP4. Nothing is filmed, generated by an image model, or licensed.
24
+
25
+ `SA` = `node <this skill's folder>/scripts/sa.ts`. Write the full command each time. One folder per short:
26
+ `WORK=./<slug>-short`. First run on a machine: `SA doctor` (checks Node, ffmpeg, Chrome, sets up the voice,
27
+ renders a 3-second test).
28
+
29
+ ## 1. Decide, do not interrogate
30
+
31
+ Decide the lines, shots, look, voice and music yourself. Ask one question only if the subject is unclear.
32
+ A pasted script is used word for word. A topic: write it per `references/writing.md`.
33
+
34
+ | they say | look |
35
+ |---|---|
36
+ | oil, painterly, Van Gogh, swirly, "like the faith shorts" | `oil` |
37
+ | watercolour, soft, dreamy, children's book | `watercolor` |
38
+ | crayon, pastel, chalk, for kids | `pastel` |
39
+ | ink, comic, graphic novel, line art | `ink` |
40
+ | flat, storybook, gouache, poster, calm | `gouache` |
41
+
42
+ Custom look: `"look": { "base": "oil", "brush": 16, "palette": {...} }`. Read `references/looks.md` only
43
+ when overriding numbers. Voice `bm_george` (default, scripture and quotes), `am_michael`, `bf_emma`
44
+ (stories), `af_heart` (kids); `speed` 0.85 to 0.95 reads calm. Their recording: `"voice": { "file":
45
+ "assets/voice.m4a" }` with `say` lines matching it in order. `"voice": "none"` for music only. Music
46
+ `mood`: `tender`, `hopeful`, `night`, `wonder`, `playful`, `none`, or a track path. `SA looks` lists all.
47
+
48
+ ## 2. Write the plan and the drawings
49
+
50
+ ```bash
51
+ SA new "$WORK" --look oil # short.json + scenes.js + assets/
52
+ ```
53
+
54
+ `short.json` lines: `{ "say": "...", "scene": "meadow" | ["a", "b"], "move": "push", "hold": 0.8 }`.
55
+ - A list of scenes cuts on the caption chunks. Cut often: a new picture every 1.5 to 3 s.
56
+ - `move`: push, pull, pan-left, pan-right, rise, fall, drift, still. `transition`: cut, fade, dip (rarely).
57
+ - `caption` shows different words; `/` forces a break. `captions.style`: box, plain, none.
58
+ - `format`: 9:16 (default), 16:9, 1:1, 4:5. `twos`: 2 (hand-drawn), 1 (smooth).
59
+
60
+ `scenes.js`: `window.SCENES = { "meadow": (k) => { k.sky({...}); k.person({...}); } }`. Read
61
+ `references/kit.md` before writing scenes. Draw like a painter:
62
+ - Big simple shapes, strong value contrast, one subject. The paint pass adds texture; draw nothing fussy.
63
+ - Keep faces out of the caption band (about 77% down).
64
+ - Every shot different: place, scale, day and night, warm and cool. Match the words.
65
+ - Small motion: `pose: "walk"`, `dx`, rain, glints. `k.t` seconds, `k.p` progress 0 to 1.
66
+ - No helper for it (a boat, an animal)? Write a small function with `k.g`, flat shapes in the palette.
67
+
68
+ ## 3. Check, then look (budgeted)
69
+
70
+ ```bash
71
+ SA check "$WORK" # no browser, about a second: missing scenes, throwing scenes, unknown kit calls,
72
+ # figures under the captions, line and total length
73
+ SA frames "$WORK" # narrates (cached), one painted frame per shot -> frames/sheet.jpg
74
+ SA still "$WORK" 7 # one shot full size, only when the sheet shows a problem you cannot place
75
+ ```
76
+
77
+ Fix every `ERROR` from `check` before spending a browser run. Then read `frames/sheet.jpg` once. Fix muddy
78
+ or empty shots, a figure lost in its background (contrast, `rim`), look-alike shots, a red "no scene"
79
+ frame, and every `⚠ the page said` line. **Budget: at most two frames rounds before the first render.**
80
+ Images are the expensive part of this loop; do not reopen a sheet you have already judged.
81
+
82
+ ## 4. Render and deliver
83
+
84
+ ```bash
85
+ SA render "$WORK" # about a minute for 40 s -> <title>.mp4, renders/vN.mp4 (+ its short.json), qa/sheet.jpg
86
+ ```
87
+
88
+ Read `qa/sheet.jpg` only if the render printed a warning or they report a problem. Tell them in two
89
+ lines what it is (look, length, shots) and: "Tell me what to change, one sentence is enough."
90
+
91
+ ## 5. One-sentence changes
92
+
93
+ Smallest change, `SA check`, render, show. `SA set "$WORK" voice.speed=0.85 music.mood=night` edits
94
+ short.json (JSON values; plain text otherwise) and refuses an invalid result.
95
+
96
+ | they say | change |
97
+ |---|---|
98
+ | more painterly, softer | `look` or its numbers (brush, swirl, wash) |
99
+ | slower, faster | `voice.speed`, `pause` |
100
+ | a woman's voice, American | `voice.id` |
101
+ | no music, more emotional | `music.mood` |
102
+ | bigger captions, no box | `captions.size` 52, `captions.style` plain |
103
+ | for YouTube | `format` 16:9, redraw shots whose layout breaks |
104
+ | change shot 4 | that scene function only |
105
+
106
+ ## When something goes wrong
107
+
108
+ | symptom | fix |
109
+ |---|---|
110
+ | no Chrome | install Chrome, or `SA doctor` fetches a headless one |
111
+ | "painting on the CPU" | works, about 10x slower |
112
+ | narrator setup fails | needs Python 3.10+ or `uv`; or use `"voice": "none"` or their recording |
113
+ | voice timing off with their recording | `say` lines must match its words, in order |
@@ -0,0 +1,59 @@
1
+ # The drawing kit
2
+
3
+ Every scene is `(k) => { ... }`. Positions are fractions of the frame (x 0→1 left→right, y 0→1 top→bottom);
4
+ sizes in px are for a 1920-tall frame and scale with it (`k.u`). Draw back to front: sky, far, middle, near.
5
+
6
+ | in `k` | what |
7
+ |---|---|
8
+ | `k.g` | the 2D canvas context (camera already applied): draw anything the helpers don't cover |
9
+ | `k.W`, `k.H`, `k.u` | frame size, and the scale (H/1920) |
10
+ | `k.X(x)`, `k.Y(y)` | fraction → pixels |
11
+ | `k.t`, `k.p` | seconds into the shot (on twos), progress 0→1 |
12
+ | `k.r()` | random, the same on every drawing of the shot |
13
+ | `k.rng(seed)`, `k.noise1(x)` | a seeded random source; smooth 1D noise |
14
+ | `k.pal`, `k.beat` | the short's palette; this shot's beat (scene, line, move) |
15
+ | `k.mix(a, b, t)`, `k.alpha(c, a)` | colour helpers |
16
+ | `IMAGES.name` | an image from `assets/` (a logo, a reference), for `k.g.drawImage` |
17
+
18
+ ## Sky and light
19
+ - `k.sky({ stops: [top, …, bottom], y0, y1 })`: a gradient
20
+ - `k.clouds({ y, band, n, size, color, shade, drift, seed })`: small cumulus in a band, drifting
21
+ - `k.sun({ x, y, r, color, glow, glowSize, glowAlpha })`: sun, moon, a bright bulb
22
+ - `k.glow({ x, y, r, color, a })`: any soft light (windows, lanterns, a halo)
23
+ - `k.stars({ n, y1 })`, `k.motes({ n, y0, y1, color })` (fireflies, embers), `k.rain({ n, a, slant })`
24
+ - `k.rays({ x, y, angle, spread, n, a, width })`: god rays through trees or clouds
25
+ - `k.haze({ y, h, color, a })`: air between planes
26
+
27
+ ## Land and water
28
+ - `k.ridge({ y, amp, freq, color, top, shape, seed })`: filled ridge: `soft` hills, `peaks` mountains,
29
+ `pines` a conifer line, `bush` a broadleaf line, `rocky`
30
+ - `k.ground({ y, color, far, stripes, stripe, flowers })`: the ground plane, mown stripes, flowers
31
+ - `k.path({ x, y, near, far, color, bend })`: a road to the horizon
32
+ - `k.water({ y0, y1, color, deep, glint, glints })`: lake or sea with moving glints
33
+ - `k.waterfall({ x, w, y0, y1 })`
34
+ - `k.shape({ pts: [[x,y],…], color, smooth })`: any shape: a cliff, a giant tree trunk, an island
35
+
36
+ ## Plants
37
+ - `k.tree({ x, y, h, kind, crown, trunk, fruit })`: kind `round` `pine` `autumn` `bare` `palm` `column`
38
+ - `k.trees({ n, y, h, x0, x1, kind, crown })`: a row
39
+ - `k.foliage({ side, x, depth, y0, y1, color, light })`: leaves hanging into frame from a side
40
+ - `k.trunks({ n, color, light, width, x0, x1 })`: inside a forest
41
+
42
+ ## Built things
43
+ - `k.house({ x, y, w, h, wall, roof, window, lit, floors, aFrame })`
44
+ - `k.city({ y, h, color, lit, litColor, density, n })`: skyline, windows lit at night
45
+ - `k.pagoda({ x, y, h, color })`, `k.pole({ x, y, h })`, `k.lamp({ x, y, h, light })`
46
+ - `k.block({ x0, y0, x1, y1, color, edge })`: walls, doorways, a lit edge
47
+ - `k.heart({ x, y, s, color })`
48
+
49
+ ## People
50
+ `k.person({ x, y, h, pose, facing, hair, skin, hairColor, top, bottom, shoes, robe, rim, silhouette, walk, dx, flip, build })`
51
+ - feet at (x, y), `h` = height as a fraction of the frame (0.25 is a full figure in a landscape)
52
+ - `pose`: `stand` `walk` `cheer` `open` `wave` `point` `reach` `pray` `hug` `sit` `kneel` `hold`
53
+ - `facing`: `front` | `back` (faces are left blank on purpose: it's the style, and it reads at any size)
54
+ - `hair`: `short` `long` `curly` `afro` `bun` `hood` `hat` `cap` `bald`
55
+ - `robe: true`: a long garment (biblical, monks, a dress)
56
+ - `silhouette: "#111"`: one colour (backlit, sunset); `rim: "#9db6f0"`: a lit outline (night)
57
+ - `dx`: moves over the shot (0.05 = 5% of the width); `walk`: steps per second
58
+ - `k.people([{...}, {...}])`: several `person` calls in one
59
+ - Two people walking together read as a relationship; a tiny figure in a huge landscape reads as awe or loneliness.
@@ -0,0 +1,32 @@
1
+ # Looks: from words to numbers
2
+
3
+ A look is a JSON object. Start from a shipped one (`"base": "oil"`) and override.
4
+
5
+ | key | what it does | range |
6
+ |---|---|---|
7
+ | `mode` | `strokes` (paint/pastel), `wash` (watercolour/ink), `flat` | |
8
+ | `brush` | stroke size in px at 1920 tall | 5 fine … 11 oil … 18 bold |
9
+ | `length` | stroke length ÷ width | 2 dabs … 4.6 hatching |
10
+ | `swirl` | strokes follow a swirl (1) or the shapes (0) | 0 … 1 |
11
+ | `curve` | strokes bend | 0 … 1 |
12
+ | `bristle` | streaks inside a stroke, broken dry ends | 0 … 1 |
13
+ | `impasto` | thick paint catching light | 0 … 1 |
14
+ | `hueJitter` `valueJitter` `satJitter` | colour variation stroke to stroke | 0 … 0.4 |
15
+ | `respect` | big strokes stop at edges (crisper subjects) | 0 … 1 |
16
+ | `boil` | share of strokes redrawn every drawing (the shimmer) | 0 still … 1 lively |
17
+ | `wash` `bleed` | watercolour pooling, blooms, granulation; how far colour runs | 0 … 1 |
18
+ | `outline` `ink` | hand-inked lines, their colour | 0 … 1 |
19
+ | `paper` `paperColor` `grain` | canvas/paper texture, grain | 0 … 1.6 |
20
+ | `posterize` | flat colour bands (screen print, retro) | 0 off, 4 … 8 |
21
+ | `saturation` `contrast` `warmth` | grade | ~0.8 … 1.3 |
22
+ | `vignette` `bloom` | dark corners, glow from lights | 0 … 0.6 |
23
+ | `captions` | `{ style, size, y, box, color, font, case }` | |
24
+
25
+ Words → moves:
26
+ - "thicker / bolder paint" → brush +4, impasto +0.2 · "finer" → brush −3
27
+ - "swirly / Van Gogh" → swirl 0.9, curve 1, hueJitter 0.25 · "calmer" → swirl 0.3, boil 0.2
28
+ - "dreamy / soft" → watercolor, bleed 1 · "crisp" → respect 1, boil 0.2
29
+ - "vintage / old film" → saturation 0.8, warmth 0.5, grain 1, vignette 0.5
30
+ - "comic / graphic novel" → ink, posterize 6 · "screen print / retro poster" → gouache, posterize 5
31
+ - "night is too dark" → contrast 0.95 and lighten the palette, not the look
32
+ - "for kids" → pastel or gouache, saturation 1.1, warm palette, playful music
@@ -0,0 +1,9 @@
1
+ # Writing the lines
2
+
3
+ - One idea per line, 4 to 12 words. Read aloud it should take 1.5 to 4 seconds.
4
+ - Open on a hook that makes the second line necessary ("You have heard that it was said,").
5
+ - A turn two-thirds in ("But…", "And yet…", "Nobody ever…").
6
+ - Land the last line; give it `hold: 0.8` and a final wide or calm shot.
7
+ - Scripture, quotes and poems: use a public-domain translation (KJV, WEB) or their own words; keep it exact.
8
+ - Never invent a quote and attribute it to a real person.
9
+ - 20 to 45 seconds is the sweet spot for Shorts, Reels and TikTok; 60 s at most.
@@ -0,0 +1,290 @@
1
+ // The music: presets, chord voicing and the arrangement. Pure given the two rngs; the only effect is
2
+ // writing into the buses it allocates itself.
3
+ import { SR, biquad, busAdd, busMixFrom, delay, human, makeBus, reverb } from "./dsp.ts";
4
+ import type { Bus, Rng, Voice } from "./dsp.ts";
5
+ import {
6
+ bass, brush, ep, epChord, hat, kick, outroImpact, outroRiser, pad, pluck, shaker, snare, softSynth, strum, vinyl,
7
+ } from "./instruments.ts";
8
+
9
+ // key = MIDI root; prog = chords as scale degrees in the key, one per bar; ext = chord size (4 = sevenths,
10
+ // 5 = ninths). Chords are voiced in the middle register with the root left to the bass.
11
+ export type Preset = {
12
+ readonly bpm: number; readonly root: number; readonly scale: readonly number[]; readonly prog: readonly number[];
13
+ readonly ext: number; readonly parts: readonly string[]; readonly swing: number; readonly verb: number;
14
+ readonly duck: number; readonly low?: number; readonly drums?: number;
15
+ };
16
+ const MAJ: readonly number[] = [0, 2, 4, 5, 7, 9, 11], MIN: readonly number[] = [0, 2, 3, 5, 7, 8, 10];
17
+
18
+ export const PRESETS: Readonly<Record<string, Preset>> = {
19
+ kinetic: { bpm: 118, root: 57, scale: MIN, prog: [0, 5, 2, 6], ext: 5, parts: ["kick4", "snare", "hatOff", "bassHouse", "epStab", "pad"], swing: 0, verb: 0.2, duck: 0.6 },
20
+ keynote: { bpm: 100, root: 60, scale: MAJ, prog: [0, 5, 3, 4], ext: 4, parts: ["kickHalf", "shaker16", "epPulse", "epHold", "pad", "bassLong"], swing: 0, verb: 0.3, duck: 0.85 },
21
+ aura: { bpm: 78, root: 63, scale: MAJ, prog: [0, 3, 5, 4], ext: 5, parts: ["pad", "epArp", "bassSwell"], swing: 0, verb: 0.45, duck: 1 },
22
+ paper: { bpm: 104, root: 65, scale: MAJ, prog: [0, 4, 5, 3], ext: 4, parts: ["kickSoft", "brush", "hat8", "pluck8", "bassBounce"], swing: 0.14, verb: 0.22, duck: 0.85 },
23
+ terminal: { bpm: 112, root: 62, scale: MIN, prog: [0, 5, 6, 0], ext: 4, parts: ["kick4", "hat16", "bass8", "synthArp", "pad"], swing: 0, verb: 0.2, duck: 0.65, low: 0.7, drums: 0.85 },
24
+ data: { bpm: 86, root: 67, scale: MAJ, prog: [1, 4, 0, 5], ext: 5, parts: ["kickBoom", "snareBoom", "hat8", "epHold", "bassBoom", "vinyl"], swing: 0.18, verb: 0.25, duck: 0.85 },
25
+ story: { bpm: 96, root: 62, scale: MAJ, prog: [0, 4, 5, 3], ext: 4, parts: ["kickSoft", "brush", "shaker8", "guitar8", "epHold", "bassLong"], swing: 0.08, verb: 0.3, duck: 0.9 },
26
+ // Shorts Animator's moods: beds for a voice to sit on, no drums in the quiet ones
27
+ tender: { bpm: 70, root: 60, scale: MAJ, prog: [0, 5, 3, 4], ext: 5, parts: ["pad", "epArp", "bassSwell"], swing: 0, verb: 0.5, duck: 1 },
28
+ hopeful: { bpm: 84, root: 62, scale: MAJ, prog: [0, 4, 5, 3], ext: 4, parts: ["pad", "guitar8", "bassLong"], swing: 0.06, verb: 0.38, duck: 1 },
29
+ night: { bpm: 64, root: 57, scale: MIN, prog: [0, 5, 3, 6], ext: 5, parts: ["pad", "epHold", "bassSwell"], swing: 0, verb: 0.55, duck: 1 },
30
+ wonder: { bpm: 76, root: 65, scale: MAJ, prog: [3, 4, 0, 5], ext: 5, parts: ["pad", "epArp", "bassSwell"], swing: 0, verb: 0.5, duck: 1 },
31
+ playful: { bpm: 100, root: 65, scale: MAJ, prog: [0, 4, 5, 3], ext: 4, parts: ["kickSoft", "brush", "pluck8", "bassBounce"], swing: 0.14, verb: 0.22, duck: 0.9 },
32
+ };
33
+
34
+ // ---------- harmony ----------
35
+ const chord = (root: number, scale: readonly number[], deg: number, size = 4): number[] =>
36
+ Array.from({ length: size }, (_, k) => { const s = deg + k * 2; return root + scale[s % 7] + 12 * Math.floor(s / 7); });
37
+ const bassNote = (root: number, scale: readonly number[], deg: number): number => {
38
+ let m = root + scale[deg % 7];
39
+ while (m > 45) m -= 12;
40
+ while (m < 33) m += 12;
41
+ return m;
42
+ };
43
+ function nextWithPc(from: number, pc: number): number {
44
+ let m = from + 1;
45
+ while (m % 12 !== pc) m++;
46
+ return m;
47
+ }
48
+ // rootless close voicing that tops out by B4, as near as it can to the last chord's lowest note
49
+ function voicing(root: number, scale: readonly number[], deg: number, size: number, prev: readonly number[] | null): number[] {
50
+ const tones = chord(root, scale, deg, size).map((m) => ((m % 12) + 12) % 12);
51
+ const pcs = size >= 4 ? tones.slice(1) : tones;
52
+ const want = prev ? (prev[0] + 56) / 2 : 56; // led by the last chord, pulled back toward the middle
53
+ const cands = Array.from({ length: 14 }, (_, i) => 51 + i).filter((lo) => lo % 12 === pcs[0]).map((lo) => {
54
+ const v = pcs.slice(1).reduce((acc, pc) => [...acc, nextWithPc(acc[acc.length - 1], pc)], [lo]);
55
+ return { v, score: Math.abs(lo - want) + (v[v.length - 1] > 71 ? 100 : 0) };
56
+ });
57
+ return cands.reduce((best, c) => (c.score < best.score ? c : best)).v;
58
+ }
59
+
60
+ // ---------- the arrangement ----------
61
+ export type Timing = { readonly duration: number; readonly outroAt?: number };
62
+ type Ctx = {
63
+ readonly P: Preset; readonly r: Rng; readonly ham: Rng;
64
+ readonly spb: number; readonly bar: number; readonly s16: number; readonly end: number;
65
+ readonly drums: Bus; readonly inst: Bus; readonly low: Bus; readonly send: Bus;
66
+ readonly voices: readonly (readonly number[])[]; readonly roots: readonly number[];
67
+ };
68
+ type Bar = { readonly b: number; readonly t0: number; readonly ch: readonly number[]; readonly rt: number; readonly len: number };
69
+
70
+ const hasPart = (c: Ctx, p: string): boolean => c.P.parts.includes(p);
71
+ const intro = (c: Ctx, bar: Bar): boolean => bar.b === 0 && c.P.bpm < 90; // slow beds open on the harmony alone
72
+ const before = (c: Ctx, t: number): boolean => t < c.end - 0.02;
73
+ // every call draws one rng value for the humanising jitter, so the call order is part of the sound
74
+ function at(c: Ctx, bar: Bar, q: number): number {
75
+ const swing = q % 2 ? c.P.swing * c.s16 * 2 : 0;
76
+ return bar.t0 + q * c.s16 + swing + (c.r() - 0.5) * 0.01;
77
+ }
78
+ const steps = (step: number, count = 16 / step): number[] => Array.from({ length: count }, (_, i) => i * step);
79
+
80
+ // stops at the first step that falls past the end
81
+ function walk(c: Ctx, bar: Bar, qs: readonly number[], fn: (q: number, t: number) => void): void {
82
+ for (const q of qs) {
83
+ const t = Math.max(0, at(c, bar, q));
84
+ if (!before(c, t)) break;
85
+ fn(q, t);
86
+ }
87
+ }
88
+ // skips the steps that fall past the end, keeps going
89
+ function visit(c: Ctx, bar: Bar, qs: readonly number[], clampT: boolean, fn: (q: number, t: number) => void): void {
90
+ for (const q of qs) {
91
+ const raw = at(c, bar, q), t = clampT ? Math.max(0, raw) : raw;
92
+ if (before(c, t)) fn(q, t);
93
+ }
94
+ }
95
+ const instSend = (c: Ctx, v: Voice, t: number, ig: number, pan: number, sg: number): void => {
96
+ busAdd(c.inst, v, t, ig, pan);
97
+ busAdd(c.send, v, t, sg);
98
+ };
99
+
100
+ // ---------- drums: each step returns the kick times it played (the sidechain listens to them) ----------
101
+ type DrumStep = (c: Ctx, bar: Bar, q: number, t: number) => number[];
102
+ const kickAt = (c: Ctx, t: number, v: Float32Array, gain: number): number[] => { busAdd(c.drums, v, t, gain); return [t]; };
103
+ const snareAt = (c: Ctx, t: number, s: Float32Array, dg: number, sg: number): number[] => { busAdd(c.drums, s, t, dg); busAdd(c.send, s, t, sg); return []; };
104
+ const tick = (c: Ctx, v: Float32Array, t: number, gain: number, pan: number): number[] => { busAdd(c.drums, v, t, gain, pan); return []; };
105
+
106
+ // the order is the order the rng is drawn in
107
+ const DRUM_STEPS: readonly (readonly [string, DrumStep])[] = [
108
+ ["kick4", (c, _b, q, t) => (q % 4 === 0 ? kickAt(c, t, kick(c.r), 0.75 * human(c.r, 0.08)) : [])],
109
+ ["kickHalf", (c, _b, q, t) => (q === 0 || q === 8 ? kickAt(c, t, kick(c.r, { punch: 0.6, dec: 0.32 }), 0.45) : [])],
110
+ ["kickSoft", (c, bar, q, t) => (q === 0 || q === 8 || (q === 6 && bar.b % 2 === 1) ? kickAt(c, t, kick(c.r, { punch: 0.7, dec: 0.3 }), 0.55) : [])],
111
+ ["kickBoom", (c, _b, q, t) => (q === 0 || q === 7 || q === 10 ? kickAt(c, t, kick(c.r, { pitch: 48, dec: 0.4 }), q === 7 ? 0.4 : 0.65) : [])],
112
+ ["snare", (c, _b, q, t) => (q === 4 || q === 12 ? snareAt(c, t, snare(c.r, { tone: 200, top: 8000 }), 0.42, 0.18) : [])],
113
+ ["snareBoom", (c, _b, q, t) => (q === 4 || q === 12 ? snareAt(c, t, snare(c.r, { dec: 0.16, top: 6000 }), 0.5, 0.12) : [])],
114
+ ["brush", (c, _b, q, t) => {
115
+ if (q !== 4 && q !== 12) return [];
116
+ const s = brush(c.r);
117
+ busAdd(c.drums, s, t, 0.5, 0.1); busAdd(c.send, s, t, 0.2);
118
+ return [];
119
+ }],
120
+ ["hatOff", (c, _b, q, t) => (q % 4 === 2 ? tick(c, hat(c.r), t, 0.3 * human(c.r, 0.3), 0.25) : [])],
121
+ ["hat8", (c, _b, q, t) => (q % 2 === 0 ? tick(c, hat(c.r), t, (q % 4 === 2 ? 0.2 : 0.11) * human(c.r, 0.4), 0.3) : [])],
122
+ ["shaker16", (c, _b, q, t) => tick(c, shaker(c.r), t, (q % 4 === 2 ? 0.2 : q % 2 ? 0.07 : 0.12) * human(c.r, 0.4), 0.35)],
123
+ ["shaker8", (c, _b, q, t) => (q % 2 === 0 ? tick(c, shaker(c.r), t, (q % 4 === 2 ? 0.2 : 0.1) * human(c.r, 0.4), 0.35) : [])],
124
+ ["hat16", (c, _b, q, t) => tick(c, hat(c.r), t, (q % 4 === 2 ? 0.16 : 0.08) * human(c.r, 0.4), -0.2)],
125
+ ];
126
+
127
+ // recursion (not a loop) so the kick times come back as a value; stops at the first step past the end
128
+ function drumWalk(c: Ctx, bar: Bar, q: number): number[] {
129
+ if (q >= 16) return [];
130
+ const t = Math.max(0, at(c, bar, q));
131
+ if (!before(c, t)) return [];
132
+ const here = DRUM_STEPS.flatMap(([name, fn]) => (hasPart(c, name) ? fn(c, bar, q, t) : []));
133
+ return [...here, ...drumWalk(c, bar, q + 1)];
134
+ }
135
+
136
+ // ---------- harmony and bass parts ----------
137
+ type Part = (c: Ctx, bar: Bar) => void;
138
+
139
+ const padPart: Part = (c, bar) => {
140
+ const p = pad(bar.ch, bar.len + 1.2, c.r, { cutoff: c.P.bpm < 90 ? 2800 : 2400, attack: intro(c, bar) ? 1.4 : 0.5 });
141
+ instSend(c, p, bar.t0, hasPart(c, "epArp") ? 0.5 : 0.28, 0, 0.4);
142
+ };
143
+ const epHoldPart: Part = (c, bar) => visit(c, bar, bar.b % 2 ? [0, 10] : [0], false, (q, t) => {
144
+ const v = epChord(bar.ch, q ? c.spb * 1.5 : bar.len + 0.3, 0.42, c.r, c.ham);
145
+ instSend(c, v, Math.max(0, t), q ? 0.3 : 0.45, 0, 0.3);
146
+ });
147
+ const epStabPart: Part = (c, bar) => {
148
+ if (intro(c, bar)) return;
149
+ visit(c, bar, [3, 6, 10, 14], false, (q, t) => instSend(c, epChord(bar.ch, c.spb * 0.55, 0.5, c.r, c.ham, { roll: 0.004 }), t, 0.5, q % 2 ? 0.15 : -0.15, 0.25));
150
+ };
151
+ const epPulsePart: Part = (c, bar) => walk(c, bar, steps(2), (q, t) => {
152
+ const m = q % 8 === 0 ? bar.ch[0] : q % 4 === 0 ? bar.ch[2] ?? bar.ch[1] : bar.ch[1];
153
+ const v = ep(m - 12 >= 48 ? m - 12 : m, c.spb * 0.9, 0.38 * human(c.r, 0.15), c.ham);
154
+ instSend(c, v, t, 0.38, q % 4 ? 0.2 : -0.2, 0.25);
155
+ });
156
+ const epArpPart: Part = (c, bar) => walk(c, bar, steps(4), (q, t) => {
157
+ const v = ep(bar.ch[(q / 4) % bar.ch.length], c.spb * 3, 0.3 * human(c.r, 0.2), c.ham);
158
+ instSend(c, v, t, 0.42, (q / 4 - 1.5) * 0.3, 0.5);
159
+ });
160
+ const PLUCK8_PAT = [0, 2, 1, 3, 2, 1, 0, 2];
161
+ const pluck8Part: Part = (c, bar) => {
162
+ if (intro(c, bar)) return;
163
+ // steps 3 and 7 are rests: they draw no jitter and cannot end the walk
164
+ walk(c, bar, [0, 1, 2, 4, 5, 6].map((k) => k * 2), (q, t) => {
165
+ const k = q / 2, s = pluck(bar.ch[PLUCK8_PAT[k] % bar.ch.length], 0.8, c.r, { bright: 0.28, decay: 0.993 });
166
+ instSend(c, s, t, human(c.r, 0.2), (k % 3 - 1) * 0.3, 0.3);
167
+ });
168
+ };
169
+ const GUITAR8_PAT = [0, 1, 2, 3, 2, 1, 2, 3];
170
+ const guitar8Part: Part = (c, bar) => walk(c, bar, steps(2), (q, t) => {
171
+ const k = q / 2, ch = bar.ch;
172
+ const m = k === 0 ? (ch[0] - 12 >= 45 ? ch[0] - 12 : ch[0]) : ch[GUITAR8_PAT[k] % ch.length];
173
+ const s = pluck(m, 1.2, c.r, { bright: 0.22, decay: 0.996 });
174
+ instSend(c, s, t, 0.9 * human(c.r, 0.2), k % 2 ? 0.25 : -0.25, 0.3);
175
+ });
176
+ const SYNTH_ARP_PAT = [0, 1, 2, 1, 3, 2];
177
+ const synthArpPart: Part = (c, bar) => {
178
+ if (intro(c, bar)) return;
179
+ walk(c, bar, steps(1), (q, t) => {
180
+ const m = bar.ch[SYNTH_ARP_PAT[q % 6] % bar.ch.length] - 12;
181
+ const s = softSynth(m < 45 ? m + 12 : m, c.s16 * 1.6, 1900 + 700 * Math.sin((bar.b * 16 + q) / 10));
182
+ instSend(c, s, t, 0.22, q % 2 ? 0.25 : -0.25, 0.22);
183
+ });
184
+ };
185
+
186
+ const lowAt = (c: Ctx, v: Float32Array, t: number, gain: number): void => busAdd(c.low, v, t, gain);
187
+ const bassHousePart: Part = (c, bar) => {
188
+ if (intro(c, bar)) return;
189
+ visit(c, bar, [2, 6, 10, 14], false, (q, t) => lowAt(c, bass(q === 14 ? bar.rt + 7 : bar.rt, c.spb * 0.45), t, 0.55));
190
+ };
191
+ const bassLongPart: Part = (c, bar) => visit(c, bar, [0, 8], true, (_q, t) => lowAt(c, bass(bar.rt, c.spb * 1.9, { a: 0.02, r: 0.2 }), t, 0.45));
192
+ const bassSwellPart: Part = (c, bar) => lowAt(c, bass(bar.rt, bar.len + 0.4, { a: 0.6, r: 0.6, drive: 1 }), bar.t0, 0.4);
193
+ const BOUNCE: readonly (readonly [number, number])[] = [[0, 0], [6, 7], [8, 0], [14, 7]];
194
+ const bassBouncePart: Part = (c, bar) => {
195
+ if (intro(c, bar)) return;
196
+ BOUNCE.forEach(([q, d]) => visit(c, bar, [q], true, (_q, t) => lowAt(c, bass(bar.rt + d, c.spb * 0.45, { tone: 0.3 }), t, 0.5)));
197
+ };
198
+ const bass8Part: Part = (c, bar) => {
199
+ if (intro(c, bar)) return;
200
+ visit(c, bar, steps(2), true, (_q, t) => lowAt(c, bass(bar.rt, c.spb * 0.4), t, 0.34));
201
+ };
202
+ const bassBoomPart: Part = (c, bar) => visit(c, bar, [0, 10], true, (q, t) => lowAt(c, bass(bar.rt, c.spb * (q ? 1.2 : 1.5), { a: 0.01, r: 0.15 }), t, 0.55));
203
+
204
+ // the order is the order the rng is drawn in: harmony first, then bass
205
+ const PARTS: readonly (readonly [string, Part])[] = [
206
+ ["pad", padPart], ["epHold", epHoldPart], ["epStab", epStabPart], ["epPulse", epPulsePart], ["epArp", epArpPart],
207
+ ["pluck8", pluck8Part], ["guitar8", guitar8Part], ["synthArp", synthArpPart],
208
+ ["bassHouse", bassHousePart], ["bassLong", bassLongPart], ["bassSwell", bassSwellPart],
209
+ ["bassBounce", bassBouncePart], ["bass8", bass8Part], ["bassBoom", bassBoomPart],
210
+ ];
211
+
212
+ function renderBar(c: Ctx, bar: Bar): number[] {
213
+ const kicks = intro(c, bar) ? [] : drumWalk(c, bar, 0);
214
+ PARTS.forEach(([name, part]) => { if (hasPart(c, name)) part(c, bar); });
215
+ return kicks;
216
+ }
217
+
218
+ // the end: a low swell, one deep hit, the home chord left ringing
219
+ function ending(c: Ctx, duration: number): void {
220
+ const { P, r, ham, spb, end } = c;
221
+ const home = voicing(P.root, P.scale, 0, P.ext || 4, c.voices[c.voices.length - 1]);
222
+ const riserSec = Math.min(1.6, spb * 3);
223
+ busAdd(c.inst, outroRiser(r, riserSec), end - riserSec, 0.14);
224
+ const hit = outroImpact(r, P.root);
225
+ busAdd(c.inst, hit, end, 0.3); busAdd(c.send, hit, end, 0.2);
226
+ const tail = Math.max(2.4, duration - end + 1.8);
227
+ if (hasPart(c, "guitar8") || hasPart(c, "pluck8")) {
228
+ const s = strum([bassNote(P.root, P.scale, 0) + 12, ...home], tail, r, { bright: 0.24, decay: 0.998 });
229
+ busAdd(c.inst, s, end, 1.1); busAdd(c.send, s, end, 0.4);
230
+ } else {
231
+ const ch = epChord(home, tail, 0.45, r, ham, { roll: 0.03 });
232
+ busAdd(c.inst, ch, end, 0.55); busAdd(c.send, ch, end, 0.5);
233
+ }
234
+ const p = pad(home, tail, r, { cutoff: 2200, attack: 0.05, release: 1.8 });
235
+ busAdd(c.inst, p, end, 0.22); busAdd(c.send, p, end, 0.4);
236
+ busAdd(c.low, bass(bassNote(P.root, P.scale, 0), Math.max(1.5, duration - end + 0.6), { a: 0.005, r: 1.2 }), end, 0.45);
237
+ if (P.parts.some((x) => x.startsWith("kick"))) busAdd(c.drums, kick(r, { dec: 0.5 }), end, 0.7);
238
+ }
239
+
240
+ // ---------- mix ----------
241
+ function duckAround(duck: Float32Array, t: number, depth: number): void {
242
+ const i0 = Math.round(t * SR);
243
+ for (let i = 0; i < 0.22 * SR && i0 + i < duck.length; i++) duck[i0 + i] = Math.min(duck[i0 + i], 1 - depth + depth * Math.pow(i / (0.22 * SR), 0.6));
244
+ }
245
+ // sidechain: the harmony breathes under each kick (deep for the dance beds, barely for the gentle ones)
246
+ function sidechain(c: Ctx, kicks: readonly number[]): void {
247
+ const depth = 1 - c.P.duck;
248
+ if (depth <= 0.01) return;
249
+ const { inst, low } = c, duck = new Float32Array(inst.n).fill(1);
250
+ kicks.forEach((t) => duckAround(duck, t, depth));
251
+ for (let i = 0; i < inst.n; i++) { inst.L[i] *= duck[i]; inst.R[i] *= duck[i]; low.L[i] *= duck[i] * 0.7 + 0.3; low.R[i] *= duck[i] * 0.7 + 0.3; }
252
+ }
253
+ // a dark room: the reverb only hears the middle of the spectrum
254
+ function darken(send: Bus): void {
255
+ const sl = [biquad("lp", 6500), biquad("lp", 6500)], sh = [biquad("hp", 220), biquad("hp", 220)];
256
+ for (let i = 0; i < send.n; i++) { send.L[i] = sl[0](sh[0](send.L[i])); send.R[i] = sl[1](sh[1](send.R[i])); }
257
+ }
258
+ function mixDown(c: Ctx, total: number, duration: number): Bus {
259
+ const { P, send, spb } = c;
260
+ darken(send);
261
+ const wet = reverb(send, { room: P.verb > 0.4 ? 0.88 : 0.82, damp: 0.45 });
262
+ const echo = P.bpm >= 100 ? delay(send, spb * 0.75, 0.28, 2400) : null;
263
+ const music = makeBus(total);
264
+ busMixFrom(music, c.drums, P.drums ?? 1); busMixFrom(music, c.inst, 1); busMixFrom(music, c.low, P.low ?? 1); busMixFrom(music, wet, P.verb * 2.2);
265
+ if (echo) busMixFrom(music, echo, 0.1);
266
+ // fade in over half a beat, out after the ring
267
+ const fin = Math.round(spb * SR * 0.5), fout0 = Math.round((duration + 0.2) * SR), fout1 = Math.round((duration + 2.6) * SR);
268
+ for (let i = 0; i < music.n; i++) {
269
+ const g = (i < fin ? i / fin : 1) * (i > fout0 ? Math.max(0, 1 - (i - fout0) / (fout1 - fout0)) : 1);
270
+ music.L[i] *= g; music.R[i] *= g;
271
+ }
272
+ return music;
273
+ }
274
+
275
+ export function composeMusic(P: Preset, timing: Timing, r: Rng, ham: Rng): Bus {
276
+ const { duration } = timing;
277
+ const spb = 60 / P.bpm, bar = spb * 4, total = duration + 3; // ring-out
278
+ const end = timing.outroAt ?? Math.max(0, duration - 3);
279
+ const nBars = Math.ceil(duration / bar) + 1;
280
+ const deg = (b: number): number => P.prog[b % P.prog.length];
281
+ const voices = Array.from({ length: nBars }).reduce<number[][]>((acc, _, b) => [...acc, voicing(P.root, P.scale, deg(b), P.ext || 4, acc[b - 1] ?? null)], []);
282
+ const roots = Array.from({ length: nBars }, (_, b) => bassNote(P.root, P.scale, deg(b)));
283
+ const c: Ctx = { P, r, ham, spb, bar, s16: spb / 4, end, drums: makeBus(total), inst: makeBus(total), low: makeBus(total), send: makeBus(total), voices, roots };
284
+ const bars = Array.from({ length: nBars }, (_, b) => b).filter((b) => b * bar < end - 0.01);
285
+ const kicks = bars.flatMap((b) => renderBar(c, { b, t0: b * bar, ch: voices[b], rt: roots[b], len: Math.min(bar, end - b * bar) }));
286
+ if (hasPart(c, "vinyl")) busAdd(c.inst, vinyl(duration + 1, r), 0, 0.5);
287
+ ending(c, duration);
288
+ sidechain(c, kicks);
289
+ return mixDown(c, total, duration);
290
+ }