@danhachuel/thunderbolt 0.3.23 → 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 +323 -125
- package/hermes_ui/draft_video.py +127 -0
- package/hermes_ui/languages.py +88 -2
- package/package.json +1 -1
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
|
|
720
|
+
"""Render shared settings, optionally limiting the visible video sections."""
|
|
717
721
|
settings: dict[str, Any] = {}
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
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
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
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
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
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
|
|
781
|
-
st.
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
if
|
|
813
|
-
st.
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
settings["
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
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
|
-
|
|
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."""
|
|
@@ -2189,6 +2386,7 @@ def render_scripts():
|
|
|
2189
2386
|
)
|
|
2190
2387
|
language = script_settings["script_language"]
|
|
2191
2388
|
structure_notes = script_settings["script_structure_notes"]
|
|
2389
|
+
st.session_state["pipeline_scripts_generation_settings"] = dict(script_settings)
|
|
2192
2390
|
title = str(script_settings.get("video_subject") or "").strip()
|
|
2193
2391
|
brief = str(script_settings.get("video_script") or "").strip() or title
|
|
2194
2392
|
generate_col, clear_col = st.columns([1.4, 1])
|
|
@@ -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
|
+
]
|
package/hermes_ui/languages.py
CHANGED
|
@@ -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