@danhachuel/thunderbolt 0.3.88 → 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 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
- title = st.text_input("Título", value=task.get("title") or task.get("topic", "Vídeo Thunderbolt"), key=f"yt_title_{task['id']}")
4705
- description = st.text_area("Descrição", value=task.get("description", ""), key=f"yt_description_{task['id']}", height=100)
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.text_input("Idioma", value="pt-BR", key=f"yt_language_{task['id']}")
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:
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danhachuel/thunderbolt",
3
- "version": "0.3.88",
3
+ "version": "0.3.89",
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",