@danhachuel/thunderbolt 0.3.41 → 0.3.43
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/MANUAL-INSTALACAO.md +47 -12
- package/README.md +21 -9
- package/app/main.py +397 -193
- package/hermes_ui/api_key_tests.py +33 -1
- package/hermes_ui/creative_generation.py +18 -26
- package/hermes_ui/domain.py +15 -6
- package/hermes_ui/languages.py +8 -2
- package/hermes_ui/llm_providers.py +4 -0
- package/hermes_ui/media_generation.py +383 -0
- package/hermes_ui/media_providers.py +282 -0
- package/hermes_ui/pipeline_worker.py +64 -4
- package/hermes_ui/provider_routing.py +457 -0
- package/hermes_ui/storage.py +32 -8
- package/hermes_ui/thumbnail_generation.py +0 -1
- package/hermes_ui/thumbnails.py +42 -3
- package/package.json +1 -1
package/app/main.py
CHANGED
|
@@ -39,12 +39,13 @@ from hermes_ui.mcp import detect_local_service, install_skill_locally, load_inte
|
|
|
39
39
|
from hermes_ui.mcp_server import server_status, start_server, stop_server
|
|
40
40
|
from hermes_ui.material_sources import apply_material_source_cards_to_settings, ensure_material_source_cards, material_source_catalog, material_source_definition, new_material_card, normalize_material_card, selected_material_source
|
|
41
41
|
from hermes_ui.llm_providers import LLM_CARDS_KEY, LLM_ACTIVE_CARD_KEY, LLM_PROVIDER_CATALOG, apply_llm_cards_to_settings, ensure_llm_provider_cards, new_llm_card, normalize_llm_card, provider_definition, test_llm_provider_card, stamp_test_result
|
|
42
|
+
from hermes_ui.media_providers import MEDIA_CARDS_KEY, MEDIA_IMAGE_ACTIVE_CARD_KEY, MEDIA_VIDEO_ACTIVE_CARD_KEY, apply_media_provider_cards_to_settings, ensure_media_provider_cards, media_provider_catalog, media_provider_definition, new_media_card, normalize_media_card
|
|
42
43
|
from hermes_ui.music import list_music_files, materialize_suno_audio, request_suno_generation, store_music_file
|
|
43
44
|
from hermes_ui.media_downloader import AUDIO_FORMATS, VIDEO_CONTAINERS, VIDEO_QUALITY_OPTIONS, MediaDownloadError, build_download_options, clear_media_download_history, dependency_status, download_media, list_media_downloads, media_download_file
|
|
44
45
|
from hermes_ui.notifications import clear_notifications, list_notifications, mark_all_notifications_read, mark_notification_read, notification_event_catalog, notification_preferences, record_notification, reconcile_persisted_notifications, save_notification_preferences, unread_notification_count
|
|
45
46
|
from hermes_ui.logs import list_logs, logs_to_rows
|
|
46
47
|
from hermes_ui.languages import LANGUAGE_CODES, VIDEO_LANGUAGE_CODES, LANGUAGE_FLAG_DATA_URIS, language_code, language_label, ui_language_menu_label, ui_text, video_language_label, video_language_options
|
|
47
|
-
from hermes_ui.api_key_tests import test_apify_credentials, test_kaggle_credentials, test_material_source_credentials, test_nano_banana_credentials, test_postiz_credentials, test_telegram_credentials, test_tiktok_credentials, test_upload_post_credentials, test_voice_provider
|
|
48
|
+
from hermes_ui.api_key_tests import test_apify_credentials, test_kaggle_credentials, test_material_source_credentials, test_media_provider_card, test_nano_banana_credentials, test_postiz_credentials, test_telegram_credentials, test_tiktok_credentials, test_upload_post_credentials, test_voice_provider
|
|
48
49
|
from hermes_ui.tutorials import tutorial_body, tutorial_caption, tutorial_title
|
|
49
50
|
|
|
50
51
|
from hermes_ui.script_documents import list_script_documents, read_script_document, save_script_document, script_storage_path
|
|
@@ -717,6 +718,75 @@ def generate_creative_for_ui(settings: dict[str, Any], channel: dict, topic: str
|
|
|
717
718
|
return payload
|
|
718
719
|
|
|
719
720
|
|
|
721
|
+
def generate_thumbnail_for_ui(
|
|
722
|
+
settings: dict[str, Any],
|
|
723
|
+
channel: dict[str, Any],
|
|
724
|
+
topic: str,
|
|
725
|
+
*,
|
|
726
|
+
title: str = "",
|
|
727
|
+
topic_source: str = "manual",
|
|
728
|
+
) -> dict[str, Any]:
|
|
729
|
+
"""Generate exactly one thumbnail brief for an existing video topic/title."""
|
|
730
|
+
topic = str(topic or "").strip()
|
|
731
|
+
if not topic:
|
|
732
|
+
raise CreativeGenerationError("É necessário um tópico antes de gerar a thumbnail.")
|
|
733
|
+
variant = generate_thumbnail_prompt(
|
|
734
|
+
settings,
|
|
735
|
+
channel,
|
|
736
|
+
topic,
|
|
737
|
+
blueprint=blueprint_for_channel(channel),
|
|
738
|
+
language=str(channel.get("language") or "Português"),
|
|
739
|
+
)
|
|
740
|
+
return {
|
|
741
|
+
"topic": topic,
|
|
742
|
+
"title": str(title or topic).strip(),
|
|
743
|
+
"topic_source": topic_source or "manual",
|
|
744
|
+
"thumbnail_variant": variant,
|
|
745
|
+
"thumbnail_variants": [variant],
|
|
746
|
+
"thumbnail_prompt": variant.get("image_prompt", ""),
|
|
747
|
+
"thumbnail_text": variant.get("overlay_text", ""),
|
|
748
|
+
"thumbnail_status": "prompt_ready",
|
|
749
|
+
"title_candidates": [],
|
|
750
|
+
"ai_generation": {"thumbnail": variant},
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
|
|
754
|
+
def generate_thumbnail_variants_for_ui(
|
|
755
|
+
settings: dict[str, Any],
|
|
756
|
+
channel: dict[str, Any],
|
|
757
|
+
topic: str,
|
|
758
|
+
count: int,
|
|
759
|
+
*,
|
|
760
|
+
title: str = "",
|
|
761
|
+
topic_source: str = "manual",
|
|
762
|
+
) -> dict[str, Any]:
|
|
763
|
+
"""Generate one independent thumbnail brief for each video in a batch."""
|
|
764
|
+
count = max(1, min(int(count or 1), 100))
|
|
765
|
+
variants: list[dict[str, Any]] = []
|
|
766
|
+
for _index in range(count):
|
|
767
|
+
generated = generate_thumbnail_for_ui(
|
|
768
|
+
settings,
|
|
769
|
+
channel,
|
|
770
|
+
topic,
|
|
771
|
+
title=title,
|
|
772
|
+
topic_source=topic_source,
|
|
773
|
+
)
|
|
774
|
+
variants.append(dict(generated["thumbnail_variant"]))
|
|
775
|
+
first = variants[0]
|
|
776
|
+
return {
|
|
777
|
+
"topic": topic,
|
|
778
|
+
"title": str(title or topic).strip(),
|
|
779
|
+
"topic_source": topic_source or "manual",
|
|
780
|
+
"thumbnail_variant": first,
|
|
781
|
+
"thumbnail_variants": variants,
|
|
782
|
+
"thumbnail_prompt": first.get("image_prompt", ""),
|
|
783
|
+
"thumbnail_text": first.get("overlay_text", ""),
|
|
784
|
+
"thumbnail_status": "prompt_ready",
|
|
785
|
+
"title_candidates": [],
|
|
786
|
+
"ai_generation": {"thumbnail_variants": variants},
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
|
|
720
790
|
def valid_hhmm(value: str) -> bool:
|
|
721
791
|
return bool(re.fullmatch(r"(?:[01]\d|2[0-3]):[0-5]\d", str(value or "").strip()))
|
|
722
792
|
|
|
@@ -776,14 +846,14 @@ def render_video_generation_settings(
|
|
|
776
846
|
key=f"{prefix}_script_language",
|
|
777
847
|
)
|
|
778
848
|
with subject_cols[1]:
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
849
|
+
st.markdown("**Advanced Script Settings**")
|
|
850
|
+
settings["script_structure_notes"] = st.text_area(
|
|
851
|
+
"Estrutura e notas opcionais",
|
|
852
|
+
value=str(st.session_state.get(f"{prefix}_script_structure_notes", "")),
|
|
853
|
+
key=f"{prefix}_script_structure_notes",
|
|
854
|
+
height=100,
|
|
855
|
+
placeholder="Ex.: gancho forte, 6 cenas, narração documental…",
|
|
856
|
+
)
|
|
787
857
|
settings["generate_script_with_ai"] = st.checkbox("Generate Script & Keywords with AI", value=True, key=f"{prefix}_generate_script_with_ai")
|
|
788
858
|
settings["video_script"] = st.text_area(
|
|
789
859
|
"Video Script (Optional)",
|
|
@@ -825,66 +895,69 @@ def render_video_generation_settings(
|
|
|
825
895
|
st.error(st.session_state[f"{prefix}_save_draft_error"])
|
|
826
896
|
|
|
827
897
|
if "Configurações de vídeo" in visible_sections:
|
|
828
|
-
st.
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
settings["
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
898
|
+
with st.expander("Configurações de vídeo", expanded=False):
|
|
899
|
+
st.markdown("### Video Settings")
|
|
900
|
+
video_cols = st.columns(2)
|
|
901
|
+
with video_cols[0]:
|
|
902
|
+
settings["video_source"] = st.selectbox("Video Source", WIDE_STYLE_OPTIONS, key=f"{prefix}_video_source")
|
|
903
|
+
if settings["video_source"] == "full_ia":
|
|
904
|
+
settings["style_ia"] = st.selectbox("Estilo IA", AI_STYLE_OPTIONS, key=f"{prefix}_style_ia")
|
|
905
|
+
else:
|
|
906
|
+
settings["style_ia"] = ""
|
|
907
|
+
settings["video_format"] = st.selectbox("Formato", VIDEO_FORMAT_OPTIONS, key=f"{prefix}_video_format")
|
|
908
|
+
settings["video_concatenation_mode"] = st.selectbox("Video Concatenation Mode", VIDEO_CONCATENATION_OPTIONS, key=f"{prefix}_video_concatenation")
|
|
909
|
+
settings["match_visuals_to_script_order"] = st.checkbox("Match Visuals to Script Order", value=False, key=f"{prefix}_match_visuals")
|
|
910
|
+
settings["video_transition_mode"] = st.selectbox("Video Transition Mode", VIDEO_TRANSITION_OPTIONS, key=f"{prefix}_video_transition")
|
|
911
|
+
with video_cols[1]:
|
|
912
|
+
settings["video_aspect_ratio"] = st.selectbox("Video Aspect Ratio", ["Portrait 9:16", "Landscape 16:9", "Square 1:1"], key=f"{prefix}_video_aspect_ratio")
|
|
913
|
+
settings["maximum_clip_duration"] = st.selectbox("Maximum Clip Duration (seconds)", [3, 5, 8, 10, 15], key=f"{prefix}_maximum_clip_duration")
|
|
914
|
+
settings["videos_per_run"] = st.selectbox("Videos per Run", list(range(1, 11)), key=f"{prefix}_videos_per_run")
|
|
915
|
+
settings["video_encoder"] = st.selectbox("Video Encoder", VIDEO_ENCODER_OPTIONS, key=f"{prefix}_video_encoder")
|
|
845
916
|
|
|
846
917
|
if "Configurações de áudio" in visible_sections:
|
|
847
|
-
st.
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
st.session_state
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
918
|
+
with st.expander("Configurações de áudio", expanded=False):
|
|
919
|
+
st.markdown("### Audio Settings")
|
|
920
|
+
audio_cols = st.columns(2)
|
|
921
|
+
with audio_cols[0]:
|
|
922
|
+
settings["voiceover_mode"] = st.radio("Voiceover Mode", VOICEOVER_MODE_OPTIONS, horizontal=True, key=f"{prefix}_voiceover_mode")
|
|
923
|
+
settings["voiceover_service"] = st.selectbox("Voiceover Service", VOICEOVER_SERVICE_OPTIONS, key=f"{prefix}_voiceover_service")
|
|
924
|
+
if channel is not None:
|
|
925
|
+
channel_id = str(channel.get("id") or channel.get("name") or "")
|
|
926
|
+
channel_voice = str(channel.get("default_voice") or channel.get("voice") or "").strip()
|
|
927
|
+
channel_state_key = f"{prefix}_voice_channel_id"
|
|
928
|
+
if st.session_state.get(channel_state_key) != channel_id:
|
|
929
|
+
st.session_state[f"{prefix}_voice"] = channel_voice
|
|
930
|
+
st.session_state[channel_state_key] = channel_id
|
|
931
|
+
current_voice = str(st.session_state.get(f"{prefix}_voice", ""))
|
|
932
|
+
voice_options = voice_catalog(current_voice)
|
|
933
|
+
settings["voice"] = st.selectbox("Voice (match script language)", voice_options, format_func=lambda value: value or "Sem voz seleccionada", key=f"{prefix}_voice")
|
|
934
|
+
volume_speed_cols = st.columns(2)
|
|
935
|
+
with volume_speed_cols[0]:
|
|
936
|
+
settings["voiceover_volume"] = st.selectbox("Voiceover Volume", VOICEOVER_VOLUME_OPTIONS, index=VOICEOVER_VOLUME_OPTIONS.index("100%"), key=f"{prefix}_voiceover_volume")
|
|
937
|
+
with volume_speed_cols[1]:
|
|
938
|
+
settings["voiceover_speed"] = st.selectbox("Voiceover Speed", VOICEOVER_SPEED_OPTIONS, index=VOICEOVER_SPEED_OPTIONS.index("1.0x"), key=f"{prefix}_voiceover_speed")
|
|
939
|
+
st.button("Preview Voice", key=f"{prefix}_preview_voice", disabled=True, help="A pré-visualização de voz será ligada ao provider configurado.")
|
|
940
|
+
with audio_cols[1]:
|
|
941
|
+
settings["background_music_source"] = st.selectbox("Background Music Source", BACKGROUND_MUSIC_SOURCE_OPTIONS, index=3, key=f"{prefix}_background_music_source")
|
|
942
|
+
settings["background_music_volume"] = st.selectbox("Background Music Volume", BACKGROUND_MUSIC_VOLUME_OPTIONS, index=2, key=f"{prefix}_background_music_volume")
|
|
871
943
|
|
|
872
944
|
if "Configurações de legendas" in visible_sections:
|
|
873
|
-
st.
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
945
|
+
with st.expander("Configurações de legendas", expanded=False):
|
|
946
|
+
st.markdown("### Subtitle Settings")
|
|
947
|
+
subtitle_cols = st.columns(2)
|
|
948
|
+
with subtitle_cols[0]:
|
|
949
|
+
settings["enable_subtitles"] = st.checkbox("Enable Subtitles", value=True, key=f"{prefix}_enable_subtitles")
|
|
950
|
+
settings["subtitle_font"] = st.selectbox("Font", SUBTITLE_FONT_OPTIONS, key=f"{prefix}_subtitle_font")
|
|
951
|
+
settings["subtitle_position"] = st.selectbox("Position", SUBTITLE_POSITION_OPTIONS, key=f"{prefix}_subtitle_position")
|
|
952
|
+
settings["subtitle_color"] = st.color_picker("Color", "#FFFFFF", key=f"{prefix}_subtitle_color")
|
|
953
|
+
settings["subtitle_background"] = st.checkbox("Background", value=True, key=f"{prefix}_subtitle_background")
|
|
954
|
+
settings["subtitle_background_color"] = st.color_picker("Background Color", "#000000", key=f"{prefix}_subtitle_background_color")
|
|
955
|
+
settings["subtitle_rounded_background"] = st.checkbox("Rounded Background", value=False, key=f"{prefix}_subtitle_rounded_background")
|
|
956
|
+
with subtitle_cols[1]:
|
|
957
|
+
settings["subtitle_font_size"] = st.slider("Font Size", min_value=12, max_value=96, value=60, key=f"{prefix}_subtitle_font_size")
|
|
958
|
+
settings["subtitle_outline"] = st.color_picker("Outline", "#000000", key=f"{prefix}_subtitle_outline")
|
|
959
|
+
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")
|
|
960
|
+
st.button("Restore Subtitle Defaults", key=f"{prefix}_restore_subtitle_defaults", disabled=True, help="Os valores predefinidos já estão activos nesta configuração.")
|
|
888
961
|
return settings
|
|
889
962
|
|
|
890
963
|
|
|
@@ -2120,43 +2193,42 @@ def render_new_video(page_title: str = "Criação de Vídeos", prefix: str = "ne
|
|
|
2120
2193
|
st.warning(f"Pedido criado, mas não foi possível descarregar o áudio: {exc}")
|
|
2121
2194
|
music_path = st.session_state.get(f"{prefix}_music_path", "")
|
|
2122
2195
|
|
|
2196
|
+
quantity = st.number_input("Quantidade", min_value=1, max_value=100, value=1, disabled=mode != "same_channel", key=f"{prefix}_quantity")
|
|
2123
2197
|
payloads: dict[str, dict[str, Any]] = {}
|
|
2124
2198
|
if mode == "general":
|
|
2125
2199
|
existing_topics = st.session_state.get(f"{prefix}_general_topics", {})
|
|
2126
2200
|
payloads = dict(st.session_state.get(f"{prefix}_general_payloads", {}))
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2201
|
+
with st.expander("Gerar Thumbnail com IA", expanded=False):
|
|
2202
|
+
if st.button("Gerar Thumbnail com IA para todos os vídeos", key=f"{prefix}_generate_general_creative", use_container_width=True):
|
|
2203
|
+
settings = read_json("settings.json", {})
|
|
2204
|
+
new_payloads: dict[str, dict[str, Any]] = {}
|
|
2205
|
+
errors: list[str] = []
|
|
2206
|
+
with st.spinner("A gerar uma thumbnail independente por vídeo…"):
|
|
2207
|
+
for channel in all_channels:
|
|
2208
|
+
try:
|
|
2209
|
+
topic_result = existing_topics.get(channel["id"])
|
|
2210
|
+
topic = str((topic_result or {}).get("topic") or "").strip()
|
|
2211
|
+
if not topic:
|
|
2212
|
+
errors.append(f"{channel.get('name', 'Canal')}: gere primeiro o tópico individual do canal.")
|
|
2213
|
+
continue
|
|
2214
|
+
generated = generate_thumbnail_for_ui(settings, channel, topic, title=topic, topic_source=str((topic_result or {}).get("topic_source") or "llm"))
|
|
2215
|
+
generated["ai_generation"]["topic"] = topic_result
|
|
2216
|
+
new_payloads[channel["id"]] = generated
|
|
2217
|
+
except CreativeGenerationError as exc:
|
|
2218
|
+
errors.append(f"{channel.get('name', 'Canal')}: {exc}")
|
|
2219
|
+
if errors:
|
|
2220
|
+
for error in errors:
|
|
2221
|
+
st.error(error)
|
|
2222
|
+
else:
|
|
2223
|
+
st.session_state[f"{prefix}_general_payloads"] = new_payloads
|
|
2224
|
+
payloads = new_payloads
|
|
2225
|
+
st.success(f"Thumbnail pronta para {len(new_payloads)} vídeo(s).")
|
|
2150
2226
|
payloads = st.session_state.get(f"{prefix}_general_payloads", payloads)
|
|
2151
2227
|
for channel in all_channels:
|
|
2152
2228
|
payload = payloads.get(channel["id"])
|
|
2153
2229
|
if not payload:
|
|
2154
2230
|
continue
|
|
2155
|
-
with st.expander(f"{channel.get('name', 'Canal')} —
|
|
2156
|
-
title_options = [item.get("title", "") for item in payload.get("title_candidates", []) if item.get("title")]
|
|
2157
|
-
if title_options:
|
|
2158
|
-
selected_title = st.selectbox("Título escolhido", title_options, index=max(0, title_options.index(payload.get("title")) if payload.get("title") in title_options else 0), key=f"{prefix}_general_title_{channel['id']}")
|
|
2159
|
-
payload["title"] = selected_title
|
|
2231
|
+
with st.expander(f"{channel.get('name', 'Canal')} — thumbnail", expanded=False):
|
|
2160
2232
|
variants = payload.get("thumbnail_variants", [])
|
|
2161
2233
|
if variants:
|
|
2162
2234
|
labels = [f"{idx + 1}. {item.get('concept', 'Variante')}" for idx, item in enumerate(variants)]
|
|
@@ -2197,83 +2269,87 @@ def render_new_video(page_title: str = "Criação de Vídeos", prefix: str = "ne
|
|
|
2197
2269
|
st.caption("A imagem ainda não foi gerada. Configure a API key em Configuração API > API Keys.")
|
|
2198
2270
|
st.caption(f"Estado da thumbnail: {payload.get('thumbnail_status', 'prompt_ready')} · texto: {payload.get('thumbnail_text') or 'sem texto'}")
|
|
2199
2271
|
else:
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
if
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
topic_for_creative = str(topic_result["topic"]).strip()
|
|
2209
|
-
st.session_state[f"{prefix}_topic"] = topic_for_creative
|
|
2210
|
-
st.session_state[f"{prefix}_topic_meta"] = topic_result
|
|
2211
|
-
generated = generate_creative_for_ui(
|
|
2212
|
-
read_json("settings.json", {}),
|
|
2213
|
-
selected_one,
|
|
2214
|
-
topic_for_creative,
|
|
2215
|
-
topic_source=_video_topic_source(topic_for_creative, prefix),
|
|
2216
|
-
)
|
|
2217
|
-
|
|
2218
|
-
st.session_state[f"{prefix}_creative_payload"] = generated
|
|
2219
|
-
st.success("Tema, título e thumbnails gerados; escolha a variante antes de criar as tarefas.")
|
|
2220
|
-
st.rerun()
|
|
2221
|
-
except CreativeGenerationError as exc:
|
|
2222
|
-
st.error(str(exc))
|
|
2223
|
-
payload = st.session_state.get(f"{prefix}_creative_payload")
|
|
2224
|
-
if payload:
|
|
2225
|
-
st.subheader("Título e Thumbnail automáticos")
|
|
2226
|
-
title_options = [item.get("title", "") for item in payload.get("title_candidates", []) if item.get("title")]
|
|
2227
|
-
if title_options:
|
|
2228
|
-
selected_title = st.selectbox("Título escolhido", title_options, index=max(0, title_options.index(payload.get("title")) if payload.get("title") in title_options else 0), key=f"{prefix}_title_choice")
|
|
2229
|
-
payload["title"] = selected_title
|
|
2230
|
-
with st.expander(f"Ver {len(title_options)} candidatos de título"):
|
|
2231
|
-
st.dataframe(payload.get("title_candidates", []), use_container_width=True, hide_index=True)
|
|
2232
|
-
variants = payload.get("thumbnail_variants", [])
|
|
2233
|
-
if variants:
|
|
2234
|
-
labels = [f"{idx + 1}. {item.get('concept', 'Variante')}" for idx, item in enumerate(variants)]
|
|
2235
|
-
selected_variant_label = st.selectbox("Thumbnail escolhida", labels, key=f"{prefix}_thumbnail_choice")
|
|
2236
|
-
variant_index = labels.index(selected_variant_label)
|
|
2237
|
-
variant = variants[variant_index]
|
|
2238
|
-
payload["thumbnail_variant"] = variant
|
|
2239
|
-
payload["thumbnail_prompt"] = variant.get("image_prompt", "")
|
|
2240
|
-
payload["thumbnail_text"] = variant.get("overlay_text", "")
|
|
2241
|
-
st.caption(f"Composição: {variant.get('composition', '')} · Cores: {variant.get('color_palette', '')}")
|
|
2242
|
-
st.code(variant.get("image_prompt", ""), language="text")
|
|
2243
|
-
thumbnail_path = str(variant.get("image_path") or payload.get("thumbnail_path") or "").strip()
|
|
2244
|
-
if st.button("Gerar imagem da thumbnail com Nano Banana", key=f"{prefix}_generate_thumbnail_image", use_container_width=True):
|
|
2272
|
+
with st.expander("Gerar Thumbnail com IA", expanded=False):
|
|
2273
|
+
topic_for_thumbnail = str(generation_settings.get("video_subject") or "").strip()
|
|
2274
|
+
if st.button("Gerar Thumbnail com IA", key=f"{prefix}_generate_creative", use_container_width=True):
|
|
2275
|
+
if selected_one is None:
|
|
2276
|
+
st.error("Seleccione primeiro um canal.")
|
|
2277
|
+
elif not topic_for_thumbnail:
|
|
2278
|
+
st.error("Preencha o Video Subject antes de gerar a thumbnail.")
|
|
2279
|
+
else:
|
|
2245
2280
|
try:
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
)
|
|
2281
|
+
existing_payload = dict(st.session_state.get(f"{prefix}_creative_payload") or {})
|
|
2282
|
+
existing_title = str(existing_payload.get("title") or topic_for_thumbnail).strip()
|
|
2283
|
+
generated = generate_thumbnail_variants_for_ui(
|
|
2284
|
+
read_json("settings.json", {}),
|
|
2285
|
+
selected_one,
|
|
2286
|
+
topic_for_thumbnail,
|
|
2287
|
+
int(quantity if mode == "same_channel" else 1),
|
|
2288
|
+
title=existing_title,
|
|
2289
|
+
topic_source=_video_topic_source(topic_for_thumbnail, prefix),
|
|
2255
2290
|
)
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
st.success("Thumbnail
|
|
2291
|
+
generated = {**existing_payload, **generated}
|
|
2292
|
+
generated["topic"] = topic_for_thumbnail
|
|
2293
|
+
generated["title"] = existing_title
|
|
2294
|
+
generated["title_candidates"] = existing_payload.get("title_candidates", [])
|
|
2295
|
+
st.session_state[f"{prefix}_creative_payload"] = generated
|
|
2296
|
+
st.success(f"Thumbnail pronta para {int(quantity if mode == 'same_channel' else 1)} vídeo(s); o título existente foi preservado.")
|
|
2262
2297
|
st.rerun()
|
|
2263
|
-
except
|
|
2298
|
+
except CreativeGenerationError as exc:
|
|
2264
2299
|
st.error(str(exc))
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2300
|
+
payload = st.session_state.get(f"{prefix}_creative_payload")
|
|
2301
|
+
if payload:
|
|
2302
|
+
st.subheader("Thumbnail automática")
|
|
2303
|
+
st.caption(f"Título preservado: {payload.get('title') or payload.get('topic') or 'Sem título'}")
|
|
2304
|
+
title_options = [item.get("title", "") for item in payload.get("title_candidates", []) if item.get("title")]
|
|
2305
|
+
if title_options:
|
|
2306
|
+
selected_title = st.selectbox("Título escolhido", title_options, index=max(0, title_options.index(payload.get("title")) if payload.get("title") in title_options else 0), key=f"{prefix}_title_choice")
|
|
2307
|
+
payload["title"] = selected_title
|
|
2308
|
+
with st.expander(f"Ver {len(title_options)} candidatos de título"):
|
|
2309
|
+
st.dataframe(payload.get("title_candidates", []), use_container_width=True, hide_index=True)
|
|
2310
|
+
variants = payload.get("thumbnail_variants", [])
|
|
2311
|
+
if variants:
|
|
2312
|
+
labels = [f"{idx + 1}. {item.get('concept', 'Variante')}" for idx, item in enumerate(variants)]
|
|
2313
|
+
selected_variant_label = st.selectbox("Thumbnail escolhida", labels, key=f"{prefix}_thumbnail_choice")
|
|
2314
|
+
variant_index = labels.index(selected_variant_label)
|
|
2315
|
+
variant = variants[variant_index]
|
|
2316
|
+
payload["thumbnail_variant"] = variant
|
|
2317
|
+
payload["thumbnail_prompt"] = variant.get("image_prompt", "")
|
|
2318
|
+
payload["thumbnail_text"] = variant.get("overlay_text", "")
|
|
2319
|
+
st.caption(f"Composição: {variant.get('composition', '')} · Cores: {variant.get('color_palette', '')}")
|
|
2320
|
+
st.code(variant.get("image_prompt", ""), language="text")
|
|
2321
|
+
thumbnail_path = str(variant.get("image_path") or payload.get("thumbnail_path") or "").strip()
|
|
2322
|
+
if st.button("Gerar imagem da thumbnail com Nano Banana", key=f"{prefix}_generate_thumbnail_image", use_container_width=True):
|
|
2323
|
+
try:
|
|
2324
|
+
thumbnail_path = str(
|
|
2325
|
+
generate_thumbnail_image(
|
|
2326
|
+
read_json("settings.json", {}),
|
|
2327
|
+
variant.get("image_prompt", ""),
|
|
2328
|
+
topic=str(payload.get("topic") or ""),
|
|
2329
|
+
variant_index=variant_index,
|
|
2330
|
+
lettering_text=str(variant.get("overlay_text") or payload.get("thumbnail_text") or ""),
|
|
2331
|
+
lettering_prompt=str(variant.get("lettering_prompt") or ""),
|
|
2332
|
+
)
|
|
2333
|
+
)
|
|
2334
|
+
variant["image_path"] = thumbnail_path
|
|
2335
|
+
payload["thumbnail_path"] = thumbnail_path
|
|
2336
|
+
payload["thumbnail_status"] = "generated"
|
|
2337
|
+
st.session_state[f"{prefix}_creative_payload"] = payload
|
|
2338
|
+
record_notification("thumbnail_generation_completed", f"Thumbnail gerada: {payload.get('title') or payload.get('topic') or 'Vídeo'}", "A thumbnail foi gerada com sucesso pelo Nano Banana.", metadata={"channel_name": selected_one.get("name") if selected_one else "", "image_path": Path(thumbnail_path).name}, dedupe_key=f"thumbnail:{thumbnail_path}")
|
|
2339
|
+
st.success("Thumbnail gerada com Nano Banana.")
|
|
2340
|
+
st.rerun()
|
|
2341
|
+
except ThumbnailGenerationError as exc:
|
|
2342
|
+
st.error(str(exc))
|
|
2343
|
+
if thumbnail_path and Path(thumbnail_path).is_file():
|
|
2344
|
+
st.image(thumbnail_path, caption="Thumbnail gerada pelo Nano Banana", use_container_width=True)
|
|
2345
|
+
payload["thumbnail_path"] = thumbnail_path
|
|
2346
|
+
payload["thumbnail_status"] = "generated"
|
|
2347
|
+
else:
|
|
2348
|
+
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.")
|
|
2349
|
+
st.session_state[f"{prefix}_creative_payload"] = payload
|
|
2272
2350
|
|
|
2273
2351
|
st.session_state[f"{prefix}_generation_settings"] = dict(generation_settings)
|
|
2274
2352
|
with st.form(f"{prefix}_form"):
|
|
2275
|
-
|
|
2276
|
-
quantity = st.number_input("Quantidade", min_value=1, max_value=100, value=1, disabled=mode != "same_channel")
|
|
2277
2353
|
language = generation_settings["script_language"]
|
|
2278
2354
|
fmt = generation_settings["video_format"]
|
|
2279
2355
|
submitted = st.form_submit_button("Criar tarefas", type="primary")
|
|
@@ -3625,21 +3701,28 @@ def render_automation():
|
|
|
3625
3701
|
for channel in channels:
|
|
3626
3702
|
channel_id = channel["id"]
|
|
3627
3703
|
with st.container(border=True):
|
|
3628
|
-
|
|
3629
|
-
with
|
|
3704
|
+
header_cols = st.columns([0.55, 2.35, 1.35, 1.5, 1.35])
|
|
3705
|
+
with header_cols[0]:
|
|
3630
3706
|
if channel.get("thumbnail_url"):
|
|
3631
3707
|
st.image(channel["thumbnail_url"], width=48)
|
|
3632
3708
|
else:
|
|
3633
3709
|
st.markdown("### YT")
|
|
3634
|
-
with
|
|
3710
|
+
with header_cols[1]:
|
|
3635
3711
|
st.write(f"**{channel.get('name', 'Sem nome')}**")
|
|
3636
3712
|
st.caption(channel.get("handle") or channel.get("url") or "sem URL")
|
|
3713
|
+
with header_cols[2]:
|
|
3714
|
+
enabled = st.toggle("Automação ligada", value=bool(channel.get("automation_on", False)), key=f"automation_on_{channel_id}")
|
|
3715
|
+
with header_cols[3]:
|
|
3716
|
+
schedule_time = st.text_input("Horário (HH:MM)", value=channel.get("automation_time", "00:00"), key=f"automation_time_{channel_id}")
|
|
3637
3717
|
blueprint_ids, blueprint_labels, current_blueprint, voice_options, current_voice = channel_default_options(channel)
|
|
3638
|
-
default_cols = st.columns(
|
|
3718
|
+
default_cols = st.columns([1.15, 1.15, 1.5, 1.7, 1.35], gap="small")
|
|
3639
3719
|
with default_cols[0]:
|
|
3640
3720
|
st.markdown("**Idioma Padrão**")
|
|
3641
3721
|
st.caption(language_label(channel.get("language") or "pt"))
|
|
3642
3722
|
with default_cols[1]:
|
|
3723
|
+
st.markdown("**Nicho Padrão**")
|
|
3724
|
+
st.caption(channel_niche_label(channel))
|
|
3725
|
+
with default_cols[2]:
|
|
3643
3726
|
automation_blueprint = st.selectbox(
|
|
3644
3727
|
"Blueprint Padrão",
|
|
3645
3728
|
blueprint_ids,
|
|
@@ -3647,9 +3730,6 @@ def render_automation():
|
|
|
3647
3730
|
format_func=lambda item: blueprint_labels.get(item, item or "Sem Blueprint padrão"),
|
|
3648
3731
|
key=f"automation_blueprint_{channel_id}",
|
|
3649
3732
|
)
|
|
3650
|
-
with default_cols[2]:
|
|
3651
|
-
st.markdown("**Nicho Padrão**")
|
|
3652
|
-
st.caption(channel_niche_label(channel))
|
|
3653
3733
|
with default_cols[3]:
|
|
3654
3734
|
automation_voice = st.selectbox(
|
|
3655
3735
|
"Narrador/Voz Padrão",
|
|
@@ -3658,11 +3738,7 @@ def render_automation():
|
|
|
3658
3738
|
format_func=lambda item: item or "Sem voz padrão",
|
|
3659
3739
|
key=f"automation_voice_{channel_id}",
|
|
3660
3740
|
)
|
|
3661
|
-
with
|
|
3662
|
-
enabled = st.toggle("Automação ON", value=bool(channel.get("automation_on", False)), key=f"automation_on_{channel_id}")
|
|
3663
|
-
with cols[3]:
|
|
3664
|
-
schedule_time = st.text_input("Horário (HH:MM)", value=channel.get("automation_time", "00:00"), key=f"automation_time_{channel_id}")
|
|
3665
|
-
with cols[4]:
|
|
3741
|
+
with default_cols[4]:
|
|
3666
3742
|
if st.button("Guardar", key=f"automation_save_{channel_id}", use_container_width=True):
|
|
3667
3743
|
if not valid_hhmm(schedule_time):
|
|
3668
3744
|
st.error("Use o formato HH:MM, por exemplo 08:30.")
|
|
@@ -5008,6 +5084,139 @@ def render_llm_provider_cards(settings: dict[str, Any], *, embedded: bool = Fals
|
|
|
5008
5084
|
st.rerun()
|
|
5009
5085
|
|
|
5010
5086
|
|
|
5087
|
+
def _media_card_config_status(card: dict[str, Any]) -> tuple[str, str]:
|
|
5088
|
+
definition = media_provider_definition(card.get("provider"))
|
|
5089
|
+
if definition.local:
|
|
5090
|
+
if not str(card.get("base_url") or "").strip():
|
|
5091
|
+
return "missing", "Endpoint local em falta"
|
|
5092
|
+
return "local", "Local / sem API key"
|
|
5093
|
+
if definition.requires_api_key and not str(card.get("api_key") or "").strip():
|
|
5094
|
+
return "missing", "Missing key"
|
|
5095
|
+
if not str(card.get("base_url") or "").strip():
|
|
5096
|
+
return "missing", "Missing Base URL"
|
|
5097
|
+
if not str(card.get("model") or "").strip() and card.get("supports_image") and card.get("provider") not in {"cloudflare_workers_ai"}:
|
|
5098
|
+
return "missing", "Missing model"
|
|
5099
|
+
return "ready", "Configured"
|
|
5100
|
+
|
|
5101
|
+
|
|
5102
|
+
def _persist_media_cards(settings: dict[str, Any], cards: list[dict[str, Any]], image_active_id: str = "", video_active_id: str = "") -> dict[str, Any]:
|
|
5103
|
+
updated = apply_media_provider_cards_to_settings(settings, cards, image_active_id, video_active_id)
|
|
5104
|
+
write_json("settings.json", updated)
|
|
5105
|
+
settings.update(updated)
|
|
5106
|
+
return updated
|
|
5107
|
+
|
|
5108
|
+
|
|
5109
|
+
def _render_media_provider_card(settings: dict[str, Any], cards: list[dict[str, Any]], index: int, *, embedded: bool = False) -> None:
|
|
5110
|
+
card = normalize_media_card(cards[index], index)
|
|
5111
|
+
cards[index] = card
|
|
5112
|
+
card_id = str(card["id"])
|
|
5113
|
+
definition = media_provider_definition(card.get("provider"))
|
|
5114
|
+
with st.container(border=True):
|
|
5115
|
+
header_cols = st.columns([3.2, 1.2])
|
|
5116
|
+
with header_cols[0]:
|
|
5117
|
+
st.subheader(definition.label)
|
|
5118
|
+
if definition.description:
|
|
5119
|
+
st.caption(definition.description)
|
|
5120
|
+
with header_cols[1]:
|
|
5121
|
+
status_kind, status_label = _media_card_config_status(card)
|
|
5122
|
+
_api_status_badge(status_label, status_kind)
|
|
5123
|
+
card_form = nullcontext() if embedded else st.form(f"media_card_form_{card_id}")
|
|
5124
|
+
with card_form:
|
|
5125
|
+
key_col, model_col = st.columns(2)
|
|
5126
|
+
with key_col:
|
|
5127
|
+
api_key = str(card.get("api_key") or "")
|
|
5128
|
+
if definition.requires_api_key:
|
|
5129
|
+
api_key = st.text_input("API key", value=api_key, type="password", key=f"media_card_{card_id}_api_key")
|
|
5130
|
+
else:
|
|
5131
|
+
st.caption("Este provider não exige API key.")
|
|
5132
|
+
api_key = ""
|
|
5133
|
+
with model_col:
|
|
5134
|
+
model = st.text_input("Modelo", value=str(card.get("model") or ""), help="ID do modelo ou rota usada pelo provider.", key=f"media_card_{card_id}_model")
|
|
5135
|
+
base_url = st.text_input("Base URL", value=str(card.get("base_url") or definition.default_base_url), key=f"media_card_{card_id}_base_url")
|
|
5136
|
+
extra_values: dict[str, str] = {}
|
|
5137
|
+
if definition.extra_fields:
|
|
5138
|
+
extra_cols = st.columns(len(definition.extra_fields))
|
|
5139
|
+
for extra_col, field_name in zip(extra_cols, definition.extra_fields):
|
|
5140
|
+
with extra_col:
|
|
5141
|
+
extra_values[field_name] = st.text_input(field_name.replace("_", " ").title(), value=str(card.get(field_name) or ""), key=f"media_card_{card_id}_{field_name}")
|
|
5142
|
+
status_cols = st.columns(4)
|
|
5143
|
+
with status_cols[0]:
|
|
5144
|
+
enabled = st.checkbox("Provider activo", value=bool(card.get("enabled", True)), key=f"media_card_{card_id}_enabled")
|
|
5145
|
+
with status_cols[1]:
|
|
5146
|
+
supports_image = st.checkbox("Pool Imagem", value=bool(card.get("supports_image", definition.supports_image)), key=f"media_card_{card_id}_image")
|
|
5147
|
+
with status_cols[2]:
|
|
5148
|
+
supports_video = st.checkbox("Pool Vídeo", value=bool(card.get("supports_video", definition.supports_video)), key=f"media_card_{card_id}_video")
|
|
5149
|
+
with status_cols[3]:
|
|
5150
|
+
priority = st.number_input("Prioridade", min_value=0, max_value=999, value=int(card.get("priority", index)), step=1, key=f"media_card_{card_id}_priority")
|
|
5151
|
+
action_cols = st.columns(3)
|
|
5152
|
+
with action_cols[0]:
|
|
5153
|
+
test_clicked = st.form_submit_button("Testar Chamada API", use_container_width=True, key=f"media_card_{card_id}_test")
|
|
5154
|
+
with action_cols[1]:
|
|
5155
|
+
save_clicked = st.form_submit_button("Salvar", type="primary", use_container_width=True, key=f"media_card_{card_id}_save")
|
|
5156
|
+
with action_cols[2]:
|
|
5157
|
+
remove_clicked = st.form_submit_button("Remover provider", use_container_width=True, key=f"media_card_{card_id}_remove")
|
|
5158
|
+
edited = dict(card)
|
|
5159
|
+
edited.update({"api_key": str(api_key or "").strip(), "model": str(model or "").strip(), "base_url": str(base_url or "").strip(), "enabled": bool(enabled), "supports_image": bool(supports_image), "supports_video": bool(supports_video), "priority": int(priority), **extra_values})
|
|
5160
|
+
cards[index] = edited
|
|
5161
|
+
if test_clicked:
|
|
5162
|
+
result = test_media_provider_card(edited)
|
|
5163
|
+
edited["test_result"] = stamp_test_result(result)
|
|
5164
|
+
_persist_media_cards(settings, cards, str(settings.get(MEDIA_IMAGE_ACTIVE_CARD_KEY) or ""), str(settings.get(MEDIA_VIDEO_ACTIVE_CARD_KEY) or ""))
|
|
5165
|
+
elif save_clicked:
|
|
5166
|
+
_persist_media_cards(settings, cards, str(settings.get(MEDIA_IMAGE_ACTIVE_CARD_KEY) or ""), str(settings.get(MEDIA_VIDEO_ACTIVE_CARD_KEY) or ""))
|
|
5167
|
+
st.success("Cartão de imagem/vídeo guardado.")
|
|
5168
|
+
st.rerun()
|
|
5169
|
+
elif remove_clicked:
|
|
5170
|
+
remaining = [item for item in cards if str(item.get("id")) != card_id]
|
|
5171
|
+
_persist_media_cards(settings, remaining, str(settings.get(MEDIA_IMAGE_ACTIVE_CARD_KEY) or ""), str(settings.get(MEDIA_VIDEO_ACTIVE_CARD_KEY) or ""))
|
|
5172
|
+
st.success("Provider de imagem/vídeo removido.")
|
|
5173
|
+
st.rerun()
|
|
5174
|
+
saved_test = edited.get("test_result") or card.get("test_result")
|
|
5175
|
+
if isinstance(saved_test, dict) and saved_test.get("message"):
|
|
5176
|
+
if saved_test.get("status") == "success":
|
|
5177
|
+
st.success("Último teste: API Key OK")
|
|
5178
|
+
else:
|
|
5179
|
+
st.error(f"Último teste: {saved_test['message']}")
|
|
5180
|
+
|
|
5181
|
+
|
|
5182
|
+
def render_media_provider_cards(settings: dict[str, Any], *, embedded: bool = False) -> None:
|
|
5183
|
+
migrated, changed = ensure_media_provider_cards(settings)
|
|
5184
|
+
cards = [dict(item) for item in migrated.get(MEDIA_CARDS_KEY, [])]
|
|
5185
|
+
if changed:
|
|
5186
|
+
settings.update(migrated)
|
|
5187
|
+
write_json("settings.json", settings)
|
|
5188
|
+
with st.expander("Imagem e Video", expanded=False):
|
|
5189
|
+
st.caption("Configure providers de imagem e vídeo em cartões independentes. O router usa apenas o pool correspondente e faz failover entre providers activos.")
|
|
5190
|
+
image_cards = [card for card in cards if card.get("supports_image")]
|
|
5191
|
+
video_cards = [card for card in cards if card.get("supports_video")]
|
|
5192
|
+
selector_cols = st.columns(3)
|
|
5193
|
+
image_options = [""] + [str(card.get("id")) for card in image_cards]
|
|
5194
|
+
video_options = [""] + [str(card.get("id")) for card in video_cards]
|
|
5195
|
+
with selector_cols[0]:
|
|
5196
|
+
image_active_id = st.selectbox("Provider principal de imagem", image_options, index=image_options.index(str(settings.get(MEDIA_IMAGE_ACTIVE_CARD_KEY) or "")) if str(settings.get(MEDIA_IMAGE_ACTIVE_CARD_KEY) or "") in image_options else 0, format_func=lambda value: "Automático / primeiro activo" if not value else next((media_provider_definition(card.get("provider")).label for card in image_cards if str(card.get("id")) == value), value), key="media_image_active_selector")
|
|
5197
|
+
with selector_cols[1]:
|
|
5198
|
+
video_active_id = st.selectbox("Provider principal de vídeo", video_options, index=video_options.index(str(settings.get(MEDIA_VIDEO_ACTIVE_CARD_KEY) or "")) if str(settings.get(MEDIA_VIDEO_ACTIVE_CARD_KEY) or "") in video_options else 0, format_func=lambda value: "Não usar pool externo" if not value else next((media_provider_definition(card.get("provider")).label for card in video_cards if str(card.get("id")) == value), value), key="media_video_active_selector")
|
|
5199
|
+
with selector_cols[2]:
|
|
5200
|
+
video_pool_enabled = st.checkbox("Usar pool de vídeo externo", value=bool(settings.get("media_video_pool_enabled", False)), key="media_video_pool_enabled_ui")
|
|
5201
|
+
if st.button("Salvar selecção dos pools", use_container_width=True, key="save_media_pool_selection") if not embedded else st.form_submit_button("Salvar selecção dos pools", use_container_width=True, key="save_media_pool_selection"):
|
|
5202
|
+
settings["media_video_pool_enabled"] = bool(video_pool_enabled)
|
|
5203
|
+
_persist_media_cards(settings, cards, image_active_id, video_active_id)
|
|
5204
|
+
st.success("Selecção dos pools guardada.")
|
|
5205
|
+
st.rerun()
|
|
5206
|
+
for index in range(len(cards)):
|
|
5207
|
+
_render_media_provider_card(settings, cards, index, embedded=embedded)
|
|
5208
|
+
st.divider()
|
|
5209
|
+
st.markdown("**Adicionar provider de imagem/vídeo**")
|
|
5210
|
+
provider_codes = [item["code"] for item in media_provider_catalog()]
|
|
5211
|
+
provider_to_add = st.selectbox("Provider de media", provider_codes, format_func=lambda value: media_provider_definition(value).label, key="media_new_provider_choice")
|
|
5212
|
+
add_clicked = st.form_submit_button("Adicionar provider de imagem/vídeo", use_container_width=True, key="add_media_provider_card") if embedded else st.button("Adicionar provider de imagem/vídeo", use_container_width=True, key="add_media_provider_card")
|
|
5213
|
+
if add_clicked:
|
|
5214
|
+
cards.append(new_media_card(provider_to_add, card_id=f"media-{provider_to_add}-{uuid.uuid4().hex[:8]}"))
|
|
5215
|
+
_persist_media_cards(settings, cards, image_active_id, video_active_id)
|
|
5216
|
+
st.success("Novo provider de imagem/vídeo adicionado.")
|
|
5217
|
+
st.rerun()
|
|
5218
|
+
|
|
5219
|
+
|
|
5011
5220
|
def render_settings():
|
|
5012
5221
|
st.title("Configuração API")
|
|
5013
5222
|
st.caption("Configuração das APIs, providers, serviços e ferramentas técnicas usados pelo Thunderbolt. As credenciais ficam no storage local e não são enviadas para o GitHub.")
|
|
@@ -5068,23 +5277,18 @@ def render_settings():
|
|
|
5068
5277
|
|
|
5069
5278
|
render_llm_provider_cards(settings, embedded=True)
|
|
5070
5279
|
|
|
5071
|
-
with st.
|
|
5072
|
-
st.
|
|
5073
|
-
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
with
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
"nano_banana",
|
|
5084
|
-
lambda: test_nano_banana_credentials(gemini_image_api_key, gemini_image_model),
|
|
5085
|
-
widget_key="api_test_nano_banana",
|
|
5086
|
-
)
|
|
5087
|
-
|
|
5280
|
+
with st.container(border=True):
|
|
5281
|
+
st.markdown("### Limite LLM NVIDIA NIM")
|
|
5282
|
+
st.caption("Quando ligado, limita apenas cartões cujo endpoint é integrate.api.nvidia.com a 40 pedidos por janela de 60 segundos, partilhados entre UI, pipeline e automações.")
|
|
5283
|
+
rpm_cols = st.columns(3)
|
|
5284
|
+
with rpm_cols[0]:
|
|
5285
|
+
llm_rpm_limit_enabled = st.checkbox("Activar limitador NVIDIA NIM — 40 RPM", value=bool(settings.get("llm_rpm_limit_enabled", False)), key="settings_llm_rpm_limit_enabled")
|
|
5286
|
+
with rpm_cols[1]:
|
|
5287
|
+
llm_rpm_limit = st.number_input("Pedidos por janela", min_value=1, max_value=1000, value=int(settings.get("llm_rpm_limit", 40)), step=1, key="settings_llm_rpm_limit")
|
|
5288
|
+
with rpm_cols[2]:
|
|
5289
|
+
llm_rpm_window_seconds = st.number_input("Janela (segundos)", min_value=1, max_value=3600, value=int(settings.get("llm_rpm_window_seconds", 60)), step=1, key="settings_llm_rpm_window_seconds")
|
|
5290
|
+
|
|
5291
|
+
render_media_provider_cards(settings, embedded=True)
|
|
5088
5292
|
|
|
5089
5293
|
with st.expander("Voz, TTS e música — Azure Speech, restantes serviços e Suno", expanded=False):
|
|
5090
5294
|
cols = st.columns(2)
|
|
@@ -5217,7 +5421,7 @@ def render_settings():
|
|
|
5217
5421
|
"moneyprinter_path": moneyprinter_path,
|
|
5218
5422
|
"kaggle_username": kaggle_username.strip(), "kaggle_api_key": kaggle_api_key.strip(), "kaggle_kernel_slug": kaggle_kernel_slug.strip() or "thunderbolt-niche-finder",
|
|
5219
5423
|
"apify_api_token": apify_api_token.strip(), "apify_actor_id": apify_actor_id.strip() or DEFAULT_ACTOR_ID, "apify_poll_interval_seconds": int(apify_poll_interval), "apify_run_timeout_seconds": int(apify_run_timeout),
|
|
5220
|
-
"
|
|
5424
|
+
"llm_rpm_limit_enabled": bool(llm_rpm_limit_enabled), "llm_rpm_limit": int(llm_rpm_limit), "llm_rpm_window_seconds": int(llm_rpm_window_seconds),
|
|
5221
5425
|
"azure_speech_key": azure_speech_key, "azure_speech_region": azure_speech_region,
|
|
5222
5426
|
"siliconflow_tts_api_key": siliconflow_tts_api_key, "minimax_tts_api_key": minimax_tts_api_key,
|
|
5223
5427
|
"minimax_tts_base_url": minimax_tts_base_url, "minimax_tts_model_id": minimax_tts_model_id, "minimax_tts_voice_id": minimax_tts_voice_id,
|