@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 +18 -22
- package/hermes_ui/domain.py +3 -3
- package/hermes_ui/pipeline_worker.py +2 -1
- package/hermes_ui/thumbnail_blueprints.py +28 -10
- package/package.json +1 -1
- package/seed/blueprints/thumbnails/Tiktok_Generic_Thumbnail_Blueprint.md +181 -0
- /package/seed/blueprints/thumbnails/{Generic_Thumbnail_Blueprint.md → Youtube_Generic_Thumbnail_Blueprint.md} +0 -0
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
|
|
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("
|
|
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
|
|
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("
|
|
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.
|
|
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,
|
|
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 "
|
|
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 "
|
|
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, [
|
|
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("
|
|
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("
|
|
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("
|
|
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"
|
|
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("
|
|
7417
|
-
if st.button("Enviar pelo fluxo recomendado", type="primary", key=f"upload_youtube_{task['id']}"
|
|
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,
|
package/hermes_ui/domain.py
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
|
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(
|
|
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
|
|
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)
|
|
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
|
@@ -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".]
|
|
File without changes
|