@olegkoval/agent-skills 1.60.0 → 1.62.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/copilot-instructions.md +2 -0
- package/.github/prompts/carousel-maker.prompt.md +102 -0
- package/.github/prompts/shorts-animator.prompt.md +103 -0
- package/.kiro/steering/carousel-maker.md +101 -0
- package/.kiro/steering/shorts-animator.md +102 -0
- package/.windsurf/rules/carousel-maker.md +100 -0
- package/.windsurf/rules/shorts-animator.md +101 -0
- package/README.md +4 -2
- package/adapters/claude/olko-creative/skills/carousel-maker/SKILL.md +112 -0
- package/adapters/claude/olko-creative/skills/carousel-maker/scripts/cm.ts +220 -0
- package/adapters/claude/olko-creative/skills/carousel-maker/scripts/domain/check.ts +68 -0
- package/adapters/claude/olko-creative/skills/carousel-maker/scripts/domain/layout.ts +75 -0
- package/adapters/claude/olko-creative/skills/carousel-maker/scripts/domain/schema.ts +54 -0
- package/adapters/claude/olko-creative/skills/carousel-maker/scripts/domain/types.ts +33 -0
- package/adapters/claude/olko-creative/skills/carousel-maker/scripts/edge/io.ts +87 -0
- package/adapters/claude/olko-creative/skills/carousel-maker/scripts/package.json +4 -0
- package/adapters/claude/olko-creative/skills/carousel-maker/scripts/test/domain.test.ts +99 -0
- package/adapters/claude/olko-creative/skills/carousel-maker/scripts/tsconfig.json +16 -0
- package/adapters/claude/olko-creative/skills/shorts-animator/SKILL.md +113 -0
- package/adapters/claude/olko-creative/skills/shorts-animator/references/kit.md +59 -0
- package/adapters/claude/olko-creative/skills/shorts-animator/references/looks.md +32 -0
- package/adapters/claude/olko-creative/skills/shorts-animator/references/writing.md +9 -0
- package/adapters/claude/olko-creative/skills/shorts-animator/scripts/audio/compose.ts +290 -0
- package/adapters/claude/olko-creative/skills/shorts-animator/scripts/audio/dsp.ts +193 -0
- package/adapters/claude/olko-creative/skills/shorts-animator/scripts/audio/instruments.ts +185 -0
- package/adapters/claude/olko-creative/skills/shorts-animator/scripts/audio/master.ts +55 -0
- package/adapters/claude/olko-creative/skills/shorts-animator/scripts/audio/soundtrack.ts +119 -0
- package/adapters/claude/olko-creative/skills/shorts-animator/scripts/domain/captions.ts +35 -0
- package/adapters/claude/olko-creative/skills/shorts-animator/scripts/domain/check.ts +96 -0
- package/adapters/claude/olko-creative/skills/shorts-animator/scripts/domain/look.ts +26 -0
- package/adapters/claude/olko-creative/skills/shorts-animator/scripts/domain/patch.ts +38 -0
- package/adapters/claude/olko-creative/skills/shorts-animator/scripts/domain/schema.ts +81 -0
- package/adapters/claude/olko-creative/skills/shorts-animator/scripts/domain/timeline.ts +75 -0
- package/adapters/claude/olko-creative/skills/shorts-animator/scripts/domain/types.ts +91 -0
- package/adapters/claude/olko-creative/skills/shorts-animator/scripts/edge/chrome.ts +169 -0
- package/adapters/claude/olko-creative/skills/shorts-animator/scripts/edge/doctor.ts +52 -0
- package/adapters/claude/olko-creative/skills/shorts-animator/scripts/edge/media.ts +105 -0
- package/adapters/claude/olko-creative/skills/shorts-animator/scripts/edge/page.ts +53 -0
- package/adapters/claude/olko-creative/skills/shorts-animator/scripts/edge/probe.ts +75 -0
- package/adapters/claude/olko-creative/skills/shorts-animator/scripts/edge/sound.ts +46 -0
- package/adapters/claude/olko-creative/skills/shorts-animator/scripts/edge/voice.ts +183 -0
- package/adapters/claude/olko-creative/skills/shorts-animator/scripts/looks/gouache.json +7 -0
- package/adapters/claude/olko-creative/skills/shorts-animator/scripts/looks/ink.json +6 -0
- package/adapters/claude/olko-creative/skills/shorts-animator/scripts/looks/oil.json +7 -0
- package/adapters/claude/olko-creative/skills/shorts-animator/scripts/looks/pastel.json +7 -0
- package/adapters/claude/olko-creative/skills/shorts-animator/scripts/looks/watercolor.json +6 -0
- package/adapters/claude/olko-creative/skills/shorts-animator/scripts/package.json +4 -0
- package/adapters/claude/olko-creative/skills/shorts-animator/scripts/runtime/globals.d.ts +199 -0
- package/adapters/claude/olko-creative/skills/shorts-animator/scripts/runtime/kit.ts +856 -0
- package/adapters/claude/olko-creative/skills/shorts-animator/scripts/runtime/paint.ts +268 -0
- package/adapters/claude/olko-creative/skills/shorts-animator/scripts/runtime/stage.ts +181 -0
- package/adapters/claude/olko-creative/skills/shorts-animator/scripts/runtime/tsconfig.json +12 -0
- package/adapters/claude/olko-creative/skills/shorts-animator/scripts/sa.ts +262 -0
- package/adapters/claude/olko-creative/skills/shorts-animator/scripts/test/domain.test.ts +107 -0
- package/adapters/claude/olko-creative/skills/shorts-animator/scripts/tsconfig.json +16 -0
- package/adapters/claude/olko-creative/skills/shorts-animator/scripts/tts.py +20 -0
- package/adapters/claude/olko-github-pr/skills/lekker-review/SKILL.md +30 -0
- package/adapters/claude/olko-github-pr/skills/lekker-review/references/context-gathering.md +3 -0
- package/adapters/claude/olko-github-pr/skills/lekker-review/scripts/finding-state.mjs +207 -0
- package/adapters/codex/olko-creative/README.md +2 -0
- package/adapters/cursor/olko-creative/skills/carousel-maker/SKILL.md +112 -0
- package/adapters/cursor/olko-creative/skills/carousel-maker/scripts/cm.ts +220 -0
- package/adapters/cursor/olko-creative/skills/carousel-maker/scripts/domain/check.ts +68 -0
- package/adapters/cursor/olko-creative/skills/carousel-maker/scripts/domain/layout.ts +75 -0
- package/adapters/cursor/olko-creative/skills/carousel-maker/scripts/domain/schema.ts +54 -0
- package/adapters/cursor/olko-creative/skills/carousel-maker/scripts/domain/types.ts +33 -0
- package/adapters/cursor/olko-creative/skills/carousel-maker/scripts/edge/io.ts +87 -0
- package/adapters/cursor/olko-creative/skills/carousel-maker/scripts/package.json +4 -0
- package/adapters/cursor/olko-creative/skills/carousel-maker/scripts/test/domain.test.ts +99 -0
- package/adapters/cursor/olko-creative/skills/carousel-maker/scripts/tsconfig.json +16 -0
- package/adapters/cursor/olko-creative/skills/shorts-animator/SKILL.md +113 -0
- package/adapters/cursor/olko-creative/skills/shorts-animator/references/kit.md +59 -0
- package/adapters/cursor/olko-creative/skills/shorts-animator/references/looks.md +32 -0
- package/adapters/cursor/olko-creative/skills/shorts-animator/references/writing.md +9 -0
- package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/audio/compose.ts +290 -0
- package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/audio/dsp.ts +193 -0
- package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/audio/instruments.ts +185 -0
- package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/audio/master.ts +55 -0
- package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/audio/soundtrack.ts +119 -0
- package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/domain/captions.ts +35 -0
- package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/domain/check.ts +96 -0
- package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/domain/look.ts +26 -0
- package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/domain/patch.ts +38 -0
- package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/domain/schema.ts +81 -0
- package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/domain/timeline.ts +75 -0
- package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/domain/types.ts +91 -0
- package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/edge/chrome.ts +169 -0
- package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/edge/doctor.ts +52 -0
- package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/edge/media.ts +105 -0
- package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/edge/page.ts +53 -0
- package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/edge/probe.ts +75 -0
- package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/edge/sound.ts +46 -0
- package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/edge/voice.ts +183 -0
- package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/looks/gouache.json +7 -0
- package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/looks/ink.json +6 -0
- package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/looks/oil.json +7 -0
- package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/looks/pastel.json +7 -0
- package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/looks/watercolor.json +6 -0
- package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/package.json +4 -0
- package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/runtime/globals.d.ts +199 -0
- package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/runtime/kit.ts +856 -0
- package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/runtime/paint.ts +268 -0
- package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/runtime/stage.ts +181 -0
- package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/runtime/tsconfig.json +12 -0
- package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/sa.ts +262 -0
- package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/test/domain.test.ts +107 -0
- package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/tsconfig.json +16 -0
- package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/tts.py +20 -0
- package/adapters/grok/olko-creative/skills/carousel-maker/SKILL.md +112 -0
- package/adapters/grok/olko-creative/skills/carousel-maker/scripts/cm.ts +220 -0
- package/adapters/grok/olko-creative/skills/carousel-maker/scripts/domain/check.ts +68 -0
- package/adapters/grok/olko-creative/skills/carousel-maker/scripts/domain/layout.ts +75 -0
- package/adapters/grok/olko-creative/skills/carousel-maker/scripts/domain/schema.ts +54 -0
- package/adapters/grok/olko-creative/skills/carousel-maker/scripts/domain/types.ts +33 -0
- package/adapters/grok/olko-creative/skills/carousel-maker/scripts/edge/io.ts +87 -0
- package/adapters/grok/olko-creative/skills/carousel-maker/scripts/package.json +4 -0
- package/adapters/grok/olko-creative/skills/carousel-maker/scripts/test/domain.test.ts +99 -0
- package/adapters/grok/olko-creative/skills/carousel-maker/scripts/tsconfig.json +16 -0
- package/adapters/grok/olko-creative/skills/shorts-animator/SKILL.md +113 -0
- package/adapters/grok/olko-creative/skills/shorts-animator/references/kit.md +59 -0
- package/adapters/grok/olko-creative/skills/shorts-animator/references/looks.md +32 -0
- package/adapters/grok/olko-creative/skills/shorts-animator/references/writing.md +9 -0
- package/adapters/grok/olko-creative/skills/shorts-animator/scripts/audio/compose.ts +290 -0
- package/adapters/grok/olko-creative/skills/shorts-animator/scripts/audio/dsp.ts +193 -0
- package/adapters/grok/olko-creative/skills/shorts-animator/scripts/audio/instruments.ts +185 -0
- package/adapters/grok/olko-creative/skills/shorts-animator/scripts/audio/master.ts +55 -0
- package/adapters/grok/olko-creative/skills/shorts-animator/scripts/audio/soundtrack.ts +119 -0
- package/adapters/grok/olko-creative/skills/shorts-animator/scripts/domain/captions.ts +35 -0
- package/adapters/grok/olko-creative/skills/shorts-animator/scripts/domain/check.ts +96 -0
- package/adapters/grok/olko-creative/skills/shorts-animator/scripts/domain/look.ts +26 -0
- package/adapters/grok/olko-creative/skills/shorts-animator/scripts/domain/patch.ts +38 -0
- package/adapters/grok/olko-creative/skills/shorts-animator/scripts/domain/schema.ts +81 -0
- package/adapters/grok/olko-creative/skills/shorts-animator/scripts/domain/timeline.ts +75 -0
- package/adapters/grok/olko-creative/skills/shorts-animator/scripts/domain/types.ts +91 -0
- package/adapters/grok/olko-creative/skills/shorts-animator/scripts/edge/chrome.ts +169 -0
- package/adapters/grok/olko-creative/skills/shorts-animator/scripts/edge/doctor.ts +52 -0
- package/adapters/grok/olko-creative/skills/shorts-animator/scripts/edge/media.ts +105 -0
- package/adapters/grok/olko-creative/skills/shorts-animator/scripts/edge/page.ts +53 -0
- package/adapters/grok/olko-creative/skills/shorts-animator/scripts/edge/probe.ts +75 -0
- package/adapters/grok/olko-creative/skills/shorts-animator/scripts/edge/sound.ts +46 -0
- package/adapters/grok/olko-creative/skills/shorts-animator/scripts/edge/voice.ts +183 -0
- package/adapters/grok/olko-creative/skills/shorts-animator/scripts/looks/gouache.json +7 -0
- package/adapters/grok/olko-creative/skills/shorts-animator/scripts/looks/ink.json +6 -0
- package/adapters/grok/olko-creative/skills/shorts-animator/scripts/looks/oil.json +7 -0
- package/adapters/grok/olko-creative/skills/shorts-animator/scripts/looks/pastel.json +7 -0
- package/adapters/grok/olko-creative/skills/shorts-animator/scripts/looks/watercolor.json +6 -0
- package/adapters/grok/olko-creative/skills/shorts-animator/scripts/package.json +4 -0
- package/adapters/grok/olko-creative/skills/shorts-animator/scripts/runtime/globals.d.ts +199 -0
- package/adapters/grok/olko-creative/skills/shorts-animator/scripts/runtime/kit.ts +856 -0
- package/adapters/grok/olko-creative/skills/shorts-animator/scripts/runtime/paint.ts +268 -0
- package/adapters/grok/olko-creative/skills/shorts-animator/scripts/runtime/stage.ts +181 -0
- package/adapters/grok/olko-creative/skills/shorts-animator/scripts/runtime/tsconfig.json +12 -0
- package/adapters/grok/olko-creative/skills/shorts-animator/scripts/sa.ts +262 -0
- package/adapters/grok/olko-creative/skills/shorts-animator/scripts/test/domain.test.ts +107 -0
- package/adapters/grok/olko-creative/skills/shorts-animator/scripts/tsconfig.json +16 -0
- package/adapters/grok/olko-creative/skills/shorts-animator/scripts/tts.py +20 -0
- package/adapters/hermes/olko-creative/README.md +2 -0
- package/catalog/skills.json +46 -0
- package/package.json +1 -1
- package/plugins/olko-apple-kit/.claude-plugin/plugin.json +1 -1
- package/plugins/olko-creative/.claude-plugin/plugin.json +1 -1
- package/plugins/olko-creative/skills/carousel-maker/SKILL.md +111 -0
- package/plugins/olko-creative/skills/carousel-maker/scripts/cm.ts +220 -0
- package/plugins/olko-creative/skills/carousel-maker/scripts/domain/check.ts +68 -0
- package/plugins/olko-creative/skills/carousel-maker/scripts/domain/layout.ts +75 -0
- package/plugins/olko-creative/skills/carousel-maker/scripts/domain/schema.ts +54 -0
- package/plugins/olko-creative/skills/carousel-maker/scripts/domain/types.ts +33 -0
- package/plugins/olko-creative/skills/carousel-maker/scripts/edge/io.ts +87 -0
- package/plugins/olko-creative/skills/carousel-maker/scripts/package.json +4 -0
- package/plugins/olko-creative/skills/carousel-maker/scripts/test/domain.test.ts +99 -0
- package/plugins/olko-creative/skills/carousel-maker/scripts/tsconfig.json +16 -0
- package/plugins/olko-creative/skills/shorts-animator/SKILL.md +112 -0
- package/plugins/olko-creative/skills/shorts-animator/references/kit.md +59 -0
- package/plugins/olko-creative/skills/shorts-animator/references/looks.md +32 -0
- package/plugins/olko-creative/skills/shorts-animator/references/writing.md +9 -0
- package/plugins/olko-creative/skills/shorts-animator/scripts/audio/compose.ts +290 -0
- package/plugins/olko-creative/skills/shorts-animator/scripts/audio/dsp.ts +193 -0
- package/plugins/olko-creative/skills/shorts-animator/scripts/audio/instruments.ts +185 -0
- package/plugins/olko-creative/skills/shorts-animator/scripts/audio/master.ts +55 -0
- package/plugins/olko-creative/skills/shorts-animator/scripts/audio/soundtrack.ts +119 -0
- package/plugins/olko-creative/skills/shorts-animator/scripts/domain/captions.ts +35 -0
- package/plugins/olko-creative/skills/shorts-animator/scripts/domain/check.ts +96 -0
- package/plugins/olko-creative/skills/shorts-animator/scripts/domain/look.ts +26 -0
- package/plugins/olko-creative/skills/shorts-animator/scripts/domain/patch.ts +38 -0
- package/plugins/olko-creative/skills/shorts-animator/scripts/domain/schema.ts +81 -0
- package/plugins/olko-creative/skills/shorts-animator/scripts/domain/timeline.ts +75 -0
- package/plugins/olko-creative/skills/shorts-animator/scripts/domain/types.ts +91 -0
- package/plugins/olko-creative/skills/shorts-animator/scripts/edge/chrome.ts +169 -0
- package/plugins/olko-creative/skills/shorts-animator/scripts/edge/doctor.ts +52 -0
- package/plugins/olko-creative/skills/shorts-animator/scripts/edge/media.ts +105 -0
- package/plugins/olko-creative/skills/shorts-animator/scripts/edge/page.ts +53 -0
- package/plugins/olko-creative/skills/shorts-animator/scripts/edge/probe.ts +75 -0
- package/plugins/olko-creative/skills/shorts-animator/scripts/edge/sound.ts +46 -0
- package/plugins/olko-creative/skills/shorts-animator/scripts/edge/voice.ts +183 -0
- package/plugins/olko-creative/skills/shorts-animator/scripts/looks/gouache.json +7 -0
- package/plugins/olko-creative/skills/shorts-animator/scripts/looks/ink.json +6 -0
- package/plugins/olko-creative/skills/shorts-animator/scripts/looks/oil.json +7 -0
- package/plugins/olko-creative/skills/shorts-animator/scripts/looks/pastel.json +7 -0
- package/plugins/olko-creative/skills/shorts-animator/scripts/looks/watercolor.json +6 -0
- package/plugins/olko-creative/skills/shorts-animator/scripts/package.json +4 -0
- package/plugins/olko-creative/skills/shorts-animator/scripts/runtime/globals.d.ts +199 -0
- package/plugins/olko-creative/skills/shorts-animator/scripts/runtime/kit.ts +856 -0
- package/plugins/olko-creative/skills/shorts-animator/scripts/runtime/paint.ts +268 -0
- package/plugins/olko-creative/skills/shorts-animator/scripts/runtime/stage.ts +181 -0
- package/plugins/olko-creative/skills/shorts-animator/scripts/runtime/tsconfig.json +12 -0
- package/plugins/olko-creative/skills/shorts-animator/scripts/sa.ts +262 -0
- package/plugins/olko-creative/skills/shorts-animator/scripts/test/domain.test.ts +107 -0
- package/plugins/olko-creative/skills/shorts-animator/scripts/tsconfig.json +16 -0
- package/plugins/olko-creative/skills/shorts-animator/scripts/tts.py +20 -0
- package/plugins/olko-garmin-kit/.claude-plugin/plugin.json +1 -1
- package/plugins/olko-git-tools/.claude-plugin/plugin.json +1 -1
- package/plugins/olko-github-pr/.claude-plugin/plugin.json +1 -1
- package/plugins/olko-github-pr/skills/lekker-review/SKILL.md +30 -0
- package/plugins/olko-github-pr/skills/lekker-review/references/context-gathering.md +3 -0
- package/plugins/olko-github-pr/skills/lekker-review/scripts/finding-state.mjs +207 -0
- package/plugins/olko-github-pr/skills/lekker-review/workflow.js +46 -3
- package/plugins/olko-obsidian/.claude-plugin/plugin.json +1 -1
- package/plugins/olko-product/.claude-plugin/plugin.json +1 -1
- package/plugins/olko-reflection/.claude-plugin/plugin.json +1 -1
- package/plugins/olko-release/.claude-plugin/plugin.json +1 -1
- package/plugins/olko-skill-meta/.claude-plugin/plugin.json +1 -1
- package/plugins/olko-web-ops/.claude-plugin/plugin.json +1 -1
- 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)
|
|
@@ -10,3 +10,5 @@ into the active Hermes profile. The canonical packages are:
|
|
|
10
10
|
- `plugins/olko-creative/skills/music-to-plex/SKILL.md`
|
|
11
11
|
- `plugins/olko-creative/skills/vinted-listing/SKILL.md`
|
|
12
12
|
- `plugins/olko-creative/skills/wikipedia-uk-editor/SKILL.md`
|
|
13
|
+
- `plugins/olko-creative/skills/shorts-animator/SKILL.md`
|
|
14
|
+
- `plugins/olko-creative/skills/carousel-maker/SKILL.md`
|
package/catalog/skills.json
CHANGED
|
@@ -1300,6 +1300,52 @@
|
|
|
1300
1300
|
"hermes",
|
|
1301
1301
|
"pi"
|
|
1302
1302
|
]
|
|
1303
|
+
},
|
|
1304
|
+
{
|
|
1305
|
+
"name": "shorts-animator",
|
|
1306
|
+
"lookupName": "olko:shorts-animator",
|
|
1307
|
+
"path": "plugins/olko-creative/skills/shorts-animator",
|
|
1308
|
+
"description": "Make narrated, painted animated shorts (TikTok, Reels, YouTube Shorts) from a script and a look described in words: scenes drawn in code, GPU-painted, voiced locally, captioned and scored into an MP4.",
|
|
1309
|
+
"tags": [
|
|
1310
|
+
"video",
|
|
1311
|
+
"animation",
|
|
1312
|
+
"shorts",
|
|
1313
|
+
"narration",
|
|
1314
|
+
"canvas"
|
|
1315
|
+
],
|
|
1316
|
+
"adapters": [
|
|
1317
|
+
"codex",
|
|
1318
|
+
"claude",
|
|
1319
|
+
"cursor",
|
|
1320
|
+
"copilot",
|
|
1321
|
+
"windsurf",
|
|
1322
|
+
"kiro",
|
|
1323
|
+
"grok",
|
|
1324
|
+
"hermes"
|
|
1325
|
+
]
|
|
1326
|
+
},
|
|
1327
|
+
{
|
|
1328
|
+
"name": "carousel-maker",
|
|
1329
|
+
"lookupName": "olko:carousel-maker",
|
|
1330
|
+
"path": "plugins/olko-creative/skills/carousel-maker",
|
|
1331
|
+
"description": "Make Instagram or LinkedIn carousel slides from a topic or pasted copy: a brand template sets the text, and a reusable library of hand-drawn illustrations, drawn once in code per brand, supplies the pictures.",
|
|
1332
|
+
"tags": [
|
|
1333
|
+
"instagram",
|
|
1334
|
+
"carousel",
|
|
1335
|
+
"social",
|
|
1336
|
+
"design",
|
|
1337
|
+
"slides"
|
|
1338
|
+
],
|
|
1339
|
+
"adapters": [
|
|
1340
|
+
"codex",
|
|
1341
|
+
"claude",
|
|
1342
|
+
"cursor",
|
|
1343
|
+
"copilot",
|
|
1344
|
+
"windsurf",
|
|
1345
|
+
"kiro",
|
|
1346
|
+
"grok",
|
|
1347
|
+
"hermes"
|
|
1348
|
+
]
|
|
1303
1349
|
}
|
|
1304
1350
|
]
|
|
1305
1351
|
},
|
package/package.json
CHANGED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: carousel-maker
|
|
3
|
+
description: Use when someone wants Instagram or LinkedIn carousel slides from a topic, a brief or pasted copy. A fixed brand template sets the text on every slide, and an optional library of hand-drawn illustrations, drawn once in code per brand and reused across decks, supplies the pictures. 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+ and Chrome; ffmpeg for the contact sheet. The illustration library needs the shorts-animator skill installed alongside.
|
|
7
|
+
metadata:
|
|
8
|
+
targets: [_source-only]
|
|
9
|
+
author: Oleg Koval
|
|
10
|
+
tags:
|
|
11
|
+
- instagram
|
|
12
|
+
- carousel
|
|
13
|
+
- social
|
|
14
|
+
- design
|
|
15
|
+
- slides
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
# Carousel Maker
|
|
19
|
+
|
|
20
|
+
A brand and a few lines of copy in, 1080x1350 carousel slides out. The template places the text, so it
|
|
21
|
+
is never garbled and never off-brand. Pictures come from the brand's library: drawings written once as
|
|
22
|
+
canvas code, painted by shorts-animator, and reused by every deck after that. Text-only decks need no
|
|
23
|
+
library at all.
|
|
24
|
+
|
|
25
|
+
`CM` = `node <this skill's folder>/scripts/cm.ts`. Write the full command each time. One folder per
|
|
26
|
+
brand: `BRAND=./<slug>-carousels`. Decks live in `$BRAND/decks/<deck>.json`.
|
|
27
|
+
|
|
28
|
+
## 1. Decide the copy
|
|
29
|
+
|
|
30
|
+
Decide the slides yourself. Ask one question only if the subject or the brand is unclear. A pasted
|
|
31
|
+
script is used word for word.
|
|
32
|
+
|
|
33
|
+
- 5 to 8 slides: a `cover`, then `point` slides, then one `cta`.
|
|
34
|
+
- The cover is a promise in 8 words or fewer. It is the only slide most people see.
|
|
35
|
+
- One idea per point slide: a title of 7 words or fewer, a body of 22 words or fewer.
|
|
36
|
+
- The cta asks for one thing (save, follow, comment) and carries a `follow` line.
|
|
37
|
+
- `caption` is the post text: two short lines and one question for the comments.
|
|
38
|
+
|
|
39
|
+
## 2. Set up the brand
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
CM new "$BRAND" --name "Slow Hours" --handle @slowhours --look ink # brand.json, decks/, art/, library/
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`brand.json`: `name`, `handle`, `colors` (`paper`, `ink`, `accent`, `muted`, all `#rrggbb`), `fonts`
|
|
46
|
+
(`head`, `body`, CSS font stacks). Set them from what they told you; the defaults are cream, near-black
|
|
47
|
+
and orange. `--look` picks the paint for the library: `ink`, `gouache`, `watercolor`, `pastel`, `oil`.
|
|
48
|
+
|
|
49
|
+
A deck:
|
|
50
|
+
|
|
51
|
+
```json
|
|
52
|
+
{ "title": "mornings", "caption": "...", "slides": [
|
|
53
|
+
{ "kind": "cover", "title": "Slow mornings, in 4 steps", "body": "No 5am club.", "art": "window" },
|
|
54
|
+
{ "kind": "point", "title": "Coffee, outside", "body": "Five minutes on the step.", "art": "coffee" },
|
|
55
|
+
{ "kind": "point", "title": "Their own photo", "art": "photos/desk.jpg" },
|
|
56
|
+
{ "kind": "cta", "title": "Save this for tomorrow", "follow": "Follow @slowhours" } ] }
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`art` is a library drawing (a bare name), a file (a path, relative to `$BRAND`), or left out for a
|
|
60
|
+
text-only slide, which gets larger type.
|
|
61
|
+
|
|
62
|
+
## 3. Draw the library (once per brand)
|
|
63
|
+
|
|
64
|
+
Skip this for text-only decks. Every bare `art` name needs a function in `$BRAND/art/scenes.js`:
|
|
65
|
+
`window.SCENES = { "window": (k) => { k.sky({...}); ... } }`. Read `../shorts-animator/references/kit.md`
|
|
66
|
+
before writing scenes. Draw objects and places, square, one subject, big simple shapes, strong value
|
|
67
|
+
contrast; the paint pass adds texture. No people's faces close up.
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
CM art "$BRAND" # every drawing the decks use -> library/<name>.jpg
|
|
71
|
+
CM art "$BRAND" coffee # only these
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Look at the sheet it reports once. Redraw only the drawings that are muddy, empty or look alike. The
|
|
75
|
+
library is the brand's asset: the next deck reuses it and only new names cost a drawing.
|
|
76
|
+
|
|
77
|
+
## 4. Check, render, look (budgeted)
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
CM check "$BRAND" [deck] # no browser: unknown kinds, missing drawings or files, deck shape, word counts
|
|
81
|
+
CM render "$BRAND" [deck] # out/<deck>/slide-N.png, sheet.jpg, caption.txt; measures every slide
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Fix every `ERROR` from `check` before rendering. `render` also fails a slide whose text spills out of
|
|
85
|
+
its frame or whose picture did not load. Then read `out/<deck>/sheet.jpg` once. **Budget: at most two
|
|
86
|
+
looks per deck** (the library sheet and the deck sheet). Images are the expensive part of this loop; do
|
|
87
|
+
not reopen a sheet you have already judged.
|
|
88
|
+
|
|
89
|
+
Tell them in two lines what it is (slides, look) and: "Tell me what to change, one sentence is enough."
|
|
90
|
+
|
|
91
|
+
## 5. One-sentence changes
|
|
92
|
+
|
|
93
|
+
Smallest change, `CM check`, render, show.
|
|
94
|
+
|
|
95
|
+
| they say | change |
|
|
96
|
+
|---|---|
|
|
97
|
+
| shorter, punchier | the slide's `title` or `body` |
|
|
98
|
+
| different colours, our brand | `brand.json` `colors` |
|
|
99
|
+
| serif, sans, our font | `brand.json` `fonts` |
|
|
100
|
+
| a different picture on slide 3 | that slide's `art`, or one new scene plus `CM art "$BRAND" <name>` |
|
|
101
|
+
| no pictures | drop `art` from the slides |
|
|
102
|
+
| another post on the same brand | a new `decks/<deck>.json`; reuse library names |
|
|
103
|
+
|
|
104
|
+
## When something goes wrong
|
|
105
|
+
|
|
106
|
+
| symptom | fix |
|
|
107
|
+
|---|---|
|
|
108
|
+
| no Chrome | install Chrome, or set `CHROME` to a Chrome or headless-shell binary |
|
|
109
|
+
| `CM art` cannot find shorts-animator | install it alongside, or set `SHORTS_ANIMATOR` to its `scripts/sa.ts` |
|
|
110
|
+
| "text spills out of the frame" | cut words; `check` warns before this happens |
|
|
111
|
+
| no sheet.jpg | ffmpeg is missing; the slides are still in `out/<deck>/` |
|