wizz-method 2.17.2 → 2.19.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/THIRD-PARTY-NOTICES.md +12 -0
- package/package.json +5 -3
- package/removals.txt +10 -0
- package/skills-registry.yaml +72 -42
- package/src/bmm-skills/1-analysis/wizz-set-feature/SKILL.md +9 -9
- package/src/bmm-skills/4-implementation/wizz-create-story/SKILL.md +1 -1
- package/src/bmm-skills/4-implementation/wizz-dev-story/SKILL.md +1 -1
- package/src/bmm-skills/4-implementation/wizz-qa-generate-e2e-tests/SKILL.md +1 -1
- package/src/bmm-skills/4-implementation/wizz-qa-generate-e2e-tests/checklist.md +1 -1
- package/src/bmm-skills/4-implementation/wizz-quick-dev/SKILL.md +1 -1
- package/src/bmm-skills/4-implementation/wizz-sprint-status/SKILL.md +5 -5
- package/src/bmm-skills/module-help.csv +1 -1
- package/src/bmm-skills/module.yaml +1 -1
- package/src/core-skills/_shared/handoff-protocol.md +5 -3
- package/src/modules/lowticket/skills/wizz-lowticket-funil/SKILL.md +1 -1
- package/src/modules/lowticket/skills/wizz-lowticket-guia/SKILL.md +1 -1
- package/src/modules/lowticket/skills/wizz-lowticket-metodologia/SKILL.md +1 -1
- package/src/modules/lowticket/skills/wizz-lowticket-minerador/SKILL.md +1 -1
- package/src/modules/lowticket/skills/wizz-lowticket-pagina/SKILL.md +1 -1
- package/src/modules/lowticket/skills/wizz-lowticket-trafego/SKILL.md +1 -1
- package/src/modules/wizz/_shared/cerebro.md +7 -7
- package/src/modules/wizz/_shared/communication-rules.md +1 -1
- package/src/modules/wizz/_shared/despacho-barato.md +1 -1
- package/src/modules/wizz/_shared/encerramento.md +4 -3
- package/src/modules/wizz/_shared/model-ladder.md +10 -0
- package/src/modules/wizz/_shared/organizacao.md +8 -4
- package/src/modules/wizz/_shared/token-economy.md +1 -1
- package/src/modules/wizz/_shared/video-pipeline.md +13 -3
- package/src/modules/wizz/agents/wizz-ads/SKILL.md +4 -1
- package/src/modules/wizz/agents/wizz-ads/customize.toml +12 -3
- package/src/modules/wizz/agents/wizz-ads/knowledge/playbook.md +36 -0
- package/src/modules/wizz/agents/wizz-copy/SKILL.md +4 -1
- package/src/modules/wizz/agents/wizz-copy/customize.toml +2 -3
- package/src/modules/wizz/agents/wizz-copy/knowledge/playbook.md +34 -0
- package/src/modules/wizz/agents/wizz-designer/SKILL.md +2 -2
- package/src/modules/wizz/agents/wizz-designer/customize.toml +2 -3
- package/src/modules/wizz/agents/wizz-growth/SKILL.md +4 -1
- package/src/modules/wizz/agents/wizz-growth/customize.toml +33 -4
- package/src/modules/wizz/agents/wizz-growth/knowledge/playbook.md +39 -0
- package/src/modules/wizz/agents/wizz-maestro/SKILL.md +7 -9
- package/src/modules/wizz/agents/wizz-maestro/customize.toml +2 -3
- package/src/modules/wizz/agents/wizz-memoria/SKILL.md +7 -7
- package/src/modules/wizz/agents/wizz-memoria/customize.toml +5 -6
- package/src/modules/wizz/agents/wizz-qa/SKILL.md +5 -6
- package/src/modules/wizz/agents/wizz-qa/customize.toml +3 -4
- package/src/modules/wizz/agents/wizz-seo/SKILL.md +4 -1
- package/src/modules/wizz/agents/wizz-seo/customize.toml +2 -3
- package/src/modules/wizz/agents/wizz-seo/knowledge/playbook.md +31 -0
- package/src/modules/wizz/agents/wizz-social/SKILL.md +5 -3
- package/src/modules/wizz/agents/wizz-social/customize.toml +12 -3
- package/src/modules/wizz/overrides/wizz-agent-analyst.toml +2 -2
- package/src/modules/wizz/overrides/wizz-agent-architect.toml +3 -3
- package/src/modules/wizz/overrides/wizz-agent-dev.toml +3 -3
- package/src/modules/wizz/overrides/wizz-agent-pm.toml +2 -2
- package/src/modules/wizz/overrides/wizz-agent-tech-writer.toml +2 -2
- package/src/modules/wizz/overrides/wizz-agent-ux-designer.toml +2 -2
- package/src/modules/wizz/subagents/codex/wizz-exec-standard.toml +1 -1
- package/src/modules/wizz/subagents/codex/wizz-exec-strong.toml +1 -1
- package/src/modules/wizz/subagents/gemini/wizz-exec-standard.md +1 -1
- package/src/modules/wizz/subagents/gemini/wizz-exec-strong.md +1 -1
- package/src/modules/wizz/subagents/opencode/wizz-exec-standard.md +1 -1
- package/src/modules/wizz/subagents/opencode/wizz-exec-strong.md +1 -1
- package/src/modules/wizz/subagents/spec.yaml +6 -6
- package/src/modules/wizz/subagents/wizz-exec-standard.md +1 -1
- package/src/modules/wizz/subagents/wizz-exec-strong.md +1 -1
- package/src/skills-lib/_shared/complexity-signal.md +8 -9
- package/src/skills-lib/_shared/dispatch-rule.yaml +21 -0
- package/src/skills-lib/{cerebro → wizz-cerebro}/SKILL.md +1 -1
- package/src/skills-lib/{ctc-align → wizz-ctc-align}/SKILL.md +1 -1
- package/src/skills-lib/{enhance-prompt → wizz-enhance-prompt}/SKILL.md +1 -1
- package/src/skills-lib/{find-skills → wizz-find-skills}/SKILL.md +1 -1
- package/src/skills-lib/wizz-hook-extractor/LICENSE +21 -0
- package/src/skills-lib/wizz-hook-extractor/SKILL.md +56 -0
- package/src/skills-lib/wizz-hook-extractor/references/examples.md +59 -0
- package/src/skills-lib/wizz-hook-extractor/references/formulas.md +136 -0
- package/src/skills-lib/wizz-hook-extractor/scripts/read-post.sh +83 -0
- package/src/skills-lib/wizz-huashu-design/references/sfx-library.md +1 -1
- package/src/skills-lib/wizz-huashu-design/scripts/add-music.sh +1 -1
- package/src/skills-lib/wizz-huashu-design/scripts/html2pptx.js +45 -37
- package/src/skills-lib/wizz-huashu-design/scripts/mix-voiceover.sh +1 -1
- package/src/skills-lib/wizz-huashu-design/scripts/narrate-pipeline.mjs +11 -5
- package/src/skills-lib/wizz-huashu-design/scripts/test_scripts.py +209 -0
- package/src/skills-lib/wizz-huashu-design/scripts/tts-doubao.mjs +42 -28
- package/src/skills-lib/wizz-ig-profile-optimizer/LICENSE +21 -0
- package/src/skills-lib/wizz-ig-profile-optimizer/SKILL.md +74 -0
- package/src/skills-lib/wizz-ig-profile-optimizer/scripts/capture-profile.sh +41 -0
- package/src/skills-lib/wizz-ig-profile-optimizer/scripts/limits.mjs +82 -0
- package/src/skills-lib/wizz-inicio-de-projeto/SKILL.md +1 -1
- package/src/skills-lib/wizz-launch-readiness/SKILL.md +7 -7
- package/src/skills-lib/wizz-launch-readiness/references/02-seguranca.md +3 -3
- package/src/skills-lib/wizz-launch-readiness/references/03-seo-descoberta.md +5 -5
- package/src/skills-lib/wizz-launch-readiness/references/04-analytics-medicao.md +1 -1
- package/src/skills-lib/wizz-launch-readiness/references/05-conteudo-prova-social.md +6 -6
- package/src/skills-lib/wizz-launch-readiness/references/06-legal-lgpd.md +1 -1
- package/src/skills-lib/wizz-launch-readiness/references/08-fusao-priorizacao.md +1 -1
- package/src/skills-lib/wizz-offer-forge/SKILL.md +3 -3
- package/src/skills-lib/{pesquisa-de-publico-do-piva → wizz-pesquisa-de-publico-do-piva}/README.md +1 -1
- package/src/skills-lib/{pesquisa-de-publico-do-piva → wizz-pesquisa-de-publico-do-piva}/SKILL.md +1 -1
- package/src/skills-lib/wizz-router/SKILL.md +3 -3
- package/src/skills-lib/wizz-router/references/routing-table-flat.md +11 -9
- package/src/skills-lib/{security-audit-pentest → wizz-security-audit-pentest}/SKILL.md +1 -1
- package/src/skills-lib/{site-launch-kit → wizz-site-launch-kit}/SKILL.md +1 -1
- package/src/skills-lib/wizz-video-effects/SKILL.md +4 -3
- package/src/skills-lib/wizz-video-effects/references/data-contract.md +3 -3
- package/src/skills-lib/wizz-video-effects/scripts/fill_breaths.py +15 -1
- package/src/skills-lib/wizz-video-effects/scripts/test_fill_breaths.py +20 -0
- package/src/skills-lib/wizz-video-styles/SKILL.md +11 -5
- package/src/skills-lib/wizz-video-styles/data/recipes.json +65 -2
- package/src/skills-lib/wizz-video-styles/data/references.json +386 -1
- package/src/skills-lib/wizz-video-styles/data/styles.json +109 -0
- package/src/skills-lib/wizz-video-styles/references/generated-prompts.md +70 -0
- package/src/skills-lib/wizz-video-styles/references/measured-patterns.md +2 -0
- package/src/skills-lib/wizz-video-styles/references/style-picker.md +8 -4
- package/src/skills-lib/wizz-video-styles/references/styles/tipografia-cinetica.md +68 -0
- package/src/skills-lib/wizz-video-styles/references/styles/ui-morph.md +68 -0
- package/src/skills-lib/wizz-video-styles/scripts/analyze.sh +23 -10
- package/src/skills-lib/wizz-video-styles/scripts/beat_grid.py +68 -0
- package/src/skills-lib/wizz-video-styles/scripts/references.mjs +265 -0
- package/src/skills-lib/wizz-web-security/SKILL.md +1 -1
- package/tools/fetch-assets.mjs +1 -1
- package/tools/hooks/README.md +18 -14
- package/tools/hooks/rtk-rewrite.js +95 -0
- package/tools/hooks/security-defensive-context.js +17 -1
- package/tools/hooks/session-rules.js +25 -5
- package/tools/hooks/wizz-arrumar.js +126 -12
- package/tools/hooks/wizz-artifact-guard.js +41 -7
- package/tools/hooks/wizz-cerebro-lib.js +239 -0
- package/tools/hooks/wizz-close-reminder.js +124 -0
- package/tools/hooks/wizz-debug-lib.js +55 -0
- package/tools/hooks/wizz-decision-trace.js +19 -2
- package/tools/hooks/wizz-feature.js +20 -4
- package/tools/hooks/wizz-router-enforce.js +30 -13
- package/tools/installer/command-text.js +243 -0
- package/tools/installer/commands/catalog.js +2 -0
- package/tools/installer/commands/doctor.js +10 -12
- package/tools/installer/commands/fetch-assets.js +5 -5
- package/tools/installer/commands/install.js +6 -7
- package/tools/installer/commands/resolve.js +17 -17
- package/tools/installer/commands/trace-report.js +40 -37
- package/tools/installer/core/doctor.js +60 -55
- package/tools/installer/core/hooks-settings.js +88 -14
- package/tools/installer/core/hooks-smoke.js +134 -0
- package/tools/installer/core/installer.js +61 -52
- package/tools/installer/core/quick-update.js +2 -3
- package/tools/installer/install-text.js +201 -0
- package/tools/installer/modules/channel-plan.js +2 -2
- package/tools/installer/modules/env-vars.js +5 -6
- package/tools/installer/modules/skills-lib.js +1 -1
- package/tools/installer/ui.js +17 -9
- /package/src/skills-lib/{cerebro → wizz-cerebro}/references/comandos-pesquisa-e-memoria.md +0 -0
- /package/src/skills-lib/{cerebro → wizz-cerebro}/references/comandos-rotina-e-captura.md +0 -0
- /package/src/skills-lib/{cerebro → wizz-cerebro}/references/comandos-sessao.md +0 -0
- /package/src/skills-lib/{ctc-align → wizz-ctc-align}/scripts/align.py +0 -0
- /package/src/skills-lib/{enhance-prompt → wizz-enhance-prompt}/README.md +0 -0
- /package/src/skills-lib/{enhance-prompt → wizz-enhance-prompt}/references/KEYWORDS.md +0 -0
- /package/src/skills-lib/{pesquisa-de-publico-do-piva → wizz-pesquisa-de-publico-do-piva}/assets/template.html +0 -0
- /package/src/skills-lib/{pesquisa-de-publico-do-piva → wizz-pesquisa-de-publico-do-piva}/prompts.md +0 -0
- /package/src/skills-lib/{security-audit-pentest → wizz-security-audit-pentest}/references/stage-1-validacao-entrada.md +0 -0
- /package/src/skills-lib/{security-audit-pentest → wizz-security-audit-pentest}/references/stage-2-autorizacao.md +0 -0
- /package/src/skills-lib/{security-audit-pentest → wizz-security-audit-pentest}/references/stage-3-abuso-volume.md +0 -0
- /package/src/skills-lib/{security-audit-pentest → wizz-security-audit-pentest}/references/stage-4-vazamento-resposta.md +0 -0
- /package/src/skills-lib/{security-audit-pentest → wizz-security-audit-pentest}/references/stage-5-fusao-plano.md +0 -0
- /package/src/skills-lib/{site-launch-kit → wizz-site-launch-kit}/references/01-cta-principal.md +0 -0
- /package/src/skills-lib/{site-launch-kit → wizz-site-launch-kit}/references/02-barra-fixa-mobile.md +0 -0
- /package/src/skills-lib/{site-launch-kit → wizz-site-launch-kit}/references/03-tempo-de-resposta.md +0 -0
- /package/src/skills-lib/{site-launch-kit → wizz-site-launch-kit}/references/04-prova-social.md +0 -0
- /package/src/skills-lib/{site-launch-kit → wizz-site-launch-kit}/references/05-faq-decisao.md +0 -0
- /package/src/skills-lib/{site-launch-kit → wizz-site-launch-kit}/references/06-imagens-reais.md +0 -0
- /package/src/skills-lib/{site-launch-kit → wizz-site-launch-kit}/references/07-endereco-como-chegar.md +0 -0
- /package/src/skills-lib/{site-launch-kit → wizz-site-launch-kit}/references/08-titles.md +0 -0
- /package/src/skills-lib/{site-launch-kit → wizz-site-launch-kit}/references/09-open-graph.md +0 -0
- /package/src/skills-lib/{site-launch-kit → wizz-site-launch-kit}/references/10-breadcrumbs.md +0 -0
- /package/src/skills-lib/{site-launch-kit → wizz-site-launch-kit}/references/11-alt-text.md +0 -0
- /package/src/skills-lib/{site-launch-kit → wizz-site-launch-kit}/references/12-schema-local.md +0 -0
- /package/src/skills-lib/{site-launch-kit → wizz-site-launch-kit}/references/13-robots-sitemap.md +0 -0
- /package/src/skills-lib/{site-launch-kit → wizz-site-launch-kit}/references/14-privacidade-termos.md +0 -0
- /package/src/skills-lib/{site-launch-kit → wizz-site-launch-kit}/references/15-medicao.md +0 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Kinetic typography and lyric video (mexicat P(doom), PixelValet, Ninh Truong)
|
|
2
|
+
|
|
3
|
+
Measured on 5 shorts (en) in: product / SaaS / marketing / business / law, education / science / comedy, music / artists / lyric videos.
|
|
4
|
+
|
|
5
|
+
## Measured
|
|
6
|
+
|
|
7
|
+
| Metric | Value |
|
|
8
|
+
|---|---|
|
|
9
|
+
| Reach (views) | max 1,215,529 · median 353,630 |
|
|
10
|
+
| Cuts per 10 s | min 0.63 · median 3.57 · max 5.54 |
|
|
11
|
+
| Median scene length | 1.54 s |
|
|
12
|
+
| Music bed (continuous / faint / none) | 3 / 0 / 2 |
|
|
13
|
+
| Cuts with a sound effect on them | 31/94 (33%) |
|
|
14
|
+
|
|
15
|
+
Cuts come from scene detection (it undercounts overlays and cards); music and SFX from an audio heuristic that classifies type, never names the sound ([../research-method.md](../research-method.md)).
|
|
16
|
+
|
|
17
|
+
## Material it needs
|
|
18
|
+
|
|
19
|
+
An audio track with words: a song with its lyrics, or a voice-over with its script. Nobody on camera, no footage. Everything is generated (HyperFrames, three.js, canvas). Word timing from ctc-align; a beat grid when there is music.
|
|
20
|
+
|
|
21
|
+
## When it fits
|
|
22
|
+
|
|
23
|
+
Music releases and lyric videos, manifestos, a strong opinion read aloud, brand promos built on a slogan, poetry. Shares and rewatches; weak for teaching step by step.
|
|
24
|
+
|
|
25
|
+
## Recipe
|
|
26
|
+
|
|
27
|
+
**Look**
|
|
28
|
+
|
|
29
|
+
- The words ARE the image: one keyword huge in the frame, the rest of the line small. No caption strip at the bottom.
|
|
30
|
+
- Plain background (black, or a flat colour that changes per section) or one generated world the camera travels through (a map, a grid in perspective, a 3D object the line wraps around).
|
|
31
|
+
- Each word is animated by what it means: PAIN breaks, CHEERFUL bounces, LIGHT lights up, a word curves around the object it names.
|
|
32
|
+
- Code-rendered pieces add film treatment: bloom, halation, grain, a thin HUD in mono.
|
|
33
|
+
|
|
34
|
+
**Palette:** Dark base (#0A0A0A or a deep teal #0F3B3A) plus ONE glowing accent (orange #FF6A1A in P(doom), coral in Maps) and off-white text. Flat-colour variant: one saturated background per word or section, text in white or ink.
|
|
35
|
+
|
|
36
|
+
**Typography:** Two or three families with high contrast: a heavy grotesk or condensed caps for the keyword (Archivo Black, Bebas Neue), a mono for small lines and HUD (IBM Plex Mono), optionally a serif italic for a soft word (Cormorant Garamond). Mix weights inside the same line.
|
|
37
|
+
|
|
38
|
+
**Motion**
|
|
39
|
+
|
|
40
|
+
- Words enter on the syllable they are sung or spoken (forced alignment, never guessed); the keyword lands on the beat.
|
|
41
|
+
- Scene changes snap to the downbeat. P(doom) changes every ~0.3 s at peaks; the calm variant is a single continuous camera move for a whole verse.
|
|
42
|
+
- The camera moves through the type (push, orbit, travel across a map) instead of the text sliding over a still frame.
|
|
43
|
+
- Hold the last word of a line a beat longer before it leaves.
|
|
44
|
+
|
|
45
|
+
**Captions:** None separate: the type is the caption. Keep the full line readable somewhere (small, mono, corner) when the keyword alone would lose the sense.
|
|
46
|
+
|
|
47
|
+
**Hook (0 to 3 s):** The first sung or spoken word, huge, in the accent colour, landing on the first downbeat (0 to 1 s). No logo intro.
|
|
48
|
+
|
|
49
|
+
**CTA:** The title or slogan locked up big and still for 1 to 2 s, the accent lit; one small line (artist, handle, link) under it.
|
|
50
|
+
|
|
51
|
+
**Music:** The track itself is the whole soundtrack. Lyric videos: song at full level, no extra bed. Voice-over promos: a beat-driven bed under the voice.
|
|
52
|
+
|
|
53
|
+
**SFX:** Song-driven pieces: almost none (5 of 56 cuts in P(doom), 0 of 2 in Maps). Promo variant: hits on every word change (9/13 and 16/20 cuts), which the heuristic partly confuses with the kick. Never add whooshes over a song.
|
|
54
|
+
|
|
55
|
+
## Do not
|
|
56
|
+
|
|
57
|
+
- Karaoke subtitles at the bottom over a static background: that is a lyric 'overlay', not this style.
|
|
58
|
+
- Guessing word timing by ear; align it (ctc-align) and snap scenes to the beat grid.
|
|
59
|
+
- More than one accent colour, or a different font for every word.
|
|
60
|
+
- Shipping 16:9: every measured reference is horizontal; recompose for 9:16 with the keyword in the upper-middle and nothing in the bottom 20%.
|
|
61
|
+
|
|
62
|
+
## Watch before editing
|
|
63
|
+
|
|
64
|
+
- [PiXeL ARTWORKS](https://www.youtube.com/watch?v=9ngNySFSWWU) · 1,215,529 views
|
|
65
|
+
- [PixelValet](https://www.youtube.com/watch?v=Kt8vGm8_ekQ) · 928,526 views
|
|
66
|
+
- [Cinthya Studios](https://www.youtube.com/watch?v=A0CKFb24MJ8) · 353,630 views
|
|
67
|
+
- [Ninh Truong](https://www.youtube.com/watch?v=FOMWS5pChuA) · 243,944 views
|
|
68
|
+
- [mexicat](https://www.youtube.com/watch?v=5EoO5413dBY) · 120,706 views
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# UI morph: one shape, many UI states (twoclipping, verbove, AnnaCher)
|
|
2
|
+
|
|
3
|
+
Measured on 6 shorts (en) in: dev / AI / build in public (EN), product / SaaS / marketing / business / law.
|
|
4
|
+
|
|
5
|
+
## Measured
|
|
6
|
+
|
|
7
|
+
| Metric | Value |
|
|
8
|
+
|---|---|
|
|
9
|
+
| Reach (views) | max 1,164,424 · median 2,786 |
|
|
10
|
+
| Cuts per 10 s | min 0.0 · median 0.0 · max 0.0 |
|
|
11
|
+
| Median scene length | 17.05 s |
|
|
12
|
+
| Music bed (continuous / faint / none) | 4 / 0 / 2 |
|
|
13
|
+
| Cuts with a sound effect on them | 0/0 |
|
|
14
|
+
|
|
15
|
+
Cuts come from scene detection (it undercounts overlays and cards); music and SFX from an audio heuristic that classifies type, never names the sound ([../research-method.md](../research-method.md)).
|
|
16
|
+
|
|
17
|
+
## Material it needs
|
|
18
|
+
|
|
19
|
+
The product's real screens or a list of 8 to 12 UI states that tell its story (button, loader, player, slider, toggle, tabs, chart, ⌘K, toast), one accent colour and a track around 120 BPM. Nobody on camera, no footage: everything is drawn in code (HyperFrames).
|
|
20
|
+
|
|
21
|
+
## When it fits
|
|
22
|
+
|
|
23
|
+
App and SaaS launches, a feature announcement, a product promo for the site or the store page, a portfolio piece. Strong on X and LinkedIn with a product audience; it shows the product working instead of describing it. Weak for teaching step by step or for anything that needs a face.
|
|
24
|
+
|
|
25
|
+
## Recipe
|
|
26
|
+
|
|
27
|
+
**Look**
|
|
28
|
+
|
|
29
|
+
- One shape on screen the whole time, never cut: it becomes button, loader, player, slider, toggle, tabs, chart, ⌘K and toast, changing size, corners and colour (the same language as the iPhone Dynamic Island).
|
|
30
|
+
- Warm light grey canvas (#EDEBE6), components in black and white, at most one accent colour; a dark variant (navy with one yellow) for a product film.
|
|
31
|
+
- Real UI and real data: the product's own labels and numbers, never lorem ipsum or generic components.
|
|
32
|
+
- A cursor drives every change with real clicks and drags; the camera zooms so each state fills the frame.
|
|
33
|
+
|
|
34
|
+
**Palette:** Warm grey #EDEBE6 or beige #E6E2DA, black #111, white, plus ONE accent (orange, peach or yellow) used only where the eye must go. Dark variant: #0B1020 base, one yellow accent.
|
|
35
|
+
|
|
36
|
+
**Typography:** One UI sans (Inter, Geist or the product's own font) in regular and semibold; mono only for a URL or a code line. No display fonts.
|
|
37
|
+
|
|
38
|
+
**Motion**
|
|
39
|
+
|
|
40
|
+
- Every state grows out of the previous one by a spring (size, corner radius, colour), with at most one small overshoot; content swaps with a short blur.
|
|
41
|
+
- Something happens on every beat; the state changes land on the downbeats of the beat grid (scripts/beat_grid.py), not on a hand-counted 120 BPM.
|
|
42
|
+
- Build it as a pure function of time (seek(t)): closed-form springs, no CSS transitions or timers, so every frame renders the same. HyperFrames does this natively.
|
|
43
|
+
- The last frame equals the first, so the piece loops.
|
|
44
|
+
|
|
45
|
+
**Captions:** None. The text is the interface's own (button labels, track title, chart numbers). A single short line at most, in the UI font, when the product needs one sentence.
|
|
46
|
+
|
|
47
|
+
**Hook (0 to 3 s):** The first state already on screen at 0 s (a button or the brand pill) and the cursor arriving on the first beat. No logo intro, no title card.
|
|
48
|
+
|
|
49
|
+
**CTA:** The shape returns to the first state or closes on the brand pill with the URL in small mono under it, held 1 to 2 s.
|
|
50
|
+
|
|
51
|
+
**Music:** The track is the whole soundtrack: a continuous bed in 4 of 6 measured pieces, around -14 LUFS. Pick an electronic or lo-fi track with a clear kick so the beat grid is reliable.
|
|
52
|
+
|
|
53
|
+
**SFX:** Sparse and tied to UI events: a soft click on a press, a low tick on a snap. The measured pieces range from none to a hit on most state changes; never whooshes on every move.
|
|
54
|
+
|
|
55
|
+
## Do not
|
|
56
|
+
|
|
57
|
+
- Cutting between screens: the moment it cuts it becomes a screen recording, not this style.
|
|
58
|
+
- Bouncy easing, particles, glow, gradients on the UI chrome, icons with uneven stroke, dead time, template look.
|
|
59
|
+
- Generic components instead of the product's real UI and numbers.
|
|
60
|
+
- Shipping square or 16:9: every measured piece is 1:1 or 16:9; recompose for 9:16 with the shape in the upper-middle and nothing in the bottom 20 %.
|
|
61
|
+
|
|
62
|
+
## Watch before editing
|
|
63
|
+
|
|
64
|
+
- [zero](https://x.com/twoclipping/status/2103273003555402193) · 1,164,424 views
|
|
65
|
+
- [Martijn Verbove](https://x.com/verbove/status/2103483957266268381) · 17,438 views
|
|
66
|
+
- [Daniel Haida](https://x.com/daniel_haida/status/2104139720829636937) · 3,968 views
|
|
67
|
+
- [Anna Chernikova](https://x.com/AnnaCher___/status/2103571096549433425) · 1,605 views
|
|
68
|
+
- [Nez](https://x.com/nezbuilds/status/2103487427365294391) · 519 views
|
|
@@ -1,25 +1,38 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# Measures one public
|
|
2
|
+
# Measures one public short: download (720p), cuts (scene detection),
|
|
3
3
|
# audio (music bed, SFX type heuristic, loudness) and a 16-frame contact sheet.
|
|
4
|
-
# Usage: analyze.sh <youtube_id> <out_dir> → <out_dir>/<id>.json + <id>-sheet.png
|
|
4
|
+
# Usage: analyze.sh <youtube_id|url> <out_dir> → <out_dir>/<id>.json + <id>-sheet.png
|
|
5
|
+
# A URL (an X post, any page yt-dlp reads) is measured the same way; its id is
|
|
6
|
+
# x-<author>-<status> for X and a sanitised slug otherwise (2026-10-15: the
|
|
7
|
+
# code-generated UI pieces of the ui-morph style live on X, not on YouTube).
|
|
5
8
|
# The .mp4 is deleted at the end; read the contact sheet before describing the look.
|
|
6
9
|
set -euo pipefail
|
|
7
|
-
[ $# -eq 2 ] || { echo "uso: analyze.sh <youtube_id> <out_dir>" >&2; exit 2; }
|
|
10
|
+
[ $# -eq 2 ] || { echo "uso: analyze.sh <youtube_id|url> <out_dir>" >&2; exit 2; }
|
|
8
11
|
id="$1"; out="$2"
|
|
9
|
-
[[ "$id" =~ ^
|
|
12
|
+
if [[ "$id" =~ ^https?:// ]]; then
|
|
13
|
+
url="$id"
|
|
14
|
+
if [[ "$url" =~ (x|twitter)\.com/([A-Za-z0-9_]+)/status/([0-9]+) ]]; then
|
|
15
|
+
id="x-${BASH_REMATCH[2]}-${BASH_REMATCH[3]}"
|
|
16
|
+
else
|
|
17
|
+
id=$(printf '%s' "$url" | sed -E 's#^https?://##; s#[^A-Za-z0-9_-]+#-#g' | cut -c1-60)
|
|
18
|
+
fi
|
|
19
|
+
else
|
|
20
|
+
[[ "$id" =~ ^[A-Za-z0-9_-]{11}$ ]] || { echo "id inválido: $id" >&2; exit 2; }
|
|
21
|
+
url="https://www.youtube.com/shorts/$id"
|
|
22
|
+
fi
|
|
10
23
|
mkdir -p "$out"; cd "$out"
|
|
11
24
|
f="./$id" # prefixo ./ : ID que começa com hífen não vira opção do ffmpeg
|
|
12
|
-
|
|
25
|
+
# .part: a first download that fails (HTTP 403) leaves it behind before the android fallback runs.
|
|
26
|
+
trap 'rm -f "$f.wav" "$f.mp4" "$f".*.part' EXIT
|
|
13
27
|
# Python with librosa + numpy: set VIDEO_STYLES_PY to a venv python, or python3 must have them.
|
|
14
28
|
PY="${VIDEO_STYLES_PY:-python3}"
|
|
15
29
|
"$PY" -c "import librosa, numpy" 2>/dev/null || { echo "precisa de librosa e numpy: pip install librosa numpy (ou VIDEO_STYLES_PY=/caminho/do/python)" >&2; exit 2; }
|
|
16
30
|
for bin in yt-dlp ffmpeg ffprobe; do command -v "$bin" >/dev/null || { echo "falta $bin no PATH" >&2; exit 2; }; done
|
|
17
|
-
url="https://www.youtube.com/shorts/$id"
|
|
18
31
|
fmt="bv*[height<=1280][ext=mp4]+ba[ext=m4a]/b[ext=mp4]/b"
|
|
19
32
|
# When YouTube answers "confirm you're not a bot", the android player client usually still works (no cookies used).
|
|
20
33
|
[ -f "$f.mp4" ] || yt-dlp -q --no-warnings -f "$fmt" --merge-output-format mp4 -o "$f.mp4" "$url" \
|
|
21
34
|
|| yt-dlp -q --no-warnings --extractor-args "youtube:player_client=android" -f "$fmt" --merge-output-format mp4 -o "$f.mp4" "$url"
|
|
22
|
-
meta_fmt="%(view_count)s|%(like_count)s|%(channel)s|%(upload_date)s|%(title)s"
|
|
35
|
+
meta_fmt="%(view_count)s|%(like_count)s|%(channel,uploader)s|%(upload_date)s|%(title)s"
|
|
23
36
|
yt-dlp -q --no-warnings --skip-download --print "$meta_fmt" "$url" > "$f.meta" \
|
|
24
37
|
|| yt-dlp -q --no-warnings --skip-download --extractor-args "youtube:player_client=android" --print "$meta_fmt" "$url" > "$f.meta" \
|
|
25
38
|
|| echo "NA|NA|NA|NA|NA" > "$f.meta"
|
|
@@ -31,9 +44,9 @@ lufs=$(ffmpeg -i "$f.mp4" -af ebur128 -f null - 2>&1 | grep -E "^\s+I:" | tail -
|
|
|
31
44
|
d=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$f.mp4")
|
|
32
45
|
step=$(python3 -c "print(max(1.0, $d/16))")
|
|
33
46
|
ffmpeg -loglevel error -y -i "$f.mp4" -vf "fps=1/$step,scale=200:-1,tile=8x2" -frames:v 1 "$f-sheet.png"
|
|
34
|
-
"$PY" - "$id" "$lufs" <<'PY'
|
|
47
|
+
"$PY" - "$id" "$lufs" "$url" <<'PY'
|
|
35
48
|
import librosa, numpy as np, json, sys
|
|
36
|
-
id, lufs = sys.argv[1], sys.argv[2]
|
|
49
|
+
id, lufs, url = sys.argv[1], sys.argv[2], sys.argv[3]
|
|
37
50
|
y, sr = librosa.load(f"{id}.wav", sr=22050); dur = len(y)/sr
|
|
38
51
|
cuts = [float(x) for x in open(f"{id}.cuts").read().split()]
|
|
39
52
|
H, P = librosa.effects.hpss(y, margin=2.0)
|
|
@@ -57,7 +70,7 @@ on_cut = sum(1 for c in cuts if any(abs(t-c) < 0.15 for t,_ in events))
|
|
|
57
70
|
shots = np.diff([0]+cuts+[dur]) if dur else np.array([0])
|
|
58
71
|
bed = float(np.percentile(rh,15)/(np.percentile(rh,95)+1e-9))
|
|
59
72
|
meta = open(f"{id}.meta").read().strip().split("|", 4)
|
|
60
|
-
json.dump({"id": id, "url":
|
|
73
|
+
json.dump({"id": id, "url": url, "views": meta[0], "likes": meta[1], "channel": meta[2], "upload_date": meta[3], "title": meta[4],
|
|
61
74
|
"duration_s": round(dur,1), "cuts": len(cuts), "cuts_per_10s": round(len(cuts)/dur*10,2), "shot_median_s": round(float(np.median(shots)),2), "shot_mean_s": round(float(shots.mean()),2),
|
|
62
75
|
"music_bed_ratio": round(bed,3), "music_bed_guess": "continua" if bed > 0.13 else ("fraca" if bed > 0.07 else "sem"),
|
|
63
76
|
"sfx_counts": kinds, "sfx_per_10s": round(sum(kinds.values())/dur*10,2), "sfx_on_cuts": f"{on_cut}/{len(cuts)}", "sfx_events": events[:40],
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Beat grid of a song: tempo, beats and downbeats, so scene cuts can snap to the music.
|
|
2
|
+
|
|
3
|
+
Usage: python3 beat_grid.py <audio_file> [--meter 4] [--fps 30] > grid.json
|
|
4
|
+
|
|
5
|
+
Prints JSON: {"bpm", "meter", "duration_s", "beats": [s...], "downbeats": [s...],
|
|
6
|
+
"downbeat_frames": [frame...]}. Needs librosa + numpy (same Python as analyze.sh;
|
|
7
|
+
set VIDEO_STYLES_PY and call it with that interpreter).
|
|
8
|
+
|
|
9
|
+
Downbeats are estimated, not detected: of the `meter` possible phases, the one
|
|
10
|
+
whose beats carry the most low-frequency onset energy is taken as beat 1. Check
|
|
11
|
+
two or three of them by ear before cutting a whole film on the grid.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
import json
|
|
16
|
+
import sys
|
|
17
|
+
|
|
18
|
+
import librosa
|
|
19
|
+
import numpy as np
|
|
20
|
+
|
|
21
|
+
HOP = 512
|
|
22
|
+
LOW_HZ = 150
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def downbeat_phase(y_perc, sr, beat_frames, meter):
|
|
26
|
+
spec = np.abs(librosa.stft(y_perc, hop_length=HOP))
|
|
27
|
+
freqs = librosa.fft_frequencies(sr=sr)
|
|
28
|
+
low_energy = spec[freqs < LOW_HZ].sum(axis=0)
|
|
29
|
+
at_beats = low_energy[np.minimum(beat_frames, len(low_energy) - 1)]
|
|
30
|
+
scores = [at_beats[phase::meter].mean() if len(at_beats[phase::meter]) else 0.0 for phase in range(meter)]
|
|
31
|
+
return int(np.argmax(scores))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def beat_grid(path, meter, fps):
|
|
35
|
+
y, sr = librosa.load(path, sr=22050, mono=True)
|
|
36
|
+
_, y_perc = librosa.effects.hpss(y)
|
|
37
|
+
_, beat_frames = librosa.beat.beat_track(y=y_perc, sr=sr, hop_length=HOP)
|
|
38
|
+
if len(beat_frames) < meter:
|
|
39
|
+
sys.exit(f"too few beats found in {path}: {len(beat_frames)} (is there a steady rhythm?)")
|
|
40
|
+
beats = librosa.frames_to_time(beat_frames, sr=sr, hop_length=HOP)
|
|
41
|
+
phase = downbeat_phase(y_perc, sr, beat_frames, meter)
|
|
42
|
+
downbeats = beats[phase::meter]
|
|
43
|
+
return {
|
|
44
|
+
# librosa's tempo and single beat intervals are quantised to the hop; the slope of a
|
|
45
|
+
# line fitted through every beat time averages that out.
|
|
46
|
+
"bpm": round(60.0 / float(np.polyfit(np.arange(len(beats)), beats, 1)[0]), 3),
|
|
47
|
+
"meter": meter,
|
|
48
|
+
"duration_s": round(len(y) / sr, 3),
|
|
49
|
+
"beats": [round(float(t), 3) for t in beats],
|
|
50
|
+
"downbeats": [round(float(t), 3) for t in downbeats],
|
|
51
|
+
"downbeat_frames": [int(round(t * fps)) for t in downbeats],
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def main():
|
|
56
|
+
parser = argparse.ArgumentParser(description=__doc__.split("\n")[0])
|
|
57
|
+
parser.add_argument("audio")
|
|
58
|
+
parser.add_argument("--meter", type=int, default=4)
|
|
59
|
+
parser.add_argument("--fps", type=int, default=30)
|
|
60
|
+
args = parser.parse_args()
|
|
61
|
+
if args.meter < 2 or args.fps < 1:
|
|
62
|
+
sys.exit("--meter must be >= 2 and --fps >= 1")
|
|
63
|
+
json.dump(beat_grid(args.audio, args.meter, args.fps), sys.stdout, indent=1)
|
|
64
|
+
print()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
if __name__ == "__main__":
|
|
68
|
+
main()
|
|
@@ -0,0 +1,265 @@
|
|
|
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
|
+
'ui-morph': { categories: ['motion', 'explainer', '3d', 'interactive'], match: /UI states|one shape|morph|UI moments|product film|inside the app/i },
|
|
35
|
+
'motion-editorial': { categories: ['motion', 'explainer'], match: /\bUI\b|product|launch|saas|\bapp\b|logo|promo|brand/i },
|
|
36
|
+
'explicador-animado': { categories: ['explainer'], match: /./ },
|
|
37
|
+
'quadro-branco': { categories: ['explainer', 'motion'], match: /whiteboard|hand-drawn|sketch|doodle/i },
|
|
38
|
+
'meme-rapido': { categories: ['motion'], match: /brain ?rot|meme|chaotic/i },
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
function parseArgs(argv) {
|
|
42
|
+
const [command, ...rest] = argv;
|
|
43
|
+
const options = { command, project: process.cwd(), n: 4, offline: false, json: false };
|
|
44
|
+
for (let i = 0; i < rest.length; i++) {
|
|
45
|
+
const flag = rest[i];
|
|
46
|
+
if (flag === '--offline') options.offline = true;
|
|
47
|
+
else if (flag === '--json') options.json = true;
|
|
48
|
+
else if (['--style', '--project', '--n', '--piece', '--refs'].includes(flag)) options[flag.slice(2)] = rest[++i];
|
|
49
|
+
else fail(`unknown argument: ${flag}`);
|
|
50
|
+
}
|
|
51
|
+
options.n = Number(options.n);
|
|
52
|
+
if (!Number.isInteger(options.n) || options.n < 1 || options.n > 20) fail('--n must be an integer from 1 to 20');
|
|
53
|
+
return options;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function fail(message) {
|
|
57
|
+
process.stderr.write(`references: ${message}\n`);
|
|
58
|
+
process.exit(2);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function readJson(file, fallback) {
|
|
62
|
+
try {
|
|
63
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
64
|
+
} catch (error) {
|
|
65
|
+
if (error.code === 'ENOENT') return fallback;
|
|
66
|
+
fail(`cannot read ${file}: ${error.message}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function writeJsonAtomic(file, value) {
|
|
71
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
72
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
73
|
+
fs.writeFileSync(tmp, `${JSON.stringify(value, null, 2)}\n`);
|
|
74
|
+
fs.renameSync(tmp, file);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function loadHistory(project) {
|
|
78
|
+
const history = readJson(path.join(project, HISTORY_REL), []);
|
|
79
|
+
if (!Array.isArray(history)) fail(`${HISTORY_REL} must be a JSON array`);
|
|
80
|
+
return history;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function knownStyles() {
|
|
84
|
+
return Object.keys(readJson(path.join(SKILL_DIR, 'data', 'recipes.json'), { styles: {} }).styles);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function requireStyle(style) {
|
|
88
|
+
if (!style) fail('--style is required');
|
|
89
|
+
const styles = knownStyles();
|
|
90
|
+
if (!styles.includes(style)) fail(`unknown style "${style}". Known: ${styles.join(', ')}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const isHttpsUrl = (value) => typeof value === 'string' && value.startsWith('https://');
|
|
94
|
+
|
|
95
|
+
function validVideos(data) {
|
|
96
|
+
if (!Array.isArray(data)) return null;
|
|
97
|
+
return data.filter(
|
|
98
|
+
(v) => v && typeof v.slug === 'string' && typeof v.category === 'string' && isHttpsUrl(v.post_url) && isHttpsUrl(v.skillry_url),
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function cachePath() {
|
|
103
|
+
return process.env.WIZZ_VIDEO_STYLES_CACHE || path.join(os.homedir(), '.cache', 'wizz-video-styles', 'opus-videos.json');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function loadGeneratedList(offline) {
|
|
107
|
+
const file = cachePath();
|
|
108
|
+
let cached = null;
|
|
109
|
+
let age = Infinity;
|
|
110
|
+
try {
|
|
111
|
+
age = Date.now() - fs.statSync(file).mtimeMs;
|
|
112
|
+
cached = validVideos(JSON.parse(fs.readFileSync(file, 'utf8')));
|
|
113
|
+
} catch {
|
|
114
|
+
cached = null;
|
|
115
|
+
}
|
|
116
|
+
if (cached && (offline || age < CACHE_MAX_AGE_MS)) return { videos: cached, source: `cache (${Math.round(age / 3_600_000)} h old)` };
|
|
117
|
+
if (offline) return { videos: [], source: 'offline and no cache' };
|
|
118
|
+
try {
|
|
119
|
+
const response = await fetch(LIST_URL, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
|
|
120
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
121
|
+
const videos = validVideos(await response.json());
|
|
122
|
+
if (!videos) throw new Error('unexpected format');
|
|
123
|
+
writeJsonAtomic(file, videos);
|
|
124
|
+
return { videos, source: 'fetched now' };
|
|
125
|
+
} catch (error) {
|
|
126
|
+
if (cached) return { videos: cached, source: `cache, refresh failed (${error.message})` };
|
|
127
|
+
return { videos: [], source: `unavailable (${error.message})` };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Greedy pick: unused first, then a new channel/author before repeating one.
|
|
132
|
+
function pickDiverse(items, keyOf, used, count) {
|
|
133
|
+
const fresh = items.filter((item) => !used.has(item.id));
|
|
134
|
+
const chosen = [];
|
|
135
|
+
const seenKeys = new Set();
|
|
136
|
+
for (const pass of [true, false]) {
|
|
137
|
+
for (const item of fresh) {
|
|
138
|
+
if (chosen.length >= count) break;
|
|
139
|
+
if (chosen.includes(item) || (pass && seenKeys.has(keyOf(item)))) continue;
|
|
140
|
+
chosen.push(item);
|
|
141
|
+
seenKeys.add(keyOf(item));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return { chosen, exhausted: fresh.length === 0 && items.length > 0 };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function measuredFor(style) {
|
|
148
|
+
return readJson(path.join(SKILL_DIR, 'data', 'references.json'), [])
|
|
149
|
+
.filter((r) => r.style === style)
|
|
150
|
+
.sort((a, b) => Number(b.views) - Number(a.views))
|
|
151
|
+
.map((r) => ({ id: r.id, kind: 'measured', who: r.channel, url: r.url, note: `${Number(r.views).toLocaleString('en-US')} views` }));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const PROMPT_KEY_CHARS = 80;
|
|
155
|
+
const promptKey = (prompt) => String(prompt ?? '').toLowerCase().replaceAll(/[^a-z0-9]+/g, ' ').trim().slice(0, PROMPT_KEY_CHARS);
|
|
156
|
+
|
|
157
|
+
// Third-party text: one line, no markdown or HTML that could turn into a link or a tag.
|
|
158
|
+
const excerpt = (prompt) =>
|
|
159
|
+
String(prompt ?? '')
|
|
160
|
+
.replaceAll(/[`<>[\]()]/g, '')
|
|
161
|
+
.replaceAll(/\s+/g, ' ')
|
|
162
|
+
.trim()
|
|
163
|
+
.slice(0, EXCERPT_CHARS);
|
|
164
|
+
|
|
165
|
+
function generatedFor(style, videos) {
|
|
166
|
+
const rule = GENERATED[style];
|
|
167
|
+
if (!rule) return [];
|
|
168
|
+
// The same viral prompt was remade by dozens of people: one remake per prompt is enough.
|
|
169
|
+
const seenPrompts = new Set();
|
|
170
|
+
return videos
|
|
171
|
+
.filter((v) => rule.categories.includes(v.category) && rule.match.test(String(v.prompt ?? '')))
|
|
172
|
+
.sort((a, b) => String(b.added ?? '').localeCompare(String(a.added ?? '')))
|
|
173
|
+
.filter((v) => {
|
|
174
|
+
const key = promptKey(v.prompt);
|
|
175
|
+
if (seenPrompts.has(key)) return false;
|
|
176
|
+
seenPrompts.add(key);
|
|
177
|
+
return true;
|
|
178
|
+
})
|
|
179
|
+
.map((v) => ({
|
|
180
|
+
id: v.slug,
|
|
181
|
+
kind: 'generated',
|
|
182
|
+
who: String(v.author ?? '?'),
|
|
183
|
+
url: v.skillry_url,
|
|
184
|
+
post: v.post_url,
|
|
185
|
+
note: excerpt(v.prompt),
|
|
186
|
+
}));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function styleRepeat(history, style) {
|
|
190
|
+
const recent = history.slice(-REPEAT_WINDOW).map((entry) => entry.style);
|
|
191
|
+
return recent.length === REPEAT_WINDOW && recent.every((s) => s === style);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function pick(options) {
|
|
195
|
+
requireStyle(options.style);
|
|
196
|
+
const history = loadHistory(options.project);
|
|
197
|
+
const used = new Set(history.flatMap((entry) => entry.refs ?? []));
|
|
198
|
+
const measured = pickDiverse(measuredFor(options.style), (r) => r.who, used, Math.min(MEASURED_PER_PICK, options.n));
|
|
199
|
+
let generated = { chosen: [], exhausted: false };
|
|
200
|
+
let source = 'not used for this style (filmed style)';
|
|
201
|
+
if (GENERATED[options.style]) {
|
|
202
|
+
const list = await loadGeneratedList(options.offline);
|
|
203
|
+
source = list.source;
|
|
204
|
+
generated = pickDiverse(generatedFor(options.style, list.videos), (r) => r.who, used, options.n - measured.chosen.length);
|
|
205
|
+
}
|
|
206
|
+
return {
|
|
207
|
+
style: options.style,
|
|
208
|
+
recentStyles: history.slice(-5).map((entry) => entry.style),
|
|
209
|
+
styleRepeated: styleRepeat(history, options.style),
|
|
210
|
+
references: [...measured.chosen, ...generated.chosen],
|
|
211
|
+
measuredExhausted: measured.exhausted,
|
|
212
|
+
generatedExhausted: generated.exhausted,
|
|
213
|
+
generatedSource: source,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function printPick(result) {
|
|
218
|
+
const lines = [`# References for ${result.style}`, ''];
|
|
219
|
+
if (result.recentStyles.length) lines.push(`Last styles in this project: ${result.recentStyles.join(' → ')}`);
|
|
220
|
+
if (result.styleRepeated) {
|
|
221
|
+
lines.push(
|
|
222
|
+
`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.`,
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
lines.push(`Generated list: ${result.generatedSource}`, '');
|
|
226
|
+
for (const ref of result.references) {
|
|
227
|
+
const post = ref.post ? ` · [post](${ref.post})` : '';
|
|
228
|
+
lines.push(`- [${ref.kind}] ${ref.who} · [watch](${ref.url})${post} · id \`${ref.id}\` · ${ref.note}`);
|
|
229
|
+
}
|
|
230
|
+
if (result.references.length === 0) lines.push('- nothing new left for this style');
|
|
231
|
+
if (result.measuredExhausted) lines.push('', 'Every measured reference of this style was already used here: measure new ones (research-method.md).');
|
|
232
|
+
if (result.generatedExhausted) lines.push('', 'Every generated reference of this style was already used here.');
|
|
233
|
+
lines.push('', 'After delivering the piece: node references.mjs record --style <key> --piece <slug> --refs <ids you watched>');
|
|
234
|
+
process.stdout.write(`${lines.join('\n')}\n`);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function record(options) {
|
|
238
|
+
requireStyle(options.style);
|
|
239
|
+
if (!options.piece) fail('--piece is required');
|
|
240
|
+
const refs = String(options.refs ?? '')
|
|
241
|
+
.split(',')
|
|
242
|
+
.map((id) => id.trim())
|
|
243
|
+
.filter(Boolean);
|
|
244
|
+
if (refs.length === 0) fail('--refs is required (ids printed by pick)');
|
|
245
|
+
const history = loadHistory(options.project);
|
|
246
|
+
const entry = { date: new Date().toISOString().slice(0, 10), piece: options.piece, style: options.style, refs };
|
|
247
|
+
writeJsonAtomic(path.join(options.project, HISTORY_REL), [...history, entry]);
|
|
248
|
+
process.stdout.write(`recorded ${options.piece} (${options.style}, ${refs.length} refs) in ${HISTORY_REL}\n`);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function printHistory(options) {
|
|
252
|
+
const history = loadHistory(options.project);
|
|
253
|
+
if (options.json) return process.stdout.write(`${JSON.stringify(history, null, 2)}\n`);
|
|
254
|
+
if (history.length === 0) return process.stdout.write('no pieces recorded yet\n');
|
|
255
|
+
for (const entry of history) process.stdout.write(`${entry.date} · ${entry.piece} · ${entry.style} · ${(entry.refs ?? []).join(', ')}\n`);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const options = parseArgs(process.argv.slice(2));
|
|
259
|
+
if (options.command === 'pick') {
|
|
260
|
+
const result = await pick(options);
|
|
261
|
+
if (options.json) process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
262
|
+
else printPick(result);
|
|
263
|
+
} else if (options.command === 'record') record(options);
|
|
264
|
+
else if (options.command === 'history') printHistory(options);
|
|
265
|
+
else fail('usage: references.mjs pick|record|history (see the header of this file)');
|
|
@@ -32,7 +32,7 @@ The ones that show up most in an audit, with the direct fix. Use as a quick tria
|
|
|
32
32
|
|
|
33
33
|
> The first four (SQLi, IDOR, mass assignment, and permissive RLS) are **stop-the-line**: if it shows up, fix it before anything else.
|
|
34
34
|
>
|
|
35
|
-
> For a full adversarial audit (hunting with exploit proof + a prioritized plan), use the `security-audit-pentest` skill. This skill here is for **fixing** a specific finding from the table.
|
|
35
|
+
> For a full adversarial audit (hunting with exploit proof + a prioritized plan), use the `wizz-security-audit-pentest` skill. This skill here is for **fixing** a specific finding from the table.
|
|
36
36
|
|
|
37
37
|
## References (load on demand)
|
|
38
38
|
|
package/tools/fetch-assets.mjs
CHANGED
|
@@ -39,7 +39,7 @@ const __filename = fileURLToPath(import.meta.url);
|
|
|
39
39
|
const __dirname = path.dirname(__filename);
|
|
40
40
|
const PACKAGE_ROOT = path.resolve(__dirname, '..');
|
|
41
41
|
|
|
42
|
-
export const ASSETS_BASE = 'https://
|
|
42
|
+
export const ASSETS_BASE = 'https://assets.wizzcomms.com';
|
|
43
43
|
|
|
44
44
|
// Fonte única dos bundles conhecidos. `dest` é relativo ao root do pacote
|
|
45
45
|
// (repo em dev, node_modules/wizz-method quando instalado via npm — a mesma
|
package/tools/hooks/README.md
CHANGED
|
@@ -4,29 +4,33 @@ Os hooks do Claude Code usados pelo Wizz Method vivem AQUI e são instalados em
|
|
|
4
4
|
`~/.claude/hooks/` via `npm run sync:global`. Nunca edite a cópia global direto:
|
|
5
5
|
ela é substituída no próximo sync.
|
|
6
6
|
|
|
7
|
-
| Hook | Evento
|
|
8
|
-
| ------------------------------- |
|
|
9
|
-
| `wizz-router-enforce.js` | UserPromptSubmit
|
|
10
|
-
| `session-rules.js` | SessionStart
|
|
11
|
-
| `security-defensive-context.js` | PreToolUse (Skill)
|
|
12
|
-
| `wizz-artifact-guard.js` | PostToolUse (Write) | Avisa (nunca bloqueia) artefato escrito fora da subpasta da feature ativa, artefato de feature sem feature ativa, frontmatter faltando em `<tipo>-<slug>.md`, e arquivo solto na raiz fora do `layout:`, com o comando que move. Fail-open sem `_wizz/` ou config.
|
|
13
|
-
| `
|
|
7
|
+
| Hook | Evento | O que faz |
|
|
8
|
+
| ------------------------------- | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
9
|
+
| `wizz-router-enforce.js` | UserPromptSubmit | Gate `isTrivial()` + injeção curta: em projeto Wizz (`_wizz/`) manda delegar direto ao maestro/agente da área (sem router); fora, manda pro `wizz-router`. |
|
|
10
|
+
| `session-rules.js` | SessionStart | Duas coisas num processo só: injeta as regras de comunicação 1x por sessão (antes era por prompt) e resolve a FEATURE ATIVA sozinho (branch git > `_wizz/bmm/config.yaml` > nada), para ninguém precisar invocar `wizz-set-feature`. Branch de tronco e de release não viram feature. Fora de projeto Wizz e sem branch de feature a linha não é emitida (custo zero); dentro de projeto Wizz sem feature sai o lembrete curto "o primeiro pedido nomeado define uma", e sem `layout:` o lembrete de propor um. A feature também é o `tema` da memória. |
|
|
11
|
+
| `security-defensive-context.js` | PreToolUse (Skill) | Metodologia defensiva/escopada ao invocar skills de segurança. Genérico por projeto (v2). |
|
|
12
|
+
| `wizz-artifact-guard.js` | PostToolUse (Write/Edit) | Avisa (nunca bloqueia) artefato escrito fora da subpasta da feature ativa, artefato de feature sem feature ativa, frontmatter faltando em `<tipo>-<slug>.md`, e arquivo solto na raiz fora do `layout:`, com o comando que move. Fail-open sem `_wizz/` ou config. |
|
|
13
|
+
| `wizz-close-reminder.js` | Stop | Aviso ao usuário (`systemMessage`, nunca `decision: block`) quando a feature ativa está `implementado`/`no-ar` e o checklist de fechamento do `wizz-feature.js` tem pendência (ou está no ar e segue ativa). Sem subprocesso, sai cedo fora de projeto Wizz e com feature em andamento, não repete o mesmo aviso na sessão, fail-open. |
|
|
14
|
+
| `rtk-rewrite.sh` | PreToolUse (Bash) | Reescreve comandos de shell via `rtk rewrite` (token economy). Fork local do hook gerado pelo instalador upstream do rtk, com 3 patches (auditoria 2026-07-07, C4/RT1; e 2026-09-11, RT-H3): reescrita linha a linha em comando multi-linha (o binário só reescrevia a 1ª linha), timeout interno por linha (`RTK_HOOK_TIMEOUT`, default 3s) que não depende do `timeout`/`gtimeout` do GNU coreutils, e cache do version-guard (`RTK_CACHE_TTL_SECONDS`, default 1h, em `${XDG_CACHE_HOME:-$HOME/.cache}/wizz/rtk-version`) pra não rodar `rtk --version` a cada `Bash` tool call da sessão. Requer `rtk` + `jq` no PATH; fail-open em ambos (e no cache: ilegível/corrompido/expirado recomputa em silêncio). |
|
|
15
|
+
| `rtk-rewrite.js` | PreToolUse (Bash) | Versão Node do `rtk-rewrite.sh`, registrada no lugar dele só no Windows (sem bash e jq garantidos lá), desde 2026-10-15. Mesmo contrato: guard de multi-linha, linha a linha, timeout por linha, piso 0.23.0, fail-open. Sem o cache de versão. `node test/test-rtk-hook-node.js` exige a mesma saída dos dois contra o mesmo `rtk` fake. |
|
|
14
16
|
|
|
15
17
|
Arquivos de apoio (copiados junto, NUNCA registrados em settings.json; o doctor cobra a presença):
|
|
16
18
|
|
|
17
|
-
| Arquivo
|
|
18
|
-
|
|
|
19
|
-
| `wizz-feature-lib.js` | Funções puras de feature, git, config, pastas de artefato e frontmatter. Usada por todos os hooks acima.
|
|
20
|
-
| `wizz-layout-lib.js`
|
|
21
|
-
| `wizz-
|
|
22
|
-
| `wizz-
|
|
19
|
+
| Arquivo | O que é |
|
|
20
|
+
| --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
21
|
+
| `wizz-feature-lib.js` | Funções puras de feature, git, config, pastas de artefato e frontmatter. Usada por todos os hooks acima. |
|
|
22
|
+
| `wizz-layout-lib.js` | O que pode ficar na raiz e para onde vai o resto (tipos `codigo`, `scripts`, `dados`, `docs`, `midia`, `arquivo`). |
|
|
23
|
+
| `wizz-cerebro-lib.js` | Grava o fechamento no Cérebro (`encerrar --cerebro`): acha o vault e `projetos/<projeto>.md`, atualiza a linha da feature e a `Última sessão` do bloco Estado e põe 1 linha em Sessões (teto 15; as antigas vão para `_index/cerebro-archive.md`, lido só por grep). Idempotente, fail-open. |
|
|
24
|
+
| `wizz-debug-lib.js` | Diagnóstico opt-in dos hooks: com `WIZZ_DEBUG=1` cada erro engolido vira 1 linha em stderr e é anexada a `~/.claude/wizz-debug.log` (`WIZZ_DEBUG_FILE` troca o caminho). Sem a variável: zero saída, zero disco. Os hooks seguem fail-open (exit 0). O `wizz doctor` roda cada hook com esse modo ligado (check `hooks-run`). |
|
|
25
|
+
| `wizz-feature.js` | CLI do ciclo de vida da feature: `resolver`, `definir`, `artefato`, `decidir`, `status`, `verificar`, `encerrar [--cerebro]`, `listar`. É o passo verificável que router/maestro rodam antes de despachar. |
|
|
26
|
+
| `wizz-arrumar.js` | CLI de arrumação: `layout [--gravar]`, `planejar`, `aplicar` (manifesto .tsv + referências + testes antes/depois + `desfazer.sh`), `mover`, `desfazer`. |
|
|
23
27
|
|
|
24
28
|
Regra completa em `src/modules/wizz/_shared/organizacao.md` (2026-10-02, caso Spider). `session-rules.js` passou a lembrar "nenhuma feature ativa" e "projeto sem layout" dentro de projeto Wizz, e `wizz-router-enforce.js` acrescenta 1 linha de feature por prompt em projeto Wizz.
|
|
25
29
|
|
|
26
30
|
Removidos em 2026-07 (arquivados em `~/.claude/hooks-archive/`):
|
|
27
31
|
`no-narration-enforce.js` (absorvido pelo session-rules) e `vault-reminder.js`.
|
|
28
32
|
|
|
29
|
-
Testes: `npm run test:organizacao` (CLIs de feature e arrumação, guard, lembretes nos cenários sem git / main / branch), `npm run eval:feature` (hooks reais + CLI + estado final; `--llm` mede a obediência do modelo), `npm run test:hooks` (isTrivial + contexto por modo), `npm run test:feature-context` (resolução da feature ativa: tronco não vira feature, branch ganha do config, silêncio quando não há feature) e `node test/test-rtk-hook.js` (rewrite básico + multi-linha do `rtk-rewrite.sh`, smoke test que exercita o binário `rtk` real quando disponível, e o cache do version-guard: criado na 1ª chamada e reusado na 2ª, TTL expirado força recompute, path do binário mudou invalida, cache corrompido não quebra e se auto-cura).
|
|
33
|
+
Testes: `node test/test-feature-close.js` (`encerrar --cerebro` e o lembrete do Stop), `npm run test:organizacao` (CLIs de feature e arrumação, guard, lembretes nos cenários sem git / main / branch), `npm run eval:feature` (hooks reais + CLI + estado final; `--llm` mede a obediência do modelo), `npm run test:hooks` (isTrivial + contexto por modo), `npm run test:feature-context` (resolução da feature ativa: tronco não vira feature, branch ganha do config, silêncio quando não há feature) e `node test/test-rtk-hook.js` (rewrite básico + multi-linha do `rtk-rewrite.sh`, smoke test que exercita o binário `rtk` real quando disponível, e o cache do version-guard: criado na 1ª chamada e reusado na 2ª, TTL expirado força recompute, path do binário mudou invalida, cache corrompido não quebra e se auto-cura).
|
|
30
34
|
Evals de roteamento: `node evals/routing/run-routing-eval.mjs [--llm]`.
|
|
31
35
|
|
|
32
36
|
Drift repo ↔ global: `npm run sync:check` compara hash de cada hook (e skill) entre o repo e `~/.claude`, sem escrever nada — é lembrete (fail-open), não gate; roda no fim do `npm test`. Achou drift? `npm run sync:global` atualiza `~/.claude`.
|