@danhachuel/thunderbolt 0.3.19 → 0.3.21
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 +195 -139
- package/hermes_ui/languages.py +107 -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
|
|
@@ -3437,103 +3437,116 @@ def render_google_accounts():
|
|
|
3437
3437
|
missing_document_parts = list(direct_status.get("missing_cookies", []))
|
|
3438
3438
|
if not direct_status.get("has_session_info"):
|
|
3439
3439
|
missing_document_parts.append("sessionInfo")
|
|
3440
|
+
account_ready = bool(
|
|
3441
|
+
account_email_snapshot != "sem e-mail"
|
|
3442
|
+
and str(batch_account.get("client_id") or "").strip()
|
|
3443
|
+
and str(batch_account.get("client_secret") or "").strip()
|
|
3444
|
+
and bool(direct_status.get("document_exists"))
|
|
3445
|
+
and not missing_document_parts
|
|
3446
|
+
)
|
|
3440
3447
|
if missing_document_parts:
|
|
3441
3448
|
youtube_accounts_missing_document.append(account_email_snapshot)
|
|
3442
3449
|
|
|
3443
|
-
with st.
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
with
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
"
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
status_cols = st.columns(3)
|
|
3478
|
-
with status_cols[0]:
|
|
3479
|
-
(st.success if account_status.ok else st.warning)(account_status.message)
|
|
3480
|
-
with status_cols[1]:
|
|
3481
|
-
if st.button("Autorizar/Reautorizar", key=f"batch_authorize_settings_{account_id}", use_container_width=True):
|
|
3482
|
-
result = authorize_youtube_batch_account(batch_account, STORAGE)
|
|
3483
|
-
(st.success if result.ok else st.error)(result.message)
|
|
3484
|
-
if result.ok:
|
|
3485
|
-
st.rerun()
|
|
3486
|
-
with status_cols[2]:
|
|
3487
|
-
if st.button("Apagar conta", icon=":material/delete:", key=f"batch_remove_settings_{account_id}", use_container_width=True):
|
|
3488
|
-
delete_youtube_batch_token(batch_account, STORAGE)
|
|
3489
|
-
delete_credentials_document(STORAGE, batch_account)
|
|
3490
|
-
remaining_accounts = [account for account in batch_accounts if str(account.get("id")) != account_id]
|
|
3491
|
-
settings["youtube_batch_accounts"] = remaining_accounts
|
|
3492
|
-
if settings.get("youtube_batch_selected_account_id") == account_id:
|
|
3493
|
-
settings["youtube_batch_selected_account_id"] = str(remaining_accounts[0].get("id")) if remaining_accounts else ""
|
|
3494
|
-
for channel in channel_state:
|
|
3495
|
-
if str(channel.get("google_account_id") or "") == account_id:
|
|
3496
|
-
channel.update({"google_account_id": "", "google_account_email": ""})
|
|
3497
|
-
write_json("channels.json", channel_state)
|
|
3498
|
-
write_json("settings.json", settings)
|
|
3499
|
-
st.rerun()
|
|
3500
|
-
|
|
3501
|
-
if save_account:
|
|
3502
|
-
if "@" not in account_email.strip():
|
|
3503
|
-
st.error("Informe um e-mail Google válido.")
|
|
3504
|
-
elif not account_client_id.strip() or not account_client_secret.strip():
|
|
3505
|
-
st.error("Informe o Client ID e o Client Secret desta conta.")
|
|
3450
|
+
with st.container(border=True):
|
|
3451
|
+
account_header_cols = st.columns([3.2, 1.2])
|
|
3452
|
+
with account_header_cols[0]:
|
|
3453
|
+
st.subheader(f"{account_label_snapshot} — {account_email_snapshot}")
|
|
3454
|
+
with account_header_cols[1]:
|
|
3455
|
+
_api_status_badge("Configured" if account_ready else "Missing configuration", "ready" if account_ready else "missing")
|
|
3456
|
+
with st.expander("Detalhes da conta Google", expanded=False):
|
|
3457
|
+
with st.form(f"batch_account_form_{account_id}"):
|
|
3458
|
+
account_cols = st.columns(2)
|
|
3459
|
+
with account_cols[0]:
|
|
3460
|
+
account_label = st.text_input("Nome da conta", value=account_label_snapshot, key=f"batch_label_{account_id}")
|
|
3461
|
+
account_email = st.text_input("E-mail/Gmail da conta", value=account_email_snapshot if account_email_snapshot != "sem e-mail" else "", key=f"batch_email_{account_id}")
|
|
3462
|
+
account_client_id = st.text_input("OAuth Client ID", value=str(batch_account.get("client_id", "")), key=f"batch_client_id_{account_id}")
|
|
3463
|
+
with account_cols[1]:
|
|
3464
|
+
account_client_secret = st.text_input("OAuth Client Secret", value=str(batch_account.get("client_secret", "")), type="password", key=f"batch_client_secret_{account_id}")
|
|
3465
|
+
account_session_info = st.text_input(
|
|
3466
|
+
"sessionInfo token desta conta Google",
|
|
3467
|
+
value=str(batch_account.get("sessionInfo") or batch_account.get("session_info") or batch_account.get("direct_session_info", "")),
|
|
3468
|
+
type="password",
|
|
3469
|
+
key=f"batch_session_info_{account_id}",
|
|
3470
|
+
help="Token sessionInfo usado pelo Upload directo. É guardado por conta e sincronizado no credentials.json; os cookies e restantes valores continuam exclusivamente no documento.",
|
|
3471
|
+
)
|
|
3472
|
+
save_account = st.form_submit_button("Guardar dados da conta Google", type="primary", use_container_width=True)
|
|
3473
|
+
|
|
3474
|
+
st.markdown("**Documento de cookies/credenciais desta conta Google**")
|
|
3475
|
+
st.caption("O documento padrão é criado automaticamente. Suba um JSON completo ou apenas o documento de cookies; os valores preenchidos são incorporados e mantidos em credentials.json.")
|
|
3476
|
+
document_upload = st.file_uploader(
|
|
3477
|
+
"Subir documento de cookies/credenciais",
|
|
3478
|
+
type=["json"],
|
|
3479
|
+
key=f"direct_credentials_document_{account_id}",
|
|
3480
|
+
help="Aceita o JSON do YouTube-Video-Upload-Frontend-Api. Um documento parcial de cookies também é incorporado sem apagar os restantes campos.",
|
|
3481
|
+
)
|
|
3482
|
+
if missing_document_parts:
|
|
3483
|
+
st.warning(f"Documento incompleto: {', '.join(missing_document_parts)}")
|
|
3506
3484
|
else:
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3485
|
+
st.success("Documento completo para a conta Google")
|
|
3486
|
+
st.caption(f"Documento guardado em: {direct_status['document_file']}")
|
|
3487
|
+
document_save = st.button("Guardar documento nesta conta", key=f"save_direct_account_{account_id}", use_container_width=True)
|
|
3488
|
+
|
|
3489
|
+
account_status = youtube_batch_account_status(batch_account, STORAGE)
|
|
3490
|
+
status_cols = st.columns(3)
|
|
3491
|
+
with status_cols[0]:
|
|
3492
|
+
(st.success if account_status.ok else st.warning)(account_status.message)
|
|
3493
|
+
with status_cols[1]:
|
|
3494
|
+
if st.button("Autorizar/Reautorizar", key=f"batch_authorize_settings_{account_id}", use_container_width=True):
|
|
3495
|
+
result = authorize_youtube_batch_account(batch_account, STORAGE)
|
|
3496
|
+
(st.success if result.ok else st.error)(result.message)
|
|
3497
|
+
if result.ok:
|
|
3498
|
+
st.rerun()
|
|
3499
|
+
with status_cols[2]:
|
|
3500
|
+
if st.button("Apagar conta", icon=":material/delete:", key=f"batch_remove_settings_{account_id}", use_container_width=True):
|
|
3501
|
+
delete_youtube_batch_token(batch_account, STORAGE)
|
|
3502
|
+
delete_credentials_document(STORAGE, batch_account)
|
|
3503
|
+
remaining_accounts = [account for account in batch_accounts if str(account.get("id")) != account_id]
|
|
3504
|
+
settings["youtube_batch_accounts"] = remaining_accounts
|
|
3505
|
+
if settings.get("youtube_batch_selected_account_id") == account_id:
|
|
3506
|
+
settings["youtube_batch_selected_account_id"] = str(remaining_accounts[0].get("id")) if remaining_accounts else ""
|
|
3507
|
+
for channel in channel_state:
|
|
3508
|
+
if str(channel.get("google_account_id") or "") == account_id:
|
|
3509
|
+
channel.update({"google_account_id": "", "google_account_email": ""})
|
|
3510
|
+
write_json("channels.json", channel_state)
|
|
3511
|
+
write_json("settings.json", settings)
|
|
3512
|
+
st.rerun()
|
|
3519
3513
|
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3514
|
+
if save_account:
|
|
3515
|
+
if "@" not in account_email.strip():
|
|
3516
|
+
st.error("Informe um e-mail Google válido.")
|
|
3517
|
+
elif not account_client_id.strip() or not account_client_secret.strip():
|
|
3518
|
+
st.error("Informe o Client ID e o Client Secret desta conta.")
|
|
3519
|
+
else:
|
|
3520
|
+
for existing in batch_accounts:
|
|
3521
|
+
if str(existing.get("id")) == account_id:
|
|
3522
|
+
credentials_changed = any(existing.get(field, "") != value for field, value in (("email", account_email.strip()), ("client_id", account_client_id.strip()), ("client_secret", account_client_secret.strip())))
|
|
3523
|
+
if credentials_changed:
|
|
3524
|
+
delete_youtube_batch_token(existing, STORAGE)
|
|
3525
|
+
existing.update({"label": account_label.strip() or "Canais YouTube", "email": account_email.strip(), "client_id": account_client_id.strip(), "client_secret": account_client_secret.strip(), "sessionInfo": account_session_info.strip()})
|
|
3526
|
+
update_credentials_document_session_info(STORAGE, existing, account_session_info.strip())
|
|
3527
|
+
ensure_credentials_document(STORAGE, existing, settings, channel_state)
|
|
3528
|
+
settings["youtube_batch_accounts"] = batch_accounts
|
|
3529
|
+
write_json("settings.json", settings)
|
|
3530
|
+
st.success("Conta Google/YouTube guardada.")
|
|
3534
3531
|
st.rerun()
|
|
3535
|
-
|
|
3536
|
-
|
|
3532
|
+
|
|
3533
|
+
if document_save:
|
|
3534
|
+
if document_upload is None:
|
|
3535
|
+
st.error("Seleccione um documento JSON de cookies/credenciais antes de guardar.")
|
|
3536
|
+
else:
|
|
3537
|
+
try:
|
|
3538
|
+
merge_credentials_document(
|
|
3539
|
+
STORAGE,
|
|
3540
|
+
batch_account,
|
|
3541
|
+
document_upload.getvalue(),
|
|
3542
|
+
document_upload.name,
|
|
3543
|
+
session_info_override=str(batch_account.get("sessionInfo") or ""),
|
|
3544
|
+
channels=channel_state,
|
|
3545
|
+
)
|
|
3546
|
+
st.success("Documento incorporado e guardado nesta conta Google.")
|
|
3547
|
+
st.rerun()
|
|
3548
|
+
except ValueError as exc:
|
|
3549
|
+
st.error(str(exc))
|
|
3537
3550
|
|
|
3538
3551
|
if youtube_accounts_missing_document:
|
|
3539
3552
|
st.info("Contas que ainda precisam de dados no documento: " + ", ".join(youtube_accounts_missing_document))
|
|
@@ -3559,6 +3572,11 @@ def render_google_accounts():
|
|
|
3559
3572
|
legacy_account.pop("INNERTUBE_API_KEY", None)
|
|
3560
3573
|
settings["youtube_batch_accounts"] = batch_accounts
|
|
3561
3574
|
write_json("settings.json", settings)
|
|
3575
|
+
innertube_status_cols = st.columns([3.2, 1.2])
|
|
3576
|
+
with innertube_status_cols[0]:
|
|
3577
|
+
st.caption("Estado da chave global")
|
|
3578
|
+
with innertube_status_cols[1]:
|
|
3579
|
+
_render_credential_status(current_innertube_api_key)
|
|
3562
3580
|
with st.form("innertube_api_key_form"):
|
|
3563
3581
|
innertube_api_key_value = st.text_input(
|
|
3564
3582
|
"INNERTUBE_API_KEY",
|
|
@@ -3663,51 +3681,86 @@ def render_google_accounts():
|
|
|
3663
3681
|
st.success("Configuração global do YouTube guardada em Contas Google.")
|
|
3664
3682
|
st.rerun()
|
|
3665
3683
|
|
|
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
|
-
|
|
3684
|
+
def _persist_material_source_cards(settings: dict[str, Any], cards: list[dict[str, Any]], active_card_id: str = "") -> dict[str, Any]:
|
|
3685
|
+
updated = apply_material_source_cards_to_settings(settings, cards, active_card_id)
|
|
3686
|
+
write_json("settings.json", updated)
|
|
3687
|
+
return updated
|
|
3688
|
+
|
|
3689
|
+
|
|
3690
|
+
def _material_source_card_definition(provider: str) -> dict[str, str]:
|
|
3691
|
+
definition = material_source_definition(provider)
|
|
3692
|
+
if definition is not None:
|
|
3693
|
+
return definition
|
|
3694
|
+
return {
|
|
3695
|
+
"code": "local",
|
|
3696
|
+
"label": "Ficheiros locais",
|
|
3697
|
+
"description": "Usar materiais já existentes no storage local; não requer API key.",
|
|
3698
|
+
"legacy_key": "",
|
|
3699
|
+
}
|
|
3700
|
+
|
|
3701
|
+
|
|
3702
|
+
def _render_material_source_card(settings: dict[str, Any], cards: list[dict[str, Any]], index: int) -> None:
|
|
3703
|
+
card = normalize_material_card(cards[index], index)
|
|
3704
|
+
cards[index] = card
|
|
3705
|
+
card_id = str(card["id"])
|
|
3706
|
+
provider = str(card.get("provider") or "pexels")
|
|
3707
|
+
definition = _material_source_card_definition(provider)
|
|
3708
|
+
is_local = provider == "local"
|
|
3709
|
+
active_card_id = str(settings.get("material_active_card_id") or "")
|
|
3710
|
+
with st.container(border=True):
|
|
3711
|
+
header_cols = st.columns([3.2, 1.2])
|
|
3712
|
+
with header_cols[0]:
|
|
3713
|
+
st.subheader(definition["label"])
|
|
3714
|
+
st.caption(definition["description"])
|
|
3715
|
+
with header_cols[1]:
|
|
3716
|
+
_render_credential_status("" if is_local else card.get("api_key"), local=is_local, required=not is_local)
|
|
3717
|
+
with st.form(f"material_source_card_form_{card_id}"):
|
|
3718
|
+
content_cols = st.columns(2)
|
|
3719
|
+
with content_cols[0]:
|
|
3720
|
+
if is_local:
|
|
3721
|
+
st.caption("Esta fonte não usa API key.")
|
|
3722
|
+
api_key = ""
|
|
3723
|
+
else:
|
|
3724
|
+
api_key = st.text_input("API Key", value=str(card.get("api_key") or ""), type="password", key=f"material_card_{card_id}_api_key")
|
|
3725
|
+
with content_cols[1]:
|
|
3726
|
+
enabled = st.checkbox("Fonte activa", value=bool(card.get("enabled", True)), key=f"material_card_{card_id}_enabled")
|
|
3727
|
+
selected = st.checkbox(
|
|
3728
|
+
"Usar esta fonte na pipeline",
|
|
3729
|
+
value=active_card_id == card_id,
|
|
3730
|
+
key=f"material_card_{card_id}_selected",
|
|
3731
|
+
)
|
|
3732
|
+
save_card = st.form_submit_button("Salvar", type="primary", use_container_width=True, key=f"material_card_{card_id}_save")
|
|
3733
|
+
if save_card:
|
|
3734
|
+
cards[index] = {**card, "api_key": str(api_key or "").strip(), "enabled": bool(enabled)}
|
|
3735
|
+
selected_id = card_id if selected and enabled else active_card_id
|
|
3736
|
+
_persist_material_source_cards(settings, cards, selected_id)
|
|
3737
|
+
st.success(f"Fonte {definition['label']} guardada.")
|
|
3691
3738
|
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
|
|
3739
|
+
|
|
3740
|
+
|
|
3741
|
+
def render_material_source_api_keys(settings: dict[str, Any]) -> None:
|
|
3742
|
+
st.subheader("Fontes de Materiais")
|
|
3743
|
+
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.")
|
|
3744
|
+
migrated, changed = ensure_material_source_cards(settings)
|
|
3745
|
+
cards = [dict(item) for item in migrated.get("material_source_cards", [])]
|
|
3746
|
+
if changed:
|
|
3706
3747
|
write_json("settings.json", settings)
|
|
3707
|
-
|
|
3708
|
-
|
|
3748
|
+
for index in range(len(cards)):
|
|
3749
|
+
_render_material_source_card(settings, cards, index)
|
|
3750
|
+
|
|
3751
|
+
st.divider()
|
|
3752
|
+
st.markdown("**Adicionar fonte de materiais**")
|
|
3753
|
+
provider_codes = [item["code"] for item in material_source_catalog()] + ["local"]
|
|
3754
|
+
provider_to_add = st.selectbox(
|
|
3755
|
+
"Provedor de materiais",
|
|
3756
|
+
provider_codes,
|
|
3757
|
+
format_func=lambda value: _material_source_card_definition(value)["label"],
|
|
3758
|
+
key="material_new_provider_choice",
|
|
3759
|
+
)
|
|
3760
|
+
if st.button("Configurar Nova Fonte de Materiais", type="primary", use_container_width=True, key="add_material_source_card"):
|
|
3761
|
+
cards.append(new_material_card(provider_to_add, card_id=f"material-{provider_to_add}-{uuid.uuid4().hex[:8]}"))
|
|
3762
|
+
_persist_material_source_cards(settings, cards, str(settings.get("material_active_card_id") or ""))
|
|
3709
3763
|
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
3764
|
|
|
3712
3765
|
|
|
3713
3766
|
def _api_status_badge(label: str, kind: str = "missing") -> None:
|
|
@@ -3927,7 +3980,7 @@ def render_settings():
|
|
|
3927
3980
|
key=f"settings_{key}",
|
|
3928
3981
|
)
|
|
3929
3982
|
|
|
3930
|
-
api_keys_tab, material_sources_tab, voice_test_tab = render_localized_tabs(["API Keys", "Fontes de Materiais", "Teste de Voz"])
|
|
3983
|
+
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
3984
|
|
|
3932
3985
|
with api_keys_tab:
|
|
3933
3986
|
with st.container(border=True):
|
|
@@ -4062,8 +4115,11 @@ def render_settings():
|
|
|
4062
4115
|
st.success("Configurações guardadas localmente. Indique uma pasta válida do motor de vídeo para sincronizar config.toml.")
|
|
4063
4116
|
except Exception as exc:
|
|
4064
4117
|
st.warning(f"Configurações locais guardadas, mas não foi possível sincronizar config.toml: {exc}")
|
|
4065
|
-
|
|
4066
|
-
|
|
4118
|
+
with google_accounts_tab:
|
|
4119
|
+
render_google_accounts()
|
|
4120
|
+
|
|
4121
|
+
with material_sources_tab:
|
|
4122
|
+
render_material_source_api_keys(settings)
|
|
4067
4123
|
|
|
4068
4124
|
with voice_test_tab:
|
|
4069
4125
|
st.subheader("Teste de Voz")
|
|
@@ -4527,7 +4583,6 @@ def main():
|
|
|
4527
4583
|
]
|
|
4528
4584
|
settings_items = [
|
|
4529
4585
|
("MCP", ":material/hub:", "MCP"),
|
|
4530
|
-
("Contas Google", ":material/account_circle:", "Contas Google"),
|
|
4531
4586
|
("Notificações", ":material/notifications:", "Notificações"),
|
|
4532
4587
|
("Configuração API", ":material/settings:", "Configuração API"),
|
|
4533
4588
|
]
|
|
@@ -4585,7 +4640,8 @@ def main():
|
|
|
4585
4640
|
"Blueprints": "Blueprints Youtube",
|
|
4586
4641
|
"Configurações Técnicas": "Configuração API",
|
|
4587
4642
|
"Models AI": "AI Influencers",
|
|
4588
|
-
"Contas Google/YouTube — canais em lote": "
|
|
4643
|
+
"Contas Google/YouTube — canais em lote": "Configuração API",
|
|
4644
|
+
"Contas Google": "Configuração API",
|
|
4589
4645
|
}
|
|
4590
4646
|
all_children = [item for items in groups.values() for item in items]
|
|
4591
4647
|
valid_targets = {item[0] for item in top_pages + all_children}
|
package/hermes_ui/languages.py
CHANGED
|
@@ -781,6 +781,7 @@ _CONTENT_TRANSLATION_ROWS = (
|
|
|
781
781
|
("O Thunderbolt é o cliente. A API key é enviada exclusivamente ao servidor Postiz configurado; não é colocada em URLs, logs ou repositório.", "O Thunderbolt é o cliente. A API key é enviada exclusivamente ao servidor Postiz configurado; não é colocada em URLs, logs ou repositório.", "Thunderbolt is the client. The API key is sent only to the configured Postiz server; it is not placed in URLs, logs or the repository.", "Thunderbolt 是客户端。API 密钥仅发送到配置的 Postiz 服务器,不会放入 URL、日志或代码库。", "Thunderbolt ist der Client. Der API-Schlüssel wird nur an den konfigurierten Postiz-Server gesendet und nicht in URLs, Logs oder dem Repository abgelegt.", "Thunderbolt là client. API key chỉ gửi đến máy chủ Postiz đã cấu hình; không đặt trong URL, log hoặc repository.", "Thunderbolt istemcidir. API anahtarı yalnızca yapılandırılmış Postiz sunucusuna gönderilir; URL'lere, günlüklerine veya depoya yazılmaz.", "Thunderbolt — клиент. API-ключ отправляется только настроенному серверу Postiz и не помещается в URL, логи или репозиторий.", "Thunderbolt es el cliente. La clave API solo se envía al servidor Postiz configurado; no se incluye en URL, registros ni repositorio.", "Thunderbolt adalah klien. Kunci API hanya dikirim ke server Postiz yang dikonfigurasi; tidak dimasukkan ke URL, log, atau repositori.", "Thunderbolt è il client. La chiave API viene inviata solo al server Postiz configurato; non viene inserita in URL, log o repository."),
|
|
782
782
|
("Modo de ligação", "Modo de ligação", "Connection mode", "连接模式", "Verbindungsmodus", "Chế độ kết nối", "Bağlantı modu", "Режим подключения", "Modo de conexión", "Mode koneksi", "Modalità di connessione"),
|
|
783
783
|
("Metadados removidos e nova cópia criada. O original continua preservado.", "Metadados removidos e nova cópia criada. O original continua preservado.", "Metadata removed and a new copy created. The original remains preserved.", "元数据已移除并创建新副本。原文件仍然保留。", "Metadaten entfernt und eine neue Kopie erstellt. Das Original bleibt erhalten.", "Đã xóa siêu dữ liệu và tạo bản sao mới. Bản gốc vẫn được giữ nguyên.", "Üst veriler kaldırıldı ve yeni bir kopya oluşturuldu. Orijinal korunur.", "Метаданные удалены и создана новая копия. Оригинал сохранён.", "Metadatos eliminados y nueva copia creada. El original se conserva.", "Metadata dihapus dan salinan baru dibuat. File asli tetap dipertahankan.", "Metadati rimossi e nuova copia creata. L'originale è preservato."),
|
|
784
|
+
("Detalhes da conta Google", "Detalhes da conta Google", "Google account details", "Google 账户详情", "Details des Google-Kontos", "Chi tiết tài khoản Google", "Google hesap ayrıntıları", "Сведения об аккаунте Google", "Detalles de la cuenta de Google", "Detail akun Google", "Dettagli dell'account Google"),
|
|
784
785
|
)
|
|
785
786
|
UI_CONTENT_TRANSLATIONS: dict[str, dict[str, str]] = {code: {} for code in _CONTENT_TRANSLATION_CODES}
|
|
786
787
|
for _row in _CONTENT_TRANSLATION_ROWS:
|
|
@@ -922,3 +923,109 @@ __all__ = [
|
|
|
922
923
|
"language_label", "ui_language_menu_label", "language_locale", "language_option_codes", "language_option_labels",
|
|
923
924
|
"ui_text", "video_language_label", "video_language_options",
|
|
924
925
|
]
|
|
926
|
+
|
|
927
|
+
|
|
928
|
+
_MATERIAL_SOURCE_TRANSLATIONS: dict[str, dict[str, str]] = {
|
|
929
|
+
"pt": {
|
|
930
|
+
"Fontes de Materiais": "Fontes de Materiais",
|
|
931
|
+
"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.",
|
|
932
|
+
"Adicionar fonte de materiais": "Adicionar fonte de materiais",
|
|
933
|
+
"Provedor de materiais": "Provedor de materiais",
|
|
934
|
+
"Configurar Nova Fonte de Materiais": "Configurar Nova Fonte de Materiais",
|
|
935
|
+
"Fonte activa": "Fonte activa",
|
|
936
|
+
"Usar esta fonte na pipeline": "Usar esta fonte na pipeline",
|
|
937
|
+
"Esta fonte não usa API key.": "Esta fonte não usa API key.",
|
|
938
|
+
},
|
|
939
|
+
"en": {
|
|
940
|
+
"Fontes de Materiais": "Media Sources",
|
|
941
|
+
"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.",
|
|
942
|
+
"Adicionar fonte de materiais": "Add media source",
|
|
943
|
+
"Provedor de materiais": "Media provider",
|
|
944
|
+
"Configurar Nova Fonte de Materiais": "Configure New Media Source",
|
|
945
|
+
"Fonte activa": "Source active",
|
|
946
|
+
"Usar esta fonte na pipeline": "Use this source in the pipeline",
|
|
947
|
+
"Esta fonte não usa API key.": "This source does not use an API key.",
|
|
948
|
+
},
|
|
949
|
+
"zh": {
|
|
950
|
+
"Fontes de Materiais": "素材来源",
|
|
951
|
+
"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 密钥;所选来源将由流水线使用。",
|
|
952
|
+
"Adicionar fonte de materiais": "添加素材来源",
|
|
953
|
+
"Provedor de materiais": "素材提供商",
|
|
954
|
+
"Configurar Nova Fonte de Materiais": "配置新的素材来源",
|
|
955
|
+
"Fonte activa": "来源已启用",
|
|
956
|
+
"Usar esta fonte na pipeline": "在流水线中使用此来源",
|
|
957
|
+
"Esta fonte não usa API key.": "此来源不使用 API 密钥。",
|
|
958
|
+
},
|
|
959
|
+
"de": {
|
|
960
|
+
"Fontes de Materiais": "Medienquellen",
|
|
961
|
+
"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.",
|
|
962
|
+
"Adicionar fonte de materiais": "Medienquelle hinzufügen",
|
|
963
|
+
"Provedor de materiais": "Medienanbieter",
|
|
964
|
+
"Configurar Nova Fonte de Materiais": "Neue Medienquelle konfigurieren",
|
|
965
|
+
"Fonte activa": "Quelle aktiv",
|
|
966
|
+
"Usar esta fonte na pipeline": "Diese Quelle in der Pipeline verwenden",
|
|
967
|
+
"Esta fonte não usa API key.": "Diese Quelle verwendet keinen API-Schlüssel.",
|
|
968
|
+
},
|
|
969
|
+
"vi": {
|
|
970
|
+
"Fontes de Materiais": "Nguồn phương tiện",
|
|
971
|
+
"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.",
|
|
972
|
+
"Adicionar fonte de materiais": "Thêm nguồn phương tiện",
|
|
973
|
+
"Provedor de materiais": "Nhà cung cấp phương tiện",
|
|
974
|
+
"Configurar Nova Fonte de Materiais": "Định cấu hình nguồn phương tiện mới",
|
|
975
|
+
"Fonte activa": "Nguồn đang hoạt động",
|
|
976
|
+
"Usar esta fonte na pipeline": "Dùng nguồn này trong quy trình",
|
|
977
|
+
"Esta fonte não usa API key.": "Nguồn này không dùng API key.",
|
|
978
|
+
},
|
|
979
|
+
"tr": {
|
|
980
|
+
"Fontes de Materiais": "Medya kaynakları",
|
|
981
|
+
"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.",
|
|
982
|
+
"Adicionar fonte de materiais": "Medya kaynağı ekle",
|
|
983
|
+
"Provedor de materiais": "Medya sağlayıcısı",
|
|
984
|
+
"Configurar Nova Fonte de Materiais": "Yeni medya kaynağını yapılandır",
|
|
985
|
+
"Fonte activa": "Kaynak etkin",
|
|
986
|
+
"Usar esta fonte na pipeline": "Bu kaynağı akışta kullan",
|
|
987
|
+
"Esta fonte não usa API key.": "Bu kaynak API anahtarı kullanmaz.",
|
|
988
|
+
},
|
|
989
|
+
"ru": {
|
|
990
|
+
"Fontes de Materiais": "Источники материалов",
|
|
991
|
+
"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-ключей; выбранный источник будет использоваться конвейером.",
|
|
992
|
+
"Adicionar fonte de materiais": "Добавить источник материалов",
|
|
993
|
+
"Provedor de materiais": "Провайдер материалов",
|
|
994
|
+
"Configurar Nova Fonte de Materiais": "Настроить новый источник материалов",
|
|
995
|
+
"Fonte activa": "Источник активен",
|
|
996
|
+
"Usar esta fonte na pipeline": "Использовать этот источник в конвейере",
|
|
997
|
+
"Esta fonte não usa API key.": "Этот источник не использует API-ключ.",
|
|
998
|
+
},
|
|
999
|
+
"es": {
|
|
1000
|
+
"Fontes de Materiais": "Fuentes de medios",
|
|
1001
|
+
"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.",
|
|
1002
|
+
"Adicionar fonte de materiais": "Añadir fuente de medios",
|
|
1003
|
+
"Provedor de materiais": "Proveedor de medios",
|
|
1004
|
+
"Configurar Nova Fonte de Materiais": "Configurar nueva fuente de medios",
|
|
1005
|
+
"Fonte activa": "Fuente activa",
|
|
1006
|
+
"Usar esta fonte na pipeline": "Usar esta fuente en el flujo",
|
|
1007
|
+
"Esta fonte não usa API key.": "Esta fuente no utiliza una clave API.",
|
|
1008
|
+
},
|
|
1009
|
+
"id": {
|
|
1010
|
+
"Fontes de Materiais": "Sumber media",
|
|
1011
|
+
"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.",
|
|
1012
|
+
"Adicionar fonte de materiais": "Tambah sumber media",
|
|
1013
|
+
"Provedor de materiais": "Penyedia media",
|
|
1014
|
+
"Configurar Nova Fonte de Materiais": "Konfigurasikan Sumber Media Baru",
|
|
1015
|
+
"Fonte activa": "Sumber aktif",
|
|
1016
|
+
"Usar esta fonte na pipeline": "Gunakan sumber ini di pipeline",
|
|
1017
|
+
"Esta fonte não usa API key.": "Sumber ini tidak menggunakan kunci API.",
|
|
1018
|
+
},
|
|
1019
|
+
"it": {
|
|
1020
|
+
"Fontes de Materiais": "Fonti multimediali",
|
|
1021
|
+
"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.",
|
|
1022
|
+
"Adicionar fonte de materiais": "Aggiungi fonte multimediale",
|
|
1023
|
+
"Provedor de materiais": "Provider multimediale",
|
|
1024
|
+
"Configurar Nova Fonte de Materiais": "Configura nuova fonte multimediale",
|
|
1025
|
+
"Fonte activa": "Fonte attiva",
|
|
1026
|
+
"Usar esta fonte na pipeline": "Usa questa fonte nella pipeline",
|
|
1027
|
+
"Esta fonte não usa API key.": "Questa fonte non usa una API key.",
|
|
1028
|
+
},
|
|
1029
|
+
}
|
|
1030
|
+
for _language_code, _material_source_translation in _MATERIAL_SOURCE_TRANSLATIONS.items():
|
|
1031
|
+
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