@danhachuel/thunderbolt 0.7.36 → 0.7.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/app/main.py CHANGED
@@ -2155,7 +2155,7 @@ def render_thumbnail_blueprints():
2155
2155
  script = st.selectbox("Blueprint de roteiro associado", blueprint_ids, index=blueprint_ids.index(current_script) if current_script in blueprint_ids else 0, format_func=lambda item: blueprint_labels.get(item, item or "Sem Blueprint padrão"), key=f"thumbnail_script_blueprint_channel_{channel_id}")
2156
2156
  with cols[2]:
2157
2157
  if st.button("Guardar par", key=f"thumbnail_blueprint_save_{channel_id}", width="stretch"):
2158
- if thumb == "Generic_Thumbnail_Blueprint" and script:
2158
+ if thumb in {"Youtube_Generic_Thumbnail_Blueprint", "Tiktok_Generic_Thumbnail_Blueprint"} and script:
2159
2159
  st.error("Not Allowed to Associate, System Use Only")
2160
2160
  else:
2161
2161
  update_channel(channel_id, {"thumbnail_blueprint_id": thumb, "default_thumbnail_blueprint_id": thumb, "blueprint_id": script, "default_blueprint_id": script})
@@ -3176,7 +3176,7 @@ def render_channels():
3176
3176
  st.caption("Esta subaba usa a conta Google/YouTube seleccionada para listar os canais que ela gere. Não lê a caixa Gmail e não usa e-mails como pesquisa pública.")
3177
3177
  accounts = [account for account in settings.get("youtube_batch_accounts", []) if isinstance(account, dict) and account.get("id")]
3178
3178
  if not accounts:
3179
- st.info("Configure primeiro uma conta em Configurações > Configuração API > Contas Google/YouTube para canais em lote.")
3179
+ st.info("Nenhuma conta Google/YouTube disponível para a lista em lote; a lista permanece vazia.")
3180
3180
  else:
3181
3181
  account_ids = [str(account["id"]) for account in accounts]
3182
3182
  account_labels = {str(account["id"]): f"{account.get('email', 'Conta Google')} — {account.get('label', 'Canais YouTube')}" for account in accounts}
@@ -3953,9 +3953,7 @@ def render_new_video(page_title: str = "Criação de Vídeos", prefix: str = "ne
3953
3953
  with st.expander("Gerar Thumbnail com IA", expanded=False):
3954
3954
  topic_for_thumbnail = str(generation_settings.get("video_subject") or "").strip()
3955
3955
  if st.button("Gerar Thumbnail com IA", key=f"{prefix}_generate_creative", width="stretch"):
3956
- if selected_one is None:
3957
- st.error("Seleccione primeiro um canal.")
3958
- elif not topic_for_thumbnail:
3956
+ if not topic_for_thumbnail:
3959
3957
  st.error("Preencha o Video Subject antes de gerar a thumbnail.")
3960
3958
  else:
3961
3959
  try:
@@ -3963,7 +3961,7 @@ def render_new_video(page_title: str = "Criação de Vídeos", prefix: str = "ne
3963
3961
  existing_title = str(existing_payload.get("title") or topic_for_thumbnail).strip()
3964
3962
  generated = generate_thumbnail_variants_for_ui(
3965
3963
  read_json("settings.json", {}),
3966
- selected_one,
3964
+ selected_one or {},
3967
3965
  topic_for_thumbnail,
3968
3966
  same_channel_quantity,
3969
3967
  title=existing_title,
@@ -6166,8 +6164,7 @@ def render_tiktok_automation():
6166
6164
  channels = _tiktok_channel_records()
6167
6165
  prompt_ids, prompt_labels = _tiktok_prompt_options()
6168
6166
  if not channels:
6169
- st.info("Cadastre primeiro um canal TikTok.")
6170
- return
6167
+ st.info("Nenhum canal TikTok seleccionado. A lista de automação permanece vazia até existir um destino.")
6171
6168
  with st.expander("Canais cadastrados", expanded=False):
6172
6169
  for channel in channels:
6173
6170
  channel_id = str(channel["id"])
@@ -6426,8 +6423,7 @@ def render_facebook_automation() -> None:
6426
6423
  pages = _facebook_pages_for_automation()
6427
6424
  settings = read_json("settings.json", {})
6428
6425
  if not pages:
6429
- st.warning("Cadastre primeiro uma página em Canais e Perfis de Vídeos > Facebook Pages.")
6430
- return
6426
+ st.info("Nenhuma Facebook Page seleccionada. A fila pode ser criada e preenchida; o destino poderá ser escolhido mais tarde.")
6431
6427
  with st.expander("Configurar API Facebook da página", expanded=False):
6432
6428
  for page in pages:
6433
6429
  page_id = str(page.get("id") or "")
@@ -6440,9 +6436,9 @@ def render_facebook_automation() -> None:
6440
6436
  st.success("Credenciais da página guardadas localmente.")
6441
6437
  st.rerun()
6442
6438
  with st.form("facebook_automation_new_post"):
6443
- page_options = [str(page.get("id")) for page in pages]
6439
+ page_options = [""] + [str(page.get("id")) for page in pages]
6444
6440
  page_labels = {str(page.get("id")): str(page.get("name") or "Facebook Page") for page in pages}
6445
- selected_page = st.selectbox("Facebook Page", page_options, format_func=lambda value: page_labels.get(value, value))
6441
+ selected_page = st.selectbox("Facebook Page", page_options, format_func=lambda value: page_labels.get(value, "Nenhuma página seleccionada"), key="facebook_automation_target_page")
6446
6442
  form_cols = st.columns([2.2, 1, 1.3])
6447
6443
  with form_cols[0]:
6448
6444
  theme = st.text_input("Tema/ideia opcional", placeholder="Deixe vazio para o LLM sugerir um tema")
@@ -6452,7 +6448,7 @@ def render_facebook_automation() -> None:
6452
6448
  start_stage = st.selectbox("Iniciar por", ["Ideia / Tema", "Tema já definido"])
6453
6449
  create_clicked = st.form_submit_button("Criar post na fila", type="primary", width="stretch")
6454
6450
  if create_clicked:
6455
- page = next(page for page in pages if str(page.get("id")) == selected_page)
6451
+ page = next((page for page in pages if str(page.get("id")) == selected_page), {})
6456
6452
  post = create_post(page, image_count=int(image_count), theme=theme)
6457
6453
  if start_stage == "Tema já definido" and theme.strip():
6458
6454
  save_post({**post, "status": "artigo_pendente"})
@@ -6512,7 +6508,7 @@ def render_automation():
6512
6508
  st.link_button("Abrir canal", channel_url, type="primary", width="content")
6513
6509
  with header_cols[2]:
6514
6510
  st.markdown("**Thumbnail Blueprint**")
6515
- st.caption(str(paired_thumbnail.get("name") or "Generic_Thumbnail_Blueprint"))
6511
+ st.caption(str(paired_thumbnail.get("name") or "Youtube_Generic_Thumbnail_Blueprint"))
6516
6512
  with header_cols[3]:
6517
6513
  st.markdown("**Idioma do roteiro**")
6518
6514
  st.caption(video_language_label(normalize_video_language(channel.get("language") or "pt")))
@@ -6565,7 +6561,7 @@ def render_automation():
6565
6561
  "format": automation_format,
6566
6562
  })
6567
6563
  set_channel_defaults(channel_id, automation_blueprint, automation_voice)
6568
- paired_id = str(paired_thumbnail.get("id") or "Generic_Thumbnail_Blueprint")
6564
+ paired_id = str(paired_thumbnail.get("id") or "Youtube_Generic_Thumbnail_Blueprint")
6569
6565
  update_channel(channel_id, {"thumbnail_blueprint_id": paired_id, "default_thumbnail_blueprint_id": paired_id})
6570
6566
  st.success("Agendamento guardado.")
6571
6567
  st.rerun()
@@ -7280,15 +7276,15 @@ def render_upload_destination_target(destination: str, channels: list[dict[str,
7280
7276
  select_label = "Canal" if destination == "YouTube" else ("Conta TikTok" if destination == "TikTok" else ("Conta Bilibili" if destination == "Bilibili" else "Perfil / página"))
7281
7277
  empty_label = "Nenhum canal YouTube cadastrado" if destination == "YouTube" else ("Nenhuma conta TikTok cadastrada" if destination == "TikTok" else ("Nenhuma conta Bilibili activa" if destination == "Bilibili" else f"Nenhum {destination} configurado"))
7282
7278
  if not options:
7283
- st.selectbox(select_label, [empty_label], disabled=True, key=f"upload_target_{destination_key}")
7279
+ st.selectbox(select_label, [""], format_func=lambda _value: empty_label, key=f"upload_target_{destination_key}")
7284
7280
  if destination == "YouTube":
7285
- st.caption("Cadastre ou liste pelo menos um canal YouTube antes de escolher o destino de envio.")
7281
+ st.caption("Nenhum canal YouTube disponível; o destino permanece vazio e pode ser escolhido mais tarde.")
7286
7282
  elif destination == "TikTok":
7287
- st.caption("Cadastre uma conta em Pipeline TikTok > Contas TikTok antes de escolher o destino de envio.")
7283
+ st.caption("Nenhuma conta TikTok disponível; o destino permanece vazio e pode ser escolhido mais tarde.")
7288
7284
  elif destination == "Bilibili":
7289
- st.caption("Configure e active uma conta em Configuração API > API Bilibili antes de escolher o destino de envio.")
7285
+ st.caption("Nenhuma conta Bilibili disponível; o destino permanece vazio e pode ser escolhido mais tarde.")
7290
7286
  else:
7291
- st.caption(f"A lista de {destination} será ligada numa etapa própria de credenciais/API.")
7287
+ st.caption(f"Nenhum destino {destination} disponível; a lista permanece vazia.")
7292
7288
  return None
7293
7289
  return st.selectbox(
7294
7290
  select_label,
@@ -7413,8 +7409,8 @@ def render_upload_conventional():
7413
7409
  quota_count = official_upload_count(channel, account)
7414
7410
  st.caption(f"API Oficial hoje: {quota_count}/{OFFICIAL_DAILY_LIMIT} envios nesta conta Gmail.")
7415
7411
  if not selected_youtube_channel:
7416
- st.caption("Seleccione primeiro um canal YouTube no selector acima para activar este envio.")
7417
- if st.button("Enviar pelo fluxo recomendado", type="primary", key=f"upload_youtube_{task['id']}", disabled=not selected_youtube_channel, help="Escolha o canal YouTube no selector acima." if not selected_youtube_channel else None):
7412
+ st.caption("Nenhum canal YouTube seleccionado; o destino permanece vazio e poderá ser escolhido mais tarde.")
7413
+ if st.button("Enviar pelo fluxo recomendado", type="primary", key=f"upload_youtube_{task['id']}"):
7418
7414
  tags = [tag.strip() for tag in tags_raw.split(",") if tag.strip()]
7419
7415
  result = upload_with_default_route(
7420
7416
  settings,
@@ -7,7 +7,7 @@ from typing import Any
7
7
 
8
8
  from .notifications import record_notification
9
9
  from .storage import StorageIntegrityError, append_json, now, read_json, update_json, write_json
10
- from .thumbnail_blueprints import GENERIC_THUMBNAIL_BLUEPRINT_ID
10
+ from .thumbnail_blueprints import thumbnail_blueprint_for_channel
11
11
 
12
12
  STAGES = ["niche", "blueprint", "brand", "topic", "script", "title", "keywords", "video", "thumbnail_prompt", "thumbnail", "upload"]
13
13
  # Ordem executada pelo worker. Cada etapa só é executada quando o seu artefacto
@@ -242,7 +242,7 @@ def create_tasks_for_batch(batch: dict[str, Any]) -> list[dict[str, Any]]:
242
242
  "generation_settings": generation_settings,
243
243
  "blueprint_id": payload.get("blueprint_id") or channel.get("default_blueprint_id") or channel.get("blueprint_id", ""),
244
244
  "blueprint_name": payload.get("blueprint_name", ""),
245
- "thumbnail_blueprint_id": payload.get("thumbnail_blueprint_id") or channel.get("default_thumbnail_blueprint_id") or channel.get("thumbnail_blueprint_id") or GENERIC_THUMBNAIL_BLUEPRINT_ID,
245
+ "thumbnail_blueprint_id": payload.get("thumbnail_blueprint_id") or channel.get("default_thumbnail_blueprint_id") or channel.get("thumbnail_blueprint_id") or thumbnail_blueprint_for_channel({**channel, "thumbnail_blueprint_id": ""}, payload.get("format", options.get("format", ""))).get("id", ""),
246
246
  "voice": payload.get("voice") or channel.get("default_voice") or channel.get("voice", ""),
247
247
  "automation_on": bool(channel.get("automation_on", False)),
248
248
  "automation_time": channel.get("automation_time", "00:00"),
@@ -525,7 +525,7 @@ def _refresh_task_channel_video_settings(task: dict[str, Any]) -> None:
525
525
  channel.get("default_thumbnail_blueprint_id")
526
526
  or channel.get("thumbnail_blueprint_id")
527
527
  or task.get("thumbnail_blueprint_id")
528
- or GENERIC_THUMBNAIL_BLUEPRINT_ID
528
+ or thumbnail_blueprint_for_channel({**channel, "thumbnail_blueprint_id": ""}, task.get("format", "")).get("id", "")
529
529
  )
530
530
  task["blueprint_id"] = channel.get("default_blueprint_id") or channel.get("blueprint_id") or task.get("blueprint_id", "")
531
531
  if channel.get("style_wide"):
@@ -1353,7 +1353,8 @@ def _run_task(task: dict[str, Any]) -> dict[str, Any]:
1353
1353
  blueprint = _blueprint_for_channel(channel)
1354
1354
  if not blueprint and (task.get("blueprint_id") or task.get("blueprint_name")):
1355
1355
  blueprint = {"id": str(task.get("blueprint_id") or ""), "name": str(task.get("blueprint_name") or task.get("blueprint_id") or "")}
1356
- visual_blueprint = thumbnail_blueprint_for_channel(channel)
1356
+ visual_format = task.get("format") or ("portrait" if str(task.get("platform") or "").casefold() in {"tiktok", "instagram"} else "wide")
1357
+ visual_blueprint = thumbnail_blueprint_for_channel(channel, visual_format)
1357
1358
  if visual_blueprint.get("content"):
1358
1359
  blueprint = {**blueprint, "thumbnail_blueprint_rules": visual_blueprint["content"]}
1359
1360
  route = _normalise_video_route(task, settings)
@@ -7,9 +7,12 @@ from pathlib import Path
7
7
  from typing import Any, Mapping
8
8
 
9
9
  from .provider_routing import route_llm_json
10
- from .storage import BLUEPRINTS, atomic_write, list_blueprint_files, load_blueprint_file
10
+ from .storage import BLUEPRINTS, SEED_THUMBNAIL_BLUEPRINTS, atomic_write, list_blueprint_files, load_blueprint_file
11
11
 
12
- GENERIC_THUMBNAIL_BLUEPRINT_ID = "Generic_Thumbnail_Blueprint"
12
+ HORIZONTAL_GENERIC_THUMBNAIL_BLUEPRINT_ID = "Youtube_Generic_Thumbnail_Blueprint"
13
+ VERTICAL_GENERIC_THUMBNAIL_BLUEPRINT_ID = "Tiktok_Generic_Thumbnail_Blueprint"
14
+ # Backwards-compatible name for callers that only need the landscape default.
15
+ GENERIC_THUMBNAIL_BLUEPRINT_ID = HORIZONTAL_GENERIC_THUMBNAIL_BLUEPRINT_ID
13
16
  GENERIC_ASSOCIATION_ERROR = "Not Allowed to Associate, System Use Only"
14
17
 
15
18
  PROMPT_MASTER = '''You are a forensic YouTube thumbnail analyst. Build a reusable Thumbnail Blueprint from the reference channel videos below.
@@ -38,26 +41,41 @@ def resolve_thumbnail_blueprint(identifier: Any) -> dict[str, Any]:
38
41
  wanted = str(identifier or "").strip()
39
42
  if not wanted:
40
43
  return {}
41
- folder = BLUEPRINTS / "thumbnails"
42
- for path in folder.glob("*.md"):
43
- if path.stem == wanted or path.name == wanted:
44
- return {"id": path.stem, "name": path.stem, "path": str(path), "content": path.read_text(encoding="utf-8")}
44
+ if wanted == "Generic_Thumbnail_Blueprint":
45
+ wanted = HORIZONTAL_GENERIC_THUMBNAIL_BLUEPRINT_ID
46
+ folders = (BLUEPRINTS / "thumbnails", SEED_THUMBNAIL_BLUEPRINTS)
47
+ for folder in folders:
48
+ for path in folder.glob("*.md"):
49
+ if path.stem == wanted or path.name == wanted:
50
+ return {"id": path.stem, "name": path.stem, "path": str(path), "content": path.read_text(encoding="utf-8")}
45
51
  return {"id": wanted, "name": wanted}
46
52
 
47
53
 
48
- def thumbnail_blueprint_for_channel(channel: Mapping[str, Any]) -> dict[str, Any]:
54
+ def _generic_thumbnail_blueprint_id(format_value: Any = "", platform: Any = "") -> str:
55
+ raw = str(format_value or "").strip().casefold()
56
+ if raw in {"portrait", "vertical", "shorts", "9:16", "tiktok", "reels"}:
57
+ return VERTICAL_GENERIC_THUMBNAIL_BLUEPRINT_ID
58
+ platform_raw = str(platform or "").strip().casefold()
59
+ if platform_raw in {"tiktok", "instagram", "instagram reels", "shorts"} and not raw:
60
+ return VERTICAL_GENERIC_THUMBNAIL_BLUEPRINT_ID
61
+ return HORIZONTAL_GENERIC_THUMBNAIL_BLUEPRINT_ID
62
+
63
+
64
+ def thumbnail_blueprint_for_channel(channel: Mapping[str, Any], format_value: Any = "") -> dict[str, Any]:
49
65
  direct = channel.get("default_thumbnail_blueprint_id") or channel.get("thumbnail_blueprint_id")
50
66
  if direct:
51
67
  return resolve_thumbnail_blueprint(direct)
52
68
  script_id = str(channel.get("default_blueprint_id") or channel.get("blueprint_id") or "").strip()
53
69
  pairs = _pair_state()
54
- return resolve_thumbnail_blueprint(pairs.get(script_id, "") or GENERIC_THUMBNAIL_BLUEPRINT_ID)
70
+ return resolve_thumbnail_blueprint(
71
+ pairs.get(script_id, "") or _generic_thumbnail_blueprint_id(format_value, channel.get("platform"))
72
+ )
55
73
 
56
74
 
57
75
  def thumbnail_blueprint_for_blueprint(blueprint_id: Any) -> dict[str, Any]:
58
76
  """Resolve the visual pair for a script Blueprint, falling back to Generic."""
59
77
  paired_id = _pair_state().get(str(blueprint_id or "").strip(), "")
60
- return resolve_thumbnail_blueprint(paired_id or GENERIC_THUMBNAIL_BLUEPRINT_ID)
78
+ return resolve_thumbnail_blueprint(paired_id or HORIZONTAL_GENERIC_THUMBNAIL_BLUEPRINT_ID)
61
79
 
62
80
 
63
81
  def _pair_state() -> dict[str, str]:
@@ -70,7 +88,7 @@ def _pair_state() -> dict[str, str]:
70
88
 
71
89
 
72
90
  def save_thumbnail_blueprint_pair(thumbnail_id: str, blueprint_id: str) -> None:
73
- if str(thumbnail_id) == GENERIC_THUMBNAIL_BLUEPRINT_ID and str(blueprint_id):
91
+ if str(thumbnail_id) in {HORIZONTAL_GENERIC_THUMBNAIL_BLUEPRINT_ID, VERTICAL_GENERIC_THUMBNAIL_BLUEPRINT_ID} and str(blueprint_id):
74
92
  raise ValueError(GENERIC_ASSOCIATION_ERROR)
75
93
  pairs = _pair_state()
76
94
  if blueprint_id:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danhachuel/thunderbolt",
3
- "version": "0.7.36",
3
+ "version": "0.7.38",
4
4
  "description": "Thunderbolt — interface local para operação de canais faceless e motor MoneyPrinterTurbo",
5
5
  "license": "MIT",
6
6
  "main": "scripts/cli.mjs",
@@ -0,0 +1,181 @@
1
+ 🔒 TRAVA DE ESTILO — NÃO NEGOCIÁVEL
2
+ [Um parágrafo descrevendo o estilo visual geral do canal: ex.: "Estilo limpo, minimalista, com iluminação high‑key, cores sólidas e composição centrada no rosto/produto, otimizado para consumo em tela vertical de celular."]
3
+
4
+ ❌ Não permitido (elementos que o canal nunca usa, com base nas capas observadas):
5
+
6
+ [ex.: Sem texto no rodapé da tela, sem rostos cortados, sem cores saturadas, etc.]
7
+
8
+ [Liste todas as proibições observadas]
9
+
10
+ 📱 ZONAS SEGURAS (VERTICAL 9:16) — OBRIGATÓRIO
11
+ A capa é feita para 1080 × 1920 px. A UI do TikTok/Reels cobre partes da imagem — nada essencial pode ficar ali.
12
+
13
+ text
14
+ 0 px ─────────────────────────────
15
+ ┌────────────────────┐
16
+ │ ZONA MORTA TOPO │ ~150 px (barra de progresso / "Seguindo")
17
+ ├───────────────┬────┤
18
+ │ │ UI │
19
+ │ ÁREA SEGURA │ DI │ ~220 px (curtir, comentar,
20
+ │ texto + │ RE │ compartilhar, som, avatar)
21
+ │ rosto/produto│ │
22
+ │ │ │
23
+ ├───────────────┴────┤
24
+ │ ZONA MORTA BASE │ ~480 px (legenda, @usuário,
25
+ │ │ música, CTA, barra de navegação)
26
+ └────────────────────┘
27
+ 1920 px ────────────────────────────
28
+ Regras rígidas de zona segura:
29
+
30
+ Margem superior: 150 px livres
31
+
32
+ Margem inferior: 480 px livres
33
+
34
+ Margem direita: 220 px livres
35
+
36
+ Margem esquerda: 80 px livres
37
+
38
+ Área útil real: aproximadamente 780 × 1290 px (canto superior esquerdo em 80,150)
39
+
40
+ Recorte do grid do perfil (3:4):
41
+
42
+ O TikTok e o Instagram cortam a capa em 3:4 no grid do perfil.
43
+
44
+ O núcleo da mensagem (rosto + 2–4 palavras) deve caber dentro do recorte central 1080 × 1440 px (y entre 240 e 1680).
45
+
46
+ Teste sempre: "se essa capa for cortada em 3:4, ainda entendo a mensagem?"
47
+
48
+ 🧍‍♂️ ENQUADRAMENTO E POSE
49
+ Sujeito principal: [descreva o elemento visual principal – pessoa, produto, cena, etc.]
50
+
51
+ Regras de enquadramento:
52
+
53
+ [ex.: Sujeito centralizado horizontalmente, ocupando o terço médio do quadro]
54
+
55
+ [ex.: Rosto/produto nunca abaixo de y = 1400 px]
56
+
57
+ [Adicione regras]
58
+
59
+ Ângulo de câmera: [baixo, alto, nível dos olhos, etc.]
60
+
61
+ Corte (cropping): [quanto do sujeito preenche o quadro — em vertical, o sujeito costuma ocupar 50–70% da altura útil]
62
+
63
+ Energia / ação: [estático, dinâmico, expressivo, ação congelada, etc.]
64
+
65
+ Expressão / leitura à distância: [a emoção precisa ser legível a 2 metros de distância do celular]
66
+
67
+ 🎨 FUNDO E ILUMINAÇÃO
68
+ Ambiente de fundo: [estúdio, externo, escuro, cor sólida, etc.]
69
+
70
+ Iluminação: [high‑key, low‑key, natural, dramática, etc.]
71
+
72
+ Efeitos: [bokeh, flare, fumaça, glow, etc.]
73
+
74
+ Profundidade: [profundidade de campo rasa, foco profundo, etc.]
75
+
76
+ Ruído visual: [nível de detalhe, poluído vs. limpo — em vertical, prefira fundos simples para não competir com o texto]
77
+
78
+ 🧭 IDENTIFICADORES / SÍMBOLOS DO CANAL
79
+ [Descreva símbolos recorrentes que comunicam contexto instantaneamente — bandeiras, logos, ícones específicos, rostos recorrentes, cores-assinatura. Se não houver, escreva explicitamente: "O canal não utiliza identificadores simbólicos recorrentes."]
80
+
81
+ 🚨 ELEMENTO DE ATENÇÃO VISUAL
82
+ [Descreva se o canal usa setas, círculos, zooms, emojis, contornos — especifique cor, forma, posição e regras de uso. Em vertical, o elemento deve ficar acima da zona morta inferior. Se o canal NÃO usa, escreva explicitamente: "O canal não utiliza elementos de atenção visual."]
83
+
84
+ 📝 ESTILO DE TEXTO
85
+ Estrutura:
86
+
87
+ [Número de linhas / faixas de texto e posicionamento — ex.: bloco de 2 linhas no terço superior, alinhado à esquerda]
88
+
89
+ [Hierarquia — ex.: headline grande + sub‑headline menor]
90
+
91
+ Tipografia:
92
+
93
+ [Família, peso, caixa — ex.: sans‑serif bold, CAIXA ALTA, contorno grosso]
94
+
95
+ Hierarquia de cor:
96
+
97
+ [Cor de fundo do bloco, cor do texto de cada camada, contorno/sombra]
98
+
99
+ Tamanho mínimo legível:
100
+
101
+ Fonte nunca menor que 90 px de altura de caixa (em 1080 px de largura)
102
+
103
+ Contraste alto obrigatório (texto claro sobre fundo escuro ou vice‑versa)
104
+
105
+ Cobertura de texto: [% aproximado da capa ocupada por texto — ex.: faixa superior ocupando 25%]
106
+
107
+ Regra do mudo: a capa precisa funcionar com o som desligado.
108
+
109
+ 🧠 PSICOLOGIA DO TEXTO
110
+ [Descreva os gatilhos emocionais usados nas headlines — urgência, curiosidade, choque, números, perguntas, etc.]
111
+
112
+ Exemplos de palavras/frases observadas:
113
+
114
+ [ex.: "Você não vai acreditar", "Destruiu", "Novo", "Por quê", "Ninguém fala disso"]
115
+
116
+ Restrição de comprimento: 2–4 palavras. Máximo 5. Texto longo morre em tela vertical.
117
+
118
+ 🎯 REGRAS DE COMPOSIÇÃO
119
+ Aplicação da regra dos terços: [onde os elementos-chave são posicionados no quadro vertical]
120
+
121
+ Hierarquia focal: [ordem de importância visual — sujeito, texto, elemento de atenção, fundo]
122
+
123
+ Espaço negativo: [como as áreas vazias são usadas — em vertical, o espaço negativo é o que evita que o texto seja engolido pela UI]
124
+
125
+ Controle de distração: [o que é evitado para manter o foco — ex.: nada de detalhes no canto inferior direito]
126
+
127
+ Mobile‑first: [garantir leitura em tela de 5–6 polegadas, com brilho ambiente alto]
128
+
129
+ 📐 FORMATO E QUALIDADE
130
+ Proporção:
131
+ 9:16 (vertical)
132
+
133
+ Resolução:
134
+ 1080 × 1920 px (mínimo) — 1440 × 2560 px (ideal / futuro‑proof)
135
+
136
+ Compatibilidade:
137
+ TikTok · Instagram Reels · YouTube Shorts · Kwai — mesma capa, sem reenquadramento
138
+
139
+ Estilo de imagem:
140
+ [Fotorrealista / ilustrado / misto — descreva o nível de realismo]
141
+
142
+ Nitidez:
143
+ [Clareza esperada — ex.: detalhes nítidos no sujeito, fundo levemente suave]
144
+
145
+ Restrições:
146
+
147
+ [Liste restrições técnicas observadas — sem marca d'água, sem blur, sem iluminação chapada, sem texto cortado pela UI, etc.]
148
+
149
+ [Nenhum elemento essencial fora da ÁREA SEGURA definida acima]
150
+
151
+ 🧠 OBJETIVO FINAL
152
+ Objetivo primário:
153
+ Maximizar CTR / taxa de retenção nos 3 primeiros segundos para [nicho do canal — ex.: reviews de tecnologia, finanças pessoais, true crime, etc.] em formato vertical.
154
+
155
+ Gatilhos emocionais:
156
+ [Emoções-alvo — ex.: curiosidade, medo, surpresa, excitação, indignação]
157
+
158
+ Público:
159
+ [Descreva o espectador-alvo — ex.: entusiastas de tech, investidores, público 18–34]
160
+
161
+ A capa deve comunicar instantaneamente:
162
+ [Mensagem central — ex.: "Uma revelação chocante", "Um produto imperdível", "Você está fazendo isso errado"]
163
+
164
+ Teste de aprovação (checklist):
165
+
166
+ □ Mensagem compreendida em menos de 1 segundo
167
+ □ Nada importante abaixo de y = 1440 px
168
+ □ Nada importante à direita de x = 860 px
169
+ □ Funciona no recorte 3:4 do grid do perfil
170
+ □ Funciona com o som desligado
171
+ □ Legível em tela de celular a 2 metros de distância
172
+ 🧾 FORMATO DE ENTRADA FINAL
173
+ TÍTULO DO VÍDEO / TEMA:
174
+ "[Insira o título real do vídeo ou o tópico do roteiro aqui]"
175
+
176
+ ⚙️ INSTRUÇÃO FINAL DE SISTEMA
177
+ Usando o estilo travado definido acima, gere uma capa vertical para TikTok/Instagram Reels que siga rigorosamente todas as regras composicionais, textuais e visuais.
178
+
179
+ Extraia automaticamente 2–4 palavras do TÍTULO DO VÍDEO fornecido para servir como headline e posicione-a de acordo com as regras de ESTILO DE TEXTO e de ZONAS SEGURAS (nada abaixo de y = 1440 px, nada à direita de x = 860 px).
180
+
181
+ A saída deve ser uma capa pronta para upload em 9:16, mínimo 1080 × 1920 px, replicando a identidade visual exata do canal — válida simultaneamente para TikTok, Reels e Shorts, e legível também no recorte 3:4 do grid do perfil. [Especifique se o logo deve ser incluído ou não — ex.: "SEM LOGO" ou "INCLUIR LOGO DO CANAL no canto superior esquerdo, dentro da área segura".]