@danhachuel/thunderbolt 0.3.19 → 0.3.20
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 +86 -48
- package/hermes_ui/languages.py +106 -0
- package/hermes_ui/material_sources.py +124 -7
- package/package.json +1 -1
package/app/main.py
CHANGED
|
@@ -34,7 +34,7 @@ from hermes_ui.python_editor import AUDIO_EXTENSIONS, VIDEO_EXTENSIONS, PythonEd
|
|
|
34
34
|
from hermes_ui.cuts import CutsError, download_direct_video_url, generate_clips, list_generated_videos as list_cut_generated_videos, list_runs as list_cut_runs, list_video_files as list_cut_video_files, manifest_bytes as cut_manifest_bytes, store_uploaded_video, zip_run as zip_cut_run
|
|
35
35
|
from hermes_ui.mcp import detect_local_service, install_skill_locally, load_integrations, load_server_config, read_packaged_skill, save_server_config, update_integration
|
|
36
36
|
from hermes_ui.mcp_server import server_status, start_server, stop_server
|
|
37
|
-
from hermes_ui.material_sources import
|
|
37
|
+
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
|
|
38
38
|
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
|
|
39
39
|
from hermes_ui.music import list_music_files, materialize_suno_audio, request_suno_generation, store_music_file
|
|
40
40
|
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
|
|
@@ -3663,51 +3663,86 @@ def render_google_accounts():
|
|
|
3663
3663
|
st.success("Configuração global do YouTube guardada em Contas Google.")
|
|
3664
3664
|
st.rerun()
|
|
3665
3665
|
|
|
3666
|
-
def
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3666
|
+
def _persist_material_source_cards(settings: dict[str, Any], cards: list[dict[str, Any]], active_card_id: str = "") -> dict[str, Any]:
|
|
3667
|
+
updated = apply_material_source_cards_to_settings(settings, cards, active_card_id)
|
|
3668
|
+
write_json("settings.json", updated)
|
|
3669
|
+
return updated
|
|
3670
|
+
|
|
3671
|
+
|
|
3672
|
+
def _material_source_card_definition(provider: str) -> dict[str, str]:
|
|
3673
|
+
definition = material_source_definition(provider)
|
|
3674
|
+
if definition is not None:
|
|
3675
|
+
return definition
|
|
3676
|
+
return {
|
|
3677
|
+
"code": "local",
|
|
3678
|
+
"label": "Ficheiros locais",
|
|
3679
|
+
"description": "Usar materiais já existentes no storage local; não requer API key.",
|
|
3680
|
+
"legacy_key": "",
|
|
3681
|
+
}
|
|
3682
|
+
|
|
3683
|
+
|
|
3684
|
+
def _render_material_source_card(settings: dict[str, Any], cards: list[dict[str, Any]], index: int) -> None:
|
|
3685
|
+
card = normalize_material_card(cards[index], index)
|
|
3686
|
+
cards[index] = card
|
|
3687
|
+
card_id = str(card["id"])
|
|
3688
|
+
provider = str(card.get("provider") or "pexels")
|
|
3689
|
+
definition = _material_source_card_definition(provider)
|
|
3690
|
+
is_local = provider == "local"
|
|
3691
|
+
active_card_id = str(settings.get("material_active_card_id") or "")
|
|
3692
|
+
with st.container(border=True):
|
|
3693
|
+
header_cols = st.columns([3.2, 1.2])
|
|
3694
|
+
with header_cols[0]:
|
|
3695
|
+
st.subheader(definition["label"])
|
|
3696
|
+
st.caption(definition["description"])
|
|
3697
|
+
with header_cols[1]:
|
|
3698
|
+
_render_credential_status("" if is_local else card.get("api_key"), local=is_local, required=not is_local)
|
|
3699
|
+
with st.form(f"material_source_card_form_{card_id}"):
|
|
3700
|
+
content_cols = st.columns(2)
|
|
3701
|
+
with content_cols[0]:
|
|
3702
|
+
if is_local:
|
|
3703
|
+
st.caption("Esta fonte não usa API key.")
|
|
3704
|
+
api_key = ""
|
|
3705
|
+
else:
|
|
3706
|
+
api_key = st.text_input("API Key", value=str(card.get("api_key") or ""), type="password", key=f"material_card_{card_id}_api_key")
|
|
3707
|
+
with content_cols[1]:
|
|
3708
|
+
enabled = st.checkbox("Fonte activa", value=bool(card.get("enabled", True)), key=f"material_card_{card_id}_enabled")
|
|
3709
|
+
selected = st.checkbox(
|
|
3710
|
+
"Usar esta fonte na pipeline",
|
|
3711
|
+
value=active_card_id == card_id,
|
|
3712
|
+
key=f"material_card_{card_id}_selected",
|
|
3713
|
+
)
|
|
3714
|
+
save_card = st.form_submit_button("Salvar", type="primary", use_container_width=True, key=f"material_card_{card_id}_save")
|
|
3715
|
+
if save_card:
|
|
3716
|
+
cards[index] = {**card, "api_key": str(api_key or "").strip(), "enabled": bool(enabled)}
|
|
3717
|
+
selected_id = card_id if selected and enabled else active_card_id
|
|
3718
|
+
_persist_material_source_cards(settings, cards, selected_id)
|
|
3719
|
+
st.success(f"Fonte {definition['label']} guardada.")
|
|
3691
3720
|
st.rerun()
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
if add_key:
|
|
3701
|
-
st.session_state[row_key] = row_count + 1
|
|
3702
|
-
st.rerun()
|
|
3703
|
-
if save_keys:
|
|
3704
|
-
update_material_api_keys(settings, selected_source, key_values)
|
|
3705
|
-
settings["video_source"] = selected_source
|
|
3721
|
+
|
|
3722
|
+
|
|
3723
|
+
def render_material_source_api_keys(settings: dict[str, Any]) -> None:
|
|
3724
|
+
st.subheader("Fontes de Materiais")
|
|
3725
|
+
st.caption("Configure cada provedor num cartão independente. Pode repetir o mesmo provedor para guardar várias API keys; a fonte seleccionada será usada pela pipeline.")
|
|
3726
|
+
migrated, changed = ensure_material_source_cards(settings)
|
|
3727
|
+
cards = [dict(item) for item in migrated.get("material_source_cards", [])]
|
|
3728
|
+
if changed:
|
|
3706
3729
|
write_json("settings.json", settings)
|
|
3707
|
-
|
|
3708
|
-
|
|
3730
|
+
for index in range(len(cards)):
|
|
3731
|
+
_render_material_source_card(settings, cards, index)
|
|
3732
|
+
|
|
3733
|
+
st.divider()
|
|
3734
|
+
st.markdown("**Adicionar fonte de materiais**")
|
|
3735
|
+
provider_codes = [item["code"] for item in material_source_catalog()] + ["local"]
|
|
3736
|
+
provider_to_add = st.selectbox(
|
|
3737
|
+
"Provedor de materiais",
|
|
3738
|
+
provider_codes,
|
|
3739
|
+
format_func=lambda value: _material_source_card_definition(value)["label"],
|
|
3740
|
+
key="material_new_provider_choice",
|
|
3741
|
+
)
|
|
3742
|
+
if st.button("Configurar Nova Fonte de Materiais", type="primary", use_container_width=True, key="add_material_source_card"):
|
|
3743
|
+
cards.append(new_material_card(provider_to_add, card_id=f"material-{provider_to_add}-{uuid.uuid4().hex[:8]}"))
|
|
3744
|
+
_persist_material_source_cards(settings, cards, str(settings.get("material_active_card_id") or ""))
|
|
3709
3745
|
st.rerun()
|
|
3710
|
-
st.caption(f"{len(current_keys)} chave(s) actualmente guardada(s) para {source_labels.get(selected_source, selected_source)}. As chaves são mantidas no storage local e usadas com rotação interna.")
|
|
3711
3746
|
|
|
3712
3747
|
|
|
3713
3748
|
def _api_status_badge(label: str, kind: str = "missing") -> None:
|
|
@@ -3927,7 +3962,7 @@ def render_settings():
|
|
|
3927
3962
|
key=f"settings_{key}",
|
|
3928
3963
|
)
|
|
3929
3964
|
|
|
3930
|
-
api_keys_tab, material_sources_tab, voice_test_tab = render_localized_tabs(["API Keys", "Fontes de Materiais", "Teste de Voz"])
|
|
3965
|
+
api_keys_tab, google_accounts_tab, material_sources_tab, voice_test_tab = render_localized_tabs(["API Keys", "Contas Google", "Fontes de Materiais", "Teste de Voz"])
|
|
3931
3966
|
|
|
3932
3967
|
with api_keys_tab:
|
|
3933
3968
|
with st.container(border=True):
|
|
@@ -4062,8 +4097,11 @@ def render_settings():
|
|
|
4062
4097
|
st.success("Configurações guardadas localmente. Indique uma pasta válida do motor de vídeo para sincronizar config.toml.")
|
|
4063
4098
|
except Exception as exc:
|
|
4064
4099
|
st.warning(f"Configurações locais guardadas, mas não foi possível sincronizar config.toml: {exc}")
|
|
4065
|
-
|
|
4066
|
-
|
|
4100
|
+
with google_accounts_tab:
|
|
4101
|
+
render_google_accounts()
|
|
4102
|
+
|
|
4103
|
+
with material_sources_tab:
|
|
4104
|
+
render_material_source_api_keys(settings)
|
|
4067
4105
|
|
|
4068
4106
|
with voice_test_tab:
|
|
4069
4107
|
st.subheader("Teste de Voz")
|
|
@@ -4527,7 +4565,6 @@ def main():
|
|
|
4527
4565
|
]
|
|
4528
4566
|
settings_items = [
|
|
4529
4567
|
("MCP", ":material/hub:", "MCP"),
|
|
4530
|
-
("Contas Google", ":material/account_circle:", "Contas Google"),
|
|
4531
4568
|
("Notificações", ":material/notifications:", "Notificações"),
|
|
4532
4569
|
("Configuração API", ":material/settings:", "Configuração API"),
|
|
4533
4570
|
]
|
|
@@ -4585,7 +4622,8 @@ def main():
|
|
|
4585
4622
|
"Blueprints": "Blueprints Youtube",
|
|
4586
4623
|
"Configurações Técnicas": "Configuração API",
|
|
4587
4624
|
"Models AI": "AI Influencers",
|
|
4588
|
-
"Contas Google/YouTube — canais em lote": "
|
|
4625
|
+
"Contas Google/YouTube — canais em lote": "Configuração API",
|
|
4626
|
+
"Contas Google": "Configuração API",
|
|
4589
4627
|
}
|
|
4590
4628
|
all_children = [item for items in groups.values() for item in items]
|
|
4591
4629
|
valid_targets = {item[0] for item in top_pages + all_children}
|
package/hermes_ui/languages.py
CHANGED
|
@@ -922,3 +922,109 @@ __all__ = [
|
|
|
922
922
|
"language_label", "ui_language_menu_label", "language_locale", "language_option_codes", "language_option_labels",
|
|
923
923
|
"ui_text", "video_language_label", "video_language_options",
|
|
924
924
|
]
|
|
925
|
+
|
|
926
|
+
|
|
927
|
+
_MATERIAL_SOURCE_TRANSLATIONS: dict[str, dict[str, str]] = {
|
|
928
|
+
"pt": {
|
|
929
|
+
"Fontes de Materiais": "Fontes de Materiais",
|
|
930
|
+
"Configure cada provedor num cartão independente. Pode repetir o mesmo provedor para guardar várias API keys; a fonte seleccionada será usada pela pipeline.": "Configure cada provedor num cartão independente. Pode repetir o mesmo provedor para guardar várias API keys; a fonte seleccionada será usada pela pipeline.",
|
|
931
|
+
"Adicionar fonte de materiais": "Adicionar fonte de materiais",
|
|
932
|
+
"Provedor de materiais": "Provedor de materiais",
|
|
933
|
+
"Configurar Nova Fonte de Materiais": "Configurar Nova Fonte de Materiais",
|
|
934
|
+
"Fonte activa": "Fonte activa",
|
|
935
|
+
"Usar esta fonte na pipeline": "Usar esta fonte na pipeline",
|
|
936
|
+
"Esta fonte não usa API key.": "Esta fonte não usa API key.",
|
|
937
|
+
},
|
|
938
|
+
"en": {
|
|
939
|
+
"Fontes de Materiais": "Media Sources",
|
|
940
|
+
"Configure cada provedor num cartão independente. Pode repetir o mesmo provedor para guardar várias API keys; a fonte seleccionada será usada pela pipeline.": "Configure each provider in an independent card. You can repeat the same provider to store multiple API keys; the selected source will be used by the pipeline.",
|
|
941
|
+
"Adicionar fonte de materiais": "Add media source",
|
|
942
|
+
"Provedor de materiais": "Media provider",
|
|
943
|
+
"Configurar Nova Fonte de Materiais": "Configure New Media Source",
|
|
944
|
+
"Fonte activa": "Source active",
|
|
945
|
+
"Usar esta fonte na pipeline": "Use this source in the pipeline",
|
|
946
|
+
"Esta fonte não usa API key.": "This source does not use an API key.",
|
|
947
|
+
},
|
|
948
|
+
"zh": {
|
|
949
|
+
"Fontes de Materiais": "素材来源",
|
|
950
|
+
"Configure cada provedor num cartão independente. Pode repetir o mesmo provedor para guardar várias API keys; a fonte seleccionada será usada pela pipeline.": "在独立卡片中配置每个提供商。可以重复同一提供商以保存多个 API 密钥;所选来源将由流水线使用。",
|
|
951
|
+
"Adicionar fonte de materiais": "添加素材来源",
|
|
952
|
+
"Provedor de materiais": "素材提供商",
|
|
953
|
+
"Configurar Nova Fonte de Materiais": "配置新的素材来源",
|
|
954
|
+
"Fonte activa": "来源已启用",
|
|
955
|
+
"Usar esta fonte na pipeline": "在流水线中使用此来源",
|
|
956
|
+
"Esta fonte não usa API key.": "此来源不使用 API 密钥。",
|
|
957
|
+
},
|
|
958
|
+
"de": {
|
|
959
|
+
"Fontes de Materiais": "Medienquellen",
|
|
960
|
+
"Configure cada provedor num cartão independente. Pode repetir o mesmo provedor para guardar várias API keys; a fonte seleccionada será usada pela pipeline.": "Konfigurieren Sie jeden Anbieter in einer eigenen Karte. Derselbe Anbieter kann für mehrere API-Schlüssel wiederholt werden; die ausgewählte Quelle wird von der Pipeline verwendet.",
|
|
961
|
+
"Adicionar fonte de materiais": "Medienquelle hinzufügen",
|
|
962
|
+
"Provedor de materiais": "Medienanbieter",
|
|
963
|
+
"Configurar Nova Fonte de Materiais": "Neue Medienquelle konfigurieren",
|
|
964
|
+
"Fonte activa": "Quelle aktiv",
|
|
965
|
+
"Usar esta fonte na pipeline": "Diese Quelle in der Pipeline verwenden",
|
|
966
|
+
"Esta fonte não usa API key.": "Diese Quelle verwendet keinen API-Schlüssel.",
|
|
967
|
+
},
|
|
968
|
+
"vi": {
|
|
969
|
+
"Fontes de Materiais": "Nguồn phương tiện",
|
|
970
|
+
"Configure cada provedor num cartão independente. Pode repetir o mesmo provedor para guardar várias API keys; a fonte seleccionada será usada pela pipeline.": "Định cấu hình từng nhà cung cấp trong một thẻ riêng. Có thể lặp lại cùng nhà cung cấp để lưu nhiều API key; nguồn được chọn sẽ được quy trình sử dụng.",
|
|
971
|
+
"Adicionar fonte de materiais": "Thêm nguồn phương tiện",
|
|
972
|
+
"Provedor de materiais": "Nhà cung cấp phương tiện",
|
|
973
|
+
"Configurar Nova Fonte de Materiais": "Định cấu hình nguồn phương tiện mới",
|
|
974
|
+
"Fonte activa": "Nguồn đang hoạt động",
|
|
975
|
+
"Usar esta fonte na pipeline": "Dùng nguồn này trong quy trình",
|
|
976
|
+
"Esta fonte não usa API key.": "Nguồn này không dùng API key.",
|
|
977
|
+
},
|
|
978
|
+
"tr": {
|
|
979
|
+
"Fontes de Materiais": "Medya kaynakları",
|
|
980
|
+
"Configure cada provedor num cartão independente. Pode repetir o mesmo provedor para guardar várias API keys; a fonte seleccionada será usada pela pipeline.": "Her sağlayıcıyı bağımsız bir kartta yapılandırın. Birden fazla API anahtarı saklamak için aynı sağlayıcıyı tekrarlayabilirsiniz; seçilen kaynak akış tarafından kullanılacaktır.",
|
|
981
|
+
"Adicionar fonte de materiais": "Medya kaynağı ekle",
|
|
982
|
+
"Provedor de materiais": "Medya sağlayıcısı",
|
|
983
|
+
"Configurar Nova Fonte de Materiais": "Yeni medya kaynağını yapılandır",
|
|
984
|
+
"Fonte activa": "Kaynak etkin",
|
|
985
|
+
"Usar esta fonte na pipeline": "Bu kaynağı akışta kullan",
|
|
986
|
+
"Esta fonte não usa API key.": "Bu kaynak API anahtarı kullanmaz.",
|
|
987
|
+
},
|
|
988
|
+
"ru": {
|
|
989
|
+
"Fontes de Materiais": "Источники материалов",
|
|
990
|
+
"Configure cada provedor num cartão independente. Pode repetir o mesmo provedor para guardar várias API keys; a fonte seleccionada será usada pela pipeline.": "Настройте каждого провайдера в отдельной карточке. Один и тот же провайдер можно повторить для хранения нескольких API-ключей; выбранный источник будет использоваться конвейером.",
|
|
991
|
+
"Adicionar fonte de materiais": "Добавить источник материалов",
|
|
992
|
+
"Provedor de materiais": "Провайдер материалов",
|
|
993
|
+
"Configurar Nova Fonte de Materiais": "Настроить новый источник материалов",
|
|
994
|
+
"Fonte activa": "Источник активен",
|
|
995
|
+
"Usar esta fonte na pipeline": "Использовать этот источник в конвейере",
|
|
996
|
+
"Esta fonte não usa API key.": "Этот источник не использует API-ключ.",
|
|
997
|
+
},
|
|
998
|
+
"es": {
|
|
999
|
+
"Fontes de Materiais": "Fuentes de medios",
|
|
1000
|
+
"Configure cada provedor num cartão independente. Pode repetir o mesmo provedor para guardar várias API keys; a fonte seleccionada será usada pela pipeline.": "Configura cada proveedor en una tarjeta independiente. Puedes repetir el mismo proveedor para guardar varias claves API; la fuente seleccionada será utilizada por el flujo.",
|
|
1001
|
+
"Adicionar fonte de materiais": "Añadir fuente de medios",
|
|
1002
|
+
"Provedor de materiais": "Proveedor de medios",
|
|
1003
|
+
"Configurar Nova Fonte de Materiais": "Configurar nueva fuente de medios",
|
|
1004
|
+
"Fonte activa": "Fuente activa",
|
|
1005
|
+
"Usar esta fonte na pipeline": "Usar esta fuente en el flujo",
|
|
1006
|
+
"Esta fonte não usa API key.": "Esta fuente no utiliza una clave API.",
|
|
1007
|
+
},
|
|
1008
|
+
"id": {
|
|
1009
|
+
"Fontes de Materiais": "Sumber media",
|
|
1010
|
+
"Configure cada provedor num cartão independente. Pode repetir o mesmo provedor para guardar várias API keys; a fonte seleccionada será usada pela pipeline.": "Konfigurasikan setiap penyedia dalam kartu terpisah. Penyedia yang sama dapat diulang untuk menyimpan beberapa kunci API; sumber yang dipilih akan digunakan oleh pipeline.",
|
|
1011
|
+
"Adicionar fonte de materiais": "Tambah sumber media",
|
|
1012
|
+
"Provedor de materiais": "Penyedia media",
|
|
1013
|
+
"Configurar Nova Fonte de Materiais": "Konfigurasikan Sumber Media Baru",
|
|
1014
|
+
"Fonte activa": "Sumber aktif",
|
|
1015
|
+
"Usar esta fonte na pipeline": "Gunakan sumber ini di pipeline",
|
|
1016
|
+
"Esta fonte não usa API key.": "Sumber ini tidak menggunakan kunci API.",
|
|
1017
|
+
},
|
|
1018
|
+
"it": {
|
|
1019
|
+
"Fontes de Materiais": "Fonti multimediali",
|
|
1020
|
+
"Configure cada provedor num cartão independente. Pode repetir o mesmo provedor para guardar várias API keys; a fonte seleccionada será usada pela pipeline.": "Configura ogni provider in una scheda indipendente. Puoi ripetere lo stesso provider per conservare più API key; la fonte selezionata sarà usata dalla pipeline.",
|
|
1021
|
+
"Adicionar fonte de materiais": "Aggiungi fonte multimediale",
|
|
1022
|
+
"Provedor de materiais": "Provider multimediale",
|
|
1023
|
+
"Configurar Nova Fonte de Materiais": "Configura nuova fonte multimediale",
|
|
1024
|
+
"Fonte activa": "Fonte attiva",
|
|
1025
|
+
"Usar esta fonte na pipeline": "Usa questa fonte nella pipeline",
|
|
1026
|
+
"Esta fonte não usa API key.": "Questa fonte non usa una API key.",
|
|
1027
|
+
},
|
|
1028
|
+
}
|
|
1029
|
+
for _language_code, _material_source_translation in _MATERIAL_SOURCE_TRANSLATIONS.items():
|
|
1030
|
+
UI_TRANSLATIONS[_language_code].update(_material_source_translation)
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from typing import Any
|
|
4
|
+
from uuid import uuid4
|
|
4
5
|
|
|
5
6
|
|
|
7
|
+
MATERIAL_CARDS_KEY = "material_source_cards"
|
|
8
|
+
MATERIAL_ACTIVE_CARD_KEY = "material_active_card_id"
|
|
9
|
+
|
|
6
10
|
MATERIAL_SOURCE_CATALOG: tuple[dict[str, str], ...] = (
|
|
7
11
|
{"code": "pexels", "label": "Pexels", "description": "Banco de vídeos e imagens para materiais da pipeline.", "legacy_key": "pexels_api_keys"},
|
|
8
12
|
{"code": "pixabay", "label": "Pixabay", "description": "Banco de vídeos e imagens para materiais da pipeline.", "legacy_key": "pixabay_api_keys"},
|
|
@@ -13,6 +17,9 @@ MATERIAL_SOURCE_CATALOG: tuple[dict[str, str], ...] = (
|
|
|
13
17
|
)
|
|
14
18
|
|
|
15
19
|
|
|
20
|
+
_SOURCE_BY_CODE = {item["code"]: item for item in MATERIAL_SOURCE_CATALOG}
|
|
21
|
+
|
|
22
|
+
|
|
16
23
|
def _as_key_list(value: Any) -> list[str]:
|
|
17
24
|
if isinstance(value, list):
|
|
18
25
|
values = value
|
|
@@ -32,12 +39,124 @@ def material_source_catalog() -> list[dict[str, str]]:
|
|
|
32
39
|
return [dict(item) for item in MATERIAL_SOURCE_CATALOG]
|
|
33
40
|
|
|
34
41
|
|
|
35
|
-
def
|
|
42
|
+
def material_source_definition(source: Any) -> dict[str, str] | None:
|
|
43
|
+
return _SOURCE_BY_CODE.get(str(source or "").strip().lower())
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _new_card(provider: str, api_key: str = "", *, card_id: str | None = None) -> dict[str, Any]:
|
|
47
|
+
return {
|
|
48
|
+
"id": card_id or f"material-{provider}-{uuid4().hex[:8]}",
|
|
49
|
+
"provider": provider,
|
|
50
|
+
"api_key": str(api_key or "").strip(),
|
|
51
|
+
"enabled": True,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def normalize_material_card(card: Any, index: int = 0) -> dict[str, Any]:
|
|
56
|
+
raw = card if isinstance(card, dict) else {}
|
|
57
|
+
provider = str(raw.get("provider") or raw.get("source") or "pexels").strip().lower()
|
|
58
|
+
if provider not in _SOURCE_BY_CODE and provider != "local":
|
|
59
|
+
provider = "pexels"
|
|
60
|
+
card_id = str(raw.get("id") or f"material-{provider}-{index}").strip()
|
|
61
|
+
return {
|
|
62
|
+
"id": card_id or f"material-{provider}-{index}",
|
|
63
|
+
"provider": provider,
|
|
64
|
+
"api_key": str(raw.get("api_key") or raw.get("key") or "").strip() if provider != "local" else "",
|
|
65
|
+
"enabled": bool(raw.get("enabled", True)),
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _cards_key_mapping(cards: list[dict[str, Any]]) -> dict[str, list[str]]:
|
|
70
|
+
mapping = {item["code"]: [] for item in MATERIAL_SOURCE_CATALOG}
|
|
71
|
+
for index, raw_card in enumerate(cards):
|
|
72
|
+
card = normalize_material_card(raw_card, index)
|
|
73
|
+
provider = card["provider"]
|
|
74
|
+
api_key = str(card.get("api_key") or "").strip()
|
|
75
|
+
if card.get("enabled", True) and provider in mapping and api_key and api_key not in mapping[provider]:
|
|
76
|
+
mapping[provider].append(api_key)
|
|
77
|
+
return mapping
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _sync_legacy_material_keys(settings: dict[str, Any], mapping: dict[str, list[str]]) -> None:
|
|
81
|
+
settings["material_api_keys"] = mapping
|
|
82
|
+
for item in MATERIAL_SOURCE_CATALOG:
|
|
83
|
+
settings[item["legacy_key"]] = list(mapping.get(item["code"], []))
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def ensure_material_source_cards(settings: dict[str, Any]) -> tuple[dict[str, Any], bool]:
|
|
87
|
+
"""Migrate legacy per-source key lists into stable independent material cards."""
|
|
88
|
+
original = settings.get(MATERIAL_CARDS_KEY)
|
|
89
|
+
changed = False
|
|
90
|
+
if isinstance(original, list) and original:
|
|
91
|
+
cards = [normalize_material_card(item, index) for index, item in enumerate(original)]
|
|
92
|
+
changed = cards != original
|
|
93
|
+
else:
|
|
94
|
+
cards: list[dict[str, Any]] = []
|
|
95
|
+
for item in MATERIAL_SOURCE_CATALOG:
|
|
96
|
+
for key in material_api_keys(settings, item["code"], _ignore_cards=True):
|
|
97
|
+
cards.append(_new_card(item["code"], key, card_id=f"material-{item['code']}-{len(cards)}"))
|
|
98
|
+
if not cards:
|
|
99
|
+
selected = selected_material_source(settings)
|
|
100
|
+
cards.append(_new_card(selected if selected in _SOURCE_BY_CODE else "pexels", card_id="material-default-0"))
|
|
101
|
+
changed = True
|
|
102
|
+
|
|
103
|
+
settings[MATERIAL_CARDS_KEY] = cards
|
|
104
|
+
active_id = str(settings.get(MATERIAL_ACTIVE_CARD_KEY) or "").strip()
|
|
105
|
+
valid_ids = {str(card["id"]) for card in cards}
|
|
106
|
+
if active_id not in valid_ids:
|
|
107
|
+
selected_source = selected_material_source(settings)
|
|
108
|
+
matching = next((card for card in cards if card["provider"] == selected_source), None)
|
|
109
|
+
active_id = str(matching["id"]) if matching else str(cards[0]["id"])
|
|
110
|
+
settings[MATERIAL_ACTIVE_CARD_KEY] = active_id
|
|
111
|
+
changed = True
|
|
112
|
+
|
|
113
|
+
mapping = _cards_key_mapping(cards)
|
|
114
|
+
if settings.get("material_api_keys") != mapping:
|
|
115
|
+
_sync_legacy_material_keys(settings, mapping)
|
|
116
|
+
changed = True
|
|
117
|
+
return settings, changed
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def ensure_material_source_cards_for_ui(settings: dict[str, Any]) -> list[dict[str, Any]]:
|
|
121
|
+
migrated, _ = ensure_material_source_cards(settings)
|
|
122
|
+
return [dict(item) for item in migrated.get(MATERIAL_CARDS_KEY, [])]
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def new_material_card(provider: str, *, card_id: str | None = None) -> dict[str, Any]:
|
|
126
|
+
code = str(provider or "").strip().lower()
|
|
127
|
+
if code not in _SOURCE_BY_CODE:
|
|
128
|
+
raise ValueError("Fonte de materiais inválida.")
|
|
129
|
+
return _new_card(code, card_id=card_id)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def apply_material_source_cards_to_settings(
|
|
133
|
+
settings: dict[str, Any],
|
|
134
|
+
cards: list[dict[str, Any]],
|
|
135
|
+
active_card_id: str = "",
|
|
136
|
+
) -> dict[str, Any]:
|
|
137
|
+
normalized_cards = [normalize_material_card(item, index) for index, item in enumerate(cards)]
|
|
138
|
+
if not normalized_cards:
|
|
139
|
+
normalized_cards = [_new_card("pexels", card_id="material-default-0")]
|
|
140
|
+
settings[MATERIAL_CARDS_KEY] = normalized_cards
|
|
141
|
+
selected_card = next((card for card in normalized_cards if str(card["id"]) == str(active_card_id)), None)
|
|
142
|
+
if selected_card is None:
|
|
143
|
+
selected_card = next((card for card in normalized_cards if card.get("enabled", True)), normalized_cards[0])
|
|
144
|
+
settings[MATERIAL_ACTIVE_CARD_KEY] = str(selected_card["id"])
|
|
145
|
+
settings["video_source"] = str(selected_card["provider"])
|
|
146
|
+
_sync_legacy_material_keys(settings, _cards_key_mapping(normalized_cards))
|
|
147
|
+
return settings
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def material_api_keys(settings: dict[str, Any], source: str, _ignore_cards: bool = False) -> list[str]:
|
|
36
151
|
code = str(source or "").strip().lower()
|
|
152
|
+
if not _ignore_cards:
|
|
153
|
+
cards = settings.get(MATERIAL_CARDS_KEY)
|
|
154
|
+
if isinstance(cards, list) and cards:
|
|
155
|
+
return _cards_key_mapping(cards).get(code, [])
|
|
37
156
|
saved = settings.get("material_api_keys", {})
|
|
38
157
|
if isinstance(saved, dict) and code in saved:
|
|
39
158
|
return _as_key_list(saved.get(code))
|
|
40
|
-
legacy_key =
|
|
159
|
+
legacy_key = _SOURCE_BY_CODE.get(code, {}).get("legacy_key", f"{code}_api_keys")
|
|
41
160
|
return _as_key_list(settings.get(legacy_key, ""))
|
|
42
161
|
|
|
43
162
|
|
|
@@ -47,18 +166,16 @@ def all_material_api_keys(settings: dict[str, Any]) -> dict[str, list[str]]:
|
|
|
47
166
|
|
|
48
167
|
def update_material_api_keys(settings: dict[str, Any], source: str, keys: list[str]) -> dict[str, Any]:
|
|
49
168
|
code = str(source or "").strip().lower()
|
|
50
|
-
if code not in
|
|
169
|
+
if code not in _SOURCE_BY_CODE:
|
|
51
170
|
raise ValueError("Fonte de materiais inválida.")
|
|
52
171
|
cleaned = _as_key_list(keys)
|
|
53
172
|
mapping = all_material_api_keys(settings)
|
|
54
173
|
mapping[code] = cleaned
|
|
55
|
-
settings
|
|
56
|
-
source_entry = next(item for item in MATERIAL_SOURCE_CATALOG if item["code"] == code)
|
|
57
|
-
settings[source_entry["legacy_key"]] = cleaned
|
|
174
|
+
_sync_legacy_material_keys(settings, mapping)
|
|
58
175
|
return settings
|
|
59
176
|
|
|
60
177
|
|
|
61
178
|
def selected_material_source(settings: dict[str, Any]) -> str:
|
|
62
179
|
source = str(settings.get("video_source") or "pexels").strip().lower()
|
|
63
|
-
valid =
|
|
180
|
+
valid = set(_SOURCE_BY_CODE) | {"local"}
|
|
64
181
|
return source if source in valid else "pexels"
|
package/package.json
CHANGED