@danhachuel/thunderbolt 0.4.22 → 0.4.24

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
@@ -45,7 +45,7 @@ from app.modules.niche_finder.data_loader import DatasetError, download_kaggle_d
45
45
  from app.modules.niche_finder.summarizer import summarize_items
46
46
  from app.influencers_ui import render_ai_influencer_characters, render_ai_influencer_content, render_ai_influencers_api_status, render_motion_control, render_ugc_products
47
47
  from hermes_ui.blueprints import create_blueprint_from_link, list_branding_files, save_generated_blueprint
48
- from hermes_ui.thumbnail_blueprints import generate_thumbnail_blueprint, list_thumbnail_blueprint_documents, resolve_thumbnail_blueprint, save_thumbnail_blueprint, save_thumbnail_blueprint_pair, thumbnail_blueprint_catalog, thumbnail_blueprint_for_channel
48
+ from hermes_ui.thumbnail_blueprints import generate_thumbnail_blueprint, list_thumbnail_blueprint_documents, resolve_thumbnail_blueprint, save_thumbnail_blueprint, save_thumbnail_blueprint_pair, thumbnail_blueprint_catalog, thumbnail_blueprint_for_blueprint, thumbnail_blueprint_for_channel
49
49
  from hermes_ui.metadata_cleaner import build_description, clean_video_metadata, list_edit_records, metadata_manifest, normalize_tags, save_edit_record, store_external_video
50
50
  from hermes_ui.python_editor import AUDIO_EXTENSIONS, VIDEO_EXTENSIONS, PythonEditorError, change_speed, editor_manifest, extract_audio, list_edit_records as list_python_editor_records, list_generated_videos, list_scripts, list_video_files, read_script, remove_audio, replace_audio, resize_video, save_edit_record as save_python_editor_record, save_script, store_uploaded_asset, trim_video
51
51
  from hermes_ui.cuts import CutsError, download_direct_video_url, generate_clips, list_generated_videos as list_cut_generated_videos, list_runs as list_cut_runs, list_video_files as list_cut_video_files, manifest_bytes as cut_manifest_bytes, store_uploaded_video, zip_run as zip_cut_run
@@ -1570,8 +1570,11 @@ def render_thumbnail_blueprints():
1570
1570
  pair_labels = {item[0]: item[1] for item in pair_options}
1571
1571
  pair = st.selectbox("Blueprint de roteiro associado", pair_ids, format_func=lambda item: pair_labels.get(item, item or "Sem Blueprint padrão"), key=f"thumbnail_card_pair_{path.stem}")
1572
1572
  if st.button("Guardar associação deste card", key=f"thumbnail_card_pair_save_{path.stem}"):
1573
- save_thumbnail_blueprint_pair(path.stem, pair)
1574
- st.success("Associação guardada; canais com esse Blueprint usarão esta thumbnail blueprint automaticamente.")
1573
+ try:
1574
+ save_thumbnail_blueprint_pair(path.stem, pair)
1575
+ st.success("Associação guardada; canais com esse Blueprint usarão esta thumbnail blueprint automaticamente.")
1576
+ except ValueError as exc:
1577
+ st.error(str(exc))
1575
1578
  st.code(path.read_text(encoding="utf-8"), language="markdown")
1576
1579
  st.divider()
1577
1580
  st.subheader("Associar ao canal e ao Blueprint de roteiro")
@@ -1597,9 +1600,12 @@ def render_thumbnail_blueprints():
1597
1600
  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}")
1598
1601
  with cols[2]:
1599
1602
  if st.button("Guardar par", key=f"thumbnail_blueprint_save_{channel_id}", use_container_width=True):
1600
- update_channel(channel_id, {"thumbnail_blueprint_id": thumb, "default_thumbnail_blueprint_id": thumb, "blueprint_id": script, "default_blueprint_id": script})
1601
- st.success("Par Blueprint de roteiro + Thumbnail Blueprint guardado.")
1602
- st.rerun()
1603
+ if thumb == "Generic_Thumbnail_Blueprint" and script:
1604
+ st.error("Not Allowed to Associate, System Use Only")
1605
+ else:
1606
+ update_channel(channel_id, {"thumbnail_blueprint_id": thumb, "default_thumbnail_blueprint_id": thumb, "blueprint_id": script, "default_blueprint_id": script})
1607
+ st.success("Par Blueprint de roteiro + Thumbnail Blueprint guardado.")
1608
+ st.rerun()
1603
1609
  def _tiktok_accounts_from_settings(settings: dict[str, Any]) -> list[dict[str, Any]]:
1604
1610
  raw_accounts = settings.get("tiktok_accounts")
1605
1611
  if not isinstance(raw_accounts, list) or not raw_accounts:
@@ -4271,18 +4277,14 @@ def render_automation():
4271
4277
  with header_cols[3]:
4272
4278
  schedule_time = st.text_input("Horário (HH:MM)", value=channel.get("automation_time", "00:00"), key=f"automation_time_{channel_id}")
4273
4279
  blueprint_ids, blueprint_labels, current_blueprint, voice_options, current_voice = channel_default_options(channel)
4274
- thumbnail_items = thumbnail_blueprint_catalog()
4275
- thumbnail_ids = [item[0] for item in thumbnail_items]
4276
- thumbnail_labels = {item[0]: item[1] for item in thumbnail_items}
4277
- current_thumbnail = str(channel.get("default_thumbnail_blueprint_id") or channel.get("thumbnail_blueprint_id") or "")
4278
- default_cols = st.columns([1.0, 1.0, 1.35, 1.55, 1.55, 1.0], gap="small")
4280
+ default_cols = st.columns([1.0, 1.0, 1.35, 1.45, 1.55, 1.0], gap="small")
4279
4281
  with default_cols[0]:
4280
4282
  st.markdown("**Idioma Padrão**")
4281
4283
  st.caption(language_label(channel.get("language") or "pt"))
4282
4284
  with default_cols[1]:
4283
4285
  st.markdown("**Nicho Padrão**")
4284
4286
  st.caption(channel_niche_label(channel))
4285
- with default_cols[2]:
4287
+ with default_cols[3]:
4286
4288
  automation_blueprint = st.selectbox(
4287
4289
  "Blueprint Padrão",
4288
4290
  blueprint_ids,
@@ -4290,7 +4292,11 @@ def render_automation():
4290
4292
  format_func=lambda item: blueprint_labels.get(item, item or "Sem Blueprint padrão"),
4291
4293
  key=f"automation_blueprint_{channel_id}",
4292
4294
  )
4293
- with default_cols[3]:
4295
+ with default_cols[2]:
4296
+ paired_thumbnail = thumbnail_blueprint_for_blueprint(automation_blueprint)
4297
+ st.markdown("**Thumbnail Blueprint**")
4298
+ st.caption(str(paired_thumbnail.get("name") or "Generic_Thumbnail_Blueprint"))
4299
+ with default_cols[4]:
4294
4300
  automation_voice = st.selectbox(
4295
4301
  "Narrador/Voz Padrão",
4296
4302
  voice_options,
@@ -4298,14 +4304,6 @@ def render_automation():
4298
4304
  format_func=lambda item: item or "Sem voz padrão",
4299
4305
  key=f"automation_voice_{channel_id}",
4300
4306
  )
4301
- with default_cols[4]:
4302
- automation_thumbnail = st.selectbox(
4303
- "Thumbnail Blueprint",
4304
- thumbnail_ids,
4305
- index=thumbnail_ids.index(current_thumbnail) if current_thumbnail in thumbnail_ids else 0,
4306
- format_func=lambda item: thumbnail_labels.get(item, item or "Sem Thumbnail Blueprint"),
4307
- key=f"automation_thumbnail_blueprint_{channel_id}",
4308
- )
4309
4307
  with default_cols[5]:
4310
4308
  if st.button("Guardar", key=f"automation_save_{channel_id}", use_container_width=True):
4311
4309
  if not valid_hhmm(schedule_time):
@@ -4316,7 +4314,8 @@ def render_automation():
4316
4314
  "automation_time": schedule_time.strip(),
4317
4315
  })
4318
4316
  set_channel_defaults(channel_id, automation_blueprint, automation_voice)
4319
- update_channel(channel_id, {"thumbnail_blueprint_id": automation_thumbnail, "default_thumbnail_blueprint_id": automation_thumbnail})
4317
+ paired_id = str(paired_thumbnail.get("id") or "Generic_Thumbnail_Blueprint")
4318
+ update_channel(channel_id, {"thumbnail_blueprint_id": paired_id, "default_thumbnail_blueprint_id": paired_id})
4320
4319
  st.success("Agendamento guardado.")
4321
4320
  st.rerun()
4322
4321
 
@@ -6,6 +6,7 @@ from typing import Any
6
6
 
7
7
  from .notifications import record_notification
8
8
  from .storage import StorageIntegrityError, append_json, now, read_json, update_json, write_json
9
+ from .thumbnail_blueprints import GENERIC_THUMBNAIL_BLUEPRINT_ID
9
10
 
10
11
  STAGES = ["niche", "blueprint", "brand", "topic", "script", "title", "keywords", "video", "thumbnail_prompt", "thumbnail", "upload"]
11
12
  # Ordem executada pelo worker. Cada etapa só é executada quando o seu artefacto
@@ -180,7 +181,7 @@ def create_tasks_for_batch(batch: dict[str, Any]) -> list[dict[str, Any]]:
180
181
  "generation_settings": payload.get("generation_settings", options.get("generation_settings", {})),
181
182
  "blueprint_id": payload.get("blueprint_id") or channel.get("default_blueprint_id") or channel.get("blueprint_id", ""),
182
183
  "blueprint_name": payload.get("blueprint_name", ""),
183
- "thumbnail_blueprint_id": payload.get("thumbnail_blueprint_id") or channel.get("default_thumbnail_blueprint_id") or channel.get("thumbnail_blueprint_id", ""),
184
+ "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,
184
185
  "voice": payload.get("voice") or channel.get("default_voice") or channel.get("voice", ""),
185
186
  "automation_on": bool(channel.get("automation_on", False)),
186
187
  "automation_time": channel.get("automation_time", "00:00"),
@@ -24,6 +24,7 @@ from hermes_ui.media_generation import MediaGenerationError, _append_generation_
24
24
  from hermes_ui.media_providers import FULL_IA_VIDEO_PROVIDER_CODES, media_cards_for_pool, media_provider_definition
25
25
  from hermes_ui.material_sources import material_api_keys, material_source_cards, selected_material_source
26
26
  from hermes_ui.thumbnail_generation import ThumbnailGenerationError, generate_thumbnail_image
27
+ from hermes_ui.thumbnail_blueprints import thumbnail_blueprint_for_channel
27
28
 
28
29
  PIPELINE_LOCK_FILENAME = "pipeline_worker.lock"
29
30
  PIPELINE_LOG_FILENAME = "pipeline_worker.json"
@@ -1229,6 +1230,9 @@ def _run_task(task: dict[str, Any]) -> dict[str, Any]:
1229
1230
  blueprint = _blueprint_for_channel(channel)
1230
1231
  if not blueprint and (task.get("blueprint_id") or task.get("blueprint_name")):
1231
1232
  blueprint = {"id": str(task.get("blueprint_id") or ""), "name": str(task.get("blueprint_name") or task.get("blueprint_id") or "")}
1233
+ visual_blueprint = thumbnail_blueprint_for_channel(channel)
1234
+ if visual_blueprint.get("content"):
1235
+ blueprint = {**blueprint, "thumbnail_blueprint_rules": visual_blueprint["content"]}
1232
1236
  route = _normalise_video_route(task, settings)
1233
1237
  topic = str(task.get("topic") or "").strip()
1234
1238
  if route != "music" and (not topic or str(task.get("topic_source") or "") in {"auto", "llm_pending"}):
@@ -9,6 +9,9 @@ from typing import Any, Mapping
9
9
  from .provider_routing import route_llm_json
10
10
  from .storage import BLUEPRINTS, atomic_write, list_blueprint_files, load_blueprint_file
11
11
 
12
+ GENERIC_THUMBNAIL_BLUEPRINT_ID = "Generic_Thumbnail_Blueprint"
13
+ GENERIC_ASSOCIATION_ERROR = "Not Allowed to Associate, System Use Only"
14
+
12
15
  PROMPT_MASTER = '''You are a forensic YouTube thumbnail analyst. Build a reusable Thumbnail Blueprint from the reference channel videos below.
13
16
  The output must be a practical, locked visual system, not a script blueprint. Infer recurring composition, framing, lighting, color, typography, overlay text, symbols, emotional triggers, mobile readability, aspect ratio, quality and negative constraints. Use the exact Markdown structure of the requested reference: STYLE LOCK, FRAMING & POSE, BACKGROUND & LIGHTING, GEOPOLITICAL SYMBOLS when relevant, VISUAL ATTENTION ELEMENT, TEXT STYLE, TEXT PSYCHOLOGY, COMPOSITION RULES, FORMAT & QUALITY, FINAL OBJECTIVE, FINAL INPUT FORMAT and FINAL SYSTEM INSTRUCTION. Write the document in English. Do not invent channel analytics. The document must instruct future thumbnail generation and include a concise, ready-to-use image prompt template.'''
14
17
 
@@ -48,7 +51,13 @@ def thumbnail_blueprint_for_channel(channel: Mapping[str, Any]) -> dict[str, Any
48
51
  return resolve_thumbnail_blueprint(direct)
49
52
  script_id = str(channel.get("default_blueprint_id") or channel.get("blueprint_id") or "").strip()
50
53
  pairs = _pair_state()
51
- return resolve_thumbnail_blueprint(pairs.get(script_id, ""))
54
+ return resolve_thumbnail_blueprint(pairs.get(script_id, "") or GENERIC_THUMBNAIL_BLUEPRINT_ID)
55
+
56
+
57
+ def thumbnail_blueprint_for_blueprint(blueprint_id: Any) -> dict[str, Any]:
58
+ """Resolve the visual pair for a script Blueprint, falling back to Generic."""
59
+ paired_id = _pair_state().get(str(blueprint_id or "").strip(), "")
60
+ return resolve_thumbnail_blueprint(paired_id or GENERIC_THUMBNAIL_BLUEPRINT_ID)
52
61
 
53
62
 
54
63
  def _pair_state() -> dict[str, str]:
@@ -61,6 +70,8 @@ def _pair_state() -> dict[str, str]:
61
70
 
62
71
 
63
72
  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):
74
+ raise ValueError(GENERIC_ASSOCIATION_ERROR)
64
75
  pairs = _pair_state()
65
76
  if blueprint_id:
66
77
  pairs[str(blueprint_id)] = str(thumbnail_id)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danhachuel/thunderbolt",
3
- "version": "0.4.22",
3
+ "version": "0.4.24",
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,114 @@
1
+ 🔒 STYLE LOCK — NON-NEGOTIABLE
2
+
3
+ [One paragraph describing the overall visual style of the channel: e.g., "Clean, minimalist tech-review style with high‑key lighting and product‑centric compositions."]
4
+
5
+ ❌ Not allowed (list of elements the channel never uses, based on observed thumbnails):
6
+ - [e.g., No text overlays, no human faces, no saturated colours, etc.]
7
+ - [List all prohibitions observed]
8
+
9
+ 🧍‍♂️ FRAMING & POSE
10
+
11
+ Primary subject(s): [describe the typical main visual element – person, product, scene, etc.]
12
+
13
+ Framing rules:
14
+ - [e.g., Subject placed centre‑left, camera at eye level, etc.]
15
+ - [Additional rules]
16
+
17
+ Camera angle: [typical angle – low, high, eye‑level, etc.]
18
+
19
+ Cropping: [how much of the subject fills the frame, any cropping conventions]
20
+
21
+ Energy / action: [static, dynamic, action‑oriented, etc.]
22
+
23
+ 🎨 BACKGROUND & LIGHTING
24
+
25
+ Background environment: [typical setting – studio, outdoor, dark, etc.]
26
+
27
+ Lighting: [high‑key, low‑key, natural, dramatic, etc.]
28
+
29
+ Effects: [any recurring visual effects – bokeh, lens flare, smoke, etc.]
30
+
31
+ Depth: [shallow depth of field, deep focus, etc.]
32
+
33
+ Visual noise: [level of detail, busy vs. clean]
34
+
35
+ 🧭 IDENTIFICADORES / SÍMBOLOS DO CANAL
36
+
37
+ [Describe any recurring symbols that instantly communicate context – e.g., national flags, brand logos, specific icons, recurring faces, etc. If none, state explicitly: "The channel does not use any recurring symbolic identifiers."]
38
+
39
+ 🚨 VISUAL ATTENTION ELEMENT
40
+
41
+ [Describe if the channel uses arrows, circles, zooms, emojis, etc. – specify colour, shape, position, and usage rules. If the channel does NOT use any such elements, state explicitly: "The channel does not use any visual attention elements."]
42
+
43
+ 📝 TEXT STYLE
44
+
45
+ Structure:
46
+ - [Number of text lines / bands, placement – e.g., bottom banner, top left, etc.]
47
+ - [Hierarchy – e.g., main headline larger, sub‑headline smaller]
48
+
49
+ Typography:
50
+ - [Font family, weight, casing – e.g., bold sans‑serif, all caps]
51
+
52
+ Colour hierarchy:
53
+ - [Background colour, text colour for each layer]
54
+
55
+ Text coverage: [approximate % of thumbnail occupied by text – e.g., bottom 30%]
56
+
57
+ 🧠 TEXT PSYCHOLOGY
58
+
59
+ [Describe typical emotional triggers used in headlines – e.g., urgency, curiosity, shock, numbers, questions, etc.]
60
+
61
+ Examples of observed words/phrases:
62
+ - [e.g., "You Won't Believe", "Destroyed", "New", "Why", etc.]
63
+
64
+ 🎯 COMPOSITION RULES
65
+
66
+ - Rule of thirds application: [where key elements are placed]
67
+ - Focal hierarchy: [order of visual importance – main subject, secondary, text, etc.]
68
+ - Negative space: [how empty areas are used]
69
+ - Distraction control: [what is avoided to keep focus]
70
+ - Mobile‑first design: [ensuring readability on small screens]
71
+
72
+ 📐 FORMAT & QUALITY
73
+
74
+ *Aspect ratio:
75
+ 16:9
76
+
77
+ *Resolution:
78
+ 1280 × 720 minimum
79
+
80
+ *Image style:
81
+ [Photorealistic / illustrated / mixed – describe level of realism]
82
+
83
+ *Sharpness:
84
+ [Expected clarity – e.g., crisp details, slightly soft background]
85
+
86
+ *Restrictions:
87
+ - [List any technical restrictions observed – e.g., no watermarks, no blur, no flat lighting, etc.]
88
+
89
+ 🧠 FINAL OBJECTIVE
90
+
91
+ *Primary goal:
92
+ Maximize YouTube CTR for [nicho do canal – e.g., technology reviews, personal finance, true crime, etc.]
93
+
94
+ *Emotional triggers:
95
+ [Target emotions – e.g., curiosity, fear, excitement, etc.]
96
+
97
+ *Audience:
98
+ [Describe target viewer – e.g., tech enthusiasts, investors, etc.]
99
+
100
+ *Thumbnail must instantly communicate:
101
+ [Core message – e.g., "A shocking revelation", "A must‑see product", etc.]
102
+
103
+ 🧾 FINAL INPUT FORMAT
104
+
105
+ *VIDEO TITLE:
106
+ "[Insert the actual video title or script topic here]"
107
+
108
+ ⚙️ FINAL SYSTEM INSTRUCTION
109
+
110
+ Using the locked style defined above, generate a YouTube thumbnail that strictly follows all compositional, textual, and visual rules.
111
+
112
+ Automatically extract 2–4 words from the provided VIDEO TITLE to serve as the headline, and place it according to the TEXT STYLE rules (including any banner structure).
113
+
114
+ Output must be a ready‑to‑upload 16:9 thumbnail (minimum 1280×720) replicating the channel’s exact visual identity. [Specify if logo is to be included or not – e.g., "WITHOUT LOGO" or "INCLUDE CHANNEL LOGO in top‑left corner".]