wizz-method 2.17.2 → 2.18.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 (41) hide show
  1. package/THIRD-PARTY-NOTICES.md +12 -0
  2. package/package.json +1 -1
  3. package/skills-registry.yaml +24 -6
  4. package/src/core-skills/_shared/handoff-protocol.md +3 -1
  5. package/src/modules/wizz/_shared/despacho-barato.md +1 -1
  6. package/src/modules/wizz/_shared/model-ladder.md +10 -0
  7. package/src/modules/wizz/_shared/video-pipeline.md +10 -0
  8. package/src/modules/wizz/agents/wizz-maestro/SKILL.md +1 -1
  9. package/src/modules/wizz/agents/wizz-social/SKILL.md +2 -0
  10. package/src/modules/wizz/agents/wizz-social/customize.toml +10 -0
  11. package/src/modules/wizz/subagents/codex/wizz-exec-standard.toml +1 -1
  12. package/src/modules/wizz/subagents/codex/wizz-exec-strong.toml +1 -1
  13. package/src/modules/wizz/subagents/gemini/wizz-exec-standard.md +1 -1
  14. package/src/modules/wizz/subagents/gemini/wizz-exec-strong.md +1 -1
  15. package/src/modules/wizz/subagents/opencode/wizz-exec-standard.md +1 -1
  16. package/src/modules/wizz/subagents/opencode/wizz-exec-strong.md +1 -1
  17. package/src/modules/wizz/subagents/spec.yaml +6 -6
  18. package/src/modules/wizz/subagents/wizz-exec-standard.md +1 -1
  19. package/src/modules/wizz/subagents/wizz-exec-strong.md +1 -1
  20. package/src/skills-lib/wizz-hook-extractor/LICENSE +21 -0
  21. package/src/skills-lib/wizz-hook-extractor/SKILL.md +56 -0
  22. package/src/skills-lib/wizz-hook-extractor/references/examples.md +59 -0
  23. package/src/skills-lib/wizz-hook-extractor/references/formulas.md +136 -0
  24. package/src/skills-lib/wizz-hook-extractor/scripts/read-post.sh +83 -0
  25. package/src/skills-lib/wizz-ig-profile-optimizer/LICENSE +21 -0
  26. package/src/skills-lib/wizz-ig-profile-optimizer/SKILL.md +74 -0
  27. package/src/skills-lib/wizz-ig-profile-optimizer/scripts/capture-profile.sh +41 -0
  28. package/src/skills-lib/wizz-ig-profile-optimizer/scripts/limits.mjs +82 -0
  29. package/src/skills-lib/wizz-router/references/routing-table-flat.md +3 -1
  30. package/src/skills-lib/wizz-video-effects/SKILL.md +1 -0
  31. package/src/skills-lib/wizz-video-styles/SKILL.md +9 -3
  32. package/src/skills-lib/wizz-video-styles/data/recipes.json +33 -1
  33. package/src/skills-lib/wizz-video-styles/data/references.json +175 -0
  34. package/src/skills-lib/wizz-video-styles/data/styles.json +55 -0
  35. package/src/skills-lib/wizz-video-styles/references/generated-prompts.md +60 -0
  36. package/src/skills-lib/wizz-video-styles/references/measured-patterns.md +2 -0
  37. package/src/skills-lib/wizz-video-styles/references/style-picker.md +5 -3
  38. package/src/skills-lib/wizz-video-styles/references/styles/tipografia-cinetica.md +68 -0
  39. package/src/skills-lib/wizz-video-styles/scripts/analyze.sh +2 -1
  40. package/src/skills-lib/wizz-video-styles/scripts/beat_grid.py +68 -0
  41. package/src/skills-lib/wizz-video-styles/scripts/references.mjs +264 -0
@@ -0,0 +1,264 @@
1
+ #!/usr/bin/env node
2
+ // Fresh references for a style, without repeating what the project already used.
3
+ //
4
+ // node references.mjs pick --style <key> [--project <dir>] [--n 4] [--offline] [--json]
5
+ // node references.mjs record --style <key> --piece <slug> --refs <id,id> [--project <dir>]
6
+ // node references.mjs history [--project <dir>] [--json]
7
+ //
8
+ // pick: measured references of the style (data/references.json) plus, for styles that are
9
+ // generated in code, viral videos from the awesome-opus5-5-videos list (fetched fresh, cached
10
+ // for 7 days, cache used when offline). Anything already in the project's history is skipped;
11
+ // different channels/authors come first. Also warns when the same style was used in the last
12
+ // pieces, so the edit does not keep coming out with the same look.
13
+ // record: appends the piece to <project>/_wizz-output/video-styles/historico.json.
14
+ // The fetched list is third-party data: only links and a short excerpt are printed, never run.
15
+
16
+ import fs from 'node:fs';
17
+ import os from 'node:os';
18
+ import path from 'node:path';
19
+ import { fileURLToPath } from 'node:url';
20
+
21
+ const SKILL_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
22
+ const LIST_URL = 'https://raw.githubusercontent.com/yihui-dev/awesome-opus5-5-videos/main/data/videos.json';
23
+ const CACHE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
24
+ const FETCH_TIMEOUT_MS = 8000;
25
+ const EXCERPT_CHARS = 160;
26
+ const MEASURED_PER_PICK = 2;
27
+ const REPEAT_WINDOW = 2;
28
+ const HISTORY_REL = path.join('_wizz-output', 'video-styles', 'historico.json');
29
+
30
+ // Which generated videos feed which catalogue style. Styles that are filmed (talking head,
31
+ // tour, macro...) have no entry: a code-generated video is not a reference for them.
32
+ const GENERATED = {
33
+ 'tipografia-cinetica': { categories: ['motion', 'explainer', '3d'], match: /lyric|music video|kinetic typograph|\bMV\b/i },
34
+ 'motion-editorial': { categories: ['motion', 'explainer'], match: /\bUI\b|product|launch|saas|\bapp\b|logo|promo|brand/i },
35
+ 'explicador-animado': { categories: ['explainer'], match: /./ },
36
+ 'quadro-branco': { categories: ['explainer', 'motion'], match: /whiteboard|hand-drawn|sketch|doodle/i },
37
+ 'meme-rapido': { categories: ['motion'], match: /brain ?rot|meme|chaotic/i },
38
+ };
39
+
40
+ function parseArgs(argv) {
41
+ const [command, ...rest] = argv;
42
+ const options = { command, project: process.cwd(), n: 4, offline: false, json: false };
43
+ for (let i = 0; i < rest.length; i++) {
44
+ const flag = rest[i];
45
+ if (flag === '--offline') options.offline = true;
46
+ else if (flag === '--json') options.json = true;
47
+ else if (['--style', '--project', '--n', '--piece', '--refs'].includes(flag)) options[flag.slice(2)] = rest[++i];
48
+ else fail(`unknown argument: ${flag}`);
49
+ }
50
+ options.n = Number(options.n);
51
+ if (!Number.isInteger(options.n) || options.n < 1 || options.n > 20) fail('--n must be an integer from 1 to 20');
52
+ return options;
53
+ }
54
+
55
+ function fail(message) {
56
+ process.stderr.write(`references: ${message}\n`);
57
+ process.exit(2);
58
+ }
59
+
60
+ function readJson(file, fallback) {
61
+ try {
62
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
63
+ } catch (error) {
64
+ if (error.code === 'ENOENT') return fallback;
65
+ fail(`cannot read ${file}: ${error.message}`);
66
+ }
67
+ }
68
+
69
+ function writeJsonAtomic(file, value) {
70
+ fs.mkdirSync(path.dirname(file), { recursive: true });
71
+ const tmp = `${file}.${process.pid}.tmp`;
72
+ fs.writeFileSync(tmp, `${JSON.stringify(value, null, 2)}\n`);
73
+ fs.renameSync(tmp, file);
74
+ }
75
+
76
+ function loadHistory(project) {
77
+ const history = readJson(path.join(project, HISTORY_REL), []);
78
+ if (!Array.isArray(history)) fail(`${HISTORY_REL} must be a JSON array`);
79
+ return history;
80
+ }
81
+
82
+ function knownStyles() {
83
+ return Object.keys(readJson(path.join(SKILL_DIR, 'data', 'recipes.json'), { styles: {} }).styles);
84
+ }
85
+
86
+ function requireStyle(style) {
87
+ if (!style) fail('--style is required');
88
+ const styles = knownStyles();
89
+ if (!styles.includes(style)) fail(`unknown style "${style}". Known: ${styles.join(', ')}`);
90
+ }
91
+
92
+ const isHttpsUrl = (value) => typeof value === 'string' && value.startsWith('https://');
93
+
94
+ function validVideos(data) {
95
+ if (!Array.isArray(data)) return null;
96
+ return data.filter(
97
+ (v) => v && typeof v.slug === 'string' && typeof v.category === 'string' && isHttpsUrl(v.post_url) && isHttpsUrl(v.skillry_url),
98
+ );
99
+ }
100
+
101
+ function cachePath() {
102
+ return process.env.WIZZ_VIDEO_STYLES_CACHE || path.join(os.homedir(), '.cache', 'wizz-video-styles', 'opus-videos.json');
103
+ }
104
+
105
+ async function loadGeneratedList(offline) {
106
+ const file = cachePath();
107
+ let cached = null;
108
+ let age = Infinity;
109
+ try {
110
+ age = Date.now() - fs.statSync(file).mtimeMs;
111
+ cached = validVideos(JSON.parse(fs.readFileSync(file, 'utf8')));
112
+ } catch {
113
+ cached = null;
114
+ }
115
+ if (cached && (offline || age < CACHE_MAX_AGE_MS)) return { videos: cached, source: `cache (${Math.round(age / 3_600_000)} h old)` };
116
+ if (offline) return { videos: [], source: 'offline and no cache' };
117
+ try {
118
+ const response = await fetch(LIST_URL, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
119
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
120
+ const videos = validVideos(await response.json());
121
+ if (!videos) throw new Error('unexpected format');
122
+ writeJsonAtomic(file, videos);
123
+ return { videos, source: 'fetched now' };
124
+ } catch (error) {
125
+ if (cached) return { videos: cached, source: `cache, refresh failed (${error.message})` };
126
+ return { videos: [], source: `unavailable (${error.message})` };
127
+ }
128
+ }
129
+
130
+ // Greedy pick: unused first, then a new channel/author before repeating one.
131
+ function pickDiverse(items, keyOf, used, count) {
132
+ const fresh = items.filter((item) => !used.has(item.id));
133
+ const chosen = [];
134
+ const seenKeys = new Set();
135
+ for (const pass of [true, false]) {
136
+ for (const item of fresh) {
137
+ if (chosen.length >= count) break;
138
+ if (chosen.includes(item) || (pass && seenKeys.has(keyOf(item)))) continue;
139
+ chosen.push(item);
140
+ seenKeys.add(keyOf(item));
141
+ }
142
+ }
143
+ return { chosen, exhausted: fresh.length === 0 && items.length > 0 };
144
+ }
145
+
146
+ function measuredFor(style) {
147
+ return readJson(path.join(SKILL_DIR, 'data', 'references.json'), [])
148
+ .filter((r) => r.style === style)
149
+ .sort((a, b) => Number(b.views) - Number(a.views))
150
+ .map((r) => ({ id: r.id, kind: 'measured', who: r.channel, url: r.url, note: `${Number(r.views).toLocaleString('en-US')} views` }));
151
+ }
152
+
153
+ const PROMPT_KEY_CHARS = 80;
154
+ const promptKey = (prompt) => String(prompt ?? '').toLowerCase().replaceAll(/[^a-z0-9]+/g, ' ').trim().slice(0, PROMPT_KEY_CHARS);
155
+
156
+ // Third-party text: one line, no markdown or HTML that could turn into a link or a tag.
157
+ const excerpt = (prompt) =>
158
+ String(prompt ?? '')
159
+ .replaceAll(/[`<>[\]()]/g, '')
160
+ .replaceAll(/\s+/g, ' ')
161
+ .trim()
162
+ .slice(0, EXCERPT_CHARS);
163
+
164
+ function generatedFor(style, videos) {
165
+ const rule = GENERATED[style];
166
+ if (!rule) return [];
167
+ // The same viral prompt was remade by dozens of people: one remake per prompt is enough.
168
+ const seenPrompts = new Set();
169
+ return videos
170
+ .filter((v) => rule.categories.includes(v.category) && rule.match.test(String(v.prompt ?? '')))
171
+ .sort((a, b) => String(b.added ?? '').localeCompare(String(a.added ?? '')))
172
+ .filter((v) => {
173
+ const key = promptKey(v.prompt);
174
+ if (seenPrompts.has(key)) return false;
175
+ seenPrompts.add(key);
176
+ return true;
177
+ })
178
+ .map((v) => ({
179
+ id: v.slug,
180
+ kind: 'generated',
181
+ who: String(v.author ?? '?'),
182
+ url: v.skillry_url,
183
+ post: v.post_url,
184
+ note: excerpt(v.prompt),
185
+ }));
186
+ }
187
+
188
+ function styleRepeat(history, style) {
189
+ const recent = history.slice(-REPEAT_WINDOW).map((entry) => entry.style);
190
+ return recent.length === REPEAT_WINDOW && recent.every((s) => s === style);
191
+ }
192
+
193
+ async function pick(options) {
194
+ requireStyle(options.style);
195
+ const history = loadHistory(options.project);
196
+ const used = new Set(history.flatMap((entry) => entry.refs ?? []));
197
+ const measured = pickDiverse(measuredFor(options.style), (r) => r.who, used, Math.min(MEASURED_PER_PICK, options.n));
198
+ let generated = { chosen: [], exhausted: false };
199
+ let source = 'not used for this style (filmed style)';
200
+ if (GENERATED[options.style]) {
201
+ const list = await loadGeneratedList(options.offline);
202
+ source = list.source;
203
+ generated = pickDiverse(generatedFor(options.style, list.videos), (r) => r.who, used, options.n - measured.chosen.length);
204
+ }
205
+ return {
206
+ style: options.style,
207
+ recentStyles: history.slice(-5).map((entry) => entry.style),
208
+ styleRepeated: styleRepeat(history, options.style),
209
+ references: [...measured.chosen, ...generated.chosen],
210
+ measuredExhausted: measured.exhausted,
211
+ generatedExhausted: generated.exhausted,
212
+ generatedSource: source,
213
+ };
214
+ }
215
+
216
+ function printPick(result) {
217
+ const lines = [`# References for ${result.style}`, ''];
218
+ if (result.recentStyles.length) lines.push(`Last styles in this project: ${result.recentStyles.join(' → ')}`);
219
+ if (result.styleRepeated) {
220
+ lines.push(
221
+ `REPEAT: the last ${REPEAT_WINDOW} pieces already used ${result.style}. Unless this is a deliberate series, pick another style the material allows, or keep it only for the hook/CTA and mix another in the middle.`,
222
+ );
223
+ }
224
+ lines.push(`Generated list: ${result.generatedSource}`, '');
225
+ for (const ref of result.references) {
226
+ const post = ref.post ? ` · [post](${ref.post})` : '';
227
+ lines.push(`- [${ref.kind}] ${ref.who} · [watch](${ref.url})${post} · id \`${ref.id}\` · ${ref.note}`);
228
+ }
229
+ if (result.references.length === 0) lines.push('- nothing new left for this style');
230
+ if (result.measuredExhausted) lines.push('', 'Every measured reference of this style was already used here: measure new ones (research-method.md).');
231
+ if (result.generatedExhausted) lines.push('', 'Every generated reference of this style was already used here.');
232
+ lines.push('', 'After delivering the piece: node references.mjs record --style <key> --piece <slug> --refs <ids you watched>');
233
+ process.stdout.write(`${lines.join('\n')}\n`);
234
+ }
235
+
236
+ function record(options) {
237
+ requireStyle(options.style);
238
+ if (!options.piece) fail('--piece is required');
239
+ const refs = String(options.refs ?? '')
240
+ .split(',')
241
+ .map((id) => id.trim())
242
+ .filter(Boolean);
243
+ if (refs.length === 0) fail('--refs is required (ids printed by pick)');
244
+ const history = loadHistory(options.project);
245
+ const entry = { date: new Date().toISOString().slice(0, 10), piece: options.piece, style: options.style, refs };
246
+ writeJsonAtomic(path.join(options.project, HISTORY_REL), [...history, entry]);
247
+ process.stdout.write(`recorded ${options.piece} (${options.style}, ${refs.length} refs) in ${HISTORY_REL}\n`);
248
+ }
249
+
250
+ function printHistory(options) {
251
+ const history = loadHistory(options.project);
252
+ if (options.json) return process.stdout.write(`${JSON.stringify(history, null, 2)}\n`);
253
+ if (history.length === 0) return process.stdout.write('no pieces recorded yet\n');
254
+ for (const entry of history) process.stdout.write(`${entry.date} · ${entry.piece} · ${entry.style} · ${(entry.refs ?? []).join(', ')}\n`);
255
+ }
256
+
257
+ const options = parseArgs(process.argv.slice(2));
258
+ if (options.command === 'pick') {
259
+ const result = await pick(options);
260
+ if (options.json) process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
261
+ else printPick(result);
262
+ } else if (options.command === 'record') record(options);
263
+ else if (options.command === 'history') printHistory(options);
264
+ else fail('usage: references.mjs pick|record|history (see the header of this file)');