@danhachuel/thunderbolt 0.3.4 → 0.3.6

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
@@ -43,7 +43,7 @@ from hermes_ui.script_documents import list_script_documents, read_script_docume
43
43
  from hermes_ui.script_generation import generate_script_document
44
44
  from hermes_ui.voice_preview import DEFAULT_SAMPLE, load_preview_file, synthesize_preview
45
45
  from hermes_ui.thumbnail_generation import ThumbnailGenerationError, generate_thumbnail_image
46
- from hermes_ui.creative_generation import CreativeGenerationError, generate_creative_package, generate_topic_for_channel
46
+ from hermes_ui.creative_generation import CreativeGenerationError, generate_creative_package, generate_topic_for_channel, generate_video_keywords
47
47
  from integrations.platforms import IntegrationResult, TikTokAdapter, YouTubeAdapter, fetch_channel_videos_public
48
48
  from integrations.tiktok_public import fetch_public_tiktok_profile, normalize_tiktok_reference
49
49
  from integrations.postiz import PostizAdapter
@@ -492,6 +492,93 @@ def generate_topic_for_ui(settings: dict[str, Any], channel: dict, user_context:
492
492
  return generate_topic_for_channel(settings, channel, blueprint_for_channel(channel), user_context=user_context)
493
493
 
494
494
 
495
+ def generate_video_content_for_ui(
496
+ settings: dict[str, Any],
497
+ channel: dict,
498
+ subject: str,
499
+ language: str,
500
+ generation_settings: dict[str, Any] | None = None,
501
+ ) -> dict[str, Any]:
502
+ """Generate the subject, script and English keywords in one MoneyPrinter-style action."""
503
+ subject = str(subject or "").strip()
504
+ topic_result: dict[str, Any] | None = None
505
+ if not subject:
506
+ topic_result = generate_topic_for_ui(settings, channel)
507
+ subject = str(topic_result.get("topic") or "").strip()
508
+ if not subject:
509
+ raise CreativeGenerationError("A IA não devolveu um Video Subject válido.")
510
+
511
+ blueprint = blueprint_for_channel(channel)
512
+ script_result = generate_script_document(
513
+ settings,
514
+ document_type="Roteiro de vídeo",
515
+ title=subject,
516
+ brief=subject,
517
+ language=str(language or channel.get("language") or "Português"),
518
+ channel=channel,
519
+ blueprint=blueprint,
520
+ structure_notes=str((generation_settings or {}).get("script_structure_notes") or ""),
521
+ generation_settings=generation_settings or {},
522
+ )
523
+ script = str(script_result.get("content") or "").strip()
524
+ keywords = generate_video_keywords(
525
+ settings,
526
+ channel,
527
+ subject,
528
+ script,
529
+ blueprint,
530
+ language=str(language or channel.get("language") or "Português"),
531
+ )
532
+ return {
533
+ "topic": subject,
534
+ "topic_result": topic_result,
535
+ "script_result": script_result,
536
+ "script": script,
537
+ "keywords": keywords,
538
+ "topic_source": "llm" if topic_result else "manual",
539
+ }
540
+
541
+
542
+ def _generate_video_content_callback(prefix: str, channel: dict, fallback_language: str) -> None:
543
+ """Streamlit callback for the single button below Subject, Script and Keywords."""
544
+ settings = read_json("settings.json", {})
545
+ generation_settings = {
546
+ "video_subject": str(st.session_state.get(f"{prefix}_video_subject") or "").strip(),
547
+ "script_language": str(st.session_state.get(f"{prefix}_script_language") or fallback_language or "pt"),
548
+ "script_structure_notes": str(st.session_state.get(f"{prefix}_script_structure_notes") or "").strip(),
549
+ "generate_script_with_ai": True,
550
+ }
551
+ try:
552
+ result = generate_video_content_for_ui(
553
+ settings,
554
+ channel,
555
+ generation_settings["video_subject"],
556
+ generation_settings["script_language"],
557
+ generation_settings=generation_settings,
558
+ )
559
+ st.session_state[f"{prefix}_video_subject"] = result["topic"]
560
+ st.session_state[f"{prefix}_video_script"] = result["script"]
561
+ st.session_state[f"{prefix}_video_keywords"] = ", ".join(result["keywords"])
562
+ st.session_state["new_video_topic"] = result["topic"]
563
+ st.session_state["new_video_topic_meta"] = result.get("topic_result") or {
564
+ "topic": result["topic"],
565
+ "topic_source": result.get("topic_source", "manual"),
566
+ }
567
+ # A subject change invalidates a previously generated title/thumbnail package.
568
+ st.session_state.pop("new_video_creative_payload", None)
569
+ st.session_state[f"{prefix}_generate_content_notice"] = "Tema, roteiro e palavras-chave gerados com IA."
570
+ st.session_state.pop(f"{prefix}_generate_content_error", None)
571
+ except CreativeGenerationError as exc:
572
+ st.session_state[f"{prefix}_generate_content_error"] = str(exc)
573
+ st.session_state.pop(f"{prefix}_generate_content_notice", None)
574
+
575
+
576
+ def _video_topic_source(subject: str) -> str:
577
+ meta = st.session_state.get("new_video_topic_meta") or {}
578
+ generated_topic = str(meta.get("topic") or "").strip() if isinstance(meta, dict) else ""
579
+ return "llm" if generated_topic and generated_topic == str(subject or "").strip() and meta.get("topic_source") == "llm" else "manual"
580
+
581
+
495
582
  def generate_creative_for_ui(settings: dict[str, Any], channel: dict, topic: str, topic_source: str = "manual") -> dict[str, Any]:
496
583
  creative = generate_creative_package(
497
584
  settings,
@@ -530,7 +617,13 @@ def voice_catalog(current: str = "") -> list[str]:
530
617
  return voices
531
618
 
532
619
 
533
- def render_video_generation_settings(prefix: str, *, current_language: str = "") -> dict[str, Any]:
620
+ def render_video_generation_settings(
621
+ prefix: str,
622
+ *,
623
+ current_language: str = "",
624
+ channel: dict[str, Any] | None = None,
625
+ generate_content_callback: Any | None = None,
626
+ ) -> dict[str, Any]:
534
627
  """Render the shared MoneyPrinter-style settings and return a serializable payload."""
535
628
  settings: dict[str, Any] = {}
536
629
  st.markdown("### Video Subject Settings")
@@ -572,6 +665,19 @@ def render_video_generation_settings(prefix: str, *, current_language: str = "")
572
665
  key=f"{prefix}_video_keywords",
573
666
  height=90,
574
667
  )
668
+ if generate_content_callback is not None:
669
+ st.button(
670
+ "Gerar tópico, roteiro e palavras-chave com IA",
671
+ key=f"{prefix}_generate_video_content",
672
+ use_container_width=True,
673
+ type="secondary",
674
+ icon=":material/auto_awesome:",
675
+ on_click=generate_content_callback,
676
+ )
677
+ if st.session_state.get(f"{prefix}_generate_content_notice"):
678
+ st.success(st.session_state[f"{prefix}_generate_content_notice"])
679
+ if st.session_state.get(f"{prefix}_generate_content_error"):
680
+ st.error(st.session_state[f"{prefix}_generate_content_error"])
575
681
 
576
682
  st.markdown("### Video Settings")
577
683
  video_cols = st.columns(2)
@@ -596,6 +702,13 @@ def render_video_generation_settings(prefix: str, *, current_language: str = "")
596
702
  with audio_cols[0]:
597
703
  settings["voiceover_mode"] = st.radio("Voiceover Mode", VOICEOVER_MODE_OPTIONS, horizontal=True, key=f"{prefix}_voiceover_mode")
598
704
  settings["voiceover_service"] = st.selectbox("Voiceover Service", VOICEOVER_SERVICE_OPTIONS, key=f"{prefix}_voiceover_service")
705
+ if channel is not None:
706
+ channel_id = str(channel.get("id") or channel.get("name") or "")
707
+ channel_voice = str(channel.get("default_voice") or channel.get("voice") or "").strip()
708
+ channel_state_key = f"{prefix}_voice_channel_id"
709
+ if st.session_state.get(channel_state_key) != channel_id:
710
+ st.session_state[f"{prefix}_voice"] = channel_voice
711
+ st.session_state[channel_state_key] = channel_id
599
712
  current_voice = str(st.session_state.get(f"{prefix}_voice", ""))
600
713
  voice_options = voice_catalog(current_voice)
601
714
  settings["voice"] = st.selectbox("Voice (match script language)", voice_options, format_func=lambda value: value or "Sem voz seleccionada", key=f"{prefix}_voice")
@@ -1611,32 +1724,13 @@ def render_new_video(page_title: str = "Criação de Vídeos"):
1611
1724
  generation_settings = render_video_generation_settings(
1612
1725
  "new_video",
1613
1726
  current_language=str(st.session_state.get("video_language") or ""),
1727
+ channel=selected_one,
1728
+ generate_content_callback=lambda: _generate_video_content_callback(
1729
+ "new_video",
1730
+ selected_one,
1731
+ str(st.session_state.get("video_language") or "pt"),
1732
+ ),
1614
1733
  )
1615
- topic = st.text_area(
1616
- "Tópico ou briefing",
1617
- value=st.session_state.get("new_video_topic", ""),
1618
- key="new_video_topic",
1619
- placeholder="Escreva um briefing ou gere-o com IA; não é obrigatório escrever manualmente.",
1620
- help="Pode escrever o tema ou usar o botão abaixo para gerar um briefing específico com o Blueprint e o nicho do canal.",
1621
- )
1622
- topic_cols = st.columns([1, 1.8])
1623
- with topic_cols[0]:
1624
- if st.button("Gerar tópico/briefing com IA", key="new_video_generate_topic", use_container_width=True):
1625
- if selected_one is None:
1626
- st.error("Seleccione primeiro um canal.")
1627
- else:
1628
- try:
1629
- result = generate_topic_for_ui(read_json("settings.json", {}), selected_one, topic)
1630
- st.session_state["new_video_topic"] = result["topic"]
1631
- st.session_state["new_video_topic_meta"] = result
1632
- st.success("Briefing gerado; reveja e edite o texto antes de criar as tarefas.")
1633
- st.rerun()
1634
- except CreativeGenerationError as exc:
1635
- st.error(str(exc))
1636
- with topic_cols[1]:
1637
- if st.session_state.get("new_video_topic_meta"):
1638
- meta = st.session_state["new_video_topic_meta"]
1639
- st.caption(f"Origem: IA · Nicho: {meta.get('niche', '—')} · Ângulo: {meta.get('angle', '—')}")
1640
1734
 
1641
1735
  if not generation_settings:
1642
1736
  generation_settings = render_video_generation_settings(
@@ -1753,7 +1847,7 @@ def render_new_video(page_title: str = "Criação de Vídeos"):
1753
1847
  st.caption("A imagem ainda não foi gerada. Configure a API key em Configuração API > API Keys.")
1754
1848
  st.caption(f"Estado da thumbnail: {payload.get('thumbnail_status', 'prompt_ready')} · texto: {payload.get('thumbnail_text') or 'sem texto'}")
1755
1849
  else:
1756
- topic_for_creative = str(st.session_state.get("new_video_topic", "") or "").strip()
1850
+ topic_for_creative = str(generation_settings.get("video_subject") or "").strip()
1757
1851
  if st.button("Gerar títulos e thumbnails com IA", key="new_video_generate_creative", use_container_width=True):
1758
1852
  if selected_one is None:
1759
1853
  st.error("Seleccione primeiro um canal.")
@@ -1764,7 +1858,13 @@ def render_new_video(page_title: str = "Criação de Vídeos"):
1764
1858
  topic_for_creative = str(topic_result["topic"]).strip()
1765
1859
  st.session_state["new_video_topic"] = topic_for_creative
1766
1860
  st.session_state["new_video_topic_meta"] = topic_result
1767
- generated = generate_creative_for_ui(read_json("settings.json", {}), selected_one, topic_for_creative, topic_source="llm")
1861
+ generated = generate_creative_for_ui(
1862
+ read_json("settings.json", {}),
1863
+ selected_one,
1864
+ topic_for_creative,
1865
+ topic_source=_video_topic_source(topic_for_creative),
1866
+ )
1867
+
1768
1868
  st.session_state["new_video_creative_payload"] = generated
1769
1869
  st.success("Tema, título e thumbnails gerados; escolha a variante antes de criar as tarefas.")
1770
1870
  st.rerun()
@@ -1866,28 +1966,27 @@ def render_new_video(page_title: str = "Criação de Vídeos"):
1866
1966
  tasks = create_tasks_for_batch(batch)
1867
1967
  st.success(f"Lote geral {batch['id']} criado com {len(tasks)} tarefas independentes, uma por canal.")
1868
1968
  else:
1869
- topic_value = str(st.session_state.get("new_video_topic", "") or "").strip()
1969
+ topic_value = str(generation_settings.get("video_subject") or "").strip()
1870
1970
  if not selected:
1871
1971
  st.error("Seleccione um canal.")
1872
1972
  else:
1873
1973
  if not topic_value:
1874
- try:
1875
- topic_result = generate_topic_for_ui(read_json("settings.json", {}), selected_one or {})
1876
- topic_value = str(topic_result["topic"]).strip()
1877
- st.session_state["new_video_topic"] = topic_value
1878
- st.session_state["new_video_topic_meta"] = topic_result
1879
- except CreativeGenerationError as exc:
1880
- st.error(f"Não foi possível gerar automaticamente o tema: {exc}")
1881
- st.stop()
1974
+ st.error("Preencha o campo Video Subject ou use o botão de geração automática abaixo das keywords.")
1975
+ st.stop()
1882
1976
  quantity_value = int(quantity if mode == "same_channel" else 1)
1883
1977
  payload = dict(st.session_state.get("new_video_creative_payload") or {})
1884
1978
  if not payload.get("title") or not payload.get("thumbnail_variants"):
1885
1979
  try:
1886
- payload = generate_creative_for_ui(read_json("settings.json", {}), selected_one or {}, topic_value, topic_source="llm" if st.session_state.get("new_video_topic_meta") else "manual")
1980
+ payload = generate_creative_for_ui(
1981
+ read_json("settings.json", {}),
1982
+ selected_one or {},
1983
+ topic_value,
1984
+ topic_source=_video_topic_source(topic_value),
1985
+ )
1887
1986
  except CreativeGenerationError as exc:
1888
1987
  st.warning(f"Título/thumbnail automáticos pendentes: {exc} A tarefa será criada com o tópico como título e sem ficheiro de thumbnail.")
1889
1988
  payload = {"topic": topic_value, "title": topic_value, "topic_source": "manual", "thumbnail_status": "pending_provider", "thumbnail_variants": [], "thumbnail_variant": {}, "thumbnail_prompt": "", "thumbnail_text": ""}
1890
- payload.update({"topic": topic_value, "topic_source": payload.get("topic_source") or ("llm" if st.session_state.get("new_video_topic_meta") else "manual"), "language": language, "format": fmt, "style_wide": style, "style_ia": style_ia, "music_mode": style == "music", "background_mode": "none" if style == "music" else ("ai" if style == "full_ia" else "stock"), "music_path": music_path, "music_source": music_source, "generation_settings": generation_settings})
1989
+ payload.update({"topic": topic_value, "topic_source": payload.get("topic_source") or _video_topic_source(topic_value), "language": language, "format": fmt, "style_wide": style, "style_ia": style_ia, "music_mode": style == "music", "background_mode": "none" if style == "music" else ("ai" if style == "full_ia" else "stock"), "music_path": music_path, "music_source": music_source, "generation_settings": generation_settings})
1891
1990
  batch = create_batch(mode, selected, topic_value, quantity_value, {"language": language, "format": fmt, "style_wide": style, "style_ia": style_ia, "music_mode": style == "music", "background_mode": "none" if style == "music" else ("ai" if style == "full_ia" else "stock"), "music_path": music_path, "music_source": music_source, "generation_settings": generation_settings, "topic_source": payload.get("topic_source", "manual"), "channel_payloads": {selected[0]: payload}})
1892
1991
  tasks = create_tasks_for_batch(batch)
1893
1992
  st.success(f"Lote {batch['id']} criado com {len(tasks)} tarefa(s). Abra {ui_text('Backlog Vídeos', current_ui_language())} para acompanhar.")
@@ -4255,13 +4354,13 @@ def main():
4255
4354
  ("Início", ":material/home:", "Início"),
4256
4355
  ("Automação", ":material/schedule:", "Automação"),
4257
4356
  ("Niche Finder", ":material/search:", "Niche Finder"),
4258
- ("Pipeline Vídeos", ":material/account_tree:", "Pipeline Vídeos"),
4259
- ("AI Influencers", ":material/smart_toy:", "AI Influencers"),
4260
4357
  ("Arquivos Base", ":material/folder:", "Arquivos Base"),
4261
4358
  ("Canais e Perfis de Vídeos", ":material/video_library:", "Canais e Perfis de Vídeos"),
4262
- ("Música", ":material/music_note:", "Música"),
4263
- ("Edição", ":material/edit:", "Edição"),
4264
4359
  ("Growth", ":material/analytics:", "Growth"),
4360
+ ("Pipeline Vídeos", ":material/account_tree:", "Pipeline Vídeos"),
4361
+ ("Pipeline Música", ":material/music_note:", "Pipeline Música"),
4362
+ ("AI Influencers", ":material/smart_toy:", "AI Influencers"),
4363
+ ("Edição", ":material/edit:", "Edição"),
4265
4364
  ("Documentação", ":material/menu_book:", "Documentação"),
4266
4365
  ("Configurações", ":material/settings:", "Configurações"),
4267
4366
  ]
@@ -4272,7 +4371,7 @@ def main():
4272
4371
  "AI Influencers": models_ai_items,
4273
4372
  "Arquivos Base": base_files_items,
4274
4373
  "Canais e Perfis de Vídeos": channel_profile_items,
4275
- "Música": music_items,
4374
+ "Pipeline Música": music_items,
4276
4375
  "Edição": edition_items,
4277
4376
  "Growth": growth_items,
4278
4377
  "Documentação": documentation_items,
@@ -4284,6 +4383,7 @@ def main():
4284
4383
  "Vídeos": "Backlog Vídeos",
4285
4384
  "Limpador de metadado": "Limpador de Metadados",
4286
4385
  "Pipeline": "Pipeline Vídeos",
4386
+ "Música": "Pipeline Música",
4287
4387
  "Pipeline TikTok": "Canais e Perfis de Vídeos",
4288
4388
  "Prompts Master": "Prompt Masters",
4289
4389
  "Canais": "Canais YouTube",
@@ -166,6 +166,66 @@ def generate_topic_for_channel(
166
166
  return result
167
167
 
168
168
 
169
+ def generate_video_keywords(
170
+ settings: dict[str, Any],
171
+ channel: dict[str, Any],
172
+ topic: str,
173
+ script: str,
174
+ blueprint: dict[str, Any] | None = None,
175
+ language: str = "",
176
+ ) -> list[str]:
177
+ """Generate SEO keywords for a video subject and script using the configured LLM.
178
+
179
+ This mirrors MoneyPrinterTurbo's ``llm.generate_terms`` step while keeping the
180
+ Thunderbolt provider configuration and blueprint context in one place.
181
+ """
182
+ topic = str(topic or "").strip()
183
+ script = str(script or "").strip()
184
+ if not topic:
185
+ raise CreativeGenerationError("É necessário um Video Subject antes de gerar palavras-chave.")
186
+ if not script:
187
+ raise CreativeGenerationError("É necessário um roteiro antes de gerar palavras-chave.")
188
+
189
+ context = channel_context(channel, blueprint)
190
+ system = (
191
+ "És um especialista de SEO para vídeos faceless e pesquisa de materiais. "
192
+ "Extrai entre 8 e 15 palavras-chave curtas, concretas e úteis para o vídeo. "
193
+ "Devolve as palavras-chave em inglês, sem hashtags, sem frases longas, sem duplicados "
194
+ "e sem comentários adicionais. Responde apenas com JSON válido na chave keywords."
195
+ )
196
+ user = json.dumps(
197
+ {
198
+ "channel": context,
199
+ "language": language or context["language"],
200
+ "video_subject": topic,
201
+ "video_script": script,
202
+ "reference_rules": reference_bundle(),
203
+ "requirements": {
204
+ "count": "8 to 15",
205
+ "language": "English",
206
+ "format": "short keyword phrases without hashtags",
207
+ },
208
+ },
209
+ ensure_ascii=False,
210
+ )
211
+ result = _chat_json(settings, system, user)
212
+ raw_keywords = result.get("keywords")
213
+ if isinstance(raw_keywords, str):
214
+ raw_keywords = re.split(r"[,\n;|]+", raw_keywords)
215
+ keywords = []
216
+ seen = set()
217
+ if isinstance(raw_keywords, list):
218
+ for item in raw_keywords:
219
+ value = re.sub(r"^#+", "", str(item or "").strip())
220
+ normalized = value.casefold()
221
+ if value and normalized not in seen:
222
+ keywords.append(value)
223
+ seen.add(normalized)
224
+ if not keywords:
225
+ keywords = _keywords_from_text(topic, script)
226
+ return keywords[:15]
227
+
228
+
169
229
  def _score(value: Any) -> int:
170
230
  try:
171
231
  return max(0, min(3, int(value)))
@@ -146,40 +146,107 @@ UI_TRANSLATIONS: dict[str, dict[str, str]] = {
146
146
  # translation path as the legacy pages.
147
147
  UI_NAV_TRANSLATIONS: dict[str, dict[str, str]] = {
148
148
  "pt": {
149
- "Arquivos Base": "Arquivos Base", "Pipeline Vídeos": "Pipeline Vídeos", "Canais e Perfis de Vídeos": "Canais e Perfis de Vídeos", "Canais YouTube": "Canais YouTube", "Facebook Pages": "Facebook Pages", "Prompt Masters": "Prompt Masters", "Backlog Vídeos": "Backlog Vídeos", "Música": "Música", "Upload Música": "Upload Música", "Geração de Conteúdo IA": "Geração de Conteúdo IA", "Motion Control": "Motion Control", "UGC Products": "UGC Products", "Growth": "Growth", "Analista Growth Youtube": "Analista Growth Youtube", "Analista Growth Tiktok": "Analista Growth Tiktok", "Analista Growth Instagram": "Analista Growth Instagram", "Documentação": "Documentação", "Tutorial Kaggle": "Tutorial Kaggle", "Tutorial Apify": "Tutorial Apify", "Pipeline Músicas": "Pipeline Músicas", "módulos disponíveis": "módulos disponíveis",
149
+ "Arquivos Base": "Arquivos Base", "Pipeline Vídeos": "Pipeline Vídeos", "Canais e Perfis de Vídeos": "Canais e Perfis de Vídeos", "Canais YouTube": "Canais YouTube", "Facebook Pages": "Facebook Pages", "Prompt Masters": "Prompt Masters", "Backlog Vídeos": "Backlog Vídeos", "Música": "Música", "Upload Música": "Upload Música", "Geração de Conteúdo IA": "Geração de Conteúdo IA", "Motion Control": "Motion Control", "UGC Products": "UGC Products", "Growth": "Growth", "Analista Growth Youtube": "Analista Growth Youtube", "Analista Growth Tiktok": "Analista Growth Tiktok", "Analista Growth Instagram": "Analista Growth Instagram", "Documentação": "Documentação", "Tutorial Kaggle": "Tutorial Kaggle", "Tutorial Apify": "Tutorial Apify", "Pipeline Música": "Pipeline Música", "módulos disponíveis": "módulos disponíveis",
150
150
  },
151
151
  "en": {
152
- "Arquivos Base": "Base Files", "Pipeline Vídeos": "Video Pipeline", "Canais e Perfis de Vídeos": "Video Channels & Profiles", "Canais YouTube": "YouTube Channels", "Facebook Pages": "Facebook Pages", "Prompt Masters": "Prompt Masters", "Backlog Vídeos": "Video Backlog", "Música": "Music", "Upload Música": "Music Upload", "Geração de Conteúdo IA": "AI Content Generation", "Motion Control": "Motion Control", "UGC Products": "UGC Products", "Growth": "Growth", "Analista Growth Youtube": "YouTube Growth Analyst", "Analista Growth Tiktok": "TikTok Growth Analyst", "Analista Growth Instagram": "Instagram Growth Analyst", "Documentação": "Documentation", "Tutorial Kaggle": "Kaggle Tutorial", "Tutorial Apify": "Apify Tutorial", "Pipeline Músicas": "Music Pipeline", "módulos disponíveis": "available modules",
152
+ "Arquivos Base": "Base Files", "Pipeline Vídeos": "Video Pipeline", "Canais e Perfis de Vídeos": "Video Channels & Profiles", "Canais YouTube": "YouTube Channels", "Facebook Pages": "Facebook Pages", "Prompt Masters": "Prompt Masters", "Backlog Vídeos": "Video Backlog", "Música": "Music", "Upload Música": "Music Upload", "Geração de Conteúdo IA": "AI Content Generation", "Motion Control": "Motion Control", "UGC Products": "UGC Products", "Growth": "Growth", "Analista Growth Youtube": "YouTube Growth Analyst", "Analista Growth Tiktok": "TikTok Growth Analyst", "Analista Growth Instagram": "Instagram Growth Analyst", "Documentação": "Documentation", "Tutorial Kaggle": "Kaggle Tutorial", "Tutorial Apify": "Apify Tutorial", "Pipeline Música": "Music Pipeline", "módulos disponíveis": "available modules",
153
153
  },
154
154
  "zh": {
155
- "Arquivos Base": "基础文件", "Pipeline Vídeos": "视频流程", "Canais e Perfis de Vídeos": "视频频道与资料", "Canais YouTube": "YouTube 频道", "Facebook Pages": "Facebook 页面", "Prompt Masters": "主提示词", "Backlog Vídeos": "视频待办", "Música": "音乐", "Upload Música": "音乐上传", "Geração de Conteúdo IA": "AI 内容生成", "Motion Control": "动作控制", "UGC Products": "UGC 产品", "Growth": "增长", "Analista Growth Youtube": "YouTube 增长分析师", "Analista Growth Tiktok": "TikTok 增长分析师", "Analista Growth Instagram": "Instagram 增长分析师", "Documentação": "文档", "Tutorial Kaggle": "Kaggle 教程", "Tutorial Apify": "Apify 教程", "Pipeline Músicas": "音乐流程", "módulos disponíveis": "可用模块",
155
+ "Arquivos Base": "基础文件", "Pipeline Vídeos": "视频流程", "Canais e Perfis de Vídeos": "视频频道与资料", "Canais YouTube": "YouTube 频道", "Facebook Pages": "Facebook 页面", "Prompt Masters": "主提示词", "Backlog Vídeos": "视频待办", "Música": "音乐", "Upload Música": "音乐上传", "Geração de Conteúdo IA": "AI 内容生成", "Motion Control": "动作控制", "UGC Products": "UGC 产品", "Growth": "增长", "Analista Growth Youtube": "YouTube 增长分析师", "Analista Growth Tiktok": "TikTok 增长分析师", "Analista Growth Instagram": "Instagram 增长分析师", "Documentação": "文档", "Tutorial Kaggle": "Kaggle 教程", "Tutorial Apify": "Apify 教程", "Pipeline Música": "音乐流程", "módulos disponíveis": "可用模块",
156
156
  },
157
157
  "de": {
158
- "Arquivos Base": "Basisdateien", "Pipeline Vídeos": "Video-Pipeline", "Canais e Perfis de Vídeos": "Videokanäle und Profile", "Canais YouTube": "YouTube-Kanäle", "Facebook Pages": "Facebook-Seiten", "Prompt Masters": "Prompt Masters", "Backlog Vídeos": "Video-Backlog", "Música": "Musik", "Upload Música": "Musik-Upload", "Geração de Conteúdo IA": "KI-Inhaltserstellung", "Motion Control": "Motion Control", "UGC Products": "UGC-Produkte", "Growth": "Wachstum", "Analista Growth Youtube": "YouTube-Wachstumsanalyst", "Analista Growth Tiktok": "TikTok-Wachstumsanalyst", "Analista Growth Instagram": "Instagram-Wachstumsanalyst", "Documentação": "Dokumentation", "Tutorial Kaggle": "Kaggle-Tutorial", "Tutorial Apify": "Apify-Tutorial", "Pipeline Músicas": "Musik-Pipeline", "módulos disponíveis": "verfügbare Module",
158
+ "Arquivos Base": "Basisdateien", "Pipeline Vídeos": "Video-Pipeline", "Canais e Perfis de Vídeos": "Videokanäle und Profile", "Canais YouTube": "YouTube-Kanäle", "Facebook Pages": "Facebook-Seiten", "Prompt Masters": "Prompt Masters", "Backlog Vídeos": "Video-Backlog", "Música": "Musik", "Upload Música": "Musik-Upload", "Geração de Conteúdo IA": "KI-Inhaltserstellung", "Motion Control": "Motion Control", "UGC Products": "UGC-Produkte", "Growth": "Wachstum", "Analista Growth Youtube": "YouTube-Wachstumsanalyst", "Analista Growth Tiktok": "TikTok-Wachstumsanalyst", "Analista Growth Instagram": "Instagram-Wachstumsanalyst", "Documentação": "Dokumentation", "Tutorial Kaggle": "Kaggle-Tutorial", "Tutorial Apify": "Apify-Tutorial", "Pipeline Música": "Musik-Pipeline", "módulos disponíveis": "verfügbare Module",
159
159
  },
160
160
  "vi": {
161
- "Arquivos Base": "Tệp cơ sở", "Pipeline Vídeos": "Quy trình video", "Canais e Perfis de Vídeos": "Kênh và hồ sơ video", "Canais YouTube": "Kênh YouTube", "Facebook Pages": "Trang Facebook", "Prompt Masters": "Prompt Master", "Backlog Vídeos": "Danh sách video", "Música": "Âm nhạc", "Upload Música": "Tải nhạc lên", "Geração de Conteúdo IA": "Tạo nội dung AI", "Motion Control": "Điều khiển chuyển động", "UGC Products": "Sản phẩm UGC", "Growth": "Tăng trưởng", "Analista Growth Youtube": "Chuyên viên phân tích tăng trưởng YouTube", "Analista Growth Tiktok": "Chuyên viên phân tích tăng trưởng TikTok", "Analista Growth Instagram": "Chuyên viên phân tích tăng trưởng Instagram", "Documentação": "Tài liệu", "Tutorial Kaggle": "Hướng dẫn Kaggle", "Tutorial Apify": "Hướng dẫn Apify", "Pipeline Músicas": "Quy trình âm nhạc", "módulos disponíveis": "mô-đun khả dụng",
161
+ "Arquivos Base": "Tệp cơ sở", "Pipeline Vídeos": "Quy trình video", "Canais e Perfis de Vídeos": "Kênh và hồ sơ video", "Canais YouTube": "Kênh YouTube", "Facebook Pages": "Trang Facebook", "Prompt Masters": "Prompt Master", "Backlog Vídeos": "Danh sách video", "Música": "Âm nhạc", "Upload Música": "Tải nhạc lên", "Geração de Conteúdo IA": "Tạo nội dung AI", "Motion Control": "Điều khiển chuyển động", "UGC Products": "Sản phẩm UGC", "Growth": "Tăng trưởng", "Analista Growth Youtube": "Chuyên viên phân tích tăng trưởng YouTube", "Analista Growth Tiktok": "Chuyên viên phân tích tăng trưởng TikTok", "Analista Growth Instagram": "Chuyên viên phân tích tăng trưởng Instagram", "Documentação": "Tài liệu", "Tutorial Kaggle": "Hướng dẫn Kaggle", "Tutorial Apify": "Hướng dẫn Apify", "Pipeline Música": "Quy trình âm nhạc", "módulos disponíveis": "mô-đun khả dụng",
162
162
  },
163
163
  "tr": {
164
- "Arquivos Base": "Temel Dosyalar", "Pipeline Vídeos": "Video Akışı", "Canais e Perfis de Vídeos": "Video Kanalları ve Profilleri", "Canais YouTube": "YouTube Kanalları", "Facebook Pages": "Facebook Sayfaları", "Prompt Masters": "Prompt Master'lar", "Backlog Vídeos": "Video Bekleme Listesi", "Música": "Müzik", "Upload Música": "Müzik Yükleme", "Geração de Conteúdo IA": "Yapay Zekâ İçeriği Oluşturma", "Motion Control": "Hareket Kontrolü", "UGC Products": "UGC Ürünleri", "Growth": "Büyüme", "Analista Growth Youtube": "YouTube Büyüme Analisti", "Analista Growth Tiktok": "TikTok Büyüme Analisti", "Analista Growth Instagram": "Instagram Büyüme Analisti", "Documentação": "Dokümantasyon", "Tutorial Kaggle": "Kaggle Eğitimi", "Tutorial Apify": "Apify Eğitimi", "Pipeline Músicas": "Müzik Akışı", "módulos disponíveis": "kullanılabilir modüller",
164
+ "Arquivos Base": "Temel Dosyalar", "Pipeline Vídeos": "Video Akışı", "Canais e Perfis de Vídeos": "Video Kanalları ve Profilleri", "Canais YouTube": "YouTube Kanalları", "Facebook Pages": "Facebook Sayfaları", "Prompt Masters": "Prompt Master'lar", "Backlog Vídeos": "Video Bekleme Listesi", "Música": "Müzik", "Upload Música": "Müzik Yükleme", "Geração de Conteúdo IA": "Yapay Zekâ İçeriği Oluşturma", "Motion Control": "Hareket Kontrolü", "UGC Products": "UGC Ürünleri", "Growth": "Büyüme", "Analista Growth Youtube": "YouTube Büyüme Analisti", "Analista Growth Tiktok": "TikTok Büyüme Analisti", "Analista Growth Instagram": "Instagram Büyüme Analisti", "Documentação": "Dokümantasyon", "Tutorial Kaggle": "Kaggle Eğitimi", "Tutorial Apify": "Apify Eğitimi", "Pipeline Música": "Müzik Akışı", "módulos disponíveis": "kullanılabilir modüller",
165
165
  },
166
166
  "ru": {
167
- "Arquivos Base": "Базовые файлы", "Pipeline Vídeos": "Конвейер видео", "Canais e Perfis de Vídeos": "Каналы и профили видео", "Canais YouTube": "Каналы YouTube", "Facebook Pages": "Страницы Facebook", "Prompt Masters": "Мастер-промпты", "Backlog Vídeos": "Очередь видео", "Música": "Музыка", "Upload Música": "Загрузка музыки", "Geração de Conteúdo IA": "Генерация контента ИИ", "Motion Control": "Управление движением", "UGC Products": "UGC-продукты", "Growth": "Рост", "Analista Growth Youtube": "Аналитик роста YouTube", "Analista Growth Tiktok": "Аналитик роста TikTok", "Analista Growth Instagram": "Аналитик роста Instagram", "Documentação": "Документация", "Tutorial Kaggle": "Руководство Kaggle", "Tutorial Apify": "Руководство Apify", "Pipeline Músicas": "Конвейер музыки", "módulos disponíveis": "доступные модули",
167
+ "Arquivos Base": "Базовые файлы", "Pipeline Vídeos": "Конвейер видео", "Canais e Perfis de Vídeos": "Каналы и профили видео", "Canais YouTube": "Каналы YouTube", "Facebook Pages": "Страницы Facebook", "Prompt Masters": "Мастер-промпты", "Backlog Vídeos": "Очередь видео", "Música": "Музыка", "Upload Música": "Загрузка музыки", "Geração de Conteúdo IA": "Генерация контента ИИ", "Motion Control": "Управление движением", "UGC Products": "UGC-продукты", "Growth": "Рост", "Analista Growth Youtube": "Аналитик роста YouTube", "Analista Growth Tiktok": "Аналитик роста TikTok", "Analista Growth Instagram": "Аналитик роста Instagram", "Documentação": "Документация", "Tutorial Kaggle": "Руководство Kaggle", "Tutorial Apify": "Руководство Apify", "Pipeline Música": "Конвейер музыки", "módulos disponíveis": "доступные модули",
168
168
  },
169
169
  "es": {
170
- "Arquivos Base": "Archivos base", "Pipeline Vídeos": "Flujo de vídeos", "Canais e Perfis de Vídeos": "Canales y perfiles de vídeo", "Canais YouTube": "Canales de YouTube", "Facebook Pages": "Páginas de Facebook", "Prompt Masters": "Prompts maestros", "Backlog Vídeos": "Cola de vídeos", "Música": "Música", "Upload Música": "Subir música", "Geração de Conteúdo IA": "Generación de contenido con IA", "Motion Control": "Control de movimiento", "UGC Products": "Productos UGC", "Growth": "Crecimiento", "Analista Growth Youtube": "Analista de crecimiento de YouTube", "Analista Growth Tiktok": "Analista de crecimiento de TikTok", "Analista Growth Instagram": "Analista de crecimiento de Instagram", "Documentação": "Documentación", "Tutorial Kaggle": "Tutorial de Kaggle", "Tutorial Apify": "Tutorial de Apify", "Pipeline Músicas": "Flujo de música", "módulos disponíveis": "módulos disponibles",
170
+ "Arquivos Base": "Archivos base", "Pipeline Vídeos": "Flujo de vídeos", "Canais e Perfis de Vídeos": "Canales y perfiles de vídeo", "Canais YouTube": "Canales de YouTube", "Facebook Pages": "Páginas de Facebook", "Prompt Masters": "Prompts maestros", "Backlog Vídeos": "Cola de vídeos", "Música": "Música", "Upload Música": "Subir música", "Geração de Conteúdo IA": "Generación de contenido con IA", "Motion Control": "Control de movimiento", "UGC Products": "Productos UGC", "Growth": "Crecimiento", "Analista Growth Youtube": "Analista de crecimiento de YouTube", "Analista Growth Tiktok": "Analista de crecimiento de TikTok", "Analista Growth Instagram": "Analista de crecimiento de Instagram", "Documentação": "Documentación", "Tutorial Kaggle": "Tutorial de Kaggle", "Tutorial Apify": "Tutorial de Apify", "Pipeline Música": "Flujo de música", "módulos disponíveis": "módulos disponibles",
171
171
  },
172
172
  "id": {
173
- "Arquivos Base": "File Dasar", "Pipeline Vídeos": "Alur Video", "Canais e Perfis de Vídeos": "Kanal dan Profil Video", "Canais YouTube": "Kanal YouTube", "Facebook Pages": "Halaman Facebook", "Prompt Masters": "Prompt Master", "Backlog Vídeos": "Antrean Video", "Música": "Musik", "Upload Música": "Unggah Musik", "Geração de Conteúdo IA": "Pembuatan Konten AI", "Motion Control": "Kontrol Gerakan", "UGC Products": "Produk UGC", "Growth": "Pertumbuhan", "Analista Growth Youtube": "Analis Pertumbuhan YouTube", "Analista Growth Tiktok": "Analis Pertumbuhan TikTok", "Analista Growth Instagram": "Analis Pertumbuhan Instagram", "Documentação": "Dokumentasi", "Tutorial Kaggle": "Tutorial Kaggle", "Tutorial Apify": "Tutorial Apify", "Pipeline Músicas": "Alur Musik", "módulos disponíveis": "modul tersedia",
173
+ "Arquivos Base": "File Dasar", "Pipeline Vídeos": "Alur Video", "Canais e Perfis de Vídeos": "Kanal dan Profil Video", "Canais YouTube": "Kanal YouTube", "Facebook Pages": "Halaman Facebook", "Prompt Masters": "Prompt Master", "Backlog Vídeos": "Antrean Video", "Música": "Musik", "Upload Música": "Unggah Musik", "Geração de Conteúdo IA": "Pembuatan Konten AI", "Motion Control": "Kontrol Gerakan", "UGC Products": "Produk UGC", "Growth": "Pertumbuhan", "Analista Growth Youtube": "Analis Pertumbuhan YouTube", "Analista Growth Tiktok": "Analis Pertumbuhan TikTok", "Analista Growth Instagram": "Analis Pertumbuhan Instagram", "Documentação": "Dokumentasi", "Tutorial Kaggle": "Tutorial Kaggle", "Tutorial Apify": "Tutorial Apify", "Pipeline Música": "Alur Musik", "módulos disponíveis": "modul tersedia",
174
174
  },
175
175
  "it": {
176
- "Arquivos Base": "File di base", "Pipeline Vídeos": "Pipeline video", "Canais e Perfis de Vídeos": "Canali e profili video", "Canais YouTube": "Canali YouTube", "Facebook Pages": "Pagine Facebook", "Prompt Masters": "Prompt Master", "Backlog Vídeos": "Coda video", "Música": "Musica", "Upload Música": "Caricamento musica", "Geração de Conteúdo IA": "Generazione di contenuti IA", "Motion Control": "Controllo del movimento", "UGC Products": "Prodotti UGC", "Growth": "Crescita", "Analista Growth Youtube": "Analista della crescita YouTube", "Analista Growth Tiktok": "Analista della crescita TikTok", "Analista Growth Instagram": "Analista della crescita Instagram", "Documentação": "Documentazione", "Tutorial Kaggle": "Tutorial Kaggle", "Tutorial Apify": "Tutorial Apify", "Pipeline Músicas": "Pipeline musicale", "módulos disponíveis": "moduli disponibili",
176
+ "Arquivos Base": "File di base", "Pipeline Vídeos": "Pipeline video", "Canais e Perfis de Vídeos": "Canali e profili video", "Canais YouTube": "Canali YouTube", "Facebook Pages": "Pagine Facebook", "Prompt Masters": "Prompt Master", "Backlog Vídeos": "Coda video", "Música": "Musica", "Upload Música": "Caricamento musica", "Geração de Conteúdo IA": "Generazione di contenuti IA", "Motion Control": "Controllo del movimento", "UGC Products": "Prodotti UGC", "Growth": "Crescita", "Analista Growth Youtube": "Analista della crescita YouTube", "Analista Growth Tiktok": "Analista della crescita TikTok", "Analista Growth Instagram": "Analista della crescita Instagram", "Documentação": "Documentazione", "Tutorial Kaggle": "Tutorial Kaggle", "Tutorial Apify": "Tutorial Apify", "Pipeline Música": "Pipeline musicale", "módulos disponíveis": "moduli disponibili",
177
177
  },
178
178
  }
179
179
  for _language_code, _navigation_values in UI_NAV_TRANSLATIONS.items():
180
180
  UI_TRANSLATIONS[_language_code].update(_navigation_values)
181
181
 
182
182
 
183
+ VIDEO_GENERATION_TRANSLATIONS: dict[str, dict[str, str]] = {
184
+ "pt": {
185
+ "Gerar tópico, roteiro e palavras-chave com IA": "Gerar tópico, roteiro e palavras-chave com IA",
186
+ "A gerar tópico, roteiro e palavras-chave com IA…": "A gerar tópico, roteiro e palavras-chave com IA…",
187
+ "Tema, roteiro e palavras-chave gerados com IA.": "Tema, roteiro e palavras-chave gerados com IA.",
188
+ "Preencha o campo Video Subject ou use o botão de geração automática abaixo das keywords.": "Preencha o campo Video Subject ou use o botão de geração automática abaixo das keywords.",
189
+ },
190
+ "en": {
191
+ "Gerar tópico, roteiro e palavras-chave com IA": "Generate topic, script and keywords with AI",
192
+ "A gerar tópico, roteiro e palavras-chave com IA…": "Generating topic, script and keywords with AI…",
193
+ "Tema, roteiro e palavras-chave gerados com IA.": "Topic, script and keywords generated with AI.",
194
+ "Preencha o campo Video Subject ou use o botão de geração automática abaixo das keywords.": "Fill in Video Subject or use the automatic generation button below the keywords.",
195
+ },
196
+ "zh": {
197
+ "Gerar tópico, roteiro e palavras-chave com IA": "使用 AI 生成主题、脚本和关键词",
198
+ "A gerar tópico, roteiro e palavras-chave com IA…": "正在使用 AI 生成主题、脚本和关键词…",
199
+ "Tema, roteiro e palavras-chave gerados com IA.": "已使用 AI 生成主题、脚本和关键词。",
200
+ "Preencha o campo Video Subject ou use o botão de geração automática abaixo das keywords.": "请填写 Video Subject,或使用关键词下方的自动生成按钮。",
201
+ },
202
+ "de": {
203
+ "Gerar tópico, roteiro e palavras-chave com IA": "Thema, Skript und Keywords mit KI generieren",
204
+ "A gerar tópico, roteiro e palavras-chave com IA…": "Thema, Skript und Keywords werden mit KI generiert…",
205
+ "Tema, roteiro e palavras-chave gerados com IA.": "Thema, Skript und Keywords wurden mit KI generiert.",
206
+ "Preencha o campo Video Subject ou use o botão de geração automática abaixo das keywords.": "Füllen Sie Video Subject aus oder verwenden Sie die automatische Schaltfläche unter den Keywords.",
207
+ },
208
+ "vi": {
209
+ "Gerar tópico, roteiro e palavras-chave com IA": "Tạo chủ đề, kịch bản và từ khóa bằng AI",
210
+ "A gerar tópico, roteiro e palavras-chave com IA…": "Đang tạo chủ đề, kịch bản và từ khóa bằng AI…",
211
+ "Tema, roteiro e palavras-chave gerados com IA.": "Đã tạo chủ đề, kịch bản và từ khóa bằng AI.",
212
+ "Preencha o campo Video Subject ou use o botão de geração automática abaixo das keywords.": "Hãy điền Video Subject hoặc dùng nút tạo tự động bên dưới từ khóa.",
213
+ },
214
+ "tr": {
215
+ "Gerar tópico, roteiro e palavras-chave com IA": "Yapay zekâ ile konu, senaryo ve anahtar kelimeler oluştur",
216
+ "A gerar tópico, roteiro e palavras-chave com IA…": "Konu, senaryo ve anahtar kelimeler yapay zekâ ile oluşturuluyor…",
217
+ "Tema, roteiro e palavras-chave gerados com IA.": "Konu, senaryo ve anahtar kelimeler yapay zekâ ile oluşturuldu.",
218
+ "Preencha o campo Video Subject ou use o botão de geração automática abaixo das keywords.": "Video Subject alanını doldurun veya anahtar kelimelerin altındaki otomatik oluşturma düğmesini kullanın.",
219
+ },
220
+ "ru": {
221
+ "Gerar tópico, roteiro e palavras-chave com IA": "Создать тему, сценарий и ключевые слова с помощью ИИ",
222
+ "A gerar tópico, roteiro e palavras-chave com IA…": "Создание темы, сценария и ключевых слов с помощью ИИ…",
223
+ "Tema, roteiro e palavras-chave gerados com IA.": "Тема, сценарий и ключевые слова созданы с помощью ИИ.",
224
+ "Preencha o campo Video Subject ou use o botão de geração automática abaixo das keywords.": "Заполните Video Subject или используйте кнопку автоматической генерации под ключевыми словами.",
225
+ },
226
+ "es": {
227
+ "Gerar tópico, roteiro e palavras-chave com IA": "Generar tema, guion y palabras clave con IA",
228
+ "A gerar tópico, roteiro e palavras-chave com IA…": "Generando tema, guion y palabras clave con IA…",
229
+ "Tema, roteiro e palavras-chave gerados com IA.": "Tema, guion y palabras clave generados con IA.",
230
+ "Preencha o campo Video Subject ou use o botão de geração automática abaixo das keywords.": "Completa Video Subject o utiliza el botón de generación automática debajo de las palabras clave.",
231
+ },
232
+ "id": {
233
+ "Gerar tópico, roteiro e palavras-chave com IA": "Buat topik, skrip, dan kata kunci dengan AI",
234
+ "A gerar tópico, roteiro e palavras-chave com IA…": "Membuat topik, skrip, dan kata kunci dengan AI…",
235
+ "Tema, roteiro e palavras-chave gerados com IA.": "Topik, skrip, dan kata kunci dibuat dengan AI.",
236
+ "Preencha o campo Video Subject ou use o botão de geração automática abaixo das keywords.": "Isi Video Subject atau gunakan tombol pembuatan otomatis di bawah kata kunci.",
237
+ },
238
+ "it": {
239
+ "Gerar tópico, roteiro e palavras-chave com IA": "Genera argomento, copione e parole chiave con l'IA",
240
+ "A gerar tópico, roteiro e palavras-chave com IA…": "Generazione di argomento, copione e parole chiave con l'IA…",
241
+ "Tema, roteiro e palavras-chave gerados com IA.": "Argomento, copione e parole chiave generati con l'IA.",
242
+ "Preencha o campo Video Subject ou use o botão de geração automática abaixo das keywords.": "Compila Video Subject oppure usa il pulsante di generazione automatica sotto le parole chiave.",
243
+ },
244
+ }
245
+
246
+ for _language_code, _video_generation_translation in VIDEO_GENERATION_TRANSLATIONS.items():
247
+ UI_TRANSLATIONS[_language_code].update(_video_generation_translation)
248
+
249
+
183
250
  _TAB_LABELS = (
184
251
  "Blueprints", "Brandings", "Pesquisa pública", "Cadastro manual", "Contas cadastradas", "Biblioteca",
185
252
  "Importar do YouTube", "Canais em lote gmail", "Criar vídeo", "Vídeos", "Novo roteiro/letra", "Histórico guardado",
@@ -174,17 +174,34 @@ def _run_task(task: dict[str, Any]) -> dict[str, Any]:
174
174
  _update(task_id, topic=topic, topic_source="llm", ai_generation={"topic": topic_result}, progress=12)
175
175
 
176
176
  _update(task_id, stage="script", state="doing", progress=18, error=None)
177
- script = generate_script_document(
178
- settings,
179
- document_type="Roteiro de vídeo",
180
- title=str(task.get("title") or topic),
181
- brief=topic,
182
- language=str(task.get("language") or channel.get("language") or "Português"),
183
- channel=channel,
184
- blueprint=blueprint,
185
- structure_notes=str((task.get("generation_settings") or {}).get("script_structure_notes") or ""),
186
- generation_settings=task.get("generation_settings") if isinstance(task.get("generation_settings"), dict) else {},
187
- )
177
+ generation_settings = task.get("generation_settings") if isinstance(task.get("generation_settings"), dict) else {}
178
+ provided_script = str(generation_settings.get("video_script") or "").strip()
179
+ if provided_script:
180
+ script = {
181
+ "document_type": "video_script",
182
+ "title": str(task.get("title") or topic),
183
+ "summary": topic,
184
+ "content": provided_script,
185
+ "language": str(task.get("language") or channel.get("language") or "Português"),
186
+ "blueprint_id": str(blueprint.get("id") or ""),
187
+ "blueprint_name": str(blueprint.get("name") or "SEM BLUEPRINT CONFIGURADO"),
188
+ "channel_id": str(channel.get("id") or ""),
189
+ "channel_name": str(channel.get("name") or "Canal sem nome"),
190
+ "generation_settings": generation_settings,
191
+ "generated_by": "video_creation_form",
192
+ }
193
+ else:
194
+ script = generate_script_document(
195
+ settings,
196
+ document_type="Roteiro de vídeo",
197
+ title=str(task.get("title") or topic),
198
+ brief=topic,
199
+ language=str(task.get("language") or channel.get("language") or "Português"),
200
+ channel=channel,
201
+ blueprint=blueprint,
202
+ structure_notes=str(generation_settings.get("script_structure_notes") or ""),
203
+ generation_settings=generation_settings,
204
+ )
188
205
  script_record = save_script_document(script)
189
206
  artifacts = dict(task.get("artifacts") or {})
190
207
  artifacts["script"] = script_record.get("path", "")
@@ -193,7 +210,11 @@ def _run_task(task: dict[str, Any]) -> dict[str, Any]:
193
210
  _update(task_id, stage="title", state="doing", progress=35)
194
211
  creative = generate_creative_package(settings, channel, topic, blueprint, language=str(task.get("language") or channel.get("language") or "Português"))
195
212
  title = str(creative.get("title") or topic).strip()
196
- keywords = creative.get("keywords") if isinstance(creative.get("keywords"), list) else _keywords(topic, title, str(channel.get("niche") or ""))
213
+ provided_keywords = generation_settings.get("video_keywords")
214
+ if isinstance(provided_keywords, str):
215
+ provided_keywords = re.split(r"[,\n;|]+", provided_keywords)
216
+ provided_keywords = [str(item).strip() for item in provided_keywords or [] if str(item).strip()]
217
+ keywords = provided_keywords[:15] or (creative.get("keywords") if isinstance(creative.get("keywords"), list) else _keywords(topic, title, str(channel.get("niche") or "")))
197
218
  title_artifact = _save_json_artifact(task_id, "title-keywords", {"topic": topic, "title": title, "keywords": keywords, "title_candidates": creative.get("title_candidates", [])})
198
219
  _update(task_id, title=title, tags=keywords, artifacts={**artifacts, "title_keywords": title_artifact}, title_candidates=creative.get("title_candidates", []), progress=45)
199
220
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danhachuel/thunderbolt",
3
- "version": "0.3.4",
3
+ "version": "0.3.6",
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",