wizz-method 2.15.0 → 2.17.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 (85) hide show
  1. package/README.md +12 -1
  2. package/THIRD-PARTY-NOTICES.md +6 -0
  3. package/package.json +5 -2
  4. package/skills-registry.yaml +18 -6
  5. package/src/bmm-skills/1-analysis/wizz-agent-analyst/customize.toml +1 -1
  6. package/src/bmm-skills/1-analysis/wizz-set-feature/SKILL.md +34 -12
  7. package/src/bmm-skills/2-plan-workflows/wizz-agent-pm/customize.toml +1 -1
  8. package/src/bmm-skills/2-plan-workflows/wizz-agent-ux-designer/customize.toml +1 -1
  9. package/src/bmm-skills/3-solutioning/wizz-agent-architect/customize.toml +1 -1
  10. package/src/bmm-skills/4-implementation/wizz-agent-dev/customize.toml +1 -1
  11. package/src/bmm-skills/module.yaml +10 -0
  12. package/src/core-skills/_shared/handoff-protocol.md +10 -0
  13. package/src/modules/wizz/_shared/encerramento.md +4 -0
  14. package/src/modules/wizz/_shared/organizacao.md +58 -0
  15. package/src/modules/wizz/_shared/video-pipeline.md +1 -0
  16. package/src/modules/wizz/agents/wizz-maestro/SKILL.md +9 -1
  17. package/src/modules/wizz/agents/wizz-maestro/customize.toml +8 -2
  18. package/src/skills-lib/wizz-inicio-de-projeto/SKILL.md +11 -0
  19. package/src/skills-lib/wizz-router/SKILL.md +3 -1
  20. package/src/skills-lib/wizz-router/references/routing-table-flat.md +1 -0
  21. package/src/skills-lib/wizz-video-styles/SKILL.md +66 -0
  22. package/src/skills-lib/wizz-video-styles/assets/audio/CREDITS.md +54 -0
  23. package/src/skills-lib/wizz-video-styles/assets/audio/LIBRARIES.md +31 -0
  24. package/src/skills-lib/wizz-video-styles/assets/audio/kit.json +182 -0
  25. package/src/skills-lib/wizz-video-styles/assets/audio/music/acoustic-lifestyle.mp3 +0 -0
  26. package/src/skills-lib/wizz-video-styles/assets/audio/music/cinematic-tense.mp3 +0 -0
  27. package/src/skills-lib/wizz-video-styles/assets/audio/music/classical-light.mp3 +0 -0
  28. package/src/skills-lib/wizz-video-styles/assets/audio/music/corporate-light.mp3 +0 -0
  29. package/src/skills-lib/wizz-video-styles/assets/audio/music/lofi-chill.mp3 +0 -0
  30. package/src/skills-lib/wizz-video-styles/assets/audio/music/meme-beat.mp3 +0 -0
  31. package/src/skills-lib/wizz-video-styles/assets/audio/music/piano-editorial.mp3 +0 -0
  32. package/src/skills-lib/wizz-video-styles/assets/audio/music/tech-pulse.mp3 +0 -0
  33. package/src/skills-lib/wizz-video-styles/assets/audio/sfx/boom-impact.mp3 +0 -0
  34. package/src/skills-lib/wizz-video-styles/assets/audio/sfx/click-mouse.mp3 +0 -0
  35. package/src/skills-lib/wizz-video-styles/assets/audio/sfx/ding-notify.mp3 +0 -0
  36. package/src/skills-lib/wizz-video-styles/assets/audio/sfx/glitch-short.mp3 +0 -0
  37. package/src/skills-lib/wizz-video-styles/assets/audio/sfx/ka-ching.mp3 +0 -0
  38. package/src/skills-lib/wizz-video-styles/assets/audio/sfx/pop-bubble.mp3 +0 -0
  39. package/src/skills-lib/wizz-video-styles/assets/audio/sfx/record-scratch.mp3 +0 -0
  40. package/src/skills-lib/wizz-video-styles/assets/audio/sfx/riser-short.mp3 +0 -0
  41. package/src/skills-lib/wizz-video-styles/assets/audio/sfx/swoosh-text.mp3 +0 -0
  42. package/src/skills-lib/wizz-video-styles/assets/audio/sfx/typing-short.mp3 +0 -0
  43. package/src/skills-lib/wizz-video-styles/assets/audio/sfx/whoosh-fast.mp3 +0 -0
  44. package/src/skills-lib/wizz-video-styles/assets/audio/sfx/whoosh-soft.mp3 +0 -0
  45. package/src/skills-lib/wizz-video-styles/assets/remotion/editorial/EditorialReel.tsx +111 -0
  46. package/src/skills-lib/wizz-video-styles/assets/remotion/editorial/README.md +77 -0
  47. package/src/skills-lib/wizz-video-styles/assets/remotion/editorial/captions.tsx +56 -0
  48. package/src/skills-lib/wizz-video-styles/assets/remotion/editorial/example-words.json +285 -0
  49. package/src/skills-lib/wizz-video-styles/assets/remotion/editorial/primitives.tsx +241 -0
  50. package/src/skills-lib/wizz-video-styles/assets/remotion/editorial/scenes.tsx +113 -0
  51. package/src/skills-lib/wizz-video-styles/assets/remotion/editorial/theme.ts +68 -0
  52. package/src/skills-lib/wizz-video-styles/assets/remotion/editorial/timeline.ts +65 -0
  53. package/src/skills-lib/wizz-video-styles/data/recipes.json +341 -0
  54. package/src/skills-lib/wizz-video-styles/data/references.json +3397 -0
  55. package/src/skills-lib/wizz-video-styles/data/styles.json +726 -0
  56. package/src/skills-lib/wizz-video-styles/references/audio.md +54 -0
  57. package/src/skills-lib/wizz-video-styles/references/measured-patterns.md +38 -0
  58. package/src/skills-lib/wizz-video-styles/references/research-method.md +33 -0
  59. package/src/skills-lib/wizz-video-styles/references/style-picker.md +51 -0
  60. package/src/skills-lib/wizz-video-styles/references/styles/camera-no-setup.md +60 -0
  61. package/src/skills-lib/wizz-video-styles/references/styles/desafio-numero.md +60 -0
  62. package/src/skills-lib/wizz-video-styles/references/styles/esquete.md +57 -0
  63. package/src/skills-lib/wizz-video-styles/references/styles/experimento.md +56 -0
  64. package/src/skills-lib/wizz-video-styles/references/styles/explicador-animado.md +62 -0
  65. package/src/skills-lib/wizz-video-styles/references/styles/macro-asmr.md +61 -0
  66. package/src/skills-lib/wizz-video-styles/references/styles/meme-rapido.md +65 -0
  67. package/src/skills-lib/wizz-video-styles/references/styles/motion-editorial.md +69 -0
  68. package/src/skills-lib/wizz-video-styles/references/styles/podcast-clip.md +61 -0
  69. package/src/skills-lib/wizz-video-styles/references/styles/pov-vlog.md +57 -0
  70. package/src/skills-lib/wizz-video-styles/references/styles/quadro-branco.md +60 -0
  71. package/src/skills-lib/wizz-video-styles/references/styles/talking-head-cards.md +62 -0
  72. package/src/skills-lib/wizz-video-styles/references/styles/talking-head-legenda.md +62 -0
  73. package/src/skills-lib/wizz-video-styles/references/styles/tour.md +59 -0
  74. package/src/skills-lib/wizz-video-styles/scripts/analyze.sh +66 -0
  75. package/src/skills-lib/wizz-video-styles/scripts/build_catalog.py +128 -0
  76. package/tools/hooks/README.md +14 -2
  77. package/tools/hooks/session-rules.js +65 -140
  78. package/tools/hooks/wizz-arrumar.js +418 -0
  79. package/tools/hooks/wizz-artifact-guard.js +160 -0
  80. package/tools/hooks/wizz-feature-lib.js +414 -0
  81. package/tools/hooks/wizz-feature.js +401 -0
  82. package/tools/hooks/wizz-layout-lib.js +152 -0
  83. package/tools/hooks/wizz-router-enforce.js +21 -1
  84. package/tools/installer/core/doctor.js +5 -1
  85. package/tools/installer/core/hooks-settings.js +10 -0
@@ -0,0 +1,66 @@
1
+ ---
2
+ name: wizz-video-styles
3
+ description: Catalogue of short vertical video styles (Reels, Shorts, TikTok) measured on real high-reach videos, for any niche. Takes an inventory of the material the project actually has (a real person on camera, voice only, b-roll, app screens, a physical product), crosses it with niche and goal, picks the style to copy and hands over its recipe - rhythm, captions, palette, typography, motion vocabulary, music and SFX rules - plus a Remotion template when one exists and a licensed audio kit. Use when a video is about to be edited or planned and nobody has decided what it should look like, when someone says a video "looks amateur" or asks to copy a creator's style, or before wizz-video-effects and Remotion take over. Triggers - estilo do vídeo, editar no estilo de, copiar a edição de, que estilo usar no reel, referência de edição, catálogo de estilos, o vídeo ficou amador. Does not decide per-event SFX or gates (that is wizz-video-effects) and does not assemble the film (that is Remotion).
4
+ ---
5
+
6
+ # wizz-video-styles: which style to copy, and its recipe
7
+
8
+ Every number in this skill was measured on a real public short ([references/research-method.md](references/research-method.md)). A style that has no measured reference does not enter the catalogue. When a number below was not measured, the page says so.
9
+
10
+ ## Where it sits in the video pipeline
11
+
12
+ 1. Script exists (social area, `wizz-social-content`).
13
+ 2. **This skill:** inventory of the material, then style, then recipe.
14
+ 3. Narration and word timing (the person's own recording or TTS, then `ctc-align`).
15
+ 4. Footage and screens: `get-brolls` for b-roll, HyperFrames for silent screens, real screenshots for app screens.
16
+ 5. Effects and sound per event, plus gates: `wizz-video-effects`. Pass it this style's SFX density ceiling and music level.
17
+ 6. Assembly in Remotion: invoke `wizz-remotion-best-practices`; copy the style's template from `assets/remotion/` when it has one.
18
+
19
+ It can also run at step 1, so the script is written for the style (room for b-roll, a hook that the style can show).
20
+
21
+ ## Per piece
22
+
23
+ 1. **Inventory first, always.** Ask or read from the plan, one line each:
24
+ - Is a real person on camera? Which shots exist (face to camera, podcast, stage, street, hands only)?
25
+ - Voice: the person's own recording, a podcast/interview track, TTS, or none?
26
+ - Screens: app or site screenshots, terminal, documents? (Capture screens as screenshots; a browser screen *recording* can come out stretched.)
27
+ - B-roll or stock, a physical product, food, a place?
28
+ - Niche, goal of the piece (follow, comment, save, click), duration.
29
+ Never pick a style the material cannot feed. "Talking head" needs a person on camera; "motion editorial" needs nothing but a voice; "macro / ASMR" needs a physical product filmed close.
30
+ 2. **Pick the style** with [references/style-picker.md](references/style-picker.md): material decides which styles are possible, niche and goal rank them. Mixed is allowed and common: person on camera for the hook and the CTA, another style in the middle.
31
+ 3. **Open the style card** in `references/styles/` and follow it: rhythm, captions, palette, typography, motion, music, SFX, hook, CTA, and its "do not" list.
32
+ 4. **Watch two of its references** (links in the card) before editing. Reading a card is not watching a video.
33
+ 5. **Audio:** music and SFX choices from [references/audio.md](references/audio.md); the redistributable kit is in `assets/audio/` (credits in `assets/audio/CREDITS.md`).
34
+ 6. **Hand over** to `wizz-video-effects` with the card's SFX ceiling and music level, then to Remotion.
35
+ 7. **Look at the final file**: extract frames and compare them side by side with a contact sheet of one reference.
36
+
37
+ ## What the measurements say across every style
38
+
39
+ - **SFX are rare.** Across 99 measured shorts, sound effects landed on a cut in a small minority of cuts (per-style counts in each card). Putting a sound on every cut is the most common amateur tell.
40
+ - **Music bed: style-dependent.** Fast montage, animated explainers and travel POV carry a continuous bed; talking heads and podcast clips mostly have none or a faint one.
41
+ - **Captions** in reach-heavy styles are 1 to 3 words at a time, bold, lower-centre, with one keyword in an accent colour; editorial and explainer styles keep them small or drop them.
42
+ - **Rhythm is a style property, not a virtue.** The highest-reach videos in the set range from single takes with no detected cut to a cut every second.
43
+
44
+ Details and the per-style table: [references/measured-patterns.md](references/measured-patterns.md).
45
+
46
+ ## References
47
+
48
+ | File | Answers |
49
+ |---|---|
50
+ | [references/style-picker.md](references/style-picker.md) | **start here.** Material and niche → which styles |
51
+ | [references/styles/](references/styles/) | one card per style: recipe, references, do-not list |
52
+ | [references/measured-patterns.md](references/measured-patterns.md) | the numbers behind the cards, per style and per niche |
53
+ | [references/audio.md](references/audio.md) | music and SFX per style, the kit, libraries and licences |
54
+ | [references/research-method.md](references/research-method.md) | how the catalogue was measured, its limits, how to add a niche or a style |
55
+ | `data/references.json` | every measured short: link, channel, views, niche, style, metrics |
56
+ | `scripts/analyze.sh` | measures one short (cuts, music bed, SFX heuristics, loudness, contact sheet) |
57
+ | `assets/remotion/editorial/` | Remotion template of the motion editorial style |
58
+ | `assets/audio/` | licensed music beds and SFX that may ship with a project |
59
+
60
+ ## Out of scope
61
+
62
+ - Which SFX plays on which event, ducking, loudness and safe-zone gates: `wizz-video-effects`.
63
+ - Assembling the film and the `<Audio>` API: `wizz-remotion-best-practices`.
64
+ - Script, hook wording, CTA wording: `wizz-social-content` and the copy area.
65
+ - Sourcing b-roll: `get-brolls`. Drawing silent screens: HyperFrames.
66
+ - Launch films with art direction: `wizz-huashu-design`.
@@ -0,0 +1,54 @@
1
+ # Audio kit credits
2
+
3
+ Licence checked on each source's page on 2026-10-04. Only CC0 / public domain or CC BY 4.0 files are included.
4
+ Freesound source: downloading the original requires login, so the public HQ mp3 preview that the page itself exposes was used (lower quality than the original; re-encoded at 160 kbps).
5
+ Kenney: CC0 licence confirmed on each pack's page and in the zip's `License.txt`.
6
+
7
+ | File | Original title | Author | Licence | Source page | Checked on | Changes |
8
+ |---|---|---|---|---|---|---|
9
+ | `music/piano-editorial.mp3` | Erik Satie - Gymnopédies - la 1ère. Lent et douloureux | Robin Alciatore (Musopen); composition by Erik Satie | [Public domain (dedicated by the recording's author)](https://commons.wikimedia.org/wiki/File:Erik_Satie_-_gymnopedies_-_la_1_ere._lent_et_douloureux.ogg) | https://commons.wikimedia.org/wiki/File:Erik_Satie_-_gymnopedies_-_la_1_ere._lent_et_douloureux.ogg | 2026-10-04 | cut 0-70 s, fade-in 0.5 s, fade-out 2 s, loudnorm -20 LUFS, mp3 160 kbps |
10
+ | `music/lofi-chill.mp3` | Lofi Hip Hop Loop | omfgdude | [CC0 1.0](https://creativecommons.org/publicdomain/zero/1.0/) | https://opengameart.org/content/lofi-hip-hop-loop | 2026-10-04 | cut 0-70 s, fades, loudnorm -20 LUFS, mp3 160 kbps |
11
+ | `music/tech-pulse.mp3` | Synthwave House Loop | Fupi | [CC0 1.0](https://creativecommons.org/publicdomain/zero/1.0/) | https://opengameart.org/content/synthwave-house-loop | 2026-10-04 | original of 50.5 s repeated 2x and cut to 70 s (seam at the loop point), fades, loudnorm -20 LUFS, mp3 160 kbps |
12
+ | `music/corporate-light.mp3` | Feel good slow ambient track with a beat. | annandistance | [CC0 1.0](https://creativecommons.org/publicdomain/zero/1.0/) | https://opengameart.org/content/feel-good-slow-ambient-track-with-a-beat | 2026-10-04 | cut 0-70 s, fades, loudnorm -20 LUFS, mp3 160 kbps |
13
+ | `music/cinematic-tense.mp3` | Hitman | Kevin MacLeod (incompetech.com) | [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) | https://commons.wikimedia.org/wiki/File:Hitman_by_Kevin_MacLeod.ogg | 2026-10-04 | cut 0-70 s, fades, loudnorm -20 LUFS, mp3 160 kbps |
14
+ | `music/acoustic-lifestyle.mp3` | Sunset Plains | Yoiyami | [CC0 1.0](https://creativecommons.org/publicdomain/zero/1.0/) | https://opengameart.org/content/sunset-plains | 2026-10-04 | cut 10-80 s of the original WAV, fades, loudnorm -20 LUFS, mp3 160 kbps |
15
+ | `music/classical-light.mp3` | Chopin Nocturne No. 2 in E-flat Major, Op. 9 | Peter Johnston (Musopen); composition by Frédéric Chopin | [CC0 1.0](https://creativecommons.org/publicdomain/zero/1.0/) | https://commons.wikimedia.org/wiki/File:Chopin_Nocturne_No._2_in_E_Flat_Major,_Op._9.ogg | 2026-10-04 | cut 0-70 s, fades, loudnorm -20 LUFS, mp3 160 kbps |
16
+ | `music/meme-beat.mp3` | Monkeys Spinning Monkeys | Kevin MacLeod (incompetech.com) | [CC BY 4.0 (the page offers CC BY 3.0 or 4.0 at the user's choice; used under 4.0)](https://creativecommons.org/licenses/by/4.0/) | https://commons.wikimedia.org/wiki/File:Kevin_MacLeod_~_Monkeys_Spinning_Monkeys.ogg | 2026-10-04 | cut 0-70 s, fades, loudnorm -20 LUFS, mp3 160 kbps |
17
+ | `sfx/whoosh-soft.mp3` | Whoosh | qubodup | [CC0 1.0](https://creativecommons.org/publicdomain/zero/1.0/) | https://freesound.org/people/qubodup/sounds/60013/ | 2026-10-04 | public HQ mp3 preview (the original download requires login); 0.45 s + 0.15 s of silence, loudnorm -16 LUFS, re-encoded to mp3 160 kbps |
18
+ | `sfx/whoosh-fast.mp3` | swoosh.wav | Garysguff | [CC0 1.0](https://creativecommons.org/publicdomain/zero/1.0/) | https://freesound.org/people/Garysguff/sounds/575836/ | 2026-10-04 | public HQ mp3 preview; trim 0.30-0.75 s (first swoosh), loudnorm -16 LUFS (peak-limited), mp3 160 kbps |
19
+ | `sfx/swoosh-text.mp3` | swish-3.wav (Swishes Sound Pack) | artisticdude | [CC0 1.0](https://creativecommons.org/publicdomain/zero/1.0/) | https://opengameart.org/content/swishes-sound-pack | 2026-10-04 | 0.146 s + silence padded to 0.6 s, loudnorm -16 LUFS, mp3 160 kbps |
20
+ | `sfx/pop-bubble.mp3` | drop_003.ogg (Interface Sounds) | Kenney (kenney.nl) | [CC0 1.0](https://creativecommons.org/publicdomain/zero/1.0/) | https://kenney.nl/assets/interface-sounds | 2026-10-04 | silence padded to 0.6 s, loudnorm -16 LUFS (peak-limited), mp3 160 kbps |
21
+ | `sfx/click-mouse.mp3` | mouseclick1.ogg (UI Audio) | Kenney (kenney.nl) | [CC0 1.0](https://creativecommons.org/publicdomain/zero/1.0/) | https://kenney.nl/assets/ui-audio | 2026-10-04 | silence padded to 0.6 s, loudnorm -16 LUFS (peak-limited), mp3 160 kbps |
22
+ | `sfx/typing-short.mp3` | typing soundeffect | johndekale | [CC0 1.0](https://creativecommons.org/publicdomain/zero/1.0/) | https://opengameart.org/content/typing-soundeffect | 2026-10-04 | cut 0-1.6 s, loudnorm -16 LUFS (peak-limited), mp3 160 kbps |
23
+ | `sfx/ding-notify.mp3` | confirmation_002.ogg (Interface Sounds) | Kenney (kenney.nl) | [CC0 1.0](https://creativecommons.org/publicdomain/zero/1.0/) | https://kenney.nl/assets/interface-sounds | 2026-10-04 | silence padded to 0.6 s, loudnorm -16 LUFS, mp3 160 kbps |
24
+ | `sfx/boom-impact.mp3` | impactPunch_heavy_000.ogg (Impact Sounds) | Kenney (kenney.nl) | [CC0 1.0](https://creativecommons.org/publicdomain/zero/1.0/) | https://kenney.nl/assets/impact-sounds | 2026-10-04 | loudnorm -16 LUFS (peak-limited), mp3 160 kbps |
25
+ | `sfx/riser-short.mp3` | phaserUp3.ogg (Digital Audio) | Kenney (kenney.nl) | [CC0 1.0](https://creativecommons.org/publicdomain/zero/1.0/) | https://kenney.nl/assets/digital-audio | 2026-10-04 | silence padded to 0.6 s, loudnorm -16 LUFS, mp3 160 kbps |
26
+ | `sfx/glitch-short.mp3` | lowRandom.ogg (Digital Audio) | Kenney (kenney.nl) | [CC0 1.0](https://creativecommons.org/publicdomain/zero/1.0/) | https://kenney.nl/assets/digital-audio | 2026-10-04 | silence padded to 0.6 s, loudnorm -16 LUFS, mp3 160 kbps |
27
+ | `sfx/ka-ching.mp3` | Cha Ching.wav | deleted_user_96253 (deleted account) | [CC0 1.0](https://creativecommons.org/publicdomain/zero/1.0/) | https://freesound.org/people/deleted_user_96253/sounds/351304/ | 2026-10-04 | public HQ mp3 preview; cut 0-2.2 s, loudnorm -16 LUFS, mp3 160 kbps |
28
+ | `sfx/record-scratch.mp3` | Vinyl Backspin and Beat Scratch.wav | ZeSoundResearchInc. | [CC0 1.0](https://creativecommons.org/publicdomain/zero/1.0/) | https://freesound.org/people/ZeSoundResearchInc./sounds/117511/ | 2026-10-04 | public HQ mp3 preview; trim 0-0.65 s (first scratch), loudnorm -16 LUFS, mp3 160 kbps |
29
+
30
+ ## Required attribution (CC BY 4.0)
31
+
32
+ Paste into the video credits or description when you use these files. The other files (CC0 and public domain) require no credit.
33
+
34
+ - `music/cinematic-tense.mp3`:
35
+
36
+ ```
37
+ "Hitman" Kevin MacLeod (incompetech.com)
38
+ Licensed under Creative Commons: By Attribution 4.0 License
39
+ http://creativecommons.org/licenses/by/4.0/
40
+ Changes: trimmed to 70 s, fade in/out, loudness normalised to -20 LUFS.
41
+ ```
42
+
43
+ - `music/meme-beat.mp3`:
44
+
45
+ ```
46
+ "Monkeys Spinning Monkeys" Kevin MacLeod (incompetech.com)
47
+ Licensed under Creative Commons: By Attribution 4.0 License
48
+ http://creativecommons.org/licenses/by/4.0/
49
+ Changes: trimmed to 70 s, fade in/out, loudness normalised to -20 LUFS.
50
+ ```
51
+
52
+ Licence link: https://creativecommons.org/licenses/by/4.0/
53
+
54
+ The Musopen recordings (Satie, Chopin) are public domain/CC0; crediting the performer is optional, not required.
@@ -0,0 +1,31 @@
1
+ # Bibliotecas para buscar mais áudio
2
+
3
+ Resumos de licença abaixo vêm da checagem de 2026-10-04 (páginas dos arquivos do kit) ou, onde marcado "não verificado", do conhecimento geral. Confira a licença na página de cada arquivo antes de usar.
4
+
5
+ ## Pode ir no pacote (redistribuição permitida)
6
+
7
+ | Biblioteca | Licença | Pode | Não pode / cuidado |
8
+ |---|---|---|---|
9
+ | [Kenney](https://kenney.nl/assets/category:Audio) | CC0 (confirmado) | Tudo, inclusive redistribuir e vender | nada; crédito opcional |
10
+ | [OpenGameArt](https://opengameart.org/art-search-advanced?field_art_type_tid%5B%5D=12&field_art_licenses_tid%5B%5D=4) (filtrar CC0) | Varia por arquivo: CC0, CC-BY 3.0/4.0, OGA-BY, GPL (confirmado que varia) | CC0 sem restrição | Só CC0 ou CC-BY 4.0 no pacote; CC-BY 3.0, GPL e CC-BY-SA não entram (a pasta do kit só aceita CC0/4.0) |
11
+ | [Freesound](https://freesound.org/search/?f=license%3A%22Creative+Commons+0%22) (filtro "Creative Commons 0") | Varia por arquivo: CC0, CC-BY, CC-BY-NC | CC0 sem restrição | Baixar o original exige login; o preview HQ mp3 é público. CC-BY-NC nunca entra |
12
+ | [Wikimedia Commons - Musopen](https://commons.wikimedia.org/wiki/Category:Musopen) | Domínio público / CC0 por arquivo (confirmado nos arquivos do kit) | Clássicos (Satie, Chopin, Bach) | Ler a licença de cada arquivo: há versões de outros intérpretes com licença diferente |
13
+ | [incompetech (Kevin MacLeod)](https://incompetech.com/music/royalty-free/) | CC BY 4.0 nas faixas atuais; arquivos antigos na Commons estão em CC BY 3.0 (confirmado na Commons) | Redistribuir com atribuição e indicar alterações | Exige a linha de crédito; só entra no pacote a faixa confirmada em 4.0. A página é JS, confirme na página da faixa |
14
+ | [Free Music Archive](https://freemusicarchive.org/search?license=CC0) (filtro CC0) | Varia: CC0, CC-BY, NC, ND | CC0 sem restrição | Não verificado nesta rodada. NC/ND nunca entram |
15
+ | [ccMixter](https://ccmixter.org/) | CC0 ou CC-BY por faixa | Idem | Não verificado nesta rodada |
16
+
17
+ FreePD (freepd.com) mostrava aviso de encerramento em 2026-10-04: não usar como fonte nova. Arquivos antigos de lá só se a licença for confirmada em outra página.
18
+
19
+ ## Só usar no projeto (não redistribuir o arquivo)
20
+
21
+ Servem para a edição de um vídeo, mas o arquivo não pode ir dentro de pacote ou repositório público. Dê só o link.
22
+
23
+ | Biblioteca | Uso permitido (resumo, não verificado) | Por que fica fora do pacote |
24
+ |---|---|---|
25
+ | [Pixabay](https://pixabay.com/music/) | Uso comercial em projetos sem crédito | A Pixabay Content License proíbe redistribuir o arquivo isolado/como conteúdo autônomo |
26
+ | [YouTube Audio Library](https://studio.youtube.com/channel/UC/music) | Uso em vídeos | Termos ligados à conta e ao YouTube; sem redistribuição |
27
+ | [Uppbeat](https://uppbeat.io/) | Plano grátis com crédito e limite mensal | Licença por conta/uso, sem redistribuição |
28
+ | [Epidemic Sound](https://www.epidemicsound.com/) | Assinatura, uso em canais licenciados | Sem redistribuição; some se a assinatura acabar |
29
+ | [Artlist](https://artlist.io/) | Assinatura, licença perpétua para os vídeos feitos | Sem redistribuição |
30
+ | [Mixkit](https://mixkit.co/free-stock-music/) | Uso em vídeos com a Mixkit License | Não verificado; não redistribuir como arquivo avulso |
31
+ | [Bensound](https://www.bensound.com/) | Grátis com crédito | Licença própria, sem redistribuição |
@@ -0,0 +1,182 @@
1
+ [
2
+ {
3
+ "file": "music/piano-editorial.mp3",
4
+ "tipo": "music",
5
+ "clima_ou_uso": "piano calmo / editorial",
6
+ "duracao_s": 70.0,
7
+ "lufs": -20.4,
8
+ "licenca": "Public domain (dedicado pelo autor da gravação)",
9
+ "fonte": "https://commons.wikimedia.org/wiki/File:Erik_Satie_-_gymnopedies_-_la_1_ere._lent_et_douloureux.ogg"
10
+ },
11
+ {
12
+ "file": "music/lofi-chill.mp3",
13
+ "tipo": "music",
14
+ "clima_ou_uso": "lo-fi / chill beat",
15
+ "duracao_s": 70.0,
16
+ "lufs": -20.5,
17
+ "licenca": "CC0 1.0",
18
+ "fonte": "https://opengameart.org/content/lofi-hip-hop-loop"
19
+ },
20
+ {
21
+ "file": "music/tech-pulse.mp3",
22
+ "tipo": "music",
23
+ "clima_ou_uso": "eletrônica pulsante / tech (synthwave-house 114 BPM)",
24
+ "duracao_s": 70.0,
25
+ "lufs": -20.4,
26
+ "licenca": "CC0 1.0",
27
+ "fonte": "https://opengameart.org/content/synthwave-house-loop"
28
+ },
29
+ {
30
+ "file": "music/corporate-light.mp3",
31
+ "tipo": "music",
32
+ "clima_ou_uso": "corporativo leve / otimista (ambient com batida lenta; aproximação)",
33
+ "duracao_s": 70.0,
34
+ "lufs": -20.4,
35
+ "licenca": "CC0 1.0",
36
+ "fonte": "https://opengameart.org/content/feel-good-slow-ambient-track-with-a-beat"
37
+ },
38
+ {
39
+ "file": "music/cinematic-tense.mp3",
40
+ "tipo": "music",
41
+ "clima_ou_uso": "cinematográfico / tenso (mistério, crime)",
42
+ "duracao_s": 70.0,
43
+ "lufs": -20.4,
44
+ "licenca": "CC BY 4.0",
45
+ "fonte": "https://commons.wikimedia.org/wiki/File:Hitman_by_Kevin_MacLeod.ogg"
46
+ },
47
+ {
48
+ "file": "music/acoustic-lifestyle.mp3",
49
+ "tipo": "music",
50
+ "clima_ou_uso": "acústico / lifestyle (violão suave + pads)",
51
+ "duracao_s": 70.0,
52
+ "lufs": -20.4,
53
+ "licenca": "CC0 1.0",
54
+ "fonte": "https://opengameart.org/content/sunset-plains"
55
+ },
56
+ {
57
+ "file": "music/classical-light.mp3",
58
+ "tipo": "music",
59
+ "clima_ou_uso": "clássico leve (piano romântico)",
60
+ "duracao_s": 70.0,
61
+ "lufs": -20.4,
62
+ "licenca": "CC0 1.0",
63
+ "fonte": "https://commons.wikimedia.org/wiki/File:Chopin_Nocturne_No._2_in_E_Flat_Major,_Op._9.ogg"
64
+ },
65
+ {
66
+ "file": "music/meme-beat.mp3",
67
+ "tipo": "music",
68
+ "clima_ou_uso": "humor / meme (pizzicato e flautas, 144 BPM)",
69
+ "duracao_s": 70.0,
70
+ "lufs": -20.4,
71
+ "licenca": "CC BY 4.0",
72
+ "fonte": "https://commons.wikimedia.org/wiki/File:Kevin_MacLeod_~_Monkeys_Spinning_Monkeys.ogg"
73
+ },
74
+ {
75
+ "file": "sfx/whoosh-soft.mp3",
76
+ "tipo": "sfx",
77
+ "clima_ou_uso": "whoosh suave",
78
+ "duracao_s": 0.6,
79
+ "lufs": -16.8,
80
+ "licenca": "CC0 1.0",
81
+ "fonte": "https://freesound.org/people/qubodup/sounds/60013/"
82
+ },
83
+ {
84
+ "file": "sfx/whoosh-fast.mp3",
85
+ "tipo": "sfx",
86
+ "clima_ou_uso": "whoosh rápido",
87
+ "duracao_s": 0.6,
88
+ "lufs": -19.3,
89
+ "licenca": "CC0 1.0",
90
+ "fonte": "https://freesound.org/people/Garysguff/sounds/575836/"
91
+ },
92
+ {
93
+ "file": "sfx/swoosh-text.mp3",
94
+ "tipo": "sfx",
95
+ "clima_ou_uso": "swoosh de texto (entrada de título)",
96
+ "duracao_s": 0.6,
97
+ "lufs": -16.4,
98
+ "licenca": "CC0 1.0",
99
+ "fonte": "https://opengameart.org/content/swishes-sound-pack"
100
+ },
101
+ {
102
+ "file": "sfx/pop-bubble.mp3",
103
+ "tipo": "sfx",
104
+ "clima_ou_uso": "pop / bolha",
105
+ "duracao_s": 0.6,
106
+ "lufs": -19.9,
107
+ "licenca": "CC0 1.0",
108
+ "fonte": "https://kenney.nl/assets/interface-sounds"
109
+ },
110
+ {
111
+ "file": "sfx/click-mouse.mp3",
112
+ "tipo": "sfx",
113
+ "clima_ou_uso": "clique de mouse",
114
+ "duracao_s": 0.6,
115
+ "lufs": -25.1,
116
+ "licenca": "CC0 1.0",
117
+ "fonte": "https://kenney.nl/assets/ui-audio"
118
+ },
119
+ {
120
+ "file": "sfx/typing-short.mp3",
121
+ "tipo": "sfx",
122
+ "clima_ou_uso": "digitação curta",
123
+ "duracao_s": 1.6,
124
+ "lufs": -19.0,
125
+ "licenca": "CC0 1.0",
126
+ "fonte": "https://opengameart.org/content/typing-soundeffect"
127
+ },
128
+ {
129
+ "file": "sfx/ding-notify.mp3",
130
+ "tipo": "sfx",
131
+ "clima_ou_uso": "ding / notificação",
132
+ "duracao_s": 0.6,
133
+ "lufs": -16.4,
134
+ "licenca": "CC0 1.0",
135
+ "fonte": "https://kenney.nl/assets/interface-sounds"
136
+ },
137
+ {
138
+ "file": "sfx/boom-impact.mp3",
139
+ "tipo": "sfx",
140
+ "clima_ou_uso": "impacto grave (boom)",
141
+ "duracao_s": 0.6,
142
+ "lufs": -17.1,
143
+ "licenca": "CC0 1.0",
144
+ "fonte": "https://kenney.nl/assets/impact-sounds"
145
+ },
146
+ {
147
+ "file": "sfx/riser-short.mp3",
148
+ "tipo": "sfx",
149
+ "clima_ou_uso": "riser curto",
150
+ "duracao_s": 0.6,
151
+ "lufs": -16.4,
152
+ "licenca": "CC0 1.0",
153
+ "fonte": "https://kenney.nl/assets/digital-audio"
154
+ },
155
+ {
156
+ "file": "sfx/glitch-short.mp3",
157
+ "tipo": "sfx",
158
+ "clima_ou_uso": "glitch curto (bipes aleatórios; aproximação)",
159
+ "duracao_s": 0.6,
160
+ "lufs": -16.4,
161
+ "licenca": "CC0 1.0",
162
+ "fonte": "https://kenney.nl/assets/digital-audio"
163
+ },
164
+ {
165
+ "file": "sfx/ka-ching.mp3",
166
+ "tipo": "sfx",
167
+ "clima_ou_uso": "ka-ching / caixa registradora",
168
+ "duracao_s": 2.2,
169
+ "lufs": -16.4,
170
+ "licenca": "CC0 1.0",
171
+ "fonte": "https://freesound.org/people/deleted_user_96253/sounds/351304/"
172
+ },
173
+ {
174
+ "file": "sfx/record-scratch.mp3",
175
+ "tipo": "sfx",
176
+ "clima_ou_uso": "record scratch (meme)",
177
+ "duracao_s": 0.7,
178
+ "lufs": -16.5,
179
+ "licenca": "CC0 1.0",
180
+ "fonte": "https://freesound.org/people/ZeSoundResearchInc./sounds/117511/"
181
+ }
182
+ ]
@@ -0,0 +1,111 @@
1
+ import React, { useMemo } from "react";
2
+ import { AbsoluteFill, Audio, Sequence, interpolate, staticFile } from "remotion";
3
+ import { Captions } from "./captions";
4
+ import { Backdrop, Stage, clamp } from "./primitives";
5
+ import { SceneView, type SceneDef } from "./scenes";
6
+ import { ThemeContext, mergeTheme, type EditorialTheme } from "./theme";
7
+ import { at, buildScenes, toFrames, totalSeconds, type WordsData } from "./timeline";
8
+ import exampleWords from "./example-words.json";
9
+
10
+ // Composição genérica do estilo "motion editorial": um objeto por vez no centro,
11
+ // troca por transformação (cena sobe enquanto a próxima entra), música baixa contínua,
12
+ // SFX raro, legenda pequena. Tudo que muda de vídeo para vídeo entra por props.
13
+
14
+ export type SfxDef = { atWord: string; offset?: number; src: string; volume?: number; durationSec?: number };
15
+
16
+ export type AudioProps = {
17
+ voice?: string; // staticFile, ex.: "editorial/voice.wav"
18
+ music?: string; // cama contínua
19
+ musicVolume?: number; // padrão 0.2
20
+ sfx?: SfxDef[]; // em ordem de aparição na fala; no máximo ~1 a cada 10 s
21
+ };
22
+
23
+ export type EditorialReelProps = {
24
+ words: WordsData;
25
+ scenes: SceneDef[];
26
+ theme?: Partial<EditorialTheme>;
27
+ audio?: AudioProps;
28
+ overlap?: number; // s de sobreposição entre cenas
29
+ tailSeconds?: number; // respiro depois da última palavra
30
+ };
31
+
32
+ export const exampleScenes: SceneDef[] = [
33
+ { kind: "hook", at: "I", a: "I shipped an app", b: "in one weekend." },
34
+ { kind: "prompt", at: "Here", a: "I only tell", b: "the agent what I want.", prompt: "build the signup page", marks: { typed: "want" }, typedMark: "typed" },
35
+ { kind: "phone", at: "Look", scale: 1 },
36
+ {
37
+ kind: "numbers", at: "In", chip: "48 hours",
38
+ marks: { c1: "31", c2: "12" },
39
+ counters: [{ to: 31, label: "commits", mark: "c1" }, { to: 12, label: "tests", mark: "c2" }],
40
+ },
41
+ { kind: "steps", at: "Three", prompt: "three agents", items: ["writes", "tests", "reviews"], marks: { typed: "agents" }, typedMark: "typed" },
42
+ {
43
+ kind: "doc", at: "First", title: "spec.md", altTitle: "agent coding",
44
+ marks: { code: "then", review: "code" },
45
+ pages: [
46
+ { lines: ["# Signup page", "", "- email and password", "- validate on submit", "- show errors inline", "- redirect to dashboard"] },
47
+ { mono: true, mark: "code", lines: ["export async function signUp(", " input: SignUpInput", ") {", " const data = schema.parse(input)", " return createUser(data)", "}"] },
48
+ ],
49
+ reviewers: { labels: ["correctness", "security", "regression", "scope"], mark: "review" },
50
+ },
51
+ { kind: "cta", at: "Comment", a: "Comment", b: "SHIP", keyword: "SHIP", footer: "and I send you the prompt", hideCaptions: true },
52
+ ];
53
+
54
+ export const exampleProps: EditorialReelProps = {
55
+ words: exampleWords as WordsData,
56
+ scenes: exampleScenes,
57
+ // Áudio entra por staticFile (arquivos em public/): descomente e troque os nomes.
58
+ // audio: { voice: "editorial/voice.wav", music: "editorial/music.mp3", sfx: [{ atWord: "Here", offset: -0.15, src: "editorial/swoosh.mp3", volume: 0.16 }] },
59
+ };
60
+
61
+ export const exampleSeconds = totalSeconds(exampleProps.words);
62
+
63
+ export const EditorialReel: React.FC<EditorialReelProps> = ({
64
+ words, scenes, theme, audio, overlap = 0.12, tailSeconds = 1.4,
65
+ }) => {
66
+ const merged = useMemo(() => mergeTheme(theme), [theme]);
67
+ const end = totalSeconds(words, tailSeconds);
68
+ const timed = useMemo(() => buildScenes(words.words, scenes, { end, overlap }), [words, scenes, end, overlap]);
69
+ const ctaStart = timed.find((s) => s.def.kind === "cta" && s.def.hideCaptions)?.from;
70
+ const sfx = useMemo(
71
+ () =>
72
+ (audio?.sfx ?? []).reduce<{ cursor: number; items: { from: number; def: SfxDef }[] }>(
73
+ (acc, def) => {
74
+ const t = at(words.words, def.atWord, acc.cursor);
75
+ return { cursor: t, items: [...acc.items, { from: Math.max(0, t + (def.offset ?? 0)), def }] };
76
+ },
77
+ { cursor: 0, items: [] },
78
+ ).items,
79
+ [audio, words],
80
+ );
81
+ const endFrame = toFrames(end);
82
+ const fadeOutFrame = toFrames(end - 2);
83
+ const musicVolume = audio?.musicVolume ?? 0.2;
84
+ return (
85
+ <ThemeContext.Provider value={merged}>
86
+ <AbsoluteFill>
87
+ <Backdrop />
88
+ {audio?.music ? (
89
+ <Audio
90
+ src={staticFile(audio.music)}
91
+ volume={(f) => interpolate(f, [0, 15, fadeOutFrame, endFrame], [0, musicVolume, musicVolume, 0], clamp)}
92
+ />
93
+ ) : null}
94
+ {audio?.voice ? <Audio src={staticFile(audio.voice)} /> : null}
95
+ {timed.map((s, i) => (
96
+ <Sequence key={i} from={toFrames(s.from)} durationInFrames={Math.max(1, toFrames(s.dur))}>
97
+ <Stage dur={s.dur}>
98
+ <SceneView scene={s.def} ctx={{ dur: s.dur, mark: (name, fb = 0) => (name ? s.marks[name] ?? fb : fb) }} />
99
+ </Stage>
100
+ </Sequence>
101
+ ))}
102
+ {sfx.map((s, i) => (
103
+ <Sequence key={`sfx${i}`} from={toFrames(s.from)} durationInFrames={toFrames(s.def.durationSec ?? 1)}>
104
+ <Audio src={staticFile(s.def.src)} volume={s.def.volume ?? 0.16} />
105
+ </Sequence>
106
+ ))}
107
+ <Captions words={words.words} hideFrom={ctaStart === undefined ? undefined : ctaStart} />
108
+ </AbsoluteFill>
109
+ </ThemeContext.Provider>
110
+ );
111
+ };
@@ -0,0 +1,77 @@
1
+ # remotion-editorial
2
+
3
+ Molde do estilo "motion editorial" (referência: shorts do Greg Isenberg): um objeto por vez no centro de um fundo claro com grade, cards escuros com sombra funda, serifa itálica bicolor, texto digitado em pílulas, conectores tracejados, música baixa e legenda pequena. Tudo guiado por palavras-âncora da fala (ctc-align).
4
+
5
+ Testado com Remotion 4.0.527, React 18, `@remotion/google-fonts` 4.0.527. Canvas 1080x1920, 30 fps. Sem outras dependências.
6
+
7
+ ## Arquivos
8
+
9
+ | arquivo | papel |
10
+ |---|---|
11
+ | `theme.ts` | `EditorialTheme`, `defaultTheme`, `mergeTheme`, `ThemeContext`, fontes (Playfair Display itálico, Inter, JetBrains Mono), `W/H/FPS` |
12
+ | `primitives.tsx` | `Stage`, `Backdrop`, `Headline`, `Rays`, `Ring`, `Pill`, `Chip`, `Dash`, `PhoneCard`, `Counter`, `DocCard` |
13
+ | `captions.tsx` | `Captions` (blocos de até 4 palavras, palavra falada em destaque) e `buildChunks` |
14
+ | `timeline.ts` | `at(words, texto, after)`, `resolveMarks`, `buildScenes` (âncoras + sobreposição), `totalSeconds` |
15
+ | `scenes.tsx` | tipo `SceneDef` (cenas como dados) e `SceneView` (7 tipos: hook, prompt, phone, numbers, steps, doc, cta) |
16
+ | `EditorialReel.tsx` | composição genérica + `exampleScenes`, `exampleProps`, `exampleSeconds` |
17
+ | `example-words.json` | palavras de exemplo no formato do ctc-align |
18
+
19
+ ## Como usar
20
+
21
+ 1. Copie a pasta inteira para `src/` do projeto Remotion (ex.: `src/editorial/`).
22
+ 2. Registre no `Root.tsx`:
23
+
24
+ ```tsx
25
+ import { EditorialReel, exampleProps, exampleSeconds } from "./editorial/EditorialReel";
26
+ import { FPS, H, W } from "./editorial/theme";
27
+
28
+ <Composition
29
+ id="editorial-example"
30
+ component={EditorialReel}
31
+ defaultProps={exampleProps}
32
+ durationInFrames={Math.ceil(exampleSeconds * FPS)}
33
+ fps={FPS}
34
+ width={W}
35
+ height={H}
36
+ />
37
+ ```
38
+
39
+ 3. Troque as três entradas, nesta ordem:
40
+ - **palavras**: `words` = saída do ctc-align `{ words: [{ text, start, end }], total }`. Trocou a voz, realinhe o JSON; as cenas acompanham.
41
+ - **cenas**: array de `SceneDef`. Cada cena tem `at` (palavra onde ela começa) e `marks` opcional (nome -> palavra interna, ex.: quando o contador sobe). Palavra repetida: a busca é monotônica, então basta manter as cenas na ordem da fala. Palavra inexistente lança erro claro.
42
+ - **tema**: `theme={{ bg, accent, ... }}` parcial. Campos: `bg grid glow vignette ink card cardHi line onAccent accent accentWarm accentLight accentShadow dash shadow serif sans mono`.
43
+ 4. Duração: `totalSeconds(words, tail)` = fim da fala + 1,4 s.
44
+ 5. Áudio (opcional, arquivos em `public/`): `audio={{ voice: "x/voz.wav", music: "x/cama.mp3", musicVolume: 0.2, sfx: [{ atWord: "Here", offset: -0.15, src: "x/swoosh.mp3", volume: 0.16 }] }}`. Nomes passam por `staticFile`.
45
+ 6. Screenshot: `{ kind: "phone", src: "x/tela.png", ratio: altura/largura }` (iPhone 1206x2622 = `2622/1206`, é o padrão). Sem `src`, desenha uma tela de exemplo. `src` também aceita URL `https:` ou `data:`.
46
+ 7. Fontes: Playfair Display itálico (600/700) via `@remotion/google-fonts/PlayfairDisplay`, carregada em `theme.ts`.
47
+
48
+ ## Tipos de cena
49
+
50
+ - `hook`: `a`, `b` (título bicolor) + anel girando.
51
+ - `prompt`: título + pílula digitada + raios; `typedMark` define até quando digita.
52
+ - `phone`: screenshot em card de celular.
53
+ - `numbers`: chip + até 2 contadores (`mark` = palavra em que cada um começa a subir).
54
+ - `steps`: pílula + raios + 3 chips ligados por tracejado.
55
+ - `doc`: card de documento com páginas (texto, depois código mono) e até 4 chips de revisão.
56
+ - `cta`: título + palavra-chave digitada + frase; `hideCaptions: true` some com a legenda dali em diante.
57
+
58
+ Para criar um tipo novo: componha as primitivas num componente em `scenes.tsx`, acrescente o ramo na união `SceneDef` e no `switch` de `SceneView`.
59
+
60
+ ## Regras do estilo
61
+
62
+ - Um objeto por vez no centro. Faixa útil y 180 a 1300; o terço de baixo é da legenda.
63
+ - Troca por transformação, nunca corte seco: a cena sai subindo e sumindo enquanto a próxima entra por mola (`overlap` 0,12 s).
64
+ - Música baixa e contínua, volume ~0,2, com fade de entrada e de saída.
65
+ - SFX raro: no máximo ~1 a cada 10 s, volume baixo (~0,16), só em viradas grandes.
66
+ - Legenda pequena, sem contorno, palavra falada em destaque; some quando a cena já carrega texto grande.
67
+ - Serifa itálica só nos momentos-chave (títulos, pílulas, chips); sans na legenda e no corpo do documento; mono só em código.
68
+ - Cor de destaque única; sobre a cor de destaque use `onAccent` (claro).
69
+ - Timing sempre por âncora de palavra, nunca por segundo fixo.
70
+
71
+ ## Verificar
72
+
73
+ ```bash
74
+ npx tsc --noEmit
75
+ npx remotion still src/index.ts editorial-example out/f.png --frame=330
76
+ ```
77
+
@@ -0,0 +1,56 @@
1
+ import React, { useMemo } from "react";
2
+ import { interpolate } from "remotion";
3
+ import { clamp, useSeconds } from "./primitives";
4
+ import { useTheme } from "./theme";
5
+ import type { Word } from "./timeline";
6
+
7
+ // Legenda discreta: blocos curtos de até 4 palavras, sem contorno, palavra falada em destaque.
8
+
9
+ export type Chunk = { words: Word[]; start: number; end: number };
10
+
11
+ const BREAK = /[,.:!?]$/;
12
+
13
+ // Agrupa palavras em blocos: corta em 4 palavras, pontuação ou pausa > 0.35 s.
14
+ export const buildChunks = (all: readonly Word[], maxWords = 4): Chunk[] => {
15
+ const ws = all.filter((w) => w.text.trim());
16
+ const raw = ws.reduce<{ done: Chunk[]; cur: Word[] }>(
17
+ (acc, w, i) => {
18
+ const cur = [...acc.cur, w];
19
+ const next = ws[i + 1];
20
+ const cut = cur.length === maxWords || BREAK.test(w.text) || !next || next.start - w.end > 0.35;
21
+ return cut
22
+ ? { done: [...acc.done, { words: cur, start: cur[0].start, end: w.end }], cur: [] }
23
+ : { ...acc, cur };
24
+ },
25
+ { done: [], cur: [] },
26
+ ).done;
27
+ // Cola o fim do bloco no começo do próximo quando a pausa é curta; senão segura 0,15 s.
28
+ return raw.map((c, i) => {
29
+ const next = raw[i + 1];
30
+ return { ...c, end: next && next.start - c.end < 0.5 ? next.start : c.end + 0.15 };
31
+ });
32
+ };
33
+
34
+ export const Captions: React.FC<{
35
+ words: readonly Word[];
36
+ hideFrom?: number; // s: some a partir daqui (ex.: cena final com texto próprio)
37
+ top?: number;
38
+ size?: number;
39
+ }> = ({ words, hideFrom, top = 1560, size = 50 }) => {
40
+ const c = useTheme();
41
+ const t = useSeconds();
42
+ const chunks = useMemo(() => buildChunks(words), [words]);
43
+ if (hideFrom !== undefined && t >= hideFrom) return null;
44
+ const chunk = chunks.find((x) => t >= x.start && t < x.end);
45
+ if (!chunk) return null;
46
+ const opacity = interpolate(t, [chunk.start, chunk.start + 0.1], [0, 1], clamp);
47
+ return (
48
+ <div style={{ position: "absolute", top, left: 90, right: 90, textAlign: "center", opacity, fontFamily: c.sans, fontWeight: 700, fontSize: size, lineHeight: 1.25, color: c.ink }}>
49
+ {chunk.words.map((w) => (
50
+ <span key={w.start} style={{ color: t >= w.start && t < w.end + 0.05 ? c.accent : c.ink }}>
51
+ {w.text.replace(BREAK, "")}{" "}
52
+ </span>
53
+ ))}
54
+ </div>
55
+ );
56
+ };