@danhachuel/thunderbolt 0.3.22 → 0.3.24

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
@@ -22,7 +22,7 @@ except (OSError, json.JSONDecodeError):
22
22
  APP_VERSION = ""
23
23
 
24
24
  from hermes_ui.domain import STAGES, create_batch, create_channel, create_tasks_for_batch, delete_channel, pipeline_summary, set_channel_defaults, transition_task, update_channel, update_channel_video
25
- from hermes_ui.drafts import save_draft
25
+ from hermes_ui.drafts import list_drafts, save_draft
26
26
  from hermes_ui.automation_worker import load_worker_status
27
27
  from hermes_ui.storage import BLUEPRINTS, DEFAULT_LLM_PROVIDER, MEDIA_DOWNLOADS, STORAGE, TIKTOK_PROMPT_MASTERS, ensure_storage, get_display_name, list_blueprint_files, list_prompt_master_files, load_blueprint_file, load_prompt_master_file, now, read_json, set_display_name, write_json
28
28
  from app.modules.niche_finder.apify import ApifyError, DEFAULT_ACTOR_ID, abort_actor_run, build_actor_input, get_dataset_items, normalize_video_items, start_actor_run, wait_for_actor_run
@@ -47,6 +47,7 @@ from hermes_ui.script_generation import generate_script_document
47
47
  from hermes_ui.voice_preview import DEFAULT_SAMPLE, load_preview_file, synthesize_preview
48
48
  from hermes_ui.thumbnail_generation import ThumbnailGenerationError, generate_thumbnail_image
49
49
  from hermes_ui.thumbnails import list_thumbnail_tasks, regenerate_thumbnail
50
+ from hermes_ui.draft_video import DRAFT_SETTING_SECTIONS, missing_content_fields, missing_setting_sections, normalise_saved_script, setting_widget_suffixes
50
51
  from hermes_ui.creative_generation import CreativeGenerationError, generate_creative_package, generate_topic_for_channel, generate_video_keywords
51
52
  from integrations.platforms import IntegrationResult, TikTokAdapter, YouTubeAdapter, fetch_channel_videos_public
52
53
  from integrations.tiktok_public import fetch_public_tiktok_profile, normalize_tiktok_reference
@@ -648,6 +649,7 @@ def _save_pipeline_draft_callback(
648
649
  "video_subject": subject,
649
650
  "video_script": script,
650
651
  "video_keywords": keywords,
652
+ "generation_settings": dict(st.session_state.get(f"{prefix}_generation_settings") or {}),
651
653
  "summary": str((st.session_state.get("script_draft_summary") if is_script_draft else "") or "").strip(),
652
654
  "document_type": document_type,
653
655
  "language": str(st.session_state.get(f"{prefix}_script_language") or "").strip(),
@@ -712,134 +714,141 @@ def render_video_generation_settings(
712
714
  channel: dict[str, Any] | None = None,
713
715
  generate_content_callback: Any | None = None,
714
716
  save_draft_callback: Any | None = None,
717
+ sections: set[str] | None = None,
718
+ include_content: bool = True,
715
719
  ) -> dict[str, Any]:
716
- """Render the shared MoneyPrinter-style settings and return a serializable payload."""
720
+ """Render shared settings, optionally limiting the visible video sections."""
717
721
  settings: dict[str, Any] = {}
718
- st.markdown("### Video Subject Settings")
719
- subject_cols = st.columns(2)
720
- with subject_cols[0]:
721
- settings["video_subject"] = st.text_input(
722
- "Video Subject",
723
- value=str(st.session_state.get(f"{prefix}_video_subject", "")),
724
- key=f"{prefix}_video_subject",
725
- placeholder="Ex.: How AI is changing everyday life",
726
- )
727
- normalized_current_language = "music" if str(current_language or "").strip().casefold() in {"music", "00 – apenas música de fundo (sem falas)", "00 - apenas música de fundo (sem falas)"} else language_code(current_language)
728
- settings["script_language"] = st.selectbox(
729
- "Script Language",
730
- VIDEO_LANGUAGE_SELECTION_OPTIONS,
731
- index=VIDEO_LANGUAGE_SELECTION_OPTIONS.index(normalized_current_language) if normalized_current_language in VIDEO_LANGUAGE_SELECTION_OPTIONS else 0,
732
- format_func=video_language_label,
733
- key=f"{prefix}_script_language",
734
- )
735
- with subject_cols[1]:
736
- with st.expander("Advanced Script Settings", expanded=False):
737
- settings["script_structure_notes"] = st.text_area(
738
- "Estrutura e notas opcionais",
739
- value=str(st.session_state.get(f"{prefix}_script_structure_notes", "")),
740
- key=f"{prefix}_script_structure_notes",
741
- height=100,
742
- placeholder="Ex.: gancho forte, 6 cenas, narração documental…",
722
+ visible_sections = sections if sections is not None else {"Configurações de vídeo", "Configurações de áudio", "Configurações de legendas"}
723
+
724
+ if include_content:
725
+ st.markdown("### Video Subject Settings")
726
+ subject_cols = st.columns(2)
727
+ with subject_cols[0]:
728
+ settings["video_subject"] = st.text_input(
729
+ "Video Subject",
730
+ value=str(st.session_state.get(f"{prefix}_video_subject", "")),
731
+ key=f"{prefix}_video_subject",
732
+ placeholder="Ex.: How AI is changing everyday life",
743
733
  )
744
- settings["generate_script_with_ai"] = st.checkbox("Generate Script & Keywords with AI", value=True, key=f"{prefix}_generate_script_with_ai")
745
- settings["video_script"] = st.text_area(
746
- "Video Script (Optional)",
747
- value=str(st.session_state.get(f"{prefix}_video_script", "")),
748
- key=f"{prefix}_video_script",
749
- height=130,
750
- )
751
- settings["video_keywords"] = st.text_area(
752
- "Video Keywords (English, Optional)",
753
- value=str(st.session_state.get(f"{prefix}_video_keywords", "")),
754
- key=f"{prefix}_video_keywords",
755
- height=90,
756
- )
757
- if generate_content_callback is not None:
758
- st.button(
759
- "Gerar tópico, roteiro e palavras-chave com IA",
760
- key=f"{prefix}_generate_video_content",
761
- use_container_width=True,
762
- type="secondary",
763
- icon=":material/auto_awesome:",
764
- on_click=generate_content_callback,
734
+ normalized_current_language = "music" if str(current_language or "").strip().casefold() in {"music", "00 apenas música de fundo (sem falas)", "00 - apenas música de fundo (sem falas)"} else language_code(current_language)
735
+ settings["script_language"] = st.selectbox(
736
+ "Script Language",
737
+ VIDEO_LANGUAGE_SELECTION_OPTIONS,
738
+ index=VIDEO_LANGUAGE_SELECTION_OPTIONS.index(normalized_current_language) if normalized_current_language in VIDEO_LANGUAGE_SELECTION_OPTIONS else 0,
739
+ format_func=video_language_label,
740
+ key=f"{prefix}_script_language",
741
+ )
742
+ with subject_cols[1]:
743
+ with st.expander("Advanced Script Settings", expanded=False):
744
+ settings["script_structure_notes"] = st.text_area(
745
+ "Estrutura e notas opcionais",
746
+ value=str(st.session_state.get(f"{prefix}_script_structure_notes", "")),
747
+ key=f"{prefix}_script_structure_notes",
748
+ height=100,
749
+ placeholder="Ex.: gancho forte, 6 cenas, narração documental…",
750
+ )
751
+ settings["generate_script_with_ai"] = st.checkbox("Generate Script & Keywords with AI", value=True, key=f"{prefix}_generate_script_with_ai")
752
+ settings["video_script"] = st.text_area(
753
+ "Video Script (Optional)",
754
+ value=str(st.session_state.get(f"{prefix}_video_script", "")),
755
+ key=f"{prefix}_video_script",
756
+ height=130,
765
757
  )
766
- if st.session_state.get(f"{prefix}_generate_content_notice"):
767
- st.success(st.session_state[f"{prefix}_generate_content_notice"])
768
- if st.session_state.get(f"{prefix}_generate_content_error"):
769
- st.error(st.session_state[f"{prefix}_generate_content_error"])
770
-
771
- if save_draft_callback is not None:
772
- st.button(
773
- "Salvar rascunho",
774
- key=f"{prefix}_save_draft",
775
- use_container_width=True,
776
- type="secondary",
777
- icon=":material/save:",
778
- on_click=save_draft_callback,
758
+ settings["video_keywords"] = st.text_area(
759
+ "Video Keywords (English, Optional)",
760
+ value=str(st.session_state.get(f"{prefix}_video_keywords", "")),
761
+ key=f"{prefix}_video_keywords",
762
+ height=90,
779
763
  )
780
- if st.session_state.get(f"{prefix}_save_draft_notice"):
781
- st.success(st.session_state[f"{prefix}_save_draft_notice"])
782
- if st.session_state.get(f"{prefix}_save_draft_error"):
783
- st.error(st.session_state[f"{prefix}_save_draft_error"])
784
-
785
- st.markdown("### Video Settings")
786
- video_cols = st.columns(2)
787
- with video_cols[0]:
788
- settings["video_source"] = st.selectbox("Video Source", WIDE_STYLE_OPTIONS, key=f"{prefix}_video_source")
789
- if settings["video_source"] == "full_ia":
790
- settings["style_ia"] = st.selectbox("Estilo IA", AI_STYLE_OPTIONS, key=f"{prefix}_style_ia")
791
- else:
792
- settings["style_ia"] = ""
793
- settings["video_format"] = st.selectbox("Formato", VIDEO_FORMAT_OPTIONS, key=f"{prefix}_video_format")
794
- settings["video_concatenation_mode"] = st.selectbox("Video Concatenation Mode", VIDEO_CONCATENATION_OPTIONS, key=f"{prefix}_video_concatenation")
795
- settings["match_visuals_to_script_order"] = st.checkbox("Match Visuals to Script Order", value=False, key=f"{prefix}_match_visuals")
796
- settings["video_transition_mode"] = st.selectbox("Video Transition Mode", VIDEO_TRANSITION_OPTIONS, key=f"{prefix}_video_transition")
797
- with video_cols[1]:
798
- settings["video_aspect_ratio"] = st.selectbox("Video Aspect Ratio", ["Portrait 9:16", "Landscape 16:9", "Square 1:1"], key=f"{prefix}_video_aspect_ratio")
799
- settings["maximum_clip_duration"] = st.selectbox("Maximum Clip Duration (seconds)", [3, 5, 8, 10, 15], key=f"{prefix}_maximum_clip_duration")
800
- settings["videos_per_run"] = st.selectbox("Videos per Run", list(range(1, 11)), key=f"{prefix}_videos_per_run")
801
- settings["video_encoder"] = st.selectbox("Video Encoder", VIDEO_ENCODER_OPTIONS, key=f"{prefix}_video_encoder")
802
-
803
- st.markdown("### Audio Settings")
804
- audio_cols = st.columns(2)
805
- with audio_cols[0]:
806
- settings["voiceover_mode"] = st.radio("Voiceover Mode", VOICEOVER_MODE_OPTIONS, horizontal=True, key=f"{prefix}_voiceover_mode")
807
- settings["voiceover_service"] = st.selectbox("Voiceover Service", VOICEOVER_SERVICE_OPTIONS, key=f"{prefix}_voiceover_service")
808
- if channel is not None:
809
- channel_id = str(channel.get("id") or channel.get("name") or "")
810
- channel_voice = str(channel.get("default_voice") or channel.get("voice") or "").strip()
811
- channel_state_key = f"{prefix}_voice_channel_id"
812
- if st.session_state.get(channel_state_key) != channel_id:
813
- st.session_state[f"{prefix}_voice"] = channel_voice
814
- st.session_state[channel_state_key] = channel_id
815
- current_voice = str(st.session_state.get(f"{prefix}_voice", ""))
816
- voice_options = voice_catalog(current_voice)
817
- settings["voice"] = st.selectbox("Voice (match script language)", voice_options, format_func=lambda value: value or "Sem voz seleccionada", key=f"{prefix}_voice")
818
- volume_speed_cols = st.columns(2)
819
- with volume_speed_cols[0]:
820
- settings["voiceover_volume"] = st.selectbox("Voiceover Volume", VOICEOVER_VOLUME_OPTIONS, index=VOICEOVER_VOLUME_OPTIONS.index("100%"), key=f"{prefix}_voiceover_volume")
821
- with volume_speed_cols[1]:
822
- settings["voiceover_speed"] = st.selectbox("Voiceover Speed", VOICEOVER_SPEED_OPTIONS, index=VOICEOVER_SPEED_OPTIONS.index("1.0x"), key=f"{prefix}_voiceover_speed")
823
- st.button("Preview Voice", key=f"{prefix}_preview_voice", disabled=True, help="A pré-visualização de voz será ligada ao provider configurado.")
824
- with audio_cols[1]:
825
- settings["background_music_source"] = st.selectbox("Background Music Source", BACKGROUND_MUSIC_SOURCE_OPTIONS, index=3, key=f"{prefix}_background_music_source")
826
- settings["background_music_volume"] = st.selectbox("Background Music Volume", BACKGROUND_MUSIC_VOLUME_OPTIONS, index=2, key=f"{prefix}_background_music_volume")
827
-
828
- st.markdown("### Subtitle Settings")
829
- subtitle_cols = st.columns(2)
830
- with subtitle_cols[0]:
831
- settings["enable_subtitles"] = st.checkbox("Enable Subtitles", value=True, key=f"{prefix}_enable_subtitles")
832
- settings["subtitle_font"] = st.selectbox("Font", SUBTITLE_FONT_OPTIONS, key=f"{prefix}_subtitle_font")
833
- settings["subtitle_position"] = st.selectbox("Position", SUBTITLE_POSITION_OPTIONS, key=f"{prefix}_subtitle_position")
834
- settings["subtitle_color"] = st.color_picker("Color", "#FFFFFF", key=f"{prefix}_subtitle_color")
835
- settings["subtitle_background"] = st.checkbox("Background", value=True, key=f"{prefix}_subtitle_background")
836
- settings["subtitle_background_color"] = st.color_picker("Background Color", "#000000", key=f"{prefix}_subtitle_background_color")
837
- settings["subtitle_rounded_background"] = st.checkbox("Rounded Background", value=False, key=f"{prefix}_subtitle_rounded_background")
838
- with subtitle_cols[1]:
839
- settings["subtitle_font_size"] = st.slider("Font Size", min_value=12, max_value=96, value=60, key=f"{prefix}_subtitle_font_size")
840
- settings["subtitle_outline"] = st.color_picker("Outline", "#000000", key=f"{prefix}_subtitle_outline")
841
- settings["subtitle_outline_width"] = st.slider("Outline Width", min_value=0.0, max_value=5.0, value=1.5, step=0.25, key=f"{prefix}_subtitle_outline_width")
842
- st.button("Restore Subtitle Defaults", key=f"{prefix}_restore_subtitle_defaults", disabled=True, help="Os valores predefinidos já estão activos nesta configuração.")
764
+ if generate_content_callback is not None:
765
+ st.button(
766
+ "Gerar tópico, roteiro e palavras-chave com IA",
767
+ key=f"{prefix}_generate_video_content",
768
+ use_container_width=True,
769
+ type="secondary",
770
+ icon=":material/auto_awesome:",
771
+ on_click=generate_content_callback,
772
+ )
773
+ if st.session_state.get(f"{prefix}_generate_content_notice"):
774
+ st.success(st.session_state[f"{prefix}_generate_content_notice"])
775
+ if st.session_state.get(f"{prefix}_generate_content_error"):
776
+ st.error(st.session_state[f"{prefix}_generate_content_error"])
777
+ if save_draft_callback is not None:
778
+ st.button(
779
+ "Salvar rascunho",
780
+ key=f"{prefix}_save_draft",
781
+ use_container_width=True,
782
+ type="secondary",
783
+ icon=":material/save:",
784
+ on_click=save_draft_callback,
785
+ )
786
+ if st.session_state.get(f"{prefix}_save_draft_notice"):
787
+ st.success(st.session_state[f"{prefix}_save_draft_notice"])
788
+ if st.session_state.get(f"{prefix}_save_draft_error"):
789
+ st.error(st.session_state[f"{prefix}_save_draft_error"])
790
+
791
+ if "Configurações de vídeo" in visible_sections:
792
+ st.markdown("### Video Settings")
793
+ video_cols = st.columns(2)
794
+ with video_cols[0]:
795
+ settings["video_source"] = st.selectbox("Video Source", WIDE_STYLE_OPTIONS, key=f"{prefix}_video_source")
796
+ if settings["video_source"] == "full_ia":
797
+ settings["style_ia"] = st.selectbox("Estilo IA", AI_STYLE_OPTIONS, key=f"{prefix}_style_ia")
798
+ else:
799
+ settings["style_ia"] = ""
800
+ settings["video_format"] = st.selectbox("Formato", VIDEO_FORMAT_OPTIONS, key=f"{prefix}_video_format")
801
+ settings["video_concatenation_mode"] = st.selectbox("Video Concatenation Mode", VIDEO_CONCATENATION_OPTIONS, key=f"{prefix}_video_concatenation")
802
+ settings["match_visuals_to_script_order"] = st.checkbox("Match Visuals to Script Order", value=False, key=f"{prefix}_match_visuals")
803
+ settings["video_transition_mode"] = st.selectbox("Video Transition Mode", VIDEO_TRANSITION_OPTIONS, key=f"{prefix}_video_transition")
804
+ with video_cols[1]:
805
+ settings["video_aspect_ratio"] = st.selectbox("Video Aspect Ratio", ["Portrait 9:16", "Landscape 16:9", "Square 1:1"], key=f"{prefix}_video_aspect_ratio")
806
+ settings["maximum_clip_duration"] = st.selectbox("Maximum Clip Duration (seconds)", [3, 5, 8, 10, 15], key=f"{prefix}_maximum_clip_duration")
807
+ settings["videos_per_run"] = st.selectbox("Videos per Run", list(range(1, 11)), key=f"{prefix}_videos_per_run")
808
+ settings["video_encoder"] = st.selectbox("Video Encoder", VIDEO_ENCODER_OPTIONS, key=f"{prefix}_video_encoder")
809
+
810
+ if "Configurações de áudio" in visible_sections:
811
+ st.markdown("### Audio Settings")
812
+ audio_cols = st.columns(2)
813
+ with audio_cols[0]:
814
+ settings["voiceover_mode"] = st.radio("Voiceover Mode", VOICEOVER_MODE_OPTIONS, horizontal=True, key=f"{prefix}_voiceover_mode")
815
+ settings["voiceover_service"] = st.selectbox("Voiceover Service", VOICEOVER_SERVICE_OPTIONS, key=f"{prefix}_voiceover_service")
816
+ if channel is not None:
817
+ channel_id = str(channel.get("id") or channel.get("name") or "")
818
+ channel_voice = str(channel.get("default_voice") or channel.get("voice") or "").strip()
819
+ channel_state_key = f"{prefix}_voice_channel_id"
820
+ if st.session_state.get(channel_state_key) != channel_id:
821
+ st.session_state[f"{prefix}_voice"] = channel_voice
822
+ st.session_state[channel_state_key] = channel_id
823
+ current_voice = str(st.session_state.get(f"{prefix}_voice", ""))
824
+ voice_options = voice_catalog(current_voice)
825
+ settings["voice"] = st.selectbox("Voice (match script language)", voice_options, format_func=lambda value: value or "Sem voz seleccionada", key=f"{prefix}_voice")
826
+ volume_speed_cols = st.columns(2)
827
+ with volume_speed_cols[0]:
828
+ settings["voiceover_volume"] = st.selectbox("Voiceover Volume", VOICEOVER_VOLUME_OPTIONS, index=VOICEOVER_VOLUME_OPTIONS.index("100%"), key=f"{prefix}_voiceover_volume")
829
+ with volume_speed_cols[1]:
830
+ settings["voiceover_speed"] = st.selectbox("Voiceover Speed", VOICEOVER_SPEED_OPTIONS, index=VOICEOVER_SPEED_OPTIONS.index("1.0x"), key=f"{prefix}_voiceover_speed")
831
+ st.button("Preview Voice", key=f"{prefix}_preview_voice", disabled=True, help="A pré-visualização de voz será ligada ao provider configurado.")
832
+ with audio_cols[1]:
833
+ settings["background_music_source"] = st.selectbox("Background Music Source", BACKGROUND_MUSIC_SOURCE_OPTIONS, index=3, key=f"{prefix}_background_music_source")
834
+ settings["background_music_volume"] = st.selectbox("Background Music Volume", BACKGROUND_MUSIC_VOLUME_OPTIONS, index=2, key=f"{prefix}_background_music_volume")
835
+
836
+ if "Configurações de legendas" in visible_sections:
837
+ st.markdown("### Subtitle Settings")
838
+ subtitle_cols = st.columns(2)
839
+ with subtitle_cols[0]:
840
+ settings["enable_subtitles"] = st.checkbox("Enable Subtitles", value=True, key=f"{prefix}_enable_subtitles")
841
+ settings["subtitle_font"] = st.selectbox("Font", SUBTITLE_FONT_OPTIONS, key=f"{prefix}_subtitle_font")
842
+ settings["subtitle_position"] = st.selectbox("Position", SUBTITLE_POSITION_OPTIONS, key=f"{prefix}_subtitle_position")
843
+ settings["subtitle_color"] = st.color_picker("Color", "#FFFFFF", key=f"{prefix}_subtitle_color")
844
+ settings["subtitle_background"] = st.checkbox("Background", value=True, key=f"{prefix}_subtitle_background")
845
+ settings["subtitle_background_color"] = st.color_picker("Background Color", "#000000", key=f"{prefix}_subtitle_background_color")
846
+ settings["subtitle_rounded_background"] = st.checkbox("Rounded Background", value=False, key=f"{prefix}_subtitle_rounded_background")
847
+ with subtitle_cols[1]:
848
+ settings["subtitle_font_size"] = st.slider("Font Size", min_value=12, max_value=96, value=60, key=f"{prefix}_subtitle_font_size")
849
+ settings["subtitle_outline"] = st.color_picker("Outline", "#000000", key=f"{prefix}_subtitle_outline")
850
+ settings["subtitle_outline_width"] = st.slider("Outline Width", min_value=0.0, max_value=5.0, value=1.5, step=0.25, key=f"{prefix}_subtitle_outline_width")
851
+ st.button("Restore Subtitle Defaults", key=f"{prefix}_restore_subtitle_defaults", disabled=True, help="Os valores predefinidos já estão activos nesta configuração.")
843
852
  return settings
844
853
 
845
854
 
@@ -1748,9 +1757,191 @@ def render_channels():
1748
1757
  render_channel_videos(channel)
1749
1758
 
1750
1759
 
1760
+ def _saved_video_draft_records() -> list[dict[str, Any]]:
1761
+ """Return saved video scripts and local pipeline drafts in one selectable list."""
1762
+ records: list[dict[str, Any]] = []
1763
+ for source, source_records in (("history", list_script_documents()), ("draft", list_drafts())):
1764
+ for raw_record in source_records:
1765
+ if not isinstance(raw_record, dict):
1766
+ continue
1767
+ document_type = str(raw_record.get("document_type") or "video_script").strip()
1768
+ draft_kind = str(raw_record.get("draft_kind") or "").strip()
1769
+ if document_type not in {"", "video_script"}:
1770
+ continue
1771
+ if source == "draft" and draft_kind not in {"", "video", "script"}:
1772
+ continue
1773
+ if source == "draft" and not draft_kind and str(raw_record.get("page") or "") == "Criação de Músicas":
1774
+ continue
1775
+ if source == "history":
1776
+ try:
1777
+ content = read_script_document(raw_record)
1778
+ except (OSError, UnicodeError):
1779
+ content = ""
1780
+ else:
1781
+ content = str(raw_record.get("content") or "")
1782
+ record = normalise_saved_script({**raw_record, "source": source, "source_label": "Histórico guardado" if source == "history" else "Rascunho local"}, content)
1783
+ record["resume_id"] = f"{source}:{raw_record.get('id') or raw_record.get('filename') or len(records)}"
1784
+ records.append(record)
1785
+ return records
1786
+
1787
+
1788
+ def _seed_resume_video_settings(record: dict[str, Any]) -> None:
1789
+ """Load one persisted record into the namespaced resume widgets."""
1790
+ resume_id = str(record.get("resume_id") or "")
1791
+ if st.session_state.get("new_video_resume_loaded_id") == resume_id:
1792
+ return
1793
+ generation_settings = record.get("generation_settings") if isinstance(record.get("generation_settings"), dict) else {}
1794
+ st.session_state["new_video_resume_sections"] = missing_setting_sections(generation_settings)
1795
+ for suffix in setting_widget_suffixes():
1796
+ if suffix in generation_settings:
1797
+ st.session_state[f"new_video_resume_{suffix}"] = generation_settings[suffix]
1798
+ st.session_state["new_video_resume_loaded_id"] = resume_id
1799
+
1800
+
1801
+ def _create_video_task_from_saved_script(record: dict[str, Any], channel: dict[str, Any], settings: dict[str, Any]) -> list[dict[str, Any]]:
1802
+ """Create a normal pipeline task while preserving the saved script as an override."""
1803
+ subject = str(record.get("video_subject") or "").strip()
1804
+ script = str(record.get("video_script") or "").strip()
1805
+ keywords = str(record.get("video_keywords") or "").strip()
1806
+ settings = {
1807
+ **(record.get("generation_settings") if isinstance(record.get("generation_settings"), dict) else {}),
1808
+ **settings,
1809
+ "video_subject": subject,
1810
+ "video_script": script,
1811
+ "video_keywords": keywords,
1812
+ "script_language": str(settings.get("script_language") or record.get("language") or channel.get("language") or "pt"),
1813
+ "generate_script_with_ai": False,
1814
+ }
1815
+ style_label = str(settings.get("video_source") or "Pexels/Pixabay")
1816
+ style = {"Pexels/Pixabay": "pexels", "full_ia": "full_ia", "Apenas Música": "music"}.get(style_label, style_label)
1817
+ blueprint_id = str(record.get("blueprint_id") or channel.get("default_blueprint_id") or channel.get("blueprint_id") or "")
1818
+ blueprint_name = str(record.get("blueprint_name") or blueprint_id or "SEM BLUEPRINT CONFIGURADO")
1819
+ payload = {
1820
+ "topic": subject,
1821
+ "title": str(record.get("title") or subject).strip(),
1822
+ "topic_source": "saved_script",
1823
+ "language": settings["script_language"],
1824
+ "format": settings.get("video_format", "wide"),
1825
+ "style_wide": style,
1826
+ "style_ia": settings.get("style_ia", ""),
1827
+ "music_mode": style == "music",
1828
+ "background_mode": "none" if style == "music" else ("ai" if style == "full_ia" else "stock"),
1829
+ "voice": str(settings.get("voice") or channel.get("default_voice") or channel.get("voice") or ""),
1830
+ "blueprint_id": blueprint_id,
1831
+ "blueprint_name": blueprint_name,
1832
+ "generation_settings": settings,
1833
+ }
1834
+ batch = create_batch(
1835
+ "single",
1836
+ [str(channel.get("id") or "")],
1837
+ subject,
1838
+ 1,
1839
+ {
1840
+ **payload,
1841
+ "topic_source": "saved_script",
1842
+ "channel_payloads": {str(channel.get("id") or ""): payload},
1843
+ },
1844
+ )
1845
+ return create_tasks_for_batch(batch)
1846
+
1847
+
1848
+ def render_video_from_draft() -> None:
1849
+ """Render the continuation flow for saved scripts and local pipeline drafts."""
1850
+ st.subheader("Roteiros guardados")
1851
+ st.caption("Seleccione um roteiro guardado para continuar a criação do vídeo sem perder o conteúdo já preparado.")
1852
+ records = _saved_video_draft_records()
1853
+ if not records:
1854
+ st.info("Ainda não existem roteiros guardados.")
1855
+ return
1856
+
1857
+ record_by_id = {str(record["resume_id"]): record for record in records}
1858
+ selected_id = st.selectbox(
1859
+ "Seleccione um roteiro",
1860
+ list(record_by_id),
1861
+ format_func=lambda identifier: f"{record_by_id[identifier].get('title') or 'Roteiro sem título'} · {record_by_id[identifier].get('source_label') or 'Rascunho'}",
1862
+ key="new_video_resume_selected",
1863
+ )
1864
+ record = record_by_id[selected_id]
1865
+ _seed_resume_video_settings(record)
1866
+ with st.container(border=True):
1867
+ st.markdown(f"**Video Subject:** {record.get('video_subject') or '—'}")
1868
+ st.caption(f"{record.get('source_label') or 'Rascunho'} · {record.get('channel_name') or 'Documento independente'} · Blueprint: {record.get('blueprint_name') or '—'}")
1869
+ if record.get("video_script"):
1870
+ st.text_area("Video Script (Optional)", value=str(record["video_script"]), height=150, disabled=True, key=f"resume_preview_script_{selected_id}")
1871
+ if record.get("video_keywords"):
1872
+ st.caption(f"**Video Keywords:** {record['video_keywords']}")
1873
+
1874
+ content_missing = missing_content_fields(record)
1875
+ if content_missing:
1876
+ st.warning(f"Conteúdo em falta: {', '.join(content_missing)}. Volte a Roteiros e guarde tópico, roteiro e palavras-chave antes de continuar.")
1877
+ return
1878
+
1879
+ persisted_settings = record.get("generation_settings") if isinstance(record.get("generation_settings"), dict) else {}
1880
+ missing_sections = missing_setting_sections(persisted_settings)
1881
+ if missing_sections:
1882
+ selected_sections = set(
1883
+ st.multiselect(
1884
+ "Configurações a completar",
1885
+ list(DRAFT_SETTING_SECTIONS),
1886
+ default=missing_sections,
1887
+ key="new_video_resume_sections",
1888
+ help="Seleccione uma ou mais áreas para completar antes de criar a tarefa.",
1889
+ )
1890
+ )
1891
+ st.caption("Seleccione as configurações que pretende completar.")
1892
+ else:
1893
+ selected_sections = set()
1894
+ st.success("Roteiro completo: todas as configurações estão disponíveis.")
1895
+
1896
+ all_channels = [channel for channel in read_json("channels.json", []) if isinstance(channel, dict)]
1897
+ selectable_channels = [channel for channel in all_channels if channel.get("active", True)]
1898
+ if not selectable_channels:
1899
+ st.warning("Cadastre pelo menos um canal antes de continuar.")
1900
+ return
1901
+ saved_channel_id = str(record.get("channel_id") or "")
1902
+ if saved_channel_id and not any(str(channel.get("id")) == saved_channel_id for channel in selectable_channels):
1903
+ saved_channel = next((channel for channel in all_channels if str(channel.get("id")) == saved_channel_id), None)
1904
+ if saved_channel:
1905
+ selectable_channels.insert(0, saved_channel)
1906
+ channel_index = next((index for index, channel in enumerate(selectable_channels) if str(channel.get("id")) == saved_channel_id), 0)
1907
+ selected_channel = st.selectbox(
1908
+ "Canal",
1909
+ selectable_channels,
1910
+ index=channel_index,
1911
+ format_func=lambda channel: str(channel.get("name") or "Canal sem nome"),
1912
+ key="new_video_resume_channel",
1913
+ )
1914
+
1915
+ settings_from_form = render_video_generation_settings(
1916
+ "new_video_resume",
1917
+ current_language=str(record.get("language") or "pt"),
1918
+ channel=selected_channel,
1919
+ sections=selected_sections,
1920
+ include_content=False,
1921
+ )
1922
+ merged_settings = {**persisted_settings, **settings_from_form}
1923
+ still_missing = missing_setting_sections(merged_settings)
1924
+ action_label = "Continuar criação" if still_missing else "Gerar apenas o vídeo"
1925
+ if still_missing:
1926
+ st.caption(f"Faltam: {', '.join(still_missing)}")
1927
+ elif not missing_sections:
1928
+ st.caption("Este roteiro será usado directamente, sem regenerar o conteúdo editorial guardado.")
1929
+ if st.button(action_label, type="primary", use_container_width=True, key="new_video_resume_submit", icon=":material/movie:"):
1930
+ if still_missing:
1931
+ st.error(f"Complete as configurações seleccionadas: {', '.join(still_missing)}.")
1932
+ elif not selected_channel.get("id"):
1933
+ st.error("Seleccione um canal para continuar.")
1934
+ else:
1935
+ tasks = _create_video_task_from_saved_script(record, selected_channel, merged_settings)
1936
+ st.success(f"Tarefa criada a partir de {record.get('title') or 'Roteiro sem título'}: {tasks[0].get('id') if tasks else '—'}.")
1937
+
1938
+
1751
1939
  def render_new_video(page_title: str = "Criação de Vídeos", prefix: str = "new_video"):
1752
1940
  st.title(page_title)
1753
- create_tab = render_localized_tabs(["Criar vídeo"])[0]
1941
+ tab_labels = ["Criar vídeo"] + (["Gerar de Rascunho"] if page_title == "Criação de Vídeos" else [])
1942
+ tabs = render_localized_tabs(tab_labels)
1943
+ create_tab = tabs[0]
1944
+ draft_tab = tabs[1] if len(tabs) > 1 else None
1754
1945
  with create_tab:
1755
1946
  all_channels = [c for c in read_json("channels.json", []) if isinstance(c, dict)]
1756
1947
  active_channels = [c for c in all_channels if c.get("active", True)]
@@ -2035,7 +2226,9 @@ def render_new_video(page_title: str = "Criação de Vídeos", prefix: str = "ne
2035
2226
  st.info("Escolha a variante e clique em **Gerar imagem da thumbnail com Nano Banana**. A API key é configurada em Configuração API > API Keys.")
2036
2227
  st.session_state[f"{prefix}_creative_payload"] = payload
2037
2228
 
2229
+ st.session_state[f"{prefix}_generation_settings"] = dict(generation_settings)
2038
2230
  with st.form(f"{prefix}_form"):
2231
+
2039
2232
  quantity = st.number_input("Quantidade", min_value=1, max_value=100, value=1, disabled=mode != "same_channel")
2040
2233
  language = generation_settings["script_language"]
2041
2234
  fmt = generation_settings["video_format"]
@@ -2115,6 +2308,10 @@ def render_new_video(page_title: str = "Criação de Vídeos", prefix: str = "ne
2115
2308
  tasks = create_tasks_for_batch(batch)
2116
2309
  st.success(f"Lote {batch['id']} criado com {len(tasks)} tarefa(s). Abra {ui_text('Backlog Vídeos', current_ui_language())} para acompanhar.")
2117
2310
 
2311
+ if draft_tab is not None:
2312
+ with draft_tab:
2313
+ render_video_from_draft()
2314
+
2118
2315
 
2119
2316
  def render_music_creation():
2120
2317
  """Expose the complete video-creation UI under the music-oriented navigation entry without changing the original page."""
@@ -2166,20 +2363,9 @@ def render_scripts():
2166
2363
  st.warning("Sem Blueprint seleccionado: o documento pode ser criado, mas não terá contexto editorial de Blueprint.")
2167
2364
 
2168
2365
  document_type = st.radio("Tipo de documento", ["Roteiro de vídeo", "Letra de música"], horizontal=True, key="script_document_type")
2169
- title = st.text_input("Título", key="script_title", placeholder="Ex.: A verdade esquecida sobre…")
2170
- brief = st.text_area(
2171
- "Tema ou briefing",
2172
- key="script_brief",
2173
- height=120,
2174
- placeholder="Descreva o tema, a mensagem, o conflito ou a ideia musical que o Blueprint deve orientar.",
2175
- )
2176
- legacy_script_language = str(st.session_state.get("script_language") or "")
2177
- script_subject_key = "pipeline_scripts_video_subject"
2178
- if script_subject_key not in st.session_state:
2179
- st.session_state[script_subject_key] = str(brief or title or "")
2180
2366
  script_settings = render_video_generation_settings(
2181
2367
  "pipeline_scripts",
2182
- current_language=legacy_script_language or str(read_json("settings.json", {}).get("video_language") or "pt"),
2368
+ current_language=str(st.session_state.get("script_language") or read_json("settings.json", {}).get("video_language") or "pt"),
2183
2369
  channel=selected_channel,
2184
2370
  generate_content_callback=lambda: _generate_video_content_callback(
2185
2371
  "pipeline_scripts",
@@ -2200,6 +2386,9 @@ def render_scripts():
2200
2386
  )
2201
2387
  language = script_settings["script_language"]
2202
2388
  structure_notes = script_settings["script_structure_notes"]
2389
+ st.session_state["pipeline_scripts_generation_settings"] = dict(script_settings)
2390
+ title = str(script_settings.get("video_subject") or "").strip()
2391
+ brief = str(script_settings.get("video_script") or "").strip() or title
2203
2392
  generate_col, clear_col = st.columns([1.4, 1])
2204
2393
  with generate_col:
2205
2394
  generate_clicked = st.button("Gerar com IA a partir do Blueprint", type="primary", use_container_width=True, key="generate_script_document")
@@ -2279,7 +2468,7 @@ def render_scripts():
2279
2468
  except (OSError, ValueError) as exc:
2280
2469
  st.error(f"Não foi possível guardar o documento: {exc}")
2281
2470
  else:
2282
- st.caption("Gere um rascunho com IA para o editar aqui, ou seleccione um Blueprint e preencha o briefing para começar.")
2471
+ st.caption("Gere um rascunho com IA para o editar aqui, ou seleccione um Blueprint e preencha as configurações do tema para começar.")
2283
2472
 
2284
2473
  with history_tab:
2285
2474
  st.caption(f"Histórico persistente: `{script_dir}` · índice em `{STORAGE / 'state' / 'scripts.json'}`")
@@ -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": "技能",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danhachuel/thunderbolt",
3
- "version": "0.3.22",
3
+ "version": "0.3.24",
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",