primocode 8.35.0 → 8.37.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.
@@ -84,12 +84,12 @@ cena. Se o assunto não empurrar para nenhuma, escolha a que você NÃO usou da
84
84
  | Assunto | `tema` | `trilha` | Estrutura |
85
85
  |---|---|---|---|
86
86
  | produto, tecnologia, lançamento | `meia-noite` ou `neon` | `pulso` | **Gancho** |
87
- | institucional, quem somos, serviço | `oceano` ou `floresta` | `ambiente` | **Retrato** |
87
+ | institucional, quem somos, serviço | `oceano` ou `floresta` | `foco` | **Retrato** |
88
88
  | urgência, notícia, alerta, dado duro | `brasa` | `tenso` | **Manchete** |
89
- | explicação, tutorial, passo a passo | `papel` ou `claro` | `lofi` | **Aula** |
89
+ | explicação, tutorial, passo a passo | `papel` ou `claro` | `foco` | **Aula** |
90
90
  | conquista, agradecimento, celebração | `doce` ou `neon` | `alegre` | **Brinde** |
91
91
  | memória, história, retrospectiva | `retro` ou `mono` | `lofi` | **Linha** |
92
- | curso, aula, treinamento, método | `ambar` | `lofi` | **Aula** |
92
+ | curso, aula, treinamento, método | `ambar` | `suave` | **Aula** |
93
93
  | relatório, número, análise técnica | `grafite` | `ambiente` | **Manchete** |
94
94
 
95
95
  ### As seis estruturas
@@ -161,9 +161,16 @@ Música sintetizada na hora, sem arquivo e sem licença de terceiro.
161
161
  | `epico` | grave, crescendo | 88 |
162
162
  | `alegre` | maior, arpejo rápido | 124 |
163
163
  | `tenso` | menor, suspense | 100 |
164
+ | `suave` | cama grave, sem bateria, quase não se nota | 60 |
165
+ | `foco` | pulso leve, sem caixa, para tutorial e institucional | 96 |
164
166
 
165
167
  A trilha gerada não acaba — acompanha o vídeo até a última cena.
166
168
 
169
+ **Vídeo de alguém falando pede `suave` ou `foco`.** As duas foram feitas para
170
+ ficar EMBAIXO de uma voz: moram fora da faixa da fala e não têm caixa batendo
171
+ em cima dela. `epico` e `tenso` são faixas grandes — bonitas num teaser sem
172
+ narração, e uma disputa com quem fala em qualquer outro caso.
173
+
167
174
  ### Arquivo do usuário
168
175
 
169
176
  ```json
@@ -213,9 +220,135 @@ Isso vale para o projeto do usuário, não para o PrimoCode.
213
220
 
214
221
  ## Elementos que dão cara de produção
215
222
 
216
- Estes seis existem porque um vídeo genérico é feito só de título + texto. São
223
+ Estes existem porque um vídeo genérico é feito só de título + texto. São
217
224
  baratos de escrever e mudam completamente o resultado.
218
225
 
226
+ **Todo vídeo leva pelo menos um.** Texto sobre fundo, do primeiro segundo ao
227
+ último, é o vídeo que o usuário chamou de "sem efeito nenhum" — e não é falta
228
+ de recurso: é este bloco não ter sido lido.
229
+
230
+ ## Anúncio de marca — o kit
231
+
232
+ Quando o pedido é o COMERCIAL de um produto (um cartão, um app, um plano), o
233
+ que separa uma peça de agência de um slide com música são quatro coisas, e as
234
+ quatro têm elemento próprio. Uma peça de marca sem elas sai genérica por mais
235
+ bonito que esteja o texto.
236
+
237
+ **Primeiro, declare a marca no documento.** Ela pinta o destaque de tudo —
238
+ número, botão, detalhe, o cartão — e libera o fundo `"marca"`:
239
+
240
+ ```json
241
+ "marca": { "cor": "#21C25E", "logo": "/midia/logo.svg" },
242
+ "tema": "claro"
243
+ ```
244
+
245
+ Tema CLARO na maioria dos anúncios de marca. O escuro é para a cena de
246
+ produto e para a vitrine; o resto da peça respira no branco.
247
+
248
+ ### `cartao` — o produto
249
+
250
+ ```json
251
+ { "tipo": "cartao", "nome": "PicPay", "variante": "Gold", "movimento": "flutuar" }
252
+ ```
253
+
254
+ Sai um cartão desenhado: chip, símbolo de aproximação, numeração mascarada,
255
+ nome e validade, com luz e sombra. `cor` (sem ela, a da marca), `largura`,
256
+ `numero` (só os 4 últimos aparecem), `titular`, `validade`, `face: "verso"`,
257
+ `movimento`: `flutuar` | `girar`.
258
+
259
+ **Mostre o produto antes de falar dele.** Um anúncio de cartão que só tem
260
+ frases é um anúncio sem produto.
261
+
262
+ ### `app` — a tela do aplicativo
263
+
264
+ ```json
265
+ { "tipo": "app", "topo": "Minha conta", "rotulo": "saldo disponível",
266
+ "valor": "R$ 1.284,90",
267
+ "linhas": [["Café da manhã", "R$ 18,90"], ["Mercado", "R$ 92,40"]],
268
+ "botao": "Pagar com o cartão" }
269
+ ```
270
+
271
+ Cabeçalho, um número grande, linhas com o nome à esquerda e o valor à direita
272
+ separadas por fio, e o botão cheio embaixo — que é o que faz o olho ler
273
+ "aplicativo". Aceita `["nome", "valor"]` e `"nome|valor"`. `escuro: true` para
274
+ a versão noturna.
275
+
276
+ **Ele vai DENTRO do `dispositivo`**, e aí ocupa a tela inteira do aparelho:
277
+
278
+ ```json
279
+ { "tipo": "dispositivo", "forma": "celular", "dentro": [ { "tipo": "app", ... } ] }
280
+ ```
281
+
282
+ Solto na cena ele vira o print da campanha. É o mesmo elemento de propósito:
283
+ com dois, a tela do celular e o print divergiriam na primeira mudança.
284
+
285
+ ### `vitrine` — a linha de produtos
286
+
287
+ ```json
288
+ { "tipo": "vitrine", "titulo": "Escolha seu cartão", "atual": 1, "nome": "PicPay",
289
+ "itens": [ { "nome": "Gold", "cor": "#C9A227", "detalhe": "sem anuidade" },
290
+ { "nome": "Verde", "detalhe": "cashback" },
291
+ { "nome": "Black", "cor": "#181B1F", "detalhe": "sala VIP" } ] }
292
+ ```
293
+
294
+ Os cartões numa fila, o `atual` à frente e os outros recuados. Vai bem sobre
295
+ fundo escuro (`"fundo": "#0b0f0c"`), que é como toda tela de "escolha o seu" é
296
+ desenhada.
297
+
298
+ ### `assinatura` — o ponto final
299
+
300
+ ```json
301
+ { "tipo": "assinatura", "nome": "PicPay", "frase": "o jeito fácil de pagar", "sobre": "marca" }
302
+ ```
303
+
304
+ Ela toma a CENA INTEIRA: o logo sozinho, grande, sobre a cor da casa. `sobre`:
305
+ `marca` | `claro` | `escuro`. Com `logo` ela usa a imagem; sem, o `nome`.
306
+
307
+ **Todo anúncio termina assim.** Sem a assinatura a peça acaba numa frase, e
308
+ ninguém fica sabendo quem falou.
309
+
310
+ ### A ordem que funciona
311
+
312
+ Uma peça de marca de 20 a 30 segundos, com 8 a 10 cenas:
313
+
314
+ | # | cena | o que entra |
315
+ |---|---|---|
316
+ | 1 | a promessa, em uma frase curta | `texto` pequeno, muito ar, fundo claro |
317
+ | 2 | o produto | `cartao` com `movimento` |
318
+ | 3 | o mote, na cor da casa | `fundo: "marca"`, `texto` branco, `anim: "palavra"` |
319
+ | 4 | funcionando | `dispositivo` + `app` |
320
+ | 5 | o benefício | `texto` |
321
+ | 6 | a linha | `vitrine` sobre fundo escuro |
322
+ | 7 | a última razão | `texto` |
323
+ | 8 | assinatura | `assinatura` |
324
+
325
+ E o rosto de quem fala, quando houver, entra por `clipe` entre a 4 e a 5 — o
326
+ depoimento é o que dá confiança, e nenhum desenho substitui.
327
+
328
+ ---
329
+
330
+ ### `icone` — o desenho ao lado do argumento
331
+
332
+ ```json
333
+ { "tipo": "icone", "nome": "raio", "tamanho": 120 }
334
+ { "tipo": "icone", "nome": "crescimento", "tamanho": 90, "cor": "#22d3ee", "traco": 2 }
335
+ ```
336
+
337
+ O jeito mais barato de uma cena deixar de ser um slide. Um ícone grande acima
338
+ do título, ou um por item de uma lista de três, e a mesma frase passa a ter
339
+ imagem.
340
+
341
+ Os nomes que existem (traço, no tom do tema):
342
+
343
+ `raio` `alvo` `foguete` `escudo` `relogio` `usuario` `pessoas` `coracao`
344
+ `estrela` `nuvem` `engrenagem` `lampada` `cadeado` `chave` `casa` `email`
345
+ `telefone` `camera` `fogo` `globo` `presente` `sino` `mapa` `dinheiro`
346
+ `crescimento` `balao` `pasta` `livro` `check` `grafico` `codigo` `video`
347
+ `play` `olho` `baixar` `tela` `cartao` `numero` `tabela` `imagem` `lista`
348
+
349
+ Nome que não existe desenha a estrela — então use os daqui, e não o que
350
+ parecer natural.
351
+
219
352
  ### `selo` — a pílula de estado
220
353
 
221
354
  ```json
@@ -373,16 +506,46 @@ trecho é longo, quebre em mais cenas do mesmo `src` em vez de encher a tela.
373
506
 
374
507
  ### Só a voz dele
375
508
 
376
- Para deixar apenas a voz da gravação: `"mudo": false` no clipe e trilha em
377
- volume baixo (`"volume": 0.15`) ou `"trilha": {"tipo": "nenhuma"}`. Para o
378
- contrário — a imagem dele com a sua narração por cima — `"mudo": true` e
509
+ Três combinações, e elas resolvem coisas diferentes:
510
+
511
+ **A voz dele com a imagem dele** (o padrão) — `"mudo": false` no clipe e a
512
+ trilha em volume baixo (`"volume": 0.15`) ou `"trilha": {"tipo": "nenhuma"}`.
513
+
514
+ **A imagem dele com a sua narração por cima** — `"mudo": true` no clipe e
379
515
  `narracao` na cena.
380
516
 
517
+ **A VOZ DELE COM A SUA TELA POR CIMA** — `"apenas": "som"` no clipe:
518
+
519
+ ```json
520
+ { "layout": "centro", "elementos": [
521
+ { "tipo": "clipe", "src": "/midia/gravacao.mp4", "de": 9.5, "ate": 17.1, "apenas": "som" },
522
+ { "tipo": "icone", "nome": "crescimento", "tamanho": 120 },
523
+ { "tipo": "texto", "papel": "titulo", "texto": "Três vezes mais rápido" },
524
+ { "tipo": "numeros", "itens": ["3x|velocidade", "0|instalação"], "contar": true } ] }
525
+ ```
526
+
527
+ O clipe some do quadro e continua tocando: a cena dura o trecho, o áudio dele
528
+ entra no arquivo exportado, e o que se VÊ é o que você montou. É assim que se
529
+ entrega "só a voz, com uma tela de exemplos" — a gravação vira a narração da
530
+ sua peça.
531
+
532
+ Use na parte em que ele EXPLICA, e não na em que ele aparece: o rosto de quem
533
+ fala é o que dá confiança, e escondê-lo o vídeo inteiro joga isso fora. O
534
+ desenho que costuma funcionar é alternar — ele na tela quando se apresenta e
535
+ quando conclui, a tela montada enquanto ele explica.
536
+
537
+ Não junte `"apenas": "som"` com `"mudo": true`: um cancela o outro e a cena
538
+ vira um silêncio do tamanho do trecho.
539
+
381
540
  ### A música que combina
382
541
 
383
- Não repita a mesma trilha em todo vídeo do usuário. Depoimento e história
384
- pessoal pedem `lofi` ou `ambiente`; anúncio e lançamento pedem `pulso` ou
385
- `epico`; algo leve pede `alegre`. A tabela de estilos está acima.
542
+ Não repita a mesma trilha em todo vídeo do usuário.
543
+
544
+ A primeira pergunta é se **alguém fala** no vídeo — narração ou o áudio da
545
+ gravação. Se fala, a trilha é cama: `suave` (mais discreta) ou `foco` (com
546
+ andamento). Só depois vem o assunto: depoimento e história pessoal pedem `lofi`
547
+ ou `ambiente`; anúncio e lançamento pedem `pulso` ou `epico`; algo leve pede
548
+ `alegre`. A tabela de estilos está acima.
386
549
 
387
550
  ---
388
551
 
package/studio/server.py CHANGED
@@ -751,11 +751,19 @@ def criar_analise(dados):
751
751
  for velho in sorted(ANALISES, key=lambda k: ANALISES[k]["criado"])[:len(ANALISES) - LIMITE_ANALISES + 1]:
752
752
  ANALISES.pop(velho, None)
753
753
 
754
+ # `manter` e `repetida` viajam SEM valor padrão aqui, e isso é de propósito:
755
+ # quem decide quanto de pausa fica é a página (ela conhece o tamanho de cada
756
+ # uma), e um padrão escrito também deste lado seria um segundo dono da mesma
757
+ # escolha — os dois divergem na primeira mudança e o que vale passa a ser o
758
+ # que ninguém lembra de olhar.
759
+ pedido = dados or {}
754
760
  ident = uuid.uuid4().hex[:10]
755
761
  ANALISES[ident] = {
756
762
  "id": ident, "arquivo": arquivo, "estado": "pedido",
757
- "silencio": float((dados or {}).get("silencio") or 0.45),
758
- "pausa": float((dados or {}).get("pausa") or 0.6),
763
+ "silencio": float(pedido.get("silencio") or 0.45),
764
+ "pausa": float(pedido.get("pausa") or 0.6),
765
+ "manter": None if pedido.get("manter") is None else float(pedido["manter"]),
766
+ "repetida": pedido.get("repetida") is not False,
759
767
  "criado": time.time(), "resultado": None,
760
768
  }
761
769
  return {"id": ident, "pagina": "/analise#" + ident, "estado": "pedido"}, 201
@@ -770,10 +778,16 @@ def gravar_analise(ident, dados):
770
778
  tarefa["resultado"] = {"erro": str(dados["erro"])[:300]}
771
779
  else:
772
780
  tarefa["estado"] = "pronto"
781
+ # ESTE DICIONÁRIO É UMA PENEIRA, e campo que não está aqui morre em
782
+ # silêncio. Foi assim que a lista de falas repetidas sumiu no dia em que
783
+ # a página passou a devolvê-la: o vídeo saía com o tropeço já cortado e
784
+ # nada na tela dizia o que tinha sido tirado. Campo novo na página entra
785
+ # AQUI junto, ou não existe.
773
786
  tarefa["resultado"] = {
774
787
  "duracao": dados.get("duracao"),
775
788
  "trechos": dados.get("trechos") or [],
776
789
  "pausas": dados.get("pausas") or [],
790
+ "repetidos": dados.get("repetidos") or [],
777
791
  "cortado": dados.get("cortado"),
778
792
  }
779
793
  return {"ok": True, "estado": tarefa["estado"]}, 200
@@ -820,8 +834,10 @@ def revisar_monotonia(doc, ferramenta):
820
834
 
821
835
  if ferramenta == "corte" and not doc.get("trilha"):
822
836
  avisos.append(
823
- "o vídeo está sem trilha. Seis estilos gerados existem (pulso, ambiente, "
824
- "lofi, epico, alegre, tenso) e nenhum custa arquivo nem licença — escolha "
837
+ "o vídeo está sem trilha. Oito estilos gerados existem (pulso, ambiente, "
838
+ "lofi, epico, alegre, tenso, suave, foco) e nenhum custa arquivo nem licença. "
839
+ "Se alguém FALA no vídeo, use suave ou foco: as duas foram feitas para ficar "
840
+ "embaixo de uma voz. Escolha "
825
841
  "pelo assunto.")
826
842
 
827
843
  duracoes = [c.get("duracao") for c in cenas if isinstance(c.get("duracao"), (int, float))]
@@ -840,6 +856,77 @@ def revisar_monotonia(doc, ferramenta):
840
856
  'toda animação de entrada é "%s". Varie: subir, surgir, esquerda, direita, '
841
857
  "zoom, desfoque, cair, girar, saltar, revelar, cortina, elastico." % anims[0])
842
858
 
859
+ # ── O QUE FALTA, E NÃO SÓ O QUE SE REPETE ───────────────────────────
860
+ #
861
+ # "Falta de efeitos visuais: não adiciona efeitos, não insere ícones ou
862
+ # elementos visuais de apoio no vídeo final."
863
+ #
864
+ # Os avisos acima cobram VARIEDADE — eles só disparam quando alguma coisa
865
+ # foi usada e usada sempre igual. Um vídeo que não usa NADA passava por
866
+ # todos eles calado, e era esse o vídeo do relato: texto sobre fundo, do
867
+ # primeiro segundo ao último. O que falta não aparece contando repetição;
868
+ # aparece contando ausência.
869
+ todos = [e for c in cenas for e in (c.get("elementos") or []) if isinstance(e, dict)]
870
+ tipos = {e.get("tipo") for e in todos}
871
+
872
+ if not doc.get("acabamento") and not any(c.get("acabamento") for c in cenas):
873
+ avisos.append(
874
+ "nenhuma cena tem acabamento. É a textura por cima do quadro inteiro "
875
+ "(grao, vinheta, scanlines, luz, brilho, moldura) e é o que tira a cara "
876
+ 'de template: "acabamento": "grao" no documento vale para o vídeo todo.')
877
+
878
+ apoio = {"icone", "selo", "forma", "grafico", "progresso", "dispositivo",
879
+ "conversa", "passos", "opcoes", "numeros", "avatar", "citacao"}
880
+ if not (tipos & apoio):
881
+ avisos.append(
882
+ "o vídeo é só texto e imagem: nenhum elemento de apoio em %d cenas. "
883
+ 'Um "icone" ao lado do argumento (raio, alvo, foguete, escudo, relogio, '
884
+ "lampada, cadeado, crescimento, pessoas, dinheiro, globo, fogo…), um "
885
+ '"selo" de estado, um "numeros" com "contar": true — é isto que faz a '
886
+ "tela parecer produzida em vez de um slide com legenda." % len(cenas))
887
+
888
+ movimento = any(e.get("chaves") or e.get("movimento") for e in todos)
889
+ if not movimento and not anims:
890
+ avisos.append(
891
+ "nada se mexe: nenhum elemento tem anim, chaves ou movimento. Imagem "
892
+ 'parada em vídeo denuncia preguiça — toda "imagem" leva "movimento" '
893
+ "(aproximar, afastar, esquerda, direita, cima), e uma ou duas peças por "
894
+ "vídeo levam chaves de animação.")
895
+
896
+ # ── ANÚNCIO DE MARCA SEM PRODUTO E SEM ASSINATURA ───────────────────
897
+ #
898
+ # "Ainda não tá bom, tá genérico. Quero nesse nível." — o dono, sobre um
899
+ # comercial de fintech mandado como referência.
900
+ #
901
+ # O que separava a peça daqui daquela não era tema nem cor: era não ter o
902
+ # PRODUTO na tela e não assinar no fim. Quem declarou uma `marca` está
903
+ # fazendo peça de marca, e aí estas duas ausências são o defeito inteiro.
904
+ if doc.get("marca") and len(cenas) >= 4:
905
+ if not (tipos & {"cartao", "app", "vitrine", "dispositivo", "imagem", "clipe"}):
906
+ avisos.append(
907
+ "esta peça tem marca declarada e nenhuma cena mostra o PRODUTO — são %d cenas "
908
+ "de texto sobre fundo. Um anúncio de cartão mostra o cartão (`cartao`), o app "
909
+ "funcionando dentro do celular (`dispositivo` + `app`) e a linha de produtos "
910
+ "(`vitrine`). É essa ausência que se lê como genérico." % len(cenas))
911
+ if "assinatura" not in tipos:
912
+ avisos.append(
913
+ 'o anúncio acaba sem assinar. A última cena de uma peça de marca é '
914
+ '{"tipo":"assinatura","nome":"…","sobre":"marca"} — o logo sozinho, grande, '
915
+ "sobre a cor da casa. Sem ela a peça termina numa frase e ninguém fica "
916
+ "sabendo quem falou.")
917
+
918
+ # Só a voz com tela montada: existe e quase nunca é lembrado, porque é o
919
+ # contrário do `mudo` e o contrário não estava no vocabulário até agora.
920
+ clipes = [e for e in todos if e.get("tipo") == "clipe"]
921
+ if clipes and len(clipes) >= 3 and not any(
922
+ e.get("apenas") == "som" or e.get("som") is True for e in clipes):
923
+ avisos.append(
924
+ "todos os %d clipes mostram a gravação. Se o que vale é o que ele DIZ, "
925
+ '"apenas": "som" no clipe mantém a voz e libera o quadro para a tela que '
926
+ "você montar por cima — é o vídeo dele virando narração da sua peça. "
927
+ "Vale para a parte em que ele explica, não para a em que ele aparece."
928
+ % len(clipes))
929
+
843
930
  return avisos
844
931
 
845
932
 
@@ -962,6 +1049,15 @@ def _clipe_invalido(e, i, j):
962
1049
  '"de": 12.5, "ate": 19.' % (onde, campo, v))
963
1050
  if v < 0:
964
1051
  return '%s tem "%s": %r — segundo negativo não existe.' % (onde, campo, v)
1052
+ # "apenas": "som" entrega a VOZ da gravação com a tela montada por cima.
1053
+ # Junto com "mudo": true ele não entrega nada: sem imagem porque foi
1054
+ # escondido, sem som porque foi silenciado. A cena sairia com a duração do
1055
+ # trecho e o conteúdo de um silêncio — que é exatamente o tipo de defeito
1056
+ # que só aparece quando alguém assiste o vídeo pronto.
1057
+ if (e.get("apenas") == "som" or e.get("som") is True) and e.get("mudo"):
1058
+ return ('%s tem "apenas": "som" com "mudo": true. Um cancela o outro: o clipe some da '
1059
+ 'tela E fica sem áudio, e a cena vira um silêncio do tamanho do trecho. Para a '
1060
+ 'voz dele com a sua tela por cima, tire o "mudo".' % onde)
965
1061
  de, ate = e.get("de"), e.get("ate")
966
1062
  if isinstance(de, (int, float)) and isinstance(ate, (int, float)) and ate <= de:
967
1063
  return ('%s tem "ate": %r antes ou igual a "de": %r. O trecho ficaria com duração zero ou '
@@ -987,9 +1083,16 @@ def validar_doc(modelo, doc):
987
1083
  # default do renderizador: texto cru de 16px num canvas de 1920 — ou nada,
988
1084
  # quando o texto estava noutro campo. Era o "vídeo com música mas tudo
989
1085
  # preto": documento válido na forma, invisível na tela.
1086
+ # O kit de anúncio (cartao, app, vitrine, assinatura) entra na lista abaixo.
1087
+ #
1088
+ # A LISTA É LIDA POR UM TESTE (`vocabulario.test.js`), que a recorta com uma
1089
+ # expressão regular e pega as palavras — então comentário DENTRO dela vira
1090
+ # "tipo que o render precisa desenhar", e o teste passa a cobrar `produto`,
1091
+ # `linha` e `fim`. Texto sobre os tipos mora aqui fora.
990
1092
  TIPOS_VALIDOS = {"texto", "lista", "cartoes", "numeros", "tabela", "codigo",
991
1093
  "citacao", "forma", "imagem", "icone", "grafico", "no", "conector",
992
1094
  "objeto3d", "clipe",
1095
+ "cartao", "app", "vitrine", "assinatura",
993
1096
  "selo", "progresso", "avatar", "dispositivo", "conversa",
994
1097
  "passos", "opcoes"}
995
1098
  # Os sete últimos são o vocabulário do vídeo de referência. O comentário