@danhachuel/thunderbolt 0.4.18 → 0.4.20
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 +44 -36
- package/hermes_ui/pipeline_worker.py +25 -5
- package/hermes_ui/script_documents.py +2 -1
- package/package.json +1 -1
package/app/main.py
CHANGED
|
@@ -1405,7 +1405,7 @@ def render_dashboard():
|
|
|
1405
1405
|
def render_blueprints():
|
|
1406
1406
|
st.title("Blueprints Youtube")
|
|
1407
1407
|
st.caption(f"Biblioteca local lida directamente de `{BLUEPRINTS}`")
|
|
1408
|
-
blueprint_tab
|
|
1408
|
+
blueprint_tab = st.container()
|
|
1409
1409
|
with blueprint_tab:
|
|
1410
1410
|
st.subheader("Criar blueprint a partir de link")
|
|
1411
1411
|
with st.form("create_blueprint_from_link"):
|
|
@@ -1474,42 +1474,46 @@ def render_blueprints():
|
|
|
1474
1474
|
except Exception as exc:
|
|
1475
1475
|
with st.expander(f"Inválido — {path.stem}"):
|
|
1476
1476
|
st.error(str(exc))
|
|
1477
|
-
with branding_tab:
|
|
1478
|
-
st.subheader("Brandings completos")
|
|
1479
|
-
st.caption(f"Brandings gerados ou importados da pasta `{BLUEPRINTS / 'brandings'}`")
|
|
1480
|
-
branding_upload = st.file_uploader("Subir Branding JSON", type=["json"], key="branding_upload")
|
|
1481
|
-
if branding_upload and st.button("Guardar Branding", type="secondary"):
|
|
1482
|
-
try:
|
|
1483
|
-
data = json.loads(branding_upload.getvalue().decode("utf-8"))
|
|
1484
|
-
if not isinstance(data, dict):
|
|
1485
|
-
raise ValueError("O JSON raiz deve ser um objecto.")
|
|
1486
|
-
target = BLUEPRINTS / "brandings" / (Path(branding_upload.name).stem.replace(" ", "-") + ".json")
|
|
1487
|
-
target.parent.mkdir(parents=True, exist_ok=True)
|
|
1488
|
-
atomic_write(target, data)
|
|
1489
|
-
record_notification("branding_completed", f"Branding guardado: {target.stem}", "O Branding importado foi guardado no storage local.", metadata={"name": target.stem}, dedupe_key=f"branding:{target}:{target.stat().st_mtime_ns}")
|
|
1490
|
-
st.success(f"Branding guardado em {target}")
|
|
1491
|
-
st.rerun()
|
|
1492
|
-
except (UnicodeDecodeError, json.JSONDecodeError, ValueError) as exc:
|
|
1493
|
-
st.error(f"Branding JSON inválido: {exc}")
|
|
1494
|
-
branding_files = list_branding_files()
|
|
1495
|
-
st.write(f"{len(branding_files)} branding(s) encontrado(s)")
|
|
1496
|
-
branding_search = st.text_input("Pesquisar brandings", key="branding_search")
|
|
1497
|
-
if not branding_files:
|
|
1498
|
-
st.info("Ainda não existem brandings na pasta local.")
|
|
1499
|
-
for path in branding_files:
|
|
1500
|
-
if branding_search and branding_search.lower() not in path.name.lower():
|
|
1501
|
-
continue
|
|
1502
|
-
try:
|
|
1503
|
-
data = load_blueprint_file(path)
|
|
1504
|
-
title = data.get("name") or data.get("identity", {}).get("channel_name") or path.stem
|
|
1505
|
-
with st.expander(f"{title} — {path.name}"):
|
|
1506
|
-
st.caption(f"Blueprint associado: {data.get('blueprint_id') or 'não associado'}")
|
|
1507
|
-
st.json(data)
|
|
1508
|
-
except Exception as exc:
|
|
1509
|
-
with st.expander(f"Inválido — {path.stem}"):
|
|
1510
|
-
st.error(str(exc))
|
|
1511
1477
|
|
|
1512
1478
|
|
|
1479
|
+
def render_youtube_brandings():
|
|
1480
|
+
st.title("Brandings Youtube")
|
|
1481
|
+
st.caption(f"Brandings gerados ou importados da pasta `{BLUEPRINTS / 'brandings'}`")
|
|
1482
|
+
branding_upload = st.file_uploader("Subir Branding JSON", type=["json"], key="branding_upload")
|
|
1483
|
+
if branding_upload and st.button("Guardar Branding", type="secondary"):
|
|
1484
|
+
try:
|
|
1485
|
+
data = json.loads(branding_upload.getvalue().decode("utf-8"))
|
|
1486
|
+
if not isinstance(data, dict):
|
|
1487
|
+
raise ValueError("O JSON raiz deve ser um objecto.")
|
|
1488
|
+
target = BLUEPRINTS / "brandings" / (Path(branding_upload.name).stem.replace(" ", "-") + ".json")
|
|
1489
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
1490
|
+
atomic_write(target, data)
|
|
1491
|
+
record_notification("branding_completed", f"Branding guardado: {target.stem}", "O Branding importado foi guardado no storage local.", metadata={"name": target.stem}, dedupe_key=f"branding:{target}:{target.stat().st_mtime_ns}")
|
|
1492
|
+
st.success(f"Branding guardado em {target}")
|
|
1493
|
+
st.rerun()
|
|
1494
|
+
except (UnicodeDecodeError, json.JSONDecodeError, ValueError) as exc:
|
|
1495
|
+
st.error(f"Branding JSON inválido: {exc}")
|
|
1496
|
+
branding_files = list_branding_files()
|
|
1497
|
+
st.write(f"{len(branding_files)} branding(s) encontrado(s)")
|
|
1498
|
+
branding_search = st.text_input("Pesquisar brandings", key="branding_search")
|
|
1499
|
+
if not branding_files:
|
|
1500
|
+
st.info("Ainda não existem brandings na pasta local.")
|
|
1501
|
+
for path in branding_files:
|
|
1502
|
+
if branding_search and branding_search.lower() not in path.name.lower():
|
|
1503
|
+
continue
|
|
1504
|
+
try:
|
|
1505
|
+
data = load_blueprint_file(path)
|
|
1506
|
+
title = data.get("name") or data.get("identity", {}).get("channel_name") or path.stem
|
|
1507
|
+
with st.expander(f"{title} — {path.name}"):
|
|
1508
|
+
st.caption(f"Blueprint associado: {data.get('blueprint_id') or 'não associado'}")
|
|
1509
|
+
st.json(data)
|
|
1510
|
+
except Exception as exc:
|
|
1511
|
+
with st.expander(f"Inválido — {path.stem}"):
|
|
1512
|
+
st.error(str(exc))
|
|
1513
|
+
|
|
1514
|
+
|
|
1515
|
+
def render_thumbnail_blueprints():
|
|
1516
|
+
st.title("Thumbnail Blueprints")
|
|
1513
1517
|
def _tiktok_accounts_from_settings(settings: dict[str, Any]) -> list[dict[str, Any]]:
|
|
1514
1518
|
raw_accounts = settings.get("tiktok_accounts")
|
|
1515
1519
|
if not isinstance(raw_accounts, list) or not raw_accounts:
|
|
@@ -7067,6 +7071,8 @@ def main():
|
|
|
7067
7071
|
channel_profile_items = [
|
|
7068
7072
|
("Canais YouTube", ":material/ondemand_video:", "Canais YouTube"),
|
|
7069
7073
|
("Blueprints Youtube", ":material/library_books:", "Blueprints Youtube"),
|
|
7074
|
+
("Thumbnail Blueprints", ":material/image:", "Thumbnail Blueprints"),
|
|
7075
|
+
("Brandings Youtube", ":material/brush:", "Brandings Youtube"),
|
|
7070
7076
|
("Contas TikTok", ":material/account_circle:", "Contas TikTok"),
|
|
7071
7077
|
("Prompt Masters", ":material/auto_awesome:", "Prompt Masters"),
|
|
7072
7078
|
("Facebook Pages", ":material/public:", "Facebook Pages"),
|
|
@@ -7148,7 +7154,7 @@ def main():
|
|
|
7148
7154
|
"Niche Finder": "/niche-finder", "Niche Finder Kaggle": "/niche-finder/kaggle", "Niche Finder Apify": "/niche-finder/apify",
|
|
7149
7155
|
"Pipeline Vídeos": "/pipeline-videos", "Criação de Vídeos": "/pipeline-videos/criacao", "Backlog Vídeos": "/pipeline-videos/backlog", "Roteiros": "/pipeline-videos/roteiros", "Thumbnails": "/pipeline-videos/thumbnails", "Upload": "/pipeline-videos/upload",
|
|
7150
7156
|
"Pipeline Música": "/pipeline-musica", "Criação de Músicas": "/pipeline-musica/criacao", "Music Backlog": "/pipeline-musica/backlog", "Vozes Personalizadas": "/pipeline-musica/vozes-personalizadas", "Upload Música": "/pipeline-musica/upload",
|
|
7151
|
-
"Canais/Perfis (Vídeos)": "/canais-perfis-videos", "Canais YouTube": "/canais-perfis-videos/canais-youtube", "Blueprints Youtube": "/canais-perfis-videos/blueprints-youtube", "Contas TikTok": "/canais-perfis-videos/contas-tiktok", "Prompt Masters": "/canais-perfis-videos/prompt-masters", "Facebook Pages": "/canais-perfis-videos/facebook-pages",
|
|
7157
|
+
"Canais/Perfis (Vídeos)": "/canais-perfis-videos", "Canais YouTube": "/canais-perfis-videos/canais-youtube", "Blueprints Youtube": "/canais-perfis-videos/blueprints-youtube", "Thumbnail Blueprints": "/canais-perfis-videos/thumbnail-blueprints", "Brandings Youtube": "/canais-perfis-videos/brandings-youtube", "Contas TikTok": "/canais-perfis-videos/contas-tiktok", "Prompt Masters": "/canais-perfis-videos/prompt-masters", "Facebook Pages": "/canais-perfis-videos/facebook-pages",
|
|
7152
7158
|
"AI Influencers": "/ai-influencers", "Personagens": "/ai-influencers/personagens", "Geração de Conteúdo IA": "/ai-influencers/geracao-conteudo", "Motion Control": "/ai-influencers/motion-control", "UGC Products": "/ai-influencers/ugc-products", "Redes Sociais": "/ai-influencers/redes-sociais",
|
|
7153
7159
|
"Edição": "/edicao", "Limpador de Metadados": "/edicao/limpador-metadados", "Cortes": "/edicao/cortes", "Editor Python": "/edicao/editor-python", "Download Mídia": "/edicao/download-midia",
|
|
7154
7160
|
"Growth": "/growth", "Analista Growth Youtube": "/growth/youtube", "Analista Growth Tiktok": "/growth/tiktok", "Analista Growth Instagram": "/growth/instagram", "Analista Facebook Pages": "/growth/facebook-pages", "Analista Bilibili": "/growth/bilibili",
|
|
@@ -7226,6 +7232,8 @@ def main():
|
|
|
7226
7232
|
"Thumbnails": render_thumbnails,
|
|
7227
7233
|
"Upload": render_upload,
|
|
7228
7234
|
"Blueprints Youtube": render_blueprints,
|
|
7235
|
+
"Thumbnail Blueprints": render_thumbnail_blueprints,
|
|
7236
|
+
"Brandings Youtube": render_youtube_brandings,
|
|
7229
7237
|
"Prompt Masters": render_tiktok_prompt_masters,
|
|
7230
7238
|
"Canais YouTube": render_channels,
|
|
7231
7239
|
"Contas TikTok": render_tiktok_accounts,
|
|
@@ -18,7 +18,7 @@ from integrations.upload_routing import upload_with_default_route
|
|
|
18
18
|
from hermes_ui.creative_generation import CreativeGenerationError, generate_creative_package, generate_title_and_keywords, generate_thumbnail_prompt, generate_topic_for_channel
|
|
19
19
|
from hermes_ui.script_documents import save_script_document
|
|
20
20
|
from hermes_ui.script_generation import generate_script_document
|
|
21
|
-
from hermes_ui.storage import STORAGE, atomic_write, ensure_storage, read_json, write_json
|
|
21
|
+
from hermes_ui.storage import STORAGE, atomic_write, ensure_storage, get_display_name, list_blueprint_files, load_blueprint_file, read_json, write_json
|
|
22
22
|
from hermes_ui.llm_providers import active_llm_card, provider_definition
|
|
23
23
|
from hermes_ui.media_generation import MediaGenerationError, _append_generation_constraints, generate_image_from_pool, generate_video_from_pool
|
|
24
24
|
from hermes_ui.media_providers import FULL_IA_VIDEO_PROVIDER_CODES, media_cards_for_pool, media_provider_definition
|
|
@@ -273,11 +273,29 @@ def _channel_for_task(task: dict[str, Any]) -> dict[str, Any]:
|
|
|
273
273
|
|
|
274
274
|
|
|
275
275
|
def _blueprint_for_channel(channel: dict[str, Any]) -> dict[str, Any]:
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
276
|
+
"""Resolve the Blueprint assigned to a channel from the persisted library files."""
|
|
277
|
+
blueprint_id = str(channel.get("default_blueprint_id") or channel.get("blueprint_id") or "").strip()
|
|
278
|
+
blueprint_name = ""
|
|
279
|
+
if not blueprint_id and not blueprint_name:
|
|
279
280
|
return {}
|
|
280
|
-
|
|
281
|
+
for path in list_blueprint_files():
|
|
282
|
+
try:
|
|
283
|
+
data = load_blueprint_file(path)
|
|
284
|
+
except (OSError, ValueError, json.JSONDecodeError):
|
|
285
|
+
continue
|
|
286
|
+
display_name = get_display_name("blueprints", path, str(data.get("name") or data.get("title") or path.stem))
|
|
287
|
+
identifiers = {
|
|
288
|
+
str(data.get("id") or "").strip(),
|
|
289
|
+
path.stem,
|
|
290
|
+
str(data.get("name") or "").strip(),
|
|
291
|
+
display_name,
|
|
292
|
+
}
|
|
293
|
+
if blueprint_id in identifiers or blueprint_name in identifiers:
|
|
294
|
+
resolved = dict(data)
|
|
295
|
+
resolved.setdefault("id", blueprint_id or path.stem)
|
|
296
|
+
resolved["name"] = display_name
|
|
297
|
+
return resolved
|
|
298
|
+
return {"id": blueprint_id, "name": blueprint_name or blueprint_id}
|
|
281
299
|
|
|
282
300
|
|
|
283
301
|
def _keywords(topic: str, title: str, niche: str = "") -> list[str]:
|
|
@@ -1209,6 +1227,8 @@ def _run_task(task: dict[str, Any]) -> dict[str, Any]:
|
|
|
1209
1227
|
channel = _channel_for_task(task)
|
|
1210
1228
|
settings = _settings()
|
|
1211
1229
|
blueprint = _blueprint_for_channel(channel)
|
|
1230
|
+
if not blueprint and (task.get("blueprint_id") or task.get("blueprint_name")):
|
|
1231
|
+
blueprint = {"id": str(task.get("blueprint_id") or ""), "name": str(task.get("blueprint_name") or task.get("blueprint_id") or "")}
|
|
1212
1232
|
route = _normalise_video_route(task, settings)
|
|
1213
1233
|
topic = str(task.get("topic") or "").strip()
|
|
1214
1234
|
if route != "music" and (not topic or str(task.get("topic_source") or "") in {"auto", "llm_pending"}):
|
|
@@ -43,7 +43,8 @@ def save_script_document(document: dict[str, Any]) -> dict[str, Any]:
|
|
|
43
43
|
f"title: {title}",
|
|
44
44
|
f"language: {str(document.get('language') or '')}",
|
|
45
45
|
f"channel: {str(document.get('channel_name') or '')}",
|
|
46
|
-
f"
|
|
46
|
+
f"blueprint_id: {str(document.get('blueprint_id') or '')}",
|
|
47
|
+
f"blueprint: {str(document.get('blueprint_name') or document.get('blueprint_id') or '')}",
|
|
47
48
|
f"created_at: {created_at}",
|
|
48
49
|
"---",
|
|
49
50
|
"",
|
package/package.json
CHANGED