@danhachuel/thunderbolt 0.3.87 → 0.3.89
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 +55 -17
- package/hermes_ui/creative_generation.py +40 -0
- package/package.json +1 -1
package/app/main.py
CHANGED
|
@@ -64,7 +64,7 @@ from hermes_ui.thumbnails import (
|
|
|
64
64
|
upload_thumbnail_image,
|
|
65
65
|
)
|
|
66
66
|
from hermes_ui.draft_video import DRAFT_SETTING_SECTIONS, missing_content_fields, missing_setting_sections, normalise_saved_script, setting_widget_suffixes
|
|
67
|
-
from hermes_ui.creative_generation import CreativeGenerationError, generate_creative_package, generate_thumbnail_prompt, generate_title_and_keywords, generate_topic_for_channel, generate_video_keywords
|
|
67
|
+
from hermes_ui.creative_generation import CreativeGenerationError, generate_creative_package, generate_thumbnail_prompt, generate_title_and_keywords, generate_topic_for_channel, generate_video_description, generate_video_keywords
|
|
68
68
|
from hermes_ui.media_generation import MediaGenerationError, generate_image_for_card, generate_video_for_card
|
|
69
69
|
from integrations.platforms import IntegrationResult, TikTokAdapter, YouTubeAdapter, fetch_channel_videos_public
|
|
70
70
|
from integrations.tiktok_public import fetch_public_tiktok_profile, normalize_tiktok_reference
|
|
@@ -4701,8 +4701,39 @@ def render_upload_conventional():
|
|
|
4701
4701
|
channel = selected_youtube_channel or channel_map.get(str(task.get("channel_id")), {})
|
|
4702
4702
|
account = direct_accounts.get(str(channel.get("google_account_id", "")))
|
|
4703
4703
|
if "YouTube" in destination:
|
|
4704
|
-
|
|
4705
|
-
|
|
4704
|
+
detected_language = normalize_video_language(task.get("language") or channel.get("language") or "pt")
|
|
4705
|
+
language_state_key = f"yt_language_{task['id']}"
|
|
4706
|
+
language_source_key = f"yt_language_source_{task['id']}"
|
|
4707
|
+
if st.session_state.get(language_source_key) != detected_language:
|
|
4708
|
+
st.session_state[language_state_key] = detected_language
|
|
4709
|
+
st.session_state[language_source_key] = detected_language
|
|
4710
|
+
title_state_key = f"yt_title_{task['id']}"
|
|
4711
|
+
title = st.text_input("Título", value=task.get("title") or task.get("topic", "Vídeo Thunderbolt"), key=title_state_key)
|
|
4712
|
+
description_state_key = f"yt_description_{task['id']}"
|
|
4713
|
+
description_error_key = f"yt_description_ai_error_{task['id']}"
|
|
4714
|
+
|
|
4715
|
+
def generate_upload_description_callback() -> None:
|
|
4716
|
+
current_title = str(st.session_state.get(title_state_key) or title).strip()
|
|
4717
|
+
raw_tags = st.session_state.get(f"yt_tags_{task['id']}", task.get("tags", ""))
|
|
4718
|
+
current_tags = raw_tags if isinstance(raw_tags, list) else [item.strip() for item in str(raw_tags or "").split(",") if item.strip()]
|
|
4719
|
+
try:
|
|
4720
|
+
st.session_state[description_state_key] = generate_video_description(
|
|
4721
|
+
settings,
|
|
4722
|
+
channel,
|
|
4723
|
+
str(task.get("topic") or current_title),
|
|
4724
|
+
title=current_title,
|
|
4725
|
+
tags=current_tags,
|
|
4726
|
+
language=detected_language,
|
|
4727
|
+
)
|
|
4728
|
+
except CreativeGenerationError as exc:
|
|
4729
|
+
st.session_state[description_error_key] = str(exc)
|
|
4730
|
+
else:
|
|
4731
|
+
st.session_state.pop(description_error_key, None)
|
|
4732
|
+
|
|
4733
|
+
description = st.text_area("Descrição", value=task.get("description", ""), key=description_state_key, height=100)
|
|
4734
|
+
st.button("Gerar descrição com IA", key=f"yt_description_ai_{task['id']}", use_container_width=False, on_click=generate_upload_description_callback)
|
|
4735
|
+
if description_error := str(st.session_state.get(description_error_key) or "").strip():
|
|
4736
|
+
st.error(f"Não foi possível gerar a descrição: {description_error}")
|
|
4706
4737
|
tags_raw = st.text_input("Tags separadas por vírgula", value=task.get("tags", "") if isinstance(task.get("tags", ""), str) else ", ".join(task.get("tags", [])), key=f"yt_tags_{task['id']}")
|
|
4707
4738
|
yt_cols = st.columns(3)
|
|
4708
4739
|
with yt_cols[0]:
|
|
@@ -4711,7 +4742,13 @@ def render_upload_conventional():
|
|
|
4711
4742
|
with yt_cols[1]:
|
|
4712
4743
|
category_id = st.text_input("Category ID", value="22", key=f"yt_category_{task['id']}")
|
|
4713
4744
|
with yt_cols[2]:
|
|
4714
|
-
language = st.
|
|
4745
|
+
language = st.selectbox(
|
|
4746
|
+
"Idioma",
|
|
4747
|
+
VIDEO_LANGUAGE_SELECTION_OPTIONS,
|
|
4748
|
+
index=VIDEO_LANGUAGE_SELECTION_OPTIONS.index(detected_language) if detected_language in VIDEO_LANGUAGE_SELECTION_OPTIONS else 0,
|
|
4749
|
+
format_func=video_language_label,
|
|
4750
|
+
key=language_state_key,
|
|
4751
|
+
)
|
|
4715
4752
|
quota_count = official_upload_count(channel, account)
|
|
4716
4753
|
st.caption(f"API Oficial hoje: {quota_count}/{OFFICIAL_DAILY_LIMIT} envios nesta conta Gmail.")
|
|
4717
4754
|
if not selected_youtube_channel:
|
|
@@ -5473,28 +5510,29 @@ def _render_llm_card(settings: dict[str, Any], cards: list[dict[str, Any]], inde
|
|
|
5473
5510
|
if not isinstance(model_catalog, list):
|
|
5474
5511
|
model_catalog = []
|
|
5475
5512
|
current_model = str(card.get("model") or "").strip()
|
|
5476
|
-
manual_model = "__manual_model__"
|
|
5477
5513
|
discovered_models = [str(item).strip() for item in model_catalog if str(item).strip()]
|
|
5478
|
-
|
|
5514
|
+
default_models = [
|
|
5515
|
+
"gpt-4.1-mini",
|
|
5516
|
+
"gpt-4o-mini",
|
|
5517
|
+
"meta/llama-3.1-8b-instruct",
|
|
5518
|
+
"meta/llama-3.1-70b-instruct",
|
|
5519
|
+
"meta/llama-3.3-70b-instruct",
|
|
5520
|
+
"nvidia/llama-3.1-nemotron-70b-instruct",
|
|
5521
|
+
] if definition.code == "openai" else []
|
|
5522
|
+
options = ["__select_model__", *list(dict.fromkeys([*discovered_models, *default_models]))]
|
|
5479
5523
|
# Mantém modelos guardados mesmo quando a descoberta ainda não foi
|
|
5480
5524
|
# executada ou quando o provider deixou de os devolver temporariamente.
|
|
5481
5525
|
if current_model and current_model not in options:
|
|
5482
|
-
options.insert(
|
|
5483
|
-
options.append(manual_model)
|
|
5526
|
+
options.insert(1, current_model)
|
|
5484
5527
|
selected = st.selectbox(
|
|
5485
5528
|
"Modelo",
|
|
5486
5529
|
options,
|
|
5487
|
-
index=options.index(current_model) if current_model in options else
|
|
5488
|
-
format_func=lambda value: "
|
|
5489
|
-
help="Seleccione um modelo
|
|
5530
|
+
index=options.index(current_model) if current_model in options else 0,
|
|
5531
|
+
format_func=lambda value: "Seleccione um modelo" if value == "__select_model__" else value,
|
|
5532
|
+
help="Seleccione um modelo da lista ou actualize o catálogo a partir do endpoint do provider.",
|
|
5490
5533
|
key=f"llm_card_{card_id}_model_select",
|
|
5491
5534
|
)
|
|
5492
|
-
model =
|
|
5493
|
-
"Modelo manual",
|
|
5494
|
-
value=current_model if selected == manual_model and current_model not in discovered_models else "",
|
|
5495
|
-
help="ID do modelo usado pelo endpoint Chat Completions.",
|
|
5496
|
-
key=f"llm_card_{card_id}_model_manual",
|
|
5497
|
-
) if selected == manual_model else selected
|
|
5535
|
+
model = "" if selected == "__select_model__" else selected
|
|
5498
5536
|
base_url = str(card.get("base_url") or definition.default_base_url)
|
|
5499
5537
|
endpoint_col, action_col = st.columns([1.65, 1.05])
|
|
5500
5538
|
with endpoint_col:
|
|
@@ -121,6 +121,46 @@ def channel_context(channel: dict[str, Any], blueprint: dict[str, Any] | None =
|
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
|
|
124
|
+
def generate_video_description(
|
|
125
|
+
settings: dict[str, Any],
|
|
126
|
+
channel: dict[str, Any],
|
|
127
|
+
topic: str,
|
|
128
|
+
*,
|
|
129
|
+
title: str = "",
|
|
130
|
+
tags: list[str] | None = None,
|
|
131
|
+
language: str = "",
|
|
132
|
+
) -> str:
|
|
133
|
+
"""Generate a concise YouTube description through the configured text LLM pool."""
|
|
134
|
+
topic = str(topic or "").strip()
|
|
135
|
+
title = str(title or "").strip()
|
|
136
|
+
if not topic and not title:
|
|
137
|
+
raise CreativeGenerationError("É necessário um tópico ou título antes de gerar a descrição do vídeo.")
|
|
138
|
+
context = channel_context(channel)
|
|
139
|
+
normalized_tags = [str(item).strip() for item in (tags or []) if str(item).strip()][:15]
|
|
140
|
+
system = (
|
|
141
|
+
"És um editor de metadados de YouTube. Cria uma descrição útil, clara e envolvente para o vídeo, "
|
|
142
|
+
"sem alegações não verificadas, sem clickbait falso e sem mencionar que foi gerada por IA. "
|
|
143
|
+
"Usa dois parágrafos curtos, inclui um convite natural para subscrever quando apropriado e devolve "
|
|
144
|
+
"apenas JSON válido com a chave description."
|
|
145
|
+
)
|
|
146
|
+
user = json.dumps(
|
|
147
|
+
{
|
|
148
|
+
"channel": context,
|
|
149
|
+
"language": language or context["language"],
|
|
150
|
+
"topic": topic,
|
|
151
|
+
"title": title or topic,
|
|
152
|
+
"tags": normalized_tags,
|
|
153
|
+
"requirements": {"paragraphs": 2, "max_characters": 1800, "no_unverified_claims": True},
|
|
154
|
+
},
|
|
155
|
+
ensure_ascii=False,
|
|
156
|
+
)
|
|
157
|
+
result = _chat_json(settings, system, user)
|
|
158
|
+
description = str(result.get("description") or "").strip()
|
|
159
|
+
if not description:
|
|
160
|
+
raise CreativeGenerationError("O provider LLM não devolveu uma descrição válida para o vídeo.")
|
|
161
|
+
return description[:1800]
|
|
162
|
+
|
|
163
|
+
|
|
124
164
|
def generate_topic_for_channel(
|
|
125
165
|
settings: dict[str, Any],
|
|
126
166
|
channel: dict[str, Any],
|
package/package.json
CHANGED