@crievo/omni 0.1.2 → 0.1.4

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 (122) hide show
  1. package/docs/onboarding.md +69 -161
  2. package/omnivideo.config.example.json +1 -1
  3. package/package.json +2 -1
  4. package/reference/learnings.md +12 -0
  5. package/reference/omnichat.md +5 -4
  6. package/reference/publish.md +9 -9
  7. package/reference/studio.md +1 -1
  8. package/studio/api/app.ts +2 -2
  9. package/studio/api/routes/sound.ts +22 -3
  10. package/studio/api/services/onboarding.ts +242 -21
  11. package/studio/api/services/settings.ts +4 -2
  12. package/studio/api/services/sound.ts +45 -0
  13. package/studio/shared/types/api.ts +26 -1
  14. package/studio/studio.ts +1 -0
  15. package/studio/web/components/AppShell.tsx +3 -1
  16. package/studio/web/components/CloneVoiceDialog/RecordStep.tsx +3 -0
  17. package/studio/web/components/CloneVoiceDialog/RecorderBar.tsx +37 -20
  18. package/studio/web/components/CloneVoiceDialog/useCloneVoice.ts +39 -12
  19. package/studio/web/components/MissingDmsPanel/BatchSend.tsx +13 -4
  20. package/studio/web/components/Omni/OmniWidget.tsx +14 -5
  21. package/studio/web/components/Omni/PendingActions.tsx +25 -6
  22. package/studio/web/components/OmnichatRulePanel/RuleConfig.tsx +13 -3
  23. package/studio/web/components/OmnichatRulePanel/RuleEnableConfirm.tsx +14 -4
  24. package/studio/web/components/ResultMessage.tsx +12 -3
  25. package/studio/web/components/Sidebar.tsx +63 -24
  26. package/studio/web/components/StageStepper.tsx +105 -34
  27. package/studio/web/components/Toaster.tsx +25 -0
  28. package/studio/web/components/VoiceCarousel.tsx +19 -16
  29. package/studio/web/components/VoiceProfilesSettings.tsx +8 -3
  30. package/studio/web/components/bencho/ActionNode.tsx +456 -0
  31. package/studio/web/components/bencho/AssigneePicker.tsx +246 -0
  32. package/studio/web/components/bencho/BenchoScope.tsx +8 -0
  33. package/studio/web/components/bencho/Checklist.tsx +421 -0
  34. package/studio/web/components/bencho/CreateMenu.tsx +258 -0
  35. package/studio/web/components/bencho/GenerateImage.tsx +290 -0
  36. package/studio/web/components/bencho/ImageAccordion.tsx +273 -0
  37. package/studio/web/components/bencho/LiquidToggle.tsx +289 -0
  38. package/studio/web/components/bencho/MagnifyDock.tsx +200 -0
  39. package/studio/web/components/bencho/NowPlaying.tsx +403 -0
  40. package/studio/web/components/bencho/OneTimeCode.tsx +374 -0
  41. package/studio/web/components/bencho/PullToRefresh.tsx +527 -0
  42. package/studio/web/components/bencho/SlideToConfirm.tsx +385 -0
  43. package/studio/web/components/bencho/SloshSlider.tsx +394 -0
  44. package/studio/web/components/bencho/StackedBars.tsx +342 -0
  45. package/studio/web/components/bencho/ToastStack.tsx +263 -0
  46. package/studio/web/components/bencho/UploadDropzone.tsx +394 -0
  47. package/studio/web/components/bencho/VoiceNote.tsx +705 -0
  48. package/studio/web/components/icons.tsx +27 -0
  49. package/studio/web/components/ui/slider.tsx +34 -1
  50. package/studio/web/components/ui/switch.tsx +18 -5
  51. package/studio/web/hooks/useOnboarding.ts +24 -6
  52. package/studio/web/index.html +1 -1
  53. package/studio/web/lib/alerts.ts +10 -0
  54. package/studio/web/lib/omni-panel.ts +4 -1
  55. package/studio/web/lib/toast.ts +196 -0
  56. package/studio/web/lib/upload.ts +24 -0
  57. package/studio/web/pages/BenchoPage/index.tsx +74 -0
  58. package/studio/web/pages/BenchoPage/registry.ts +174 -0
  59. package/studio/web/pages/GettingStartedPage/components/ComputerCard.tsx +102 -0
  60. package/studio/web/pages/GettingStartedPage/index.tsx +84 -58
  61. package/studio/web/pages/NewVideoPage/index.tsx +6 -2
  62. package/studio/web/pages/ReferencesPage/index.tsx +4 -1
  63. package/studio/web/pages/SettingsPage/components/AgentAccounts.tsx +30 -3
  64. package/studio/web/pages/VideoPage/components/ChangedVoices.tsx +14 -17
  65. package/studio/web/pages/VideoPage/components/LineTextEditor.tsx +1 -1
  66. package/studio/web/pages/VideoPage/components/PublishForm.tsx +24 -17
  67. package/studio/web/pages/VideoPage/components/ScheduledInfo.tsx +2 -1
  68. package/studio/web/pages/VideoPage/components/SoundTimeline/AddSound.tsx +12 -14
  69. package/studio/web/pages/VideoPage/components/SoundTimeline/BedRow.tsx +27 -11
  70. package/studio/web/pages/VideoPage/components/SoundTimeline/CaptionSaveBar.tsx +22 -18
  71. package/studio/web/pages/VideoPage/components/SoundTimeline/TimelineFooter.tsx +3 -4
  72. package/studio/web/pages/VideoPage/components/SoundTimeline/UploadTrack.tsx +59 -0
  73. package/studio/web/pages/VideoPage/components/SoundTimeline/useSoundTimeline.ts +1 -0
  74. package/studio/web/pages/VideoPage/components/VoicePauseBanner.tsx +4 -6
  75. package/studio/web/pages/VideoPage/index.tsx +4 -0
  76. package/studio/web/pages/VideosPage/components/VideoRow.tsx +2 -4
  77. package/studio/web/router.tsx +8 -0
  78. package/studio/web/styles/components/bencho/action-node.css +227 -0
  79. package/studio/web/styles/components/bencho/checklist.css +182 -0
  80. package/studio/web/styles/components/bencho/dock.css +188 -0
  81. package/studio/web/styles/components/bencho/gallery.css +109 -0
  82. package/studio/web/styles/components/bencho/generate.css +151 -0
  83. package/studio/web/styles/components/bencho/image-accordion.css +37 -0
  84. package/studio/web/styles/components/bencho/liq-create.css +279 -0
  85. package/studio/web/styles/components/bencho/liq-toggle.css +106 -0
  86. package/studio/web/styles/components/bencho/one-time-code.css +148 -0
  87. package/studio/web/styles/components/bencho/picker.css +204 -0
  88. package/studio/web/styles/components/bencho/pull.css +313 -0
  89. package/studio/web/styles/components/bencho/scope.css +42 -0
  90. package/studio/web/styles/components/bencho/slide-confirm.css +139 -0
  91. package/studio/web/styles/components/bencho/slosh.css +137 -0
  92. package/studio/web/styles/components/bencho/sound.css +260 -0
  93. package/studio/web/styles/components/bencho/stacked-bars.css +334 -0
  94. package/studio/web/styles/components/bencho/toast-stack.css +192 -0
  95. package/studio/web/styles/components/bencho/tokens.css +243 -0
  96. package/studio/web/styles/components/bencho/upload-dropzone.css +352 -0
  97. package/studio/web/styles/components/bencho/voice-note.css +337 -0
  98. package/studio/web/styles/components/clone-voice.css +12 -2
  99. package/studio/web/styles/components/omni.css +6 -1
  100. package/studio/web/styles/components/stepper.css +123 -7
  101. package/studio/web/styles/components/toaster.css +10 -0
  102. package/studio/web/styles/components/ui/slider.css +6 -16
  103. package/studio/web/styles/components/ui/switch.css +2 -41
  104. package/studio/web/styles/components/voice-carousel.css +46 -4
  105. package/studio/web/styles/index.css +23 -0
  106. package/studio/web/styles/pages/getting-started.css +46 -0
  107. package/studio/web/styles/pages/settings/shell.css +4 -0
  108. package/studio/web/styles/pages/video/layout.css +1 -4
  109. package/studio/web/styles/pages/video/sound-timeline.css +8 -0
  110. package/studio/web/styles/theme/identity/shell.css +10 -26
  111. package/tools/lib/agents.mjs +24 -13
  112. package/tools/lib/claude-settings.mjs +32 -3
  113. package/tools/lib/deps.mjs +24 -0
  114. package/tools/lib/platforms/instagram.mjs +1 -1
  115. package/tools/lib/up.mjs +5 -2
  116. package/tools/omni.mjs +7 -5
  117. package/tools/omnichat.mjs +5 -7
  118. package/tools/package.mjs +5 -3
  119. package/tools/publish.mjs +40 -137
  120. package/tools/reference.mjs +1 -1
  121. package/tools/setup.mjs +16 -15
  122. package/tools/sound.mjs +67 -3
@@ -1,123 +1,51 @@
1
1
  # Primeiros passos no OmniVideo
2
2
 
3
- Do zero ao primeiro vídeo: instalar as ferramentas, baixar o projeto, conectar o
4
- Claude Code, a voz, um estilo visual e o som, e pedir o vídeo. Publicar nas
5
- redes fica para o fim e é opcional.
6
-
7
- Cada passo diz o que fazer e como conferir. Com o studio aberto, a tela
8
- **Primeiros passos** (`http://127.0.0.1:5432/getting-started`) mostra este mesmo guia e marca
9
- sozinha o que já está pronto; no terminal, `node tools/setup.mjs check` faz a mesma
10
- conferência.
11
-
12
- ![A tela Primeiros passos do studio](onboarding/getting-started.png)
3
+ Do plugin instalado ao primeiro vídeo, sem terminal. Enquanto o OmniVideo prepara
4
+ este computador (alguns minutos), você deixa a voz, as redes e o estilo visual
5
+ prontos aqui no studio.
13
6
 
14
7
  O que você vai precisar ter ou criar:
15
8
 
16
- - um Mac (macOS) com acesso de administrador;
17
- - uma conta do Claude com Claude Code (plano Pro ou Max);
18
- - uma conta num provider de voz: Fish Audio ou Cartesia (ElevenLabs também serve);
19
- - uma assinatura da Epidemic Sound, de onde vêm os efeitos sonoros;
20
- - um vídeo ou fotos do estilo visual que você quer (um link ou imagens).
21
-
22
- <!-- step: machine -->
23
- ## 1. Ferramentas da máquina
24
-
25
- O OmniVideo roda inteiro na sua máquina. Ele precisa de Node.js, Bun, ffmpeg, Python
26
- e um Chrome. O jeito mais rápido no Mac é o [Homebrew](https://brew.sh):
27
-
28
- ```bash
29
- /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
30
- brew install node oven-sh/bun/bun ffmpeg python@3.12 yt-dlp
31
- brew install --cask google-chrome
32
- ```
33
-
34
- - **Node.js 20+** roda as ferramentas de `tools/` (render, montagem, publicação).
35
- - **Bun** roda o studio.
36
- - **ffmpeg** (com o `ffprobe`) mede, corta, mistura e exporta tudo.
37
- - **Python 3.10+** roda a voz e a legenda (o passo 2 cria um ambiente só do projeto).
38
- - **Google Chrome** é onde o motion é desenhado quadro a quadro. Chromium e Brave
39
- também servem; outro caminho vai em `CHROME_PATH`.
40
- - **yt-dlp** é opcional: baixa o estilo visual quando você manda um link.
41
-
42
- Confira com `node --version`, `bun --version`, `ffmpeg -version` e `python3 --version`.
43
-
44
- <!-- step: project -->
45
- ## 2. Baixar e preparar o projeto
46
-
47
- Clone o repositório e rode o setup. Ele não sobrescreve nada que já exista:
48
-
49
- ```bash
50
- git clone https://github.com/pedroflp/omni.git omnivideo
51
- cd omnivideo
52
- node tools/setup.mjs
53
- ```
54
-
55
- O setup:
56
-
57
- - cria o `.env` (as chaves, fora do git) e o `omnivideo.config.json` a partir dos exemplos;
58
- - cria a biblioteca de estilos visuais vazia, ao lado do projeto (`../referencias`);
59
- - cria o kit de efeitos vazio (`kit/sfx/kit.json`);
60
- - cria o Python do projeto (`.venv`, com `requests`, `soundfile` e `numpy`);
61
- - instala as dependências do studio (`bun install`).
62
-
63
- No fim, ele mostra a conferência de todos os passos deste guia. Daí em diante, abra o
64
- studio e siga pela tela:
65
-
66
- ```bash
67
- cd studio && bun start
68
- ```
69
-
70
- O studio abre em `http://127.0.0.1:5432`. Deixe esse terminal aberto enquanto usa.
71
-
72
- <!-- step: claude -->
73
- ## 3. Claude Code
74
-
75
- Quem escreve o roteiro, compõe o motion e conduz cada etapa é o Claude Code, aberto
76
- pelo studio. Instale e entre na sua conta:
77
-
78
- ```bash
79
- curl -fsSL https://claude.ai/install.sh | bash
80
- CLAUDE_CONFIG_DIR=~/.claude-personal claude
81
- ```
9
+ - o Claude Code com o plugin do OmniVideo instalado;
10
+ - uma conta num provider de voz: Fish Audio ou Cartesia;
11
+ - um vídeo ou fotos do estilo visual que você quer.
82
12
 
83
- Na primeira vez, o Claude pede para entrar: escolha a conta do Claude (Pro ou Max) e
84
- siga o login no navegador. Depois, digite `/exit`.
13
+ O kit de efeitos sonoros entra neste guia em breve.
85
14
 
86
- O studio sempre abre o Claude com `CLAUDE_CONFIG_DIR=~/.claude-personal`, para nunca
87
- misturar com outra conta da máquina (uma de trabalho, por exemplo). Por isso o login
88
- precisa ser feito nessa pasta. Para não digitar a variável toda vez, ponha no
89
- `~/.zshrc`:
15
+ <!-- step: computer -->
16
+ ## Este computador
90
17
 
91
- ```bash
92
- claude() { CLAUDE_CONFIG_DIR="$HOME/.claude-personal" command claude "$@"; }
93
- ```
18
+ Ao abrir o Claude Code com o plugin, o OmniVideo cria a pasta `~/OmniVideo`, abre o
19
+ navegador para você entrar e liga o studio deste computador. Depois, baixa sozinho o
20
+ que o vídeo precisa: o ffmpeg e o ffprobe (cortam, misturam e exportam), um Chrome
21
+ sem janela (desenha o motion quadro a quadro; se você já tem Chrome, Chromium ou
22
+ Brave, ele usa o seu) e o Python das ferramentas (voz e legenda).
94
23
 
95
- Em **Configurações → Agente de IA** você escolhe o modelo e o esforço de cada etapa e
96
- pode pôr mais contas no rodízio: outras pastas do Claude (`~/.claude-personal-2`…) e contas
97
- do Codex (o CLI que vem no app do ChatGPT, ou o `codex` no PATH). Cada sessão nova vai para a
98
- próxima conta, e a que bate o limite de uso sai do rodízio até liberar. O botão **Entrar**
99
- de cada conta abre o login no navegador.
24
+ Os itens com "preparando…" ainda estão baixando. Esta tela atualiza sozinha e
25
+ mostra **Computador pronto** quando tudo responder.
100
26
 
101
- <!-- step: voice -->
102
- ## 4. Voz
27
+ As skills do OmniVideo rodam no seu Claude Code e precisam de permissão uma vez. Se
28
+ a permissão aparecer como pendente, diga ao Claude "libere as skills do OmniVideo":
29
+ ele mostra as regras, pede o seu ok e grava.
103
30
 
104
- A narração é a sua voz clonada. O studio cria o clone na Fish Audio e na Cartesia
105
- pela API; você só precisa da chave de pelo menos uma delas.
31
+ <!-- step: voice-key -->
32
+ ## Chave de um provider de voz
106
33
 
107
- **A chave do provider.** Crie a conta e copie a chave da API:
34
+ A narração é a sua voz clonada, criada na Fish Audio ou na Cartesia pela API. Você
35
+ só precisa da chave de uma delas:
108
36
 
109
37
  - **Fish Audio**: [fish.audio](https://fish.audio), menu da conta → **API Keys**.
110
38
  - **Cartesia**: [play.cartesia.ai](https://play.cartesia.ai), **API Keys**.
111
- - **ElevenLabs** (opcional): [elevenlabs.io](https://elevenlabs.io), **Developers →
112
- API Keys**. Nela a voz entra pelo Voice ID de uma voz que você já criou lá.
113
39
 
114
- No studio, abra **Configurações → Voz → Provedores de voz** e cole a chave no
115
- provider. Ela vai para o `.env`, que nunca sobe para o git.
40
+ Abra **Provedores de voz** e cole a chave no provider. Ela fica só neste
41
+ computador.
116
42
 
117
43
  ![Provedores de voz: a chave de cada provider](onboarding/providers.png)
118
44
 
119
- **O clone.** Ainda em **Configurações → Voz**, vá até o último item da roleta,
120
- **Adicionar voz** (o "+"). Abre o quadro **Clonar a minha voz**:
45
+ <!-- step: voice-clone -->
46
+ ## Clonar a voz
47
+
48
+ Em **Clonar a minha voz**:
121
49
 
122
50
  1. escolha o idioma da fala;
123
51
  2. grave lendo o roteiro que aparece na tela, ou envie áudios seus (de 30 s a 3 min
@@ -126,87 +54,67 @@ provider. Ela vai para o `.env`, que nunca sobe para o git.
126
54
 
127
55
  ![Clonar a minha voz](onboarding/clone-voice.png)
128
56
 
129
- O studio cria a voz em cada provider marcado e grava o perfil. A primeira voz criada
130
- vira a voz ativa, que já vem marcada em cada vídeo novo. Se o plano do provider não
131
- deixar clonar pela API, o cartão dele avisa; nesse caso crie a voz no site do
132
- provider e use **Já tenho voz criada nos provedores**, com o Voice ID.
57
+ Se o plano do provider não deixar clonar pela API, o cartão dele avisa. Nesse caso,
58
+ crie a voz no site do provider e use **Já tenho voz criada nos provedores**, com o
59
+ Voice ID.
133
60
 
134
- <!-- step: reference -->
135
- ## 5. Estilo visual
61
+ <!-- step: voice-default -->
62
+ ## Voz padrão
136
63
 
137
- Todo vídeo segue um estilo visual: um vídeo (ou fotos) de que você gosta, medido
138
- (cores, ritmo, tipografia, movimento) e transformado num guia. O estilo visual é princípio,
139
- nunca asset copiado.
64
+ A voz padrão já vem marcada em cada vídeo novo. A primeira voz que você cria vira a
65
+ padrão sozinha; para trocar, abra **Perfil de voz** e escolha outra na roleta.
140
66
 
141
- Em **Estilos visuais → Novo estilo visual**:
67
+ <!-- step: platforms -->
68
+ ## Conectar YouTube, Instagram e TikTok
142
69
 
143
- 1. cole um **link** (Instagram, YouTube…; link precisa do `yt-dlp`, do passo 1), ou
144
- 2. envie **fotos** do estilo (telas, quadros, capturas), ou os dois;
145
- 3. clique em **Criar estilo visual**.
70
+ Opcional: só para publicar direto do studio. Dá para fazer agora ou depois do
71
+ primeiro vídeo.
146
72
 
147
- ![Novo estilo visual](onboarding/references.png)
73
+ Em **Configurações → Plataformas**, cada rede tem o seu cartão. O YouTube e o TikTok
74
+ pedem as chaves de um app de desenvolvedor seu (Google Cloud e TikTok for Developers,
75
+ criados uma vez) e abrem o login no navegador. O Instagram pede o token gerado no app
76
+ da Meta. O passo a passo de cada uma está em [`reference/publish.md`](../reference/publish.md).
148
77
 
149
- O Claude mede o vídeo (com fotos, lê as imagens) e escreve o guia em alguns minutos; a tela
150
- do estilo visual mostra o andamento. A biblioteca fica em `../referencias` (o caminho está em
151
- `references.dir`, na config) e serve para todos os vídeos.
78
+ ![Publicar](onboarding/publish.png)
152
79
 
153
- <!-- step: sound -->
154
- ## 6. Efeitos sonoros
80
+ Publicar é sempre um pedido seu: nada sobe sozinho, a menos que você ligue a
81
+ publicação automática em **Configurações → Publicação**.
82
+
83
+ <!-- step: style -->
84
+ ## Escolher um estilo visual
155
85
 
156
- Os efeitos vêm só da Epidemic Sound. O projeto começa com o kit vazio: na primeira
157
- composição, o Claude busca na Epidemic pelo seu Chrome, ouve, escolhe e baixa cada
158
- efeito. Os bons entram no kit (`kit/sfx/`) e passam a valer para todos os vídeos, sem
159
- buscar de novo.
86
+ Todo vídeo segue um estilo visual: um vídeo ou fotos de que você gosta, medidos
87
+ (cores, ritmo, tipografia, movimento) e transformados num guia. O estilo é princípio,
88
+ nunca asset copiado.
160
89
 
161
- Para isso:
90
+ Em **Estilos visuais → Novo estilo visual**, envie **fotos** do estilo (telas,
91
+ quadros, capturas) ou cole um **link** (Instagram, YouTube…) e clique em **Criar
92
+ estilo visual**. Fotos e links funcionam igual: o link é baixado pelo `yt-dlp`, que o
93
+ OmniVideo traz junto com o ffmpeg e o Chrome sem janela.
162
94
 
163
- 1. assine a [Epidemic Sound](https://www.epidemicsound.com) e entre na conta no
164
- Google Chrome;
165
- 2. instale a extensão **Claude in Chrome** (na Chrome Web Store) nesse Chrome e
166
- entre com a mesma conta do Claude do passo 3;
167
- 3. deixe o Chrome aberto quando pedir o primeiro vídeo.
95
+ ![Novo estilo visual](onboarding/references.png)
168
96
 
169
- Sem a extensão e com o kit vazio, a composição para e pede para abrir o Chrome: o
170
- OmniVideo nunca usa um áudio de outra fonte.
97
+ O Claude mede e escreve o guia em alguns minutos; a tela do estilo mostra o
98
+ andamento. O estilo vale para todos os vídeos.
171
99
 
172
100
  <!-- step: first-video -->
173
- ## 7. O primeiro vídeo
101
+ ## Primeiro vídeo
174
102
 
175
- Clique em **Criar vídeo**:
103
+ Em **Criar vídeo**:
176
104
 
177
105
  1. cole o pedido: um roteiro pronto, um tema, um material ou um link;
178
- 2. escolha o formato (vertical 9:16 ou horizontal 16:9), a voz e o estilo visual de
179
- estilo (ou deixe o agente escolher);
106
+ 2. escolha o formato (vertical 9:16 ou horizontal 16:9), a voz e o estilo visual
107
+ (ou deixe o agente escolher);
180
108
  3. clique em **Criar e começar**.
181
109
 
182
110
  ![Criar vídeo](onboarding/new-video.png)
183
111
 
184
- Daí o Claude trabalha sozinho e a tela do vídeo mostra cada etapa no stepper. Ele para
185
- duas vezes para você:
112
+ O Claude trabalha sozinho e a tela do vídeo mostra cada etapa. Ele para duas vezes
113
+ para você:
186
114
 
187
115
  - **O roteiro.** Aprove, peça alteração ou edite o texto. Se ninguém mexer, ele se
188
116
  aprova sozinho em 10 min.
189
- - **As cenas.** Depois do roteiro de motion, cada cena aparece como um still para
190
- aprovar ou pedir alteração. Se ninguém mexer, elas se aprovam sozinhas em 10 min.
117
+ - **As cenas.** Cada cena aparece como um still para aprovar ou pedir alteração. Se
118
+ ninguém mexer, elas se aprovam sozinhas em 10 min.
191
119
 
192
- Aprovadas as cenas, composição, narração, voz, render, legenda e montagem rodam até
193
- o MP4, que aparece na
194
- etapa **Exportação** e fica em `videos/<data-slug>/export/`. O tempo depende do
195
- tamanho do vídeo; a tela mostra o que está rodando e a fila da máquina.
196
-
197
- <!-- step: publish -->
198
- ## 8. Publicar nas redes (opcional)
199
-
200
- Com o vídeo pronto, a etapa **Publicar** sobe o MP4 no YouTube, no Instagram e no
201
- TikTok, agora ou num dia e hora marcados na **Agenda**. O título, a descrição e a
202
- legenda o Claude escreve a partir do roteiro, e você edita antes.
203
-
204
- ![Publicar](onboarding/publish.png)
205
-
206
- Cada rede precisa de um app de desenvolvedor seu, criado uma vez. Conecte em
207
- **Configurações → Plataformas**; o passo a passo de cada uma (Google Cloud, Meta e
208
- TikTok) está em [`reference/publish.md`](../reference/publish.md). O Instagram
209
- busca o vídeo por um túnel temporário do `cloudflared` (`brew install cloudflared`).
210
-
211
- Publicar é sempre um pedido seu: nada sobe sozinho, a menos que você ligue a
212
- publicação automática em **Configurações → Publicação**.
120
+ Aprovadas as cenas, o resto roda até o MP4, que aparece na etapa **Exportação**.
@@ -397,7 +397,7 @@
397
397
  }
398
398
  },
399
399
  "agents": {
400
- "accounts": [{ "id": "claude-personal", "provider": "claude", "dir": "~/.claude-personal" }],
400
+ "accounts": [],
401
401
  "codex": { "model": "gpt-6-luna", "effort": "medium", "sandbox": "auto" }
402
402
  },
403
403
  "studio": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crievo/omni",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "OmniVideo no seu computador: o CLI omni, o studio local e as ferramentas do pipeline.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -30,6 +30,7 @@
30
30
  "border-beam": "1.4.1",
31
31
  "hono": "4.13.9",
32
32
  "marked": "^18.1.0",
33
+ "motion": "^14.3.0",
33
34
  "react": "19",
34
35
  "react-day-picker": "^10.0.2",
35
36
  "react-dom": "19",
@@ -790,3 +790,15 @@ these voices).
790
790
  - **Symptom:** the cloud answered `429 error code: 1027` (the Workers Free plan's 100k requests a day), and the studio said "A cloud do omnichat não responde". The Worker had 140k requests that day, ~5k an hour since the evening before and 43k in one hour, with only a few real comments.
791
791
  - **Cause:** a read that wrote. `pullRecord` (`tools/omnichat.mjs`) rewrote `omnichat-record.json` on every read, with a fresh `pulled_at`. The write is a studio notice (`events.ts`), the notice makes every open omnichat page reread every rule (`log --json`, one cloud request each), and that read wrote again: an endless loop, ~400–600 requests a minute per open Omnichat tab. The hosted studio's offline copy reread the same routes on every notice even with no tab open (~80 a minute). `publish.mjs` (the scheduler, every minute while a cloud job runs) and `metrics.mjs` (the account file) also rewrote unchanged files.
792
792
  - **Fixed:** a pull, `writePub` and the metrics account file write only when the content changed; the studio caches the cloud health check for 60 s and each rule's log for 30 s while its record file is unchanged; the offline copy is gone. Rule for the future: anything a read path writes into a watched file must skip an unchanged write, or it loops against whatever it calls.
793
+
794
+ ## Magenta tint on semi-transparent UI under the motion-blur SVG filter (18/10/2026)
795
+
796
+ - **Symptom:** in `2026-10-18-guia-de-motion-design-spotify` (c02, mid-pan), list rows fading in (opacity < 1, with their own CSS `blur()` from the depth of field) came out purple/magenta while the camera moved; rows at opacity 1 were fine.
797
+ - **Cause:** the world layer carries `filter: url(#mb)` (an SVG `feGaussianBlur` for vertical motion blur). SVG filters default to `color-interpolation-filters: linearRGB`, and Chrome's conversion of the semi-transparent, already-blurred children through that path shifts dark colours toward magenta.
798
+ - **Fixed:** `color-interpolation-filters="sRGB"` on the `<filter>`. Any SVG blur used as motion blur over UI should carry it.
799
+
800
+ ## Frame 0 of a scene missed an element gated on its own beat (18/10/2026)
801
+
802
+ - **Symptom:** in `2026-10-18-roteiro-pronto-guia-de-motion`, c06's first frame was empty cream: the symbol that should be born at scale 0.35 on frame 0 only showed from the second frame, and the c05 → c06 cut sheet read as "ring gone, blank, symbol pops".
803
+ - **Cause:** floating point. The build sums scene durations, so `OMNIVIDEO.start` for c06 was `16.799999999999997`, while the beat `simbolo` (from `video.scenes[].beats`) was `16.8`. A helper gated with `if (T >= t0)` (`OV.signAt`) was false at `t = 0`. Progress helpers (`seg`, clamped) don't care; only hard `>=` gates do.
804
+ - **Fixed:** the gate takes a tolerance, `T >= t0 - 1e-4`. Rule: any on/off gate on a beat time in a continuous composition compares with an epsilon, never with a bare `>=`.
@@ -200,9 +200,8 @@ commented (the trigger), follows the account, tapped the button.
200
200
  ```bash
201
201
  node tools/omnichat.mjs webhook setup # creates INSTAGRAM_WEBHOOK_VERIFY_TOKEN and prints the steps
202
202
  # put the app secret in .env: INSTAGRAM_APP_SECRET (Instagram → API setup with Instagram login → App secret)
203
- node tools/omnichat.mjs webhook serve # listens on 127.0.0.1:5434/webhook
204
- cloudflared tunnel --url http://127.0.0.1:5434 # the public HTTPS URL Meta needs
205
- # Meta panel → Instagram → API setup → Configure webhooks: Callback URL = <tunnel>/webhook, Verify token = the one setup printed,
203
+ # Meta panel → Instagram → API setup → Configure webhooks: Callback URL = https://omnichat.<you>.workers.dev/webhook
204
+ # (the cloud Worker, below), Verify token = the one setup printed,
206
205
  # fields messages and messaging_postbacks
207
206
  node tools/omnichat.mjs webhook subscribe # signs the account up to those fields
208
207
  node tools/omnichat.mjs webhook status
@@ -215,7 +214,9 @@ node tools/omnichat.mjs webhook status
215
214
  - The tap is answered by the cloud Worker: the same `handleWebhook` (`tools/lib/omnichat/webhook.mjs`)
216
215
  runs in a function with its own storage; `tools/lib/omnichat/gate.mjs` has the decision and the sends.
217
216
  - The record is written under the pass lock, so the agent and the tap don't overwrite each other.
218
- - Start `webhook serve` and the tunnel only when the user asks; enabling the rule is what authorizes the replies.
217
+ - `node tools/omnichat.mjs webhook serve` (127.0.0.1:5434/webhook) is the same handler on this machine, for a
218
+ test behind any public HTTPS address; start it only when the user asks. Enabling the rule is what authorizes
219
+ the replies.
219
220
 
220
221
  ## In the cloud (Cloudflare Worker + Postgres)
221
222
 
@@ -96,12 +96,13 @@ publishing, generate another one (step 4).
96
96
 
97
97
  **How the video gets there.** With Instagram login, Meta doesn't accept the
98
98
  file: it downloads the video from a public URL (direct upload is only for apps
99
- with Facebook login). `publish.mjs` opens that URL on the spot: a server on
100
- `127.0.0.1` that serves only that MP4, at a random path, behind a `cloudflared`
101
- quick tunnel (`https://<random>.trycloudflare.com/<token>.mp4`). The tunnel
102
- takes about 15 s to respond, stays up until Instagram finishes processing and
103
- closes right after. It needs `cloudflared` installed (`brew install
104
- cloudflared`).
99
+ with Facebook login). `publish.mjs` uploads the MP4 (and the cover) to the
100
+ cloud's R2 bucket through the Worker (`PUT /api/media/<slug>/<file>`), and
101
+ Instagram downloads it from `<cloud.url>/media/schedule/now-<slug>/<random>-<file>`. The
102
+ file is deleted as soon as Instagram finishes processing; the bucket's 7-day
103
+ rule deletes anything left. It needs the cloud (`cloud.url` in the config and
104
+ `OMNICHAT_CLOUD_SECRET` in `.env`, reference/omnichat.md), the same one the
105
+ schedule uses (ADR 0004). Nothing to install here.
105
106
 
106
107
  The video goes as Reels, also to the feed (`share_to_feed`). The cover is the
107
108
  selected thumbnail (below, **Cover**); without one, the frame at `cover_at_s`
@@ -116,7 +117,7 @@ accept 3 s to 15 min.
116
117
  carousel's Publicar stage in the studio does the same). It uploads
117
118
  `carousel/export/NN.jpg` (exported on the spot if the build changed after the last
118
119
  export) as a carousel post. Images don't go up as files either: each JPEG goes
119
- through the same `cloudflared` tunnel as the Reel, becomes a child container
120
+ through the cloud's R2, like the Reel, becomes a child container
120
121
  (`is_carousel_item`), and the `CAROUSEL` container joins the children with the
121
122
  caption. The caption is `carousel/publication.json`'s (the one edited in the
122
123
  studio) or `carousel.json`'s `caption`, with the Instagram footer and the
@@ -217,8 +218,7 @@ if you switch node versions, turn it on again. The log is in
217
218
  becomes **failed**, with the reason, waiting for you to schedule again or
218
219
  publish now. The ones already up don't go up again.
219
220
  - **Publishing by hand** a scheduled platform removes it from the schedule.
220
- - **Instagram:** the `cloudflared` tunnel opens at publishing time, so the
221
- internet has to be up at that time, as with the button.
221
+ - **Instagram:** it downloads the video from the cloud's R2, like the button.
222
222
 
223
223
  ```bash
224
224
  node tools/publish.mjs schedule videos/<slug> --when "2026-10-14 18:00" --youtube --tiktok --confirm
@@ -476,7 +476,7 @@ omnichat classifier), the screen in Configurações → Agente de IA → Contas
476
476
 
477
477
  | `agents` | |
478
478
  |---|---|
479
- | `accounts` | `[{ id, provider: "claude" \| "codex", dir, label?, enabled? }]`, in rotation order. `dir` is the account's `CLAUDE_CONFIG_DIR` or `CODEX_HOME`, each with its own login (`claude auth login` / `codex login` with the variable set; the screen's "Entrar" runs it). Empty: `~/.claude-personal`. `~/.claude` is refused in code |
479
+ | `accounts` | `[{ id, provider: "claude" \| "codex", dir, label?, enabled? }]`, in rotation order. `dir` is the account's `CLAUDE_CONFIG_DIR` or `CODEX_HOME`, each with its own login (`claude auth login` / `codex login` with the variable set; the screen's "Entrar" runs it). Empty: the Claude Code this machine uses, the folder the plugin's hook recorded in `~/.omni/claude.json`, else `~/.claude` (run without `CLAUDE_CONFIG_DIR`, login in `~/.claude.json`); without a login there, the banner and Configurações → Agente de IA ask for the folder |
480
480
  | `codex.model`, `codex.effort` | the model of every Codex session (`-m`, `model_reasoning_effort`); the options come from the account's `models_cache.json` |
481
481
  | `codex.sandbox` | `auto` (default): workspace-write with network and the library writable, `approval_policy=on-request` with Codex's `auto_review` reviewer, so what must leave the sandbox (Chrome in the renders) asks and is decided like Claude's auto mode. `full`: no sandbox |
482
482
  | `codex.bin` | the CLI; default `codex` on the PATH or the one the ChatGPT app ships |
package/studio/api/app.ts CHANGED
@@ -33,7 +33,7 @@ import { schedule } from "./routes/schedule";
33
33
  import { services } from "./routes/services";
34
34
  import { sessions } from "./routes/sessions";
35
35
  import { settings } from "./routes/settings";
36
- import { sound } from "./routes/sound";
36
+ import { sound, SOUND_UPLOAD_PATH } from "./routes/sound";
37
37
  import { thumbnail } from "./routes/thumbnail";
38
38
  import { usage } from "./routes/usage";
39
39
  import { videos } from "./routes/videos";
@@ -75,7 +75,7 @@ export function createApp({ port }: AppOptions) {
75
75
  .route("/", agentHistory)
76
76
  .route("/", events);
77
77
  const app = router()
78
- .use(localOnly({ port, uploadPaths: [UPLOAD_PATH] }))
78
+ .use(localOnly({ port, uploadPaths: [UPLOAD_PATH, SOUND_UPLOAD_PATH] }))
79
79
  .use(jsonCharset)
80
80
  .route("/api", api)
81
81
  .route("/", media);
@@ -1,17 +1,36 @@
1
- /* Sound timeline (SoundTimeline): data, the draft plan (sound.mjs), save to scenes.json, reassemble
2
- and adding an Epidemic effect. */
1
+ /* Sound timeline (SoundTimeline): data, the draft plan (sound.mjs), save to scenes.json, reassemble,
2
+ adding an Epidemic effect and the user's own track (upload). */
3
3
 
4
4
  import { json } from "../lib/validate";
5
5
  import { router } from "../lib/hono";
6
6
  import { result, shaped } from "../lib/result";
7
7
  import { videoDir } from "../lib/video";
8
- import { captureFromPage, downloadEffect, getSoundData, planDraft, reassemble, saveSound } from "../services/sound";
8
+ import { noTimeout } from "../middleware/no-timeout";
9
+ import {
10
+ captureFromPage,
11
+ downloadEffect,
12
+ getSoundData,
13
+ planDraft,
14
+ reassemble,
15
+ saveSound,
16
+ uploadTrack,
17
+ } from "../services/sound";
9
18
  import { refreshStatus } from "../services/videos";
10
19
  import { emptyBody } from "../../shared/schemas/common";
11
20
  import { soundCaptureBody, soundDownloadBody, soundDraftBody } from "../../shared/schemas/sound";
12
21
  import type { SoundData } from "../../shared/types/api";
13
22
 
23
+ /* the track upload takes the raw body with the file name in x-file-name and the video in x-video (the
24
+ local-only guard checks the name, like the style upload) */
25
+ export const SOUND_UPLOAD_PATH = "/api/sound/upload";
26
+
14
27
  export const sound = router()
28
+ .post("/sound/upload", noTimeout, async c => {
29
+ const dir = videoDir(decodeURIComponent(c.req.header("x-video") ?? ""));
30
+ /* NaN when the header is missing: the tool finds an empty file by itself */
31
+ const size = Number(c.req.header("content-length") ?? Number.NaN);
32
+ return result(c, await uploadTrack(dir, c.req.header("x-file-name") ?? "", c.req.raw.body, size));
33
+ })
15
34
  .get("/videos/:slug/sound", c => {
16
35
  const dir = videoDir(c.req.param("slug"));
17
36
  try {