@danhachuel/thunderbolt 0.3.23 → 0.3.25

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.
@@ -249,6 +249,60 @@ def _keywords_from_text(*values: str) -> list[str]:
249
249
  return result[:15]
250
250
 
251
251
 
252
+ def generate_thumbnail_prompt(
253
+ settings: dict[str, Any],
254
+ channel: dict[str, Any],
255
+ topic: str,
256
+ current_prompt: str = "",
257
+ blueprint: dict[str, Any] | None = None,
258
+ language: str = "",
259
+ ) -> dict[str, Any]:
260
+ """Create one image prompt while keeping visual direction and lettering separate."""
261
+ topic = str(topic or "").strip()
262
+ if not topic:
263
+ raise CreativeGenerationError("É necessário um tópico para refazer o prompt da thumbnail.")
264
+ context = channel_context(channel, blueprint)
265
+ system = (
266
+ "És um director de arte de thumbnails para YouTube. Refaz um prompt de imagem forte e específico "
267
+ "para o tópico fornecido, mantendo a intenção visual quando já existir um prompt. "
268
+ "Separa rigorosamente a imagem sem texto do lettering: image_prompt nunca deve pedir texto renderizado, "
269
+ "enquanto overlay_text deve ser curto, legível e ter no máximo quatro palavras. "
270
+ "Responde apenas com JSON válido nas chaves concept, overlay_text, composition, color_palette, subject, "
271
+ "image_prompt, title_synergy e lettering_prompt."
272
+ )
273
+ user = json.dumps(
274
+ {
275
+ "channel": context,
276
+ "language": language or context["language"],
277
+ "topic": topic,
278
+ "current_prompt": str(current_prompt or "").strip(),
279
+ "reference_rules": reference_bundle(),
280
+ "requirements": {
281
+ "image_prompt": "cinematic visual direction, no words, letters, logos or watermarks rendered in the image",
282
+ "overlay_text": "maximum 4 words, emotionally strong, not the full title",
283
+ "lettering_prompt": "instructions for changing only the lettering after the base image exists",
284
+ },
285
+ },
286
+ ensure_ascii=False,
287
+ )
288
+ result = _chat_json(settings, system, user)
289
+ image_prompt = str(result.get("image_prompt") or "").strip()
290
+ if not image_prompt:
291
+ raise CreativeGenerationError("O provider não devolveu um prompt de imagem válido.")
292
+ overlay_text = _short_overlay(result.get("overlay_text"))
293
+ return {
294
+ "concept": str(result.get("concept") or "Thumbnail renovada").strip(),
295
+ "overlay_text": overlay_text,
296
+ "composition": str(result.get("composition") or "").strip(),
297
+ "color_palette": str(result.get("color_palette") or "").strip(),
298
+ "subject": str(result.get("subject") or topic).strip(),
299
+ "image_prompt": image_prompt,
300
+ "title_synergy": str(result.get("title_synergy") or "").strip(),
301
+ "lettering_prompt": str(result.get("lettering_prompt") or "").strip(),
302
+ "status": "prompt_ready",
303
+ }
304
+
305
+
252
306
  def generate_creative_package(
253
307
  settings: dict[str, Any],
254
308
  channel: dict[str, Any],
@@ -0,0 +1,127 @@
1
+ """Helpers for resuming video creation from persisted scripts and drafts."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ VIDEO_SETTING_KEYS = (
8
+ "video_source",
9
+ "video_format",
10
+ "video_concatenation_mode",
11
+ "match_visuals_to_script_order",
12
+ "video_transition_mode",
13
+ "video_aspect_ratio",
14
+ "maximum_clip_duration",
15
+ "videos_per_run",
16
+ "video_encoder",
17
+ )
18
+
19
+ AUDIO_SETTING_KEYS = (
20
+ "voiceover_mode",
21
+ "voiceover_service",
22
+ "voice",
23
+ "voiceover_volume",
24
+ "voiceover_speed",
25
+ "background_music_source",
26
+ "background_music_volume",
27
+ )
28
+
29
+ SUBTITLE_SETTING_KEYS = (
30
+ "enable_subtitles",
31
+ "subtitle_font",
32
+ "subtitle_position",
33
+ "subtitle_color",
34
+ "subtitle_background",
35
+ "subtitle_background_color",
36
+ "subtitle_rounded_background",
37
+ "subtitle_font_size",
38
+ "subtitle_outline",
39
+ "subtitle_outline_width",
40
+ )
41
+
42
+ DRAFT_SETTING_SECTIONS = {
43
+ "Configurações de vídeo": VIDEO_SETTING_KEYS,
44
+ "Configurações de áudio": AUDIO_SETTING_KEYS,
45
+ "Configurações de legendas": SUBTITLE_SETTING_KEYS,
46
+ }
47
+
48
+
49
+ def keyword_text(value: Any) -> str:
50
+ """Return keywords as a stable comma-separated string."""
51
+ if isinstance(value, (list, tuple, set)):
52
+ values = [str(item).strip() for item in value if str(item).strip()]
53
+ return ", ".join(values)
54
+ return str(value or "").strip()
55
+
56
+
57
+ def markdown_body(value: Any) -> str:
58
+ """Remove the local Markdown front matter before using a saved script as input."""
59
+ text = str(value or "").strip()
60
+ if text.startswith("---"):
61
+ parts = text.split("---", 2)
62
+ if len(parts) == 3:
63
+ text = parts[2].lstrip("\r\n")
64
+ return text.strip()
65
+
66
+
67
+ def normalise_saved_script(record: dict[str, Any], content: str = "") -> dict[str, Any]:
68
+ """Map a script-history record or pipeline draft to video-creation fields."""
69
+ generation_settings = record.get("generation_settings")
70
+ generation_settings = dict(generation_settings) if isinstance(generation_settings, dict) else {}
71
+ title = str(record.get("title") or record.get("video_subject") or generation_settings.get("video_subject") or record.get("topic") or "").strip()
72
+ subject = str(record.get("video_subject") or generation_settings.get("video_subject") or record.get("topic") or title).strip()
73
+ script = str(record.get("video_script") or generation_settings.get("video_script") or "").strip() or markdown_body(content)
74
+ keywords = keyword_text(record.get("video_keywords") or record.get("keywords") or generation_settings.get("video_keywords"))
75
+ if not subject:
76
+ subject = title
77
+ return {
78
+ **record,
79
+ "title": title or "Roteiro sem título",
80
+ "video_subject": subject,
81
+ "video_script": script,
82
+ "video_keywords": keywords,
83
+ "generation_settings": generation_settings,
84
+ "language": str(record.get("language") or generation_settings.get("script_language") or "pt").strip(),
85
+ "channel_id": str(record.get("channel_id") or "").strip(),
86
+ "blueprint_id": str(record.get("blueprint_id") or "").strip(),
87
+ }
88
+
89
+
90
+ def missing_setting_sections(settings: dict[str, Any]) -> list[str]:
91
+ """Return the settings sections that are incomplete in a saved record."""
92
+ return [
93
+ label
94
+ for label, keys in DRAFT_SETTING_SECTIONS.items()
95
+ if any(key not in settings for key in keys)
96
+ ]
97
+
98
+
99
+ def setting_widget_suffixes() -> tuple[str, ...]:
100
+ """Return the widget suffixes used by the shared video settings renderer."""
101
+ return tuple(dict.fromkeys(key for keys in DRAFT_SETTING_SECTIONS.values() for key in keys))
102
+
103
+
104
+ def missing_content_fields(record: dict[str, Any]) -> list[str]:
105
+ """Return the required creative fields that are absent from a saved record."""
106
+ missing: list[str] = []
107
+ if not str(record.get("video_subject") or "").strip():
108
+ missing.append("Video Subject")
109
+ if not str(record.get("video_script") or "").strip():
110
+ missing.append("Video Script")
111
+ if not str(record.get("video_keywords") or "").strip():
112
+ missing.append("Video Keywords")
113
+ return missing
114
+
115
+
116
+ __all__ = [
117
+ "AUDIO_SETTING_KEYS",
118
+ "DRAFT_SETTING_SECTIONS",
119
+ "SUBTITLE_SETTING_KEYS",
120
+ "VIDEO_SETTING_KEYS",
121
+ "keyword_text",
122
+ "markdown_body",
123
+ "missing_content_fields",
124
+ "missing_setting_sections",
125
+ "normalise_saved_script",
126
+ "setting_widget_suffixes",
127
+ ]
@@ -473,9 +473,95 @@ for _language_code, _pipeline_feature_translation in _PIPELINE_FEATURE_TRANSLATI
473
473
  UI_TRANSLATIONS[_language_code].update(_pipeline_feature_translation)
474
474
 
475
475
 
476
+ _DRAFT_VIDEO_TRANSLATIONS: dict[str, dict[str, str]] = {
477
+ "pt": {
478
+ "Gerar de Rascunho": "Gerar de Rascunho", "Roteiros guardados": "Roteiros guardados", "Seleccione um roteiro": "Seleccione um roteiro",
479
+ "Configurações a completar": "Configurações a completar", "Configurações de vídeo": "Configurações de vídeo", "Configurações de áudio": "Configurações de áudio", "Configurações de legendas": "Configurações de legendas",
480
+ "Roteiro completo: todas as configurações estão disponíveis.": "Roteiro completo: todas as configurações estão disponíveis.", "Seleccione as configurações que pretende completar.": "Seleccione as configurações que pretende completar.",
481
+ "Continuar criação": "Continuar criação", "Gerar apenas o vídeo": "Gerar apenas o vídeo", "Seleccione um canal para continuar.": "Seleccione um canal para continuar.",
482
+ "Este roteiro não tem conteúdo suficiente. Volte a Roteiros e guarde tópico, roteiro e palavras-chave.": "Este roteiro não tem conteúdo suficiente. Volte a Roteiros e guarde tópico, roteiro e palavras-chave.",
483
+ "Ainda não existem roteiros guardados.": "Ainda não existem roteiros guardados.", "Histórico guardado": "Histórico guardado", "Rascunho local": "Rascunho local", "Origem": "Origem",
484
+ },
485
+ "en": {
486
+ "Gerar de Rascunho": "Generate from Draft", "Roteiros guardados": "Saved scripts", "Seleccione um roteiro": "Select a script",
487
+ "Configurações a completar": "Settings to complete", "Configurações de vídeo": "Video settings", "Configurações de áudio": "Audio settings", "Configurações de legendas": "Subtitle settings",
488
+ "Roteiro completo: todas as configurações estão disponíveis.": "Complete script: all settings are available.", "Seleccione as configurações que pretende completar.": "Select the settings you want to complete.",
489
+ "Continuar criação": "Continue creation", "Gerar apenas o vídeo": "Generate video only", "Seleccione um canal para continuar.": "Select a channel to continue.",
490
+ "Este roteiro não tem conteúdo suficiente. Volte a Roteiros e guarde tópico, roteiro e palavras-chave.": "This script does not contain enough content. Return to Scripts and save the topic, script and keywords.",
491
+ "Ainda não existem roteiros guardados.": "There are no saved scripts yet.", "Histórico guardado": "Saved history", "Rascunho local": "Local draft", "Origem": "Source",
492
+ },
493
+ "zh": {
494
+ "Gerar de Rascunho": "从草稿生成", "Roteiros guardados": "已保存脚本", "Seleccione um roteiro": "选择脚本",
495
+ "Configurações a completar": "需要完成的设置", "Configurações de vídeo": "视频设置", "Configurações de áudio": "音频设置", "Configurações de legendas": "字幕设置",
496
+ "Roteiro completo: todas as configurações estão disponíveis.": "完整脚本:所有设置均可用。", "Seleccione as configurações que pretende completar.": "选择要完成的设置。",
497
+ "Continuar criação": "继续创建", "Gerar apenas o vídeo": "仅生成视频", "Seleccione um canal para continuar.": "选择一个频道以继续。",
498
+ "Este roteiro não tem conteúdo suficiente. Volte a Roteiros e guarde tópico, roteiro e palavras-chave.": "此脚本内容不足。请返回脚本页面并保存主题、脚本和关键词。",
499
+ "Ainda não existem roteiros guardados.": "还没有保存的脚本。", "Histórico guardado": "已保存历史", "Rascunho local": "本地草稿", "Origem": "来源",
500
+ },
501
+ "de": {
502
+ "Gerar de Rascunho": "Aus Entwurf erstellen", "Roteiros guardados": "Gespeicherte Skripte", "Seleccione um roteiro": "Skript auswählen",
503
+ "Configurações a completar": "Zu vervollständigende Einstellungen", "Configurações de vídeo": "Videoeinstellungen", "Configurações de áudio": "Audioeinstellungen", "Configurações de legendas": "Untertiteleinstellungen",
504
+ "Roteiro completo: todas as configurações estão disponíveis.": "Vollständiges Skript: Alle Einstellungen sind verfügbar.", "Seleccione as configurações que pretende completar.": "Wählen Sie die zu vervollständigenden Einstellungen.",
505
+ "Continuar criação": "Erstellung fortsetzen", "Gerar apenas o vídeo": "Nur Video erstellen", "Seleccione um canal para continuar.": "Wählen Sie einen Kanal, um fortzufahren.",
506
+ "Este roteiro não tem conteúdo suficiente. Volte a Roteiros e guarde tópico, roteiro e palavras-chave.": "Dieses Skript enthält nicht genügend Inhalt. Kehren Sie zu Skripten zurück und speichern Sie Thema, Skript und Schlüsselwörter.",
507
+ "Ainda não existem roteiros guardados.": "Es gibt noch keine gespeicherten Skripte.", "Histórico guardado": "Gespeicherter Verlauf", "Rascunho local": "Lokaler Entwurf", "Origem": "Quelle",
508
+ },
509
+ "vi": {
510
+ "Gerar de Rascunho": "Tạo từ bản nháp", "Roteiros guardados": "Kịch bản đã lưu", "Seleccione um roteiro": "Chọn kịch bản",
511
+ "Configurações a completar": "Cài đặt cần hoàn tất", "Configurações de vídeo": "Cài đặt video", "Configurações de áudio": "Cài đặt âm thanh", "Configurações de legendas": "Cài đặt phụ đề",
512
+ "Roteiro completo: todas as configurações estão disponíveis.": "Kịch bản hoàn chỉnh: tất cả cài đặt đều khả dụng.", "Seleccione as configurações que pretende completar.": "Chọn các cài đặt bạn muốn hoàn tất.",
513
+ "Continuar criação": "Tiếp tục tạo", "Gerar apenas o vídeo": "Chỉ tạo video", "Seleccione um canal para continuar.": "Chọn kênh để tiếp tục.",
514
+ "Este roteiro não tem conteúdo suficiente. Volte a Roteiros e guarde tópico, roteiro e palavras-chave.": "Kịch bản này chưa đủ nội dung. Hãy quay lại Kịch bản và lưu chủ đề, kịch bản cùng từ khóa.",
515
+ "Ainda não existem roteiros guardados.": "Chưa có kịch bản nào được lưu.", "Histórico guardado": "Lịch sử đã lưu", "Rascunho local": "Bản nháp cục bộ", "Origem": "Nguồn",
516
+ },
517
+ "tr": {
518
+ "Gerar de Rascunho": "Taslakta oluştur", "Roteiros guardados": "Kayıtlı senaryolar", "Seleccione um roteiro": "Bir senaryo seçin",
519
+ "Configurações a completar": "Tamamlanacak ayarlar", "Configurações de vídeo": "Video ayarları", "Configurações de áudio": "Ses ayarları", "Configurações de legendas": "Altyazı ayarları",
520
+ "Roteiro completo: todas as configurações estão disponíveis.": "Tam senaryo: tüm ayarlar kullanılabilir.", "Seleccione as configurações que pretende completar.": "Tamamlamak istediğiniz ayarları seçin.",
521
+ "Continuar criação": "Oluşturmaya devam et", "Gerar apenas o vídeo": "Yalnızca video oluştur", "Seleccione um canal para continuar.": "Devam etmek için bir kanal seçin.",
522
+ "Este roteiro não tem conteúdo suficiente. Volte a Roteiros e guarde tópico, roteiro e palavras-chave.": "Bu senaryo yeterli içeriğe sahip değil. Senaryolara dönüp konu, senaryo ve anahtar kelimeleri kaydedin.",
523
+ "Ainda não existem roteiros guardados.": "Henüz kayıtlı senaryo yok.", "Histórico guardado": "Kayıtlı geçmiş", "Rascunho local": "Yerel taslak", "Origem": "Kaynak",
524
+ },
525
+ "ru": {
526
+ "Gerar de Rascunho": "Создать из черновика", "Roteiros guardados": "Сохранённые сценарии", "Seleccione um roteiro": "Выберите сценарий",
527
+ "Configurações a completar": "Настройки для заполнения", "Configurações de vídeo": "Настройки видео", "Configurações de áudio": "Настройки аудио", "Configurações de legendas": "Настройки субтитров",
528
+ "Roteiro completo: todas as configurações estão disponíveis.": "Полный сценарий: все настройки доступны.", "Seleccione as configurações que pretende completar.": "Выберите настройки, которые хотите заполнить.",
529
+ "Continuar criação": "Продолжить создание", "Gerar apenas o vídeo": "Создать только видео", "Seleccione um canal para continuar.": "Выберите канал для продолжения.",
530
+ "Este roteiro não tem conteúdo suficiente. Volte a Roteiros e guarde tópico, roteiro e palavras-chave.": "В этом сценарии недостаточно содержимого. Вернитесь в раздел сценариев и сохраните тему, сценарий и ключевые слова.",
531
+ "Ainda não existem roteiros guardados.": "Сохранённых сценариев пока нет.", "Histórico guardado": "Сохранённая история", "Rascunho local": "Локальный черновик", "Origem": "Источник",
532
+ },
533
+ "es": {
534
+ "Gerar de Rascunho": "Generar desde borrador", "Roteiros guardados": "Guiones guardados", "Seleccione um roteiro": "Selecciona un guion",
535
+ "Configurações a completar": "Configuraciones que completar", "Configurações de vídeo": "Configuración de vídeo", "Configurações de áudio": "Configuración de audio", "Configurações de legendas": "Configuración de subtítulos",
536
+ "Roteiro completo: todas as configurações estão disponíveis.": "Guion completo: todas las configuraciones están disponibles.", "Seleccione as configurações que pretende completar.": "Selecciona las configuraciones que quieras completar.",
537
+ "Continuar criação": "Continuar creación", "Gerar apenas o vídeo": "Generar solo el vídeo", "Seleccione um canal para continuar.": "Selecciona un canal para continuar.",
538
+ "Este roteiro não tem conteúdo suficiente. Volte a Roteiros e guarde tópico, roteiro e palavras-chave.": "Este guion no tiene contenido suficiente. Vuelve a Guiones y guarda el tema, el guion y las palabras clave.",
539
+ "Ainda não existem roteiros guardados.": "Todavía no hay guiones guardados.", "Histórico guardado": "Historial guardado", "Rascunho local": "Borrador local", "Origem": "Origen",
540
+ },
541
+ "id": {
542
+ "Gerar de Rascunho": "Buat dari Draf", "Roteiros guardados": "Skrip tersimpan", "Seleccione um roteiro": "Pilih skrip",
543
+ "Configurações a completar": "Pengaturan yang harus dilengkapi", "Configurações de vídeo": "Pengaturan video", "Configurações de áudio": "Pengaturan audio", "Configurações de legendas": "Pengaturan subtitle",
544
+ "Roteiro completo: todas as configurações estão disponíveis.": "Skrip lengkap: semua pengaturan tersedia.", "Seleccione as configurações que pretende completar.": "Pilih pengaturan yang ingin dilengkapi.",
545
+ "Continuar criação": "Lanjutkan pembuatan", "Gerar apenas o vídeo": "Buat video saja", "Seleccione um canal para continuar.": "Pilih kanal untuk melanjutkan.",
546
+ "Este roteiro não tem conteúdo suficiente. Volte a Roteiros e guarde tópico, roteiro e palavras-chave.": "Skrip ini belum memiliki konten yang cukup. Kembali ke Skrip dan simpan topik, skrip, serta kata kunci.",
547
+ "Ainda não existem roteiros guardados.": "Belum ada skrip tersimpan.", "Histórico guardado": "Riwayat tersimpan", "Rascunho local": "Draf lokal", "Origem": "Sumber",
548
+ },
549
+ "it": {
550
+ "Gerar de Rascunho": "Genera da bozza", "Roteiros guardados": "Copioni salvati", "Seleccione um roteiro": "Seleziona un copione",
551
+ "Configurações a completar": "Impostazioni da completare", "Configurações de vídeo": "Impostazioni video", "Configurações de áudio": "Impostazioni audio", "Configurações de legendas": "Impostazioni sottotitoli",
552
+ "Roteiro completo: todas as configurações estão disponíveis.": "Copione completo: tutte le impostazioni sono disponibili.", "Seleccione as configurações que pretende completar.": "Seleziona le impostazioni da completare.",
553
+ "Continuar criação": "Continua creazione", "Gerar apenas o vídeo": "Genera solo il video", "Seleccione um canal para continuar.": "Seleziona un canale per continuare.",
554
+ "Este roteiro não tem conteúdo suficiente. Volte a Roteiros e guarde tópico, roteiro e palavras-chave.": "Questo copione non contiene abbastanza contenuti. Torna a Copioni e salva argomento, copione e parole chiave.",
555
+ "Ainda não existem roteiros guardados.": "Non ci sono ancora copioni salvati.", "Histórico guardado": "Cronologia salvata", "Rascunho local": "Bozza locale", "Origem": "Origine",
556
+ },
557
+ }
558
+ for _language_code, _draft_video_translation in _DRAFT_VIDEO_TRANSLATIONS.items():
559
+ UI_TRANSLATIONS[_language_code].update(_draft_video_translation)
560
+
561
+
476
562
  _TAB_LABELS = (
477
563
  "Blueprints", "Brandings", "Pesquisa pública", "Cadastro manual", "Contas cadastradas", "Biblioteca",
478
- "Importar do YouTube", "Canais em lote gmail", "Criar vídeo", "Vídeos", "Novo roteiro/letra", "Histórico guardado",
564
+ "Importar do YouTube", "Canais em lote gmail", "Criar vídeo", "Gerar de Rascunho", "Vídeos", "Novo roteiro/letra", "Histórico guardado",
479
565
  "Clusters encontrados", "Regras de associação", "Dados analisados", "Upload ficheiro", "URL de vídeo", "Vídeos gerados",
480
566
  "Pasta local", "Código Python", "Upload convencional", "Upload directo", "Postiz", "Upload-Post", "API Keys",
481
567
  "Teste de Voz", "Serviços e modelos", "Fontes de Materiais", "Client MCP", "Servidor MCP", "Skill",
@@ -484,7 +570,7 @@ _TAB_LABELS = (
484
570
  TAB_TRANSLATIONS: dict[str, dict[str, str]] = {
485
571
  "pt": {label: label for label in _TAB_LABELS},
486
572
  "en": {
487
- "Blueprints": "Blueprints", "Brandings": "Brandings", "Pesquisa pública": "Public search", "Cadastro manual": "Manual registration", "Contas cadastradas": "Registered accounts", "Biblioteca": "Library", "Importar do YouTube": "Import from YouTube", "Canais em lote gmail": "Bulk Gmail channels", "Criar vídeo": "Create video", "Vídeos": "Videos", "Novo roteiro/letra": "New script/lyrics", "Histórico guardado": "Saved history", "Clusters encontrados": "Found clusters", "Regras de associação": "Association rules", "Dados analisados": "Analyzed data", "Upload ficheiro": "Upload file", "URL de vídeo": "Video URL", "Vídeos gerados": "Generated videos", "Pasta local": "Local folder", "Código Python": "Python code", "Upload convencional": "Conventional upload", "Upload directo": "Direct upload", "Postiz": "Postiz", "Upload-Post": "Upload-Post", "API Keys": "API Keys", "Teste de Voz": "Voice testing", "Serviços e modelos": "Services and models", "Fontes de Materiais": "Media sources", "Client MCP": "MCP client", "Servidor MCP": "MCP server", "Skill": "Skill",
573
+ "Blueprints": "Blueprints", "Brandings": "Brandings", "Pesquisa pública": "Public search", "Cadastro manual": "Manual registration", "Contas cadastradas": "Registered accounts", "Biblioteca": "Library", "Importar do YouTube": "Import from YouTube", "Canais em lote gmail": "Bulk Gmail channels", "Criar vídeo": "Create video", "Gerar de Rascunho": "Generate from Draft", "Vídeos": "Videos", "Novo roteiro/letra": "New script/lyrics", "Histórico guardado": "Saved history", "Clusters encontrados": "Found clusters", "Regras de associação": "Association rules", "Dados analisados": "Analyzed data", "Upload ficheiro": "Upload file", "URL de vídeo": "Video URL", "Vídeos gerados": "Generated videos", "Pasta local": "Local folder", "Código Python": "Python code", "Upload convencional": "Conventional upload", "Upload directo": "Direct upload", "Postiz": "Postiz", "Upload-Post": "Upload-Post", "API Keys": "API Keys", "Teste de Voz": "Voice testing", "Serviços e modelos": "Services and models", "Fontes de Materiais": "Media sources", "Client MCP": "MCP client", "Servidor MCP": "MCP server", "Skill": "Skill",
488
574
  },
489
575
  "zh": {
490
576
  "Blueprints": "蓝图", "Brandings": "品牌", "Pesquisa pública": "公开搜索", "Cadastro manual": "手动注册", "Contas cadastradas": "已注册账户", "Biblioteca": "库", "Importar do YouTube": "从 YouTube 导入", "Canais em lote gmail": "Gmail 批量频道", "Criar vídeo": "创建视频", "Vídeos": "视频", "Novo roteiro/letra": "新建脚本/歌词", "Histórico guardado": "已保存历史", "Clusters encontrados": "找到的聚类", "Regras de associação": "关联规则", "Dados analisados": "分析数据", "Upload ficheiro": "上传文件", "URL de vídeo": "视频 URL", "Vídeos gerados": "已生成视频", "Pasta local": "本地文件夹", "Código Python": "Python 代码", "Upload convencional": "常规上传", "Upload directo": "直接上传", "Postiz": "Postiz", "Upload-Post": "Upload-Post", "API Keys": "API 密钥", "Teste de Voz": "语音测试", "Serviços e modelos": "服务与模型", "Fontes de Materiais": "媒体来源", "Client MCP": "MCP 客户端", "Servidor MCP": "MCP 服务器", "Skill": "技能",
@@ -848,6 +934,35 @@ _CONTENT_TRANSLATION_ROWS = (
848
934
  ("Modo de ligação", "Modo de ligação", "Connection mode", "连接模式", "Verbindungsmodus", "Chế độ kết nối", "Bağlantı modu", "Режим подключения", "Modo de conexión", "Mode koneksi", "Modalità di connessione"),
849
935
  ("Metadados removidos e nova cópia criada. O original continua preservado.", "Metadados removidos e nova cópia criada. O original continua preservado.", "Metadata removed and a new copy created. The original remains preserved.", "元数据已移除并创建新副本。原文件仍然保留。", "Metadaten entfernt und eine neue Kopie erstellt. Das Original bleibt erhalten.", "Đã xóa siêu dữ liệu và tạo bản sao mới. Bản gốc vẫn được giữ nguyên.", "Üst veriler kaldırıldı ve yeni bir kopya oluşturuldu. Orijinal korunur.", "Метаданные удалены и создана новая копия. Оригинал сохранён.", "Metadatos eliminados y nueva copia creada. El original se conserva.", "Metadata dihapus dan salinan baru dibuat. File asli tetap dipertahankan.", "Metadati rimossi e nuova copia creata. L'originale è preservato."),
850
936
  ("Detalhes da conta Google", "Detalhes da conta Google", "Google account details", "Google 账户详情", "Details des Google-Kontos", "Chi tiết tài khoản Google", "Google hesap ayrıntıları", "Сведения об аккаунте Google", "Detalles de la cuenta de Google", "Detail akun Google", "Dettagli dell'account Google"),
937
+ ("Refazer Prompt Thumb", "Refazer Prompt Thumb", "Regenerate Thumbnail Prompt", "重新生成缩略图提示词", "Thumbnail-Prompt neu erstellen", "Tạo lại prompt thumbnail", "Küçük resim istemini yeniden oluştur", "Перегенерировать промпт миниатюры", "Regenerar prompt de miniatura", "Buat ulang prompt thumbnail", "Rigenera prompt miniatura"),
938
+ ("Gerar Imagem", "Gerar Imagem", "Generate Image", "生成图片", "Bild generieren", "Tạo hình ảnh", "Görsel oluştur", "Создать изображение", "Generar imagen", "Buat gambar", "Genera immagine"),
939
+ ("Refazer Prompt e Gerar Imagem", "Refazer Prompt e Gerar Imagem", "Regenerate Prompt and Generate Image", "重新生成提示词并生成图片", "Prompt neu erstellen und Bild generieren", "Tạo lại prompt và hình ảnh", "İstemi Yeniden Oluştur ve Görseli Üret", "Перегенерировать промпт и создать изображение", "Regenerar prompt y generar imagen", "Buat Ulang Prompt dan Gambar", "Rigenera prompt e genera immagine"),
940
+ ("Refazer Lettering", "Refazer Lettering", "Regenerate Lettering", "重新生成文字", "Lettering neu erstellen", "Tạo lại chữ", "Yazıyı Yeniden Oluştur", "Перегенерировать надпись", "Regenerar lettering", "Buat ulang lettering", "Rigenera lettering"),
941
+ ("Upload Image", "Upload Image", "Upload Image", "上传图片", "Bild hochladen", "Tải hình ảnh lên", "Görsel Yükle", "Загрузить изображение", "Subir imagen", "Unggah gambar", "Carica immagine"),
942
+ ("A gerar a imagem com Nano Banana…", "A gerar a imagem com Nano Banana…", "Generating image with Nano Banana…", "正在使用 Nano Banana 生成图片…", "Bild wird mit Nano Banana generiert…", "Đang tạo ảnh bằng Nano Banana…", "Nano Banana ile görsel oluşturuluyor…", "Создание изображения с помощью Nano Banana…", "Generando imagen con Nano Banana…", "Membuat gambar dengan Nano Banana…", "Generazione immagine con Nano Banana…"),
943
+ ("A refazer o prompt e a imagem…", "A refazer o prompt e a imagem…", "Regenerating the prompt and image…", "正在重新生成提示词和图片…", "Prompt und Bild werden neu erstellt…", "Đang tạo lại prompt và hình ảnh…", "İstem ve görsel yeniden oluşturuluyor…", "Перегенерация промпта и изображения…", "Regenerando el prompt y la imagen…", "Membuat ulang prompt dan gambar…", "Rigenerazione del prompt e dell'immagine…"),
944
+ ("A refazer apenas o prompt da thumbnail…", "A refazer apenas o prompt da thumbnail…", "Regenerating only the thumbnail prompt…", "仅重新生成缩略图提示词…", "Nur der Thumbnail-Prompt wird neu erstellt…", "Chỉ tạo lại prompt thumbnail…", "Yalnızca küçük resim istemi yeniden oluşturuluyor…", "Перегенерация только промпта миниатюры…", "Regenerando solo el prompt de la miniatura…", "Membuat ulang prompt thumbnail saja…", "Rigenerazione del solo prompt della miniatura…"),
945
+ ("A refazer apenas o lettering…", "A refazer apenas o lettering…", "Regenerating only the lettering…", "仅重新生成文字…", "Nur das Lettering wird neu erstellt…", "Chỉ tạo lại chữ…", "Yalnızca yazı yeniden oluşturuluyor…", "Перегенерация только надписи…", "Regenerando solo el lettering…", "Membuat ulang lettering saja…", "Rigenerazione del solo lettering…"),
946
+ ("Prompt da thumbnail refeito", "Prompt da thumbnail refeito", "Thumbnail prompt regenerated", "缩略图提示词已重新生成", "Thumbnail-Prompt neu erstellt", "Đã tạo lại prompt thumbnail", "Küçük resim istemi yeniden oluşturuldu", "Промпт миниатюры перегенерирован", "Prompt de miniatura regenerado", "Prompt thumbnail dibuat ulang", "Prompt della miniatura rigenerato"),
947
+ ("Prompt da thumbnail actualizado; a imagem existente foi preservada.", "Prompt da thumbnail actualizado; a imagem existente foi preservada.", "Thumbnail prompt updated; the existing image was preserved.", "缩略图提示词已更新;现有图片已保留。", "Thumbnail-Prompt aktualisiert; das vorhandene Bild wurde beibehalten.", "Đã cập nhật prompt thumbnail; ảnh hiện tại được giữ nguyên.", "Küçük resim istemi güncellendi; mevcut görsel korundu.", "Промпт миниатюры обновлён; существующее изображение сохранено.", "Prompt de miniatura actualizado; la imagen existente se conservó.", "Prompt thumbnail diperbarui; gambar yang ada dipertahankan.", "Prompt della miniatura aggiornato; l'immagine esistente è stata preservata."),
948
+ ("Thumbnail gerada com sucesso.", "Thumbnail gerada com sucesso.", "Thumbnail generated successfully.", "缩略图生成成功。", "Thumbnail erfolgreich generiert.", "Đã tạo thumbnail thành công.", "Küçük resim başarıyla oluşturuldu.", "Миниатюра успешно создана.", "Miniatura generada correctamente.", "Thumbnail berhasil dibuat.", "Miniatura generata con successo."),
949
+ ("Prompt da thumbnail e imagem actualizados.", "Prompt da thumbnail e imagem actualizados.", "Thumbnail prompt and image updated.", "缩略图提示词和图片已更新。", "Thumbnail-Prompt und Bild aktualisiert.", "Đã cập nhật prompt và hình ảnh thumbnail.", "Küçük resim istemi ve görsel güncellendi.", "Промпт и изображение миниатюры обновлены.", "Prompt e imagen de miniatura actualizados.", "Prompt dan gambar thumbnail diperbarui.", "Prompt e immagine della miniatura aggiornati."),
950
+ ("Lettering refeito; a imagem original foi usada como base.", "Lettering refeito; a imagem original foi usada como base.", "Lettering regenerated; the original image was used as the base.", "文字已重新生成;原图用作基础。", "Lettering neu erstellt; das Originalbild wurde als Grundlage verwendet.", "Đã tạo lại chữ; ảnh gốc được dùng làm nền.", "Yazı yeniden oluşturuldu; orijinal görsel temel olarak kullanıldı.", "Надпись перегенерирована; исходное изображение использовано как основа.", "Lettering regenerado; la imagen original se usó como base.", "Lettering dibuat ulang; gambar asli digunakan sebagai dasar.", "Lettering rigenerato; l'immagine originale è stata usata come base."),
951
+ ("Imagem carregada e vinculada à tarefa.", "Imagem carregada e vinculada à tarefa.", "Image uploaded and linked to the task.", "图片已上传并关联到任务。", "Bild hochgeladen und mit der Aufgabe verknüpft.", "Đã tải ảnh lên và liên kết với tác vụ.", "Görsel yüklendi ve göreve bağlandı.", "Изображение загружено и привязано к задаче.", "Imagen subida y vinculada a la tarea.", "Gambar diunggah dan ditautkan ke tugas.", "Immagine caricata e collegata all'attività."),
952
+ ("A thumbnail não tem um prompt de imagem para gerar.", "A thumbnail não tem um prompt de imagem para gerar.", "This thumbnail has no image prompt to generate.", "此缩略图没有可生成的图片提示词。", "Für dieses Thumbnail ist kein Bild-Prompt zum Generieren vorhanden.", "Thumbnail này không có prompt hình ảnh để tạo.", "Bu küçük resim için oluşturulacak bir görsel istemi yok.", "У этой миниатюры нет промпта для создания изображения.", "Esta miniatura no tiene un prompt de imagen para generar.", "Thumbnail ini tidak memiliki prompt gambar untuk dibuat.", "Questa miniatura non ha un prompt immagine da generare."),
953
+ ("A thumbnail precisa de uma imagem existente para refazer o lettering.", "A thumbnail precisa de uma imagem existente para refazer o lettering.", "An existing image is required to regenerate the lettering.", "需要现有图片才能重新生成文字。", "Für die Neugenerierung des Letterings ist ein vorhandenes Bild erforderlich.", "Cần có ảnh hiện tại để tạo lại chữ.", "Lettering'i yeniden oluşturmak için mevcut bir görsel gerekir.", "Для перегенерации надписи требуется существующее изображение.", "Se necesita una imagen existente para regenerar el lettering.", "Diperlukan gambar yang ada untuk membuat ulang lettering.", "È necessaria un'immagine esistente per rigenerare il lettering."),
954
+ ("Thumbnails", "Thumbnails", "Thumbnails", "缩略图", "Thumbnails", "Thumbnails", "Küçük Resimler", "Миниатюры", "Miniaturas", "Thumbnail", "Miniature"),
955
+ ("Biblioteca de thumbnails associadas às tarefas da pipeline. Cada acção preserva a imagem anterior no histórico local.", "Biblioteca de thumbnails associadas às tarefas da pipeline. Cada acção preserva a imagem anterior no histórico local.", "Thumbnail library linked to pipeline tasks. Each action preserves the previous image in local history.", "与流程任务关联的缩略图库。每项操作都会在本地历史记录中保留上一张图片。", "Thumbnail-Bibliothek für Pipeline-Aufgaben. Jede Aktion bewahrt das vorherige Bild im lokalen Verlauf.", "Thư viện thumbnail liên kết với các tác vụ pipeline. Mỗi thao tác đều giữ ảnh trước đó trong lịch sử cục bộ.", "Pipeline görevlerine bağlı küçük resim kitaplığı. Her işlem önceki görseli yerel geçmişte korur.", "Библиотека миниатюр, связанная с задачами конвейера. Каждое действие сохраняет предыдущее изображение в локальной истории.", "Biblioteca de miniaturas vinculada a las tareas del flujo. Cada acción conserva la imagen anterior en el historial local.", "Pustaka thumbnail yang terhubung ke tugas alur. Setiap tindakan menyimpan gambar sebelumnya dalam riwayat lokal.", "Libreria di miniature collegata alle attività della pipeline. Ogni azione conserva l'immagine precedente nella cronologia locale."),
956
+ ("Ainda não existem tarefas com thumbnail gerada ou prompt de imagem disponível.", "Ainda não existem tarefas com thumbnail gerada ou prompt de imagem disponível.", "There are no tasks with a generated thumbnail or available image prompt yet.", "目前没有已生成缩略图或可用图片提示词的任务。", "Es gibt noch keine Aufgaben mit generiertem Thumbnail oder verfügbarem Bild-Prompt.", "Chưa có tác vụ nào có thumbnail được tạo hoặc prompt hình ảnh khả dụng.", "Henüz oluşturulmuş küçük resmi veya kullanılabilir görsel istemi olan görev yok.", "Пока нет задач с созданной миниатюрой или доступным промптом изображения.", "Aún no hay tareas con miniatura generada o prompt de imagen disponible.", "Belum ada tugas dengan thumbnail yang dibuat atau prompt gambar yang tersedia.", "Non ci sono ancora attività con miniatura generata o prompt immagine disponibile."),
957
+ ("Sem imagem", "Sem imagem", "No image", "无图片", "Kein Bild", "Không có ảnh", "Görsel yok", "Нет изображения", "Sin imagen", "Tidak ada gambar", "Nessuna immagine"),
958
+ ("Imagem ainda não gerada", "Imagem ainda não gerada", "Image not generated yet", "图片尚未生成", "Bild noch nicht generiert", "Ảnh chưa được tạo", "Görsel henüz oluşturulmadı", "Изображение ещё не создано", "Imagen aún no generada", "Gambar belum dibuat", "Immagine non ancora generata"),
959
+ ("Canal", "Canal", "Channel", "频道", "Kanal", "Kênh", "Kanal", "Канал", "Canal", "Kanal", "Canale"),
960
+ ("Tarefa", "Tarefa", "Task", "任务", "Aufgabe", "Tác vụ", "Görev", "Задача", "Tarea", "Tugas", "Attività"),
961
+ ("Estado", "Estado", "Status", "状态", "Status", "Trạng thái", "Durum", "Статус", "Estado", "Status", "Stato"),
962
+ ("Variante", "Variante", "Variant", "变体", "Variante", "Biến thể", "Varyant", "Вариант", "Variante", "Varian", "Variante"),
963
+ ("Ver prompt da thumbnail", "Ver prompt da thumbnail", "View thumbnail prompt", "查看缩略图提示词", "Thumbnail-Prompt anzeigen", "Xem prompt thumbnail", "Küçük resim istemini görüntüle", "Просмотреть промпт миниатюры", "Ver prompt de miniatura", "Lihat prompt thumbnail", "Visualizza prompt miniatura"),
964
+ ("Suba uma imagem para a associar a esta tarefa e à pipeline.", "Suba uma imagem para a associar a esta tarefa e à pipeline.", "Upload an image to link it to this task and the pipeline.", "上传图片以将其关联到此任务和流程。", "Laden Sie ein Bild hoch, um es mit dieser Aufgabe und der Pipeline zu verknüpfen.", "Tải ảnh lên để liên kết với tác vụ này và pipeline.", "Bu göreve ve pipeline'a bağlamak için bir görsel yükleyin.", "Загрузите изображение, чтобы связать его с этой задачей и конвейером.", "Suba una imagen para vincularla a esta tarea y al flujo.", "Unggah gambar untuk menautkannya ke tugas dan alur ini.", "Carica un'immagine per collegarla a questa attività e alla pipeline."),
965
+ ("A guardar a imagem carregada…", "A guardar a imagem carregada…", "Saving uploaded image…", "正在保存上传的图片…", "Hochgeladenes Bild wird gespeichert…", "Đang lưu ảnh đã tải lên…", "Yüklenen görsel kaydediliyor…", "Сохранение загруженного изображения…", "Guardando la imagen subida…", "Menyimpan gambar yang diunggah…", "Salvataggio dell'immagine caricata…"),
851
966
  )
852
967
  UI_CONTENT_TRANSLATIONS: dict[str, dict[str, str]] = {code: {} for code in _CONTENT_TRANSLATION_CODES}
853
968
  for _row in _CONTENT_TRANSLATION_ROWS:
@@ -1,6 +1,7 @@
1
1
  import base64
2
2
  import binascii
3
3
  import hashlib
4
+ import mimetypes
4
5
  import json
5
6
  import os
6
7
  import tempfile
@@ -72,6 +73,25 @@ def _extract_image_bytes(payload: dict[str, Any]) -> bytes:
72
73
  raise ThumbnailGenerationError("O Gemini concluiu a interação, mas não devolveu uma imagem inline.")
73
74
 
74
75
 
76
+ def _image_input(path: Path) -> dict[str, str]:
77
+ if not path.is_file():
78
+ raise ThumbnailGenerationError("A imagem de referência da thumbnail não está disponível no storage.")
79
+ try:
80
+ image_data = path.read_bytes()
81
+ except OSError as exc:
82
+ raise ThumbnailGenerationError("Não foi possível ler a imagem de referência da thumbnail.") from exc
83
+ if not image_data:
84
+ raise ThumbnailGenerationError("A imagem de referência da thumbnail está vazia.")
85
+ mime_type = mimetypes.guess_type(path.name)[0] or "image/jpeg"
86
+ if not mime_type.startswith("image/"):
87
+ mime_type = "image/jpeg"
88
+ return {
89
+ "type": "image",
90
+ "mime_type": mime_type,
91
+ "data": base64.b64encode(image_data).decode("ascii"),
92
+ }
93
+
94
+
75
95
  def _thumbnail_filename(prompt: str, topic: str, variant_index: int, model: str) -> str:
76
96
  source = f"{model}\n{topic.strip()}\n{variant_index}\n{prompt.strip()}".encode("utf-8")
77
97
  digest = hashlib.sha256(source).hexdigest()[:20]
@@ -84,6 +104,7 @@ def generate_thumbnail_image(
84
104
  *,
85
105
  topic: str = "",
86
106
  variant_index: int = 0,
107
+ reference_image: str | Path | None = None,
87
108
  ) -> Path:
88
109
  api_key = str(settings.get("gemini_image_api_key") or "").strip()
89
110
  if not api_key:
@@ -95,9 +116,15 @@ def generate_thumbnail_image(
95
116
  model = str(settings.get("gemini_image_model") or DEFAULT_GEMINI_IMAGE_MODEL).strip()
96
117
  aspect_ratio = str(settings.get("gemini_image_aspect_ratio") or DEFAULT_ASPECT_RATIO).strip()
97
118
  image_size = str(settings.get("gemini_image_size") or DEFAULT_IMAGE_SIZE).strip()
119
+ request_input: str | list[dict[str, str]] = clean_prompt
120
+ if reference_image:
121
+ request_input = [
122
+ {"type": "text", "text": clean_prompt},
123
+ _image_input(Path(reference_image)),
124
+ ]
98
125
  body = {
99
126
  "model": model,
100
- "input": clean_prompt,
127
+ "input": request_input,
101
128
  "response_format": {
102
129
  "type": "image",
103
130
  "mime_type": DEFAULT_MIME_TYPE,