@danhachuel/thunderbolt 0.4.97 → 0.4.99
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/influencers_ui.py +9 -9
- package/app/main.py +191 -191
- package/hermes_ui/notifications.py +5 -1
- package/package.json +1 -1
- package/scripts/cli.mjs +10 -1
package/app/main.py
CHANGED
|
@@ -555,9 +555,9 @@ def _render_library_name_editor(kind: str, path: Path, current_name: str) -> str
|
|
|
555
555
|
edited_name = st.text_input("Nome de apresentação", value=current_name, max_chars=120, key=f"{edit_key}_input")
|
|
556
556
|
save_col, cancel_col = st.columns(2)
|
|
557
557
|
with save_col:
|
|
558
|
-
save_name = st.form_submit_button("Guardar nome", type="primary",
|
|
558
|
+
save_name = st.form_submit_button("Guardar nome", type="primary", width='stretch')
|
|
559
559
|
with cancel_col:
|
|
560
|
-
cancel_name = st.form_submit_button("Cancelar",
|
|
560
|
+
cancel_name = st.form_submit_button("Cancelar", width='stretch')
|
|
561
561
|
if save_name:
|
|
562
562
|
try:
|
|
563
563
|
set_display_name(kind, path, edited_name)
|
|
@@ -573,7 +573,7 @@ def _render_library_name_editor(kind: str, path: Path, current_name: str) -> str
|
|
|
573
573
|
|
|
574
574
|
|
|
575
575
|
def _render_card_pencil(edit_key: str) -> None:
|
|
576
|
-
if st.button("✏️", help="Editar nome de apresentação", key=f"pencil_{edit_key}", type="tertiary",
|
|
576
|
+
if st.button("✏️", help="Editar nome de apresentação", key=f"pencil_{edit_key}", type="tertiary", width='stretch'):
|
|
577
577
|
st.session_state[edit_key] = True
|
|
578
578
|
st.rerun()
|
|
579
579
|
|
|
@@ -1040,7 +1040,7 @@ def render_video_generation_settings(
|
|
|
1040
1040
|
st.button(
|
|
1041
1041
|
"Gerar tópico, roteiro e palavras-chave com IA",
|
|
1042
1042
|
key=f"{prefix}_generate_video_content",
|
|
1043
|
-
|
|
1043
|
+
width='stretch',
|
|
1044
1044
|
type="secondary",
|
|
1045
1045
|
icon=":material/auto_awesome:",
|
|
1046
1046
|
on_click=generate_content_callback,
|
|
@@ -1053,7 +1053,7 @@ def render_video_generation_settings(
|
|
|
1053
1053
|
st.button(
|
|
1054
1054
|
"Salvar rascunho",
|
|
1055
1055
|
key=f"{prefix}_save_draft",
|
|
1056
|
-
|
|
1056
|
+
width='stretch',
|
|
1057
1057
|
type="secondary",
|
|
1058
1058
|
icon=":material/save:",
|
|
1059
1059
|
on_click=save_draft_callback,
|
|
@@ -1112,7 +1112,7 @@ def render_video_generation_settings(
|
|
|
1112
1112
|
type=["mp3", "wav", "m4a", "aac", "flac", "ogg"],
|
|
1113
1113
|
key=f"{prefix}_voiceover_upload",
|
|
1114
1114
|
)
|
|
1115
|
-
if uploaded_voiceover is not None and st.button("Guardar áudio de narração", key=f"{prefix}_voiceover_store",
|
|
1115
|
+
if uploaded_voiceover is not None and st.button("Guardar áudio de narração", key=f"{prefix}_voiceover_store", width='stretch'):
|
|
1116
1116
|
try:
|
|
1117
1117
|
stored_voiceover = store_voiceover_file(uploaded_voiceover.name, uploaded_voiceover.getvalue())
|
|
1118
1118
|
st.session_state[f"{prefix}_voiceover_file"] = str(stored_voiceover)
|
|
@@ -1244,7 +1244,7 @@ def channel_videos_for(channel: dict, limit: int = 10) -> list[dict[str, Any]]:
|
|
|
1244
1244
|
def render_channel_video_editor(video: dict, channel_id: str) -> None:
|
|
1245
1245
|
video_id = str(video.get("id") or "")
|
|
1246
1246
|
edit_key = f"channel_video_edit_{video_id}"
|
|
1247
|
-
if st.button("Editar vídeo", key=f"edit_channel_video_{video_id}",
|
|
1247
|
+
if st.button("Editar vídeo", key=f"edit_channel_video_{video_id}", width='stretch'):
|
|
1248
1248
|
st.session_state[edit_key] = True
|
|
1249
1249
|
st.rerun()
|
|
1250
1250
|
if st.session_state.get(edit_key):
|
|
@@ -1268,7 +1268,7 @@ def render_channel_videos(channel: dict) -> None:
|
|
|
1268
1268
|
st.caption("A lista usa o feed público do YouTube, sem Data API Key. Pode actualizar manualmente e editar os metadados locais apresentados.")
|
|
1269
1269
|
refresh_col = st.columns(1)[0]
|
|
1270
1270
|
with refresh_col:
|
|
1271
|
-
if st.button("Actualizar últimos 10 vídeos", key=f"refresh_channel_videos_{channel_id}",
|
|
1271
|
+
if st.button("Actualizar últimos 10 vídeos", key=f"refresh_channel_videos_{channel_id}", width='stretch'):
|
|
1272
1272
|
result = fetch_channel_videos_public(channel, limit=10)
|
|
1273
1273
|
if result.ok:
|
|
1274
1274
|
videos = _merge_channel_videos(channel_id, result.data.get("videos", []))
|
|
@@ -1325,7 +1325,7 @@ def render_channel_edit_form(channel: dict, youtube_account_ids: list[str], yout
|
|
|
1325
1325
|
edited_description = st.text_area("Descrição", value=str(channel.get("description") or ""), height=100)
|
|
1326
1326
|
edited_automation = st.toggle("Automação ON", value=bool(channel.get("automation_on", False)), key=f"edit_automation_{channel_id}")
|
|
1327
1327
|
edited_time = st.text_input("Horário diário (HH:MM)", value=str(channel.get("automation_time") or "00:00"))
|
|
1328
|
-
save_channel = st.form_submit_button("Guardar alterações", type="primary",
|
|
1328
|
+
save_channel = st.form_submit_button("Guardar alterações", type="primary", width='stretch')
|
|
1329
1329
|
cancel_edit = st.form_submit_button("Cancelar edição")
|
|
1330
1330
|
if cancel_edit:
|
|
1331
1331
|
st.session_state.pop(f"edit_channel_{channel_id}", None)
|
|
@@ -1654,7 +1654,7 @@ def render_thumbnail_blueprints():
|
|
|
1654
1654
|
with cols[1]:
|
|
1655
1655
|
script = st.selectbox("Blueprint de roteiro associado", blueprint_ids, index=blueprint_ids.index(current_script) if current_script in blueprint_ids else 0, format_func=lambda item: blueprint_labels.get(item, item or "Sem Blueprint padrão"), key=f"thumbnail_script_blueprint_channel_{channel_id}")
|
|
1656
1656
|
with cols[2]:
|
|
1657
|
-
if st.button("Guardar par", key=f"thumbnail_blueprint_save_{channel_id}",
|
|
1657
|
+
if st.button("Guardar par", key=f"thumbnail_blueprint_save_{channel_id}", width='stretch'):
|
|
1658
1658
|
if thumb == "Generic_Thumbnail_Blueprint" and script:
|
|
1659
1659
|
st.error("Not Allowed to Associate, System Use Only")
|
|
1660
1660
|
else:
|
|
@@ -1727,7 +1727,7 @@ def render_tiktok_accounts():
|
|
|
1727
1727
|
st.info("A pesquisa consulta apenas a página pública do perfil e pode devolver dados incompletos. Se o TikTok bloquear a consulta, utilize o cadastro manual.")
|
|
1728
1728
|
with st.form("tiktok_public_lookup_form"):
|
|
1729
1729
|
lookup_source = st.text_input("URL pública ou @handle", placeholder="https://www.tiktok.com/@conta ou @conta", key="tiktok_public_lookup_source")
|
|
1730
|
-
lookup_submitted = st.form_submit_button("Pesquisar perfil público", type="primary",
|
|
1730
|
+
lookup_submitted = st.form_submit_button("Pesquisar perfil público", type="primary", width='stretch')
|
|
1731
1731
|
if lookup_submitted:
|
|
1732
1732
|
result = fetch_public_tiktok_profile(lookup_source)
|
|
1733
1733
|
st.session_state["tiktok_public_lookup"] = {"ok": result.ok, "message": result.message, "data": result.data}
|
|
@@ -1747,7 +1747,7 @@ def render_tiktok_accounts():
|
|
|
1747
1747
|
with metric_cols[3]: st.metric("Vídeos", lookup_data.get("video_count") if lookup_data.get("video_count") is not None else "—")
|
|
1748
1748
|
display_name = st.text_input("Nome da conta", value=str(lookup_data.get("name") or lookup_data.get("username") or ""), key="tiktok_lookup_display_name")
|
|
1749
1749
|
notes = st.text_area("Observações internas", value=str(lookup_data.get("notes") or ""), key="tiktok_lookup_notes", height=80)
|
|
1750
|
-
if st.button("Cadastrar conta TikTok", type="primary",
|
|
1750
|
+
if st.button("Cadastrar conta TikTok", type="primary", width='stretch', key="tiktok_register_public_account"):
|
|
1751
1751
|
try:
|
|
1752
1752
|
stored = {**lookup_data, "name": display_name.strip() or lookup_data.get("username", ""), "notes": notes.strip(), "source": "public_lookup"}
|
|
1753
1753
|
_upsert_tiktok_account(stored)
|
|
@@ -1763,7 +1763,7 @@ def render_tiktok_accounts():
|
|
|
1763
1763
|
manual_source = st.text_input("@handle ou URL pública", placeholder="@minhaconta", key="tiktok_manual_source")
|
|
1764
1764
|
manual_name = st.text_input("Nome da conta", placeholder="Nome de apresentação", key="tiktok_manual_name")
|
|
1765
1765
|
manual_notes = st.text_area("Observações internas", height=90, key="tiktok_manual_notes")
|
|
1766
|
-
manual_submitted = st.form_submit_button("Guardar cadastro manual", type="primary",
|
|
1766
|
+
manual_submitted = st.form_submit_button("Guardar cadastro manual", type="primary", width='stretch')
|
|
1767
1767
|
if manual_submitted:
|
|
1768
1768
|
try:
|
|
1769
1769
|
reference = normalize_tiktok_reference(manual_source)
|
|
@@ -1802,7 +1802,7 @@ def render_tiktok_accounts():
|
|
|
1802
1802
|
edited_name = st.text_input("Nome da conta", value=str(account.get("name") or account.get("username") or ""), key=f"edit_tiktok_name_{account_id}")
|
|
1803
1803
|
edited_source = st.text_input("@handle ou URL pública", value=str(account.get("public_url") or account.get("url") or account.get("handle") or ""), key=f"edit_tiktok_source_{account_id}")
|
|
1804
1804
|
edited_notes = st.text_area("Observações internas", value=str(account.get("notes") or ""), key=f"edit_tiktok_notes_{account_id}", height=80)
|
|
1805
|
-
edit_submitted = st.form_submit_button("Guardar conta", type="primary",
|
|
1805
|
+
edit_submitted = st.form_submit_button("Guardar conta", type="primary", width='stretch')
|
|
1806
1806
|
if edit_submitted:
|
|
1807
1807
|
try:
|
|
1808
1808
|
reference = normalize_tiktok_reference(edited_source)
|
|
@@ -1827,7 +1827,7 @@ def render_tiktok_prompt_masters():
|
|
|
1827
1827
|
uploaded_name = Path(uploaded_prompt.name).stem
|
|
1828
1828
|
prompt_name = st.text_input("Nome do Prompt Master", value=uploaded_name, key="tiktok_prompt_master_name")
|
|
1829
1829
|
replace_prompt = st.checkbox("Permitir substituir um ficheiro existente", key="tiktok_prompt_master_replace")
|
|
1830
|
-
if st.button("Guardar Prompt Master", type="primary",
|
|
1830
|
+
if st.button("Guardar Prompt Master", type="primary", width='stretch', key="save_tiktok_prompt_master"):
|
|
1831
1831
|
safe_stem = re.sub(r"[^A-Za-z0-9À-ÿ._-]+", "-", prompt_name.strip() or uploaded_name).strip(".-") or "prompt-master"
|
|
1832
1832
|
destination = TIKTOK_PROMPT_MASTERS / f"{safe_stem}.md"
|
|
1833
1833
|
if destination.exists() and not replace_prompt:
|
|
@@ -1873,14 +1873,14 @@ def render_tiktok_prompt_masters():
|
|
|
1873
1873
|
edited_content = st.text_area("Conteúdo Markdown", value=content, height=360, key=f"tiktok_prompt_master_editor_{path.stem}")
|
|
1874
1874
|
prompt_cols = st.columns(3)
|
|
1875
1875
|
with prompt_cols[0]:
|
|
1876
|
-
if st.button("Guardar alterações", type="primary",
|
|
1876
|
+
if st.button("Guardar alterações", type="primary", width='stretch', key=f"save_prompt_master_{path.stem}"):
|
|
1877
1877
|
path.write_text(edited_content.rstrip() + "\n", encoding="utf-8")
|
|
1878
1878
|
st.success("Prompt Master actualizado.")
|
|
1879
1879
|
st.rerun()
|
|
1880
1880
|
with prompt_cols[1]:
|
|
1881
|
-
st.download_button("Descarregar", data=content.encode("utf-8"), file_name=path.name, mime="text/markdown",
|
|
1881
|
+
st.download_button("Descarregar", data=content.encode("utf-8"), file_name=path.name, mime="text/markdown", width='stretch', key=f"download_prompt_master_{path.stem}")
|
|
1882
1882
|
with prompt_cols[2]:
|
|
1883
|
-
if st.button("Apagar",
|
|
1883
|
+
if st.button("Apagar", width='stretch', key=f"delete_prompt_master_{path.stem}"):
|
|
1884
1884
|
path.unlink(missing_ok=True)
|
|
1885
1885
|
st.success("Prompt Master removido da biblioteca TikTok.")
|
|
1886
1886
|
st.rerun()
|
|
@@ -1956,11 +1956,11 @@ def render_tiktok_channels():
|
|
|
1956
1956
|
source = st.text_input("URL pública ou @handle", placeholder="https://www.tiktok.com/@conta", key="tiktok_channel_source")
|
|
1957
1957
|
lookup_cols = st.columns([1, 1])
|
|
1958
1958
|
with lookup_cols[0]:
|
|
1959
|
-
if st.button("Buscar no Tiktok", type="primary",
|
|
1959
|
+
if st.button("Buscar no Tiktok", type="primary", width='stretch', key="tiktok_channel_lookup"):
|
|
1960
1960
|
result = fetch_public_tiktok_profile(source)
|
|
1961
1961
|
st.session_state["tiktok_channel_import"] = {"ok": result.ok, "message": result.message, "data": result.data}
|
|
1962
1962
|
with lookup_cols[1]:
|
|
1963
|
-
if st.button("Limpar importação",
|
|
1963
|
+
if st.button("Limpar importação", width='stretch', key="tiktok_channel_clear"):
|
|
1964
1964
|
for key in ("tiktok_channel_import", "tiktok_import_language", "tiktok_import_niche", "tiktok_import_prompt", "tiktok_import_description"):
|
|
1965
1965
|
st.session_state.pop(key, None)
|
|
1966
1966
|
st.rerun()
|
|
@@ -2145,7 +2145,7 @@ def render_tiktok_channels():
|
|
|
2145
2145
|
if channels:
|
|
2146
2146
|
st.dataframe(
|
|
2147
2147
|
[{k: c.get(v, "") for k, v in {"URL canal": "url", "Nome canal": "name", "Handle canal": "handle", "Idioma": "language", "Nicho": "niche", "Prompt Master": "default_prompt_master", "Narrador/Voz Padrão": "default_voice", "Estilo wide": "style_wide", "Automação ligada": "automation_on", "Horário diário (HH:MM)": "automation_time", "Proporção": "video_aspect_ratio", "Activo": "active", "Descrição": "description"}.items()} for c in channels],
|
|
2148
|
-
|
|
2148
|
+
width='stretch',
|
|
2149
2149
|
hide_index=True,
|
|
2150
2150
|
height=360,
|
|
2151
2151
|
)
|
|
@@ -2202,7 +2202,7 @@ def render_channels():
|
|
|
2202
2202
|
st.caption("Os canais cadastrados são apresentados apenas em lista.")
|
|
2203
2203
|
col1, col2 = st.columns([1, 1])
|
|
2204
2204
|
with col1:
|
|
2205
|
-
if st.button("Buscar no YouTube", type="primary",
|
|
2205
|
+
if st.button("Buscar no YouTube", type="primary", width='stretch', key="youtube_channel_lookup"):
|
|
2206
2206
|
search_source = str(source or "").strip()
|
|
2207
2207
|
st.session_state.pop("yt_import", None)
|
|
2208
2208
|
st.session_state.pop("yt_message", None)
|
|
@@ -2227,7 +2227,7 @@ def render_channels():
|
|
|
2227
2227
|
st.session_state["yt_message"] = f"A pesquisa do YouTube falhou ({type(exc).__name__}). Confirme o URL/handle e tente novamente. Detalhe: {str(exc)[:240]}"
|
|
2228
2228
|
st.session_state["yt_ok"] = False
|
|
2229
2229
|
with col2:
|
|
2230
|
-
if st.button("Limpar importação",
|
|
2230
|
+
if st.button("Limpar importação", width='stretch', key="youtube_channel_clear"):
|
|
2231
2231
|
for key in ("yt_import", "yt_message", "yt_ok"):
|
|
2232
2232
|
st.session_state.pop(key, None)
|
|
2233
2233
|
st.rerun()
|
|
@@ -2309,7 +2309,7 @@ def render_channels():
|
|
|
2309
2309
|
data=build_channel_template_xlsx(),
|
|
2310
2310
|
file_name="modelo_canais_youtube.xlsx",
|
|
2311
2311
|
mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
2312
|
-
|
|
2312
|
+
width='stretch',
|
|
2313
2313
|
key="download_channel_spreadsheet_template",
|
|
2314
2314
|
)
|
|
2315
2315
|
with help_col:
|
|
@@ -2321,7 +2321,7 @@ def render_channels():
|
|
|
2321
2321
|
key="channel_spreadsheet_upload",
|
|
2322
2322
|
help="Apenas a primeira aba do arquivo será importada. Linhas sem URL, nome e handle são ignoradas.",
|
|
2323
2323
|
)
|
|
2324
|
-
if st.button("Ler e preparar planilha", type="primary",
|
|
2324
|
+
if st.button("Ler e preparar planilha", type="primary", width='stretch', key="read_channel_spreadsheet"):
|
|
2325
2325
|
if uploaded_sheet is None:
|
|
2326
2326
|
st.error("Selecione um arquivo Excel antes de continuar.")
|
|
2327
2327
|
else:
|
|
@@ -2371,9 +2371,9 @@ def render_channels():
|
|
|
2371
2371
|
"Descrição": "Preencher via YouTube" if not row.get("description") else "Da planilha",
|
|
2372
2372
|
"Estado": "Já cadastrado" if find_duplicate_channel({"name": row.get("name"), "handle": row.get("handle"), "url": row.get("url")}, existing_spreadsheet_channels) else "Novo",
|
|
2373
2373
|
})
|
|
2374
|
-
st.dataframe(preview_rows,
|
|
2374
|
+
st.dataframe(preview_rows, width='stretch', hide_index=True, height=min(420, 86 + 38 * len(preview_rows)))
|
|
2375
2375
|
st.caption("Blueprints e vozes são resolvidos pelos catálogos atuais. Por exemplo, `finanças`, `blueprint_finanças` e `Blueprint Canal Finanças` apontam para o mesmo Blueprint quando ele existe.")
|
|
2376
|
-
if st.button("Cadastrar canais da planilha", type="primary",
|
|
2376
|
+
if st.button("Cadastrar canais da planilha", type="primary", width='stretch', key="import_channel_spreadsheet"):
|
|
2377
2377
|
created_names: list[str] = []
|
|
2378
2378
|
skipped_names: list[str] = []
|
|
2379
2379
|
spreadsheet_errors: list[str] = []
|
|
@@ -2471,7 +2471,7 @@ def render_channels():
|
|
|
2471
2471
|
]
|
|
2472
2472
|
st.dataframe(
|
|
2473
2473
|
registered_rows,
|
|
2474
|
-
|
|
2474
|
+
width='stretch',
|
|
2475
2475
|
hide_index=True,
|
|
2476
2476
|
height=420,
|
|
2477
2477
|
column_config={
|
|
@@ -2512,7 +2512,7 @@ def render_channels():
|
|
|
2512
2512
|
(st.success if account_status.ok else st.warning)(account_status.message)
|
|
2513
2513
|
auth_cols = st.columns([1, 2])
|
|
2514
2514
|
with auth_cols[0]:
|
|
2515
|
-
if st.button("Autorizar conta Google", type="secondary",
|
|
2515
|
+
if st.button("Autorizar conta Google", type="secondary", width='stretch', key="batch_authorize_account"):
|
|
2516
2516
|
result = authorize_youtube_batch_account(selected_account, STORAGE)
|
|
2517
2517
|
(st.success if result.ok else st.error)(result.message)
|
|
2518
2518
|
if result.ok:
|
|
@@ -2520,7 +2520,7 @@ def render_channels():
|
|
|
2520
2520
|
with auth_cols[1]:
|
|
2521
2521
|
st.caption("A autorização é feita no browser do sistema e fica guardada separadamente para esta conta.")
|
|
2522
2522
|
batch_key = f"youtube_batch_channels_{selected_account_id}"
|
|
2523
|
-
if st.button("Listar canais desta conta", type="primary",
|
|
2523
|
+
if st.button("Listar canais desta conta", type="primary", width='stretch', key="batch_list_channels"):
|
|
2524
2524
|
result = list_youtube_batch_channels(selected_account, STORAGE)
|
|
2525
2525
|
st.session_state[batch_key] = result.data.get("channels", []) if result.ok else []
|
|
2526
2526
|
st.session_state[f"{batch_key}_message"] = result.message
|
|
@@ -2550,7 +2550,7 @@ def render_channels():
|
|
|
2550
2550
|
batch_language = st.selectbox("Idioma", list(LANGUAGE_CODES), index=list(LANGUAGE_CODES).index("pt"), format_func=language_label, key="batch_channel_language")
|
|
2551
2551
|
with defaults_cols[3]:
|
|
2552
2552
|
batch_style = st.selectbox("Estilo wide", ["Pexels/Pixabay", "full_ia", "Apenas Música"], key="batch_channel_style")
|
|
2553
|
-
import_selected = st.form_submit_button("Cadastrar canais seleccionados", type="primary",
|
|
2553
|
+
import_selected = st.form_submit_button("Cadastrar canais seleccionados", type="primary", width='stretch')
|
|
2554
2554
|
if import_selected:
|
|
2555
2555
|
created_names = []
|
|
2556
2556
|
skipped_names = []
|
|
@@ -2671,12 +2671,12 @@ def render_channels():
|
|
|
2671
2671
|
st.rerun()
|
|
2672
2672
|
action_cols = st.columns(2)
|
|
2673
2673
|
with action_cols[0]:
|
|
2674
|
-
if st.button("Editar", key=f"edit_channel_button_{channel_id}",
|
|
2674
|
+
if st.button("Editar", key=f"edit_channel_button_{channel_id}", width='stretch'):
|
|
2675
2675
|
st.session_state[edit_key] = True
|
|
2676
2676
|
st.rerun()
|
|
2677
2677
|
with action_cols[1]:
|
|
2678
2678
|
delete_key = f"delete_pending_{channel_id}"
|
|
2679
|
-
if st.button("Apagar", key=f"delete_{channel_id}",
|
|
2679
|
+
if st.button("Apagar", key=f"delete_{channel_id}", width='stretch'):
|
|
2680
2680
|
st.session_state[delete_key] = True
|
|
2681
2681
|
st.rerun()
|
|
2682
2682
|
if st.session_state.get(delete_key):
|
|
@@ -2704,17 +2704,17 @@ def render_channels():
|
|
|
2704
2704
|
block_cols = st.columns(4, gap="small")
|
|
2705
2705
|
with block_cols[0]:
|
|
2706
2706
|
st.markdown(f"**Blueprint Padrão**\n\n{summary['name']}")
|
|
2707
|
-
if st.button("Editar Blueprint", key=f"edit_prompts_{channel_id}",
|
|
2707
|
+
if st.button("Editar Blueprint", key=f"edit_prompts_{channel_id}", width='stretch'):
|
|
2708
2708
|
st.session_state[edit_key] = True
|
|
2709
2709
|
st.rerun()
|
|
2710
2710
|
with block_cols[1]:
|
|
2711
2711
|
st.markdown(f"**Nicho**\n\n{channel_niche_label(channel)}")
|
|
2712
|
-
if st.button("Editar Nicho", key=f"edit_niche_{channel_id}",
|
|
2712
|
+
if st.button("Editar Nicho", key=f"edit_niche_{channel_id}", width='stretch'):
|
|
2713
2713
|
st.session_state[edit_key] = True
|
|
2714
2714
|
st.rerun()
|
|
2715
2715
|
with block_cols[2]:
|
|
2716
2716
|
st.markdown(f"**Narrador/Voz Padrão**\n\n{summary['voice'] or 'Sem voz padrão'}")
|
|
2717
|
-
if st.button("Configurar Narrador/Voz", key=f"edit_voice_{channel_id}",
|
|
2717
|
+
if st.button("Configurar Narrador/Voz", key=f"edit_voice_{channel_id}", width='stretch'):
|
|
2718
2718
|
st.session_state[edit_key] = True
|
|
2719
2719
|
st.rerun()
|
|
2720
2720
|
with block_cols[3]:
|
|
@@ -2736,7 +2736,7 @@ def render_channels():
|
|
|
2736
2736
|
key=f"channel_delegated_session_id_{channel_id}",
|
|
2737
2737
|
help="Identificador individual usado pelo Upload directo deste canal. Não é partilhado com outros canais nem mostrado nos diagnósticos.",
|
|
2738
2738
|
)
|
|
2739
|
-
save_channel_direct_credentials = st.form_submit_button("Guardar conta Google e DELEGATED_SESSION_ID", type="primary",
|
|
2739
|
+
save_channel_direct_credentials = st.form_submit_button("Guardar conta Google e DELEGATED_SESSION_ID", type="primary", width='stretch')
|
|
2740
2740
|
selected_channel_account = youtube_accounts_by_id.get(channel_account_id)
|
|
2741
2741
|
if selected_channel_account:
|
|
2742
2742
|
selected_account_status = document_status(STORAGE, selected_channel_account, channel, settings, channels)
|
|
@@ -2940,7 +2940,7 @@ def render_video_from_draft() -> None:
|
|
|
2940
2940
|
st.caption(f"Faltam: {', '.join(still_missing)}")
|
|
2941
2941
|
elif not missing_sections:
|
|
2942
2942
|
st.caption("Este roteiro será usado directamente, sem regenerar o conteúdo editorial guardado.")
|
|
2943
|
-
if st.button(action_label, type="primary",
|
|
2943
|
+
if st.button(action_label, type="primary", width='stretch', key="new_video_resume_submit", icon=":material/movie:"):
|
|
2944
2944
|
if still_missing:
|
|
2945
2945
|
st.error(f"Complete as configurações seleccionadas: {', '.join(still_missing)}.")
|
|
2946
2946
|
elif not selected_channel.get("id"):
|
|
@@ -2995,7 +2995,7 @@ def render_new_video(page_title: str = "Criação de Vídeos", prefix: str = "ne
|
|
|
2995
2995
|
key=f"{prefix}_general_context",
|
|
2996
2996
|
placeholder="Opcional: campanha, época, evento ou restrição editorial comum. O tema final será individual por canal.",
|
|
2997
2997
|
)
|
|
2998
|
-
if st.button("Gerar tópicos individuais para todos os canais", key=f"{prefix}_generate_general_topics",
|
|
2998
|
+
if st.button("Gerar tópicos individuais para todos os canais", key=f"{prefix}_generate_general_topics", width='stretch'):
|
|
2999
2999
|
settings = read_json("settings.json", {})
|
|
3000
3000
|
generated_topics: dict[str, dict[str, Any]] = {}
|
|
3001
3001
|
errors: list[str] = []
|
|
@@ -3096,7 +3096,7 @@ def render_new_video(page_title: str = "Criação de Vídeos", prefix: str = "ne
|
|
|
3096
3096
|
st.warning("Ainda não existem músicas em storage/music. Escolha Carregar ficheiro ou Criar via Suno API.")
|
|
3097
3097
|
elif music_source == "Carregar ficheiro":
|
|
3098
3098
|
uploaded_music = st.file_uploader("Carregar música", type=["mp3", "wav", "m4a", "aac", "flac", "ogg"], key=f"{prefix}_music_upload")
|
|
3099
|
-
if uploaded_music and st.button("Guardar música local", key=f"{prefix}_music_store",
|
|
3099
|
+
if uploaded_music and st.button("Guardar música local", key=f"{prefix}_music_store", width='stretch'):
|
|
3100
3100
|
try:
|
|
3101
3101
|
stored_music = store_music_file(uploaded_music.name, uploaded_music.getvalue())
|
|
3102
3102
|
st.session_state[f"{prefix}_music_path"] = str(stored_music)
|
|
@@ -3107,7 +3107,7 @@ def render_new_video(page_title: str = "Criação de Vídeos", prefix: str = "ne
|
|
|
3107
3107
|
else:
|
|
3108
3108
|
suno_prompt = st.text_area("Prompt musical Suno", placeholder="Instrumental cinematográfico, calmo, sem voz...", key=f"{prefix}_suno_prompt")
|
|
3109
3109
|
suno_title = st.text_input("Título da música", value=st.session_state.get(f"{prefix}_topic") or "Thunderbolt music", key=f"{prefix}_suno_title")
|
|
3110
|
-
if st.button("Solicitar música no Suno", key=f"{prefix}_suno_request",
|
|
3110
|
+
if st.button("Solicitar música no Suno", key=f"{prefix}_suno_request", width='stretch'):
|
|
3111
3111
|
suno_result = request_suno_generation(read_json("settings.json", {}), suno_prompt, suno_title)
|
|
3112
3112
|
(st.success if suno_result["ok"] else st.error)(suno_result["message"])
|
|
3113
3113
|
if suno_result["ok"]:
|
|
@@ -3128,7 +3128,7 @@ def render_new_video(page_title: str = "Criação de Vídeos", prefix: str = "ne
|
|
|
3128
3128
|
existing_topics = st.session_state.get(f"{prefix}_general_topics", {})
|
|
3129
3129
|
payloads = dict(st.session_state.get(f"{prefix}_general_payloads", {}))
|
|
3130
3130
|
with st.expander("Gerar Thumbnail com IA", expanded=False):
|
|
3131
|
-
if st.button("Gerar Thumbnail com IA para todos os vídeos", key=f"{prefix}_generate_general_creative",
|
|
3131
|
+
if st.button("Gerar Thumbnail com IA para todos os vídeos", key=f"{prefix}_generate_general_creative", width='stretch'):
|
|
3132
3132
|
settings = read_json("settings.json", {})
|
|
3133
3133
|
new_payloads: dict[str, dict[str, Any]] = {}
|
|
3134
3134
|
errors: list[str] = []
|
|
@@ -3169,7 +3169,7 @@ def render_new_video(page_title: str = "Criação de Vídeos", prefix: str = "ne
|
|
|
3169
3169
|
payload["thumbnail_text"] = variant.get("overlay_text", "")
|
|
3170
3170
|
st.caption(f"{variant.get('composition', '')} · {variant.get('color_palette', '')}")
|
|
3171
3171
|
thumbnail_path = str(variant.get("image_path") or payload.get("thumbnail_path") or "").strip()
|
|
3172
|
-
if st.button("Gerar imagem com Nano Banana", key=f"{prefix}_general_generate_thumbnail_{channel['id']}",
|
|
3172
|
+
if st.button("Gerar imagem com Nano Banana", key=f"{prefix}_general_generate_thumbnail_{channel['id']}", width='stretch'):
|
|
3173
3173
|
try:
|
|
3174
3174
|
thumbnail_path = str(
|
|
3175
3175
|
generate_thumbnail_image(
|
|
@@ -3191,7 +3191,7 @@ def render_new_video(page_title: str = "Criação de Vídeos", prefix: str = "ne
|
|
|
3191
3191
|
except ThumbnailGenerationError as exc:
|
|
3192
3192
|
st.error(str(exc))
|
|
3193
3193
|
if thumbnail_path and Path(thumbnail_path).is_file():
|
|
3194
|
-
st.image(thumbnail_path, caption="Thumbnail gerada pelo Nano Banana",
|
|
3194
|
+
st.image(thumbnail_path, caption="Thumbnail gerada pelo Nano Banana", width='stretch')
|
|
3195
3195
|
payload["thumbnail_path"] = thumbnail_path
|
|
3196
3196
|
payload["thumbnail_status"] = "generated"
|
|
3197
3197
|
else:
|
|
@@ -3200,7 +3200,7 @@ def render_new_video(page_title: str = "Criação de Vídeos", prefix: str = "ne
|
|
|
3200
3200
|
else:
|
|
3201
3201
|
with st.expander("Gerar Thumbnail com IA", expanded=False):
|
|
3202
3202
|
topic_for_thumbnail = str(generation_settings.get("video_subject") or "").strip()
|
|
3203
|
-
if st.button("Gerar Thumbnail com IA", key=f"{prefix}_generate_creative",
|
|
3203
|
+
if st.button("Gerar Thumbnail com IA", key=f"{prefix}_generate_creative", width='stretch'):
|
|
3204
3204
|
if selected_one is None:
|
|
3205
3205
|
st.error("Seleccione primeiro um canal.")
|
|
3206
3206
|
elif not topic_for_thumbnail:
|
|
@@ -3235,7 +3235,7 @@ def render_new_video(page_title: str = "Criação de Vídeos", prefix: str = "ne
|
|
|
3235
3235
|
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")
|
|
3236
3236
|
payload["title"] = selected_title
|
|
3237
3237
|
with st.expander(f"Ver {len(title_options)} candidatos de título"):
|
|
3238
|
-
st.dataframe(payload.get("title_candidates", []),
|
|
3238
|
+
st.dataframe(payload.get("title_candidates", []), width='stretch', hide_index=True)
|
|
3239
3239
|
variants = payload.get("thumbnail_variants", [])
|
|
3240
3240
|
if variants:
|
|
3241
3241
|
labels = [f"{idx + 1}. {item.get('concept', 'Variante')}" for idx, item in enumerate(variants)]
|
|
@@ -3248,7 +3248,7 @@ def render_new_video(page_title: str = "Criação de Vídeos", prefix: str = "ne
|
|
|
3248
3248
|
st.caption(f"Composição: {variant.get('composition', '')} · Cores: {variant.get('color_palette', '')}")
|
|
3249
3249
|
st.code(variant.get("image_prompt", ""), language="text")
|
|
3250
3250
|
thumbnail_path = str(variant.get("image_path") or payload.get("thumbnail_path") or "").strip()
|
|
3251
|
-
if st.button("Gerar imagem da thumbnail com Nano Banana", key=f"{prefix}_generate_thumbnail_image",
|
|
3251
|
+
if st.button("Gerar imagem da thumbnail com Nano Banana", key=f"{prefix}_generate_thumbnail_image", width='stretch'):
|
|
3252
3252
|
try:
|
|
3253
3253
|
thumbnail_path = str(
|
|
3254
3254
|
generate_thumbnail_image(
|
|
@@ -3270,7 +3270,7 @@ def render_new_video(page_title: str = "Criação de Vídeos", prefix: str = "ne
|
|
|
3270
3270
|
except ThumbnailGenerationError as exc:
|
|
3271
3271
|
st.error(str(exc))
|
|
3272
3272
|
if thumbnail_path and Path(thumbnail_path).is_file():
|
|
3273
|
-
st.image(thumbnail_path, caption="Thumbnail gerada pelo Nano Banana",
|
|
3273
|
+
st.image(thumbnail_path, caption="Thumbnail gerada pelo Nano Banana", width='stretch')
|
|
3274
3274
|
payload["thumbnail_path"] = thumbnail_path
|
|
3275
3275
|
payload["thumbnail_status"] = "generated"
|
|
3276
3276
|
else:
|
|
@@ -3427,7 +3427,7 @@ def render_music_creation():
|
|
|
3427
3427
|
key="music_task_prompt",
|
|
3428
3428
|
height=300,
|
|
3429
3429
|
)
|
|
3430
|
-
if st.button("Gerar campos musicais com IA", key="music_task_generate_fields",
|
|
3430
|
+
if st.button("Gerar campos musicais com IA", key="music_task_generate_fields", width='stretch', icon=":material/auto_awesome:"):
|
|
3431
3431
|
try:
|
|
3432
3432
|
with st.spinner("A criar título, letra e prompt musical originais…"):
|
|
3433
3433
|
generated = generate_music_fields(
|
|
@@ -3447,7 +3447,7 @@ def render_music_creation():
|
|
|
3447
3447
|
models = ["lyria-3-clip-preview", "lyria-3-pro-preview"]
|
|
3448
3448
|
configured = str(settings.get("lyria_model") or models[0])
|
|
3449
3449
|
lyria_model = st.selectbox("Modelo Lyria", models, index=models.index(configured) if configured in models else 0, key="music_task_lyria_model")
|
|
3450
|
-
if st.button("Gerar Música", key="music_task_submit", type="primary",
|
|
3450
|
+
if st.button("Gerar Música", key="music_task_submit", type="primary", width='stretch', icon=":material/music_note:"):
|
|
3451
3451
|
try:
|
|
3452
3452
|
task = create_music_task(
|
|
3453
3453
|
provider_label,
|
|
@@ -3475,7 +3475,7 @@ def render_custom_music_voices() -> None:
|
|
|
3475
3475
|
settings = read_json("settings.json", {})
|
|
3476
3476
|
refresh_col, cache_col = st.columns([1, 2])
|
|
3477
3477
|
with refresh_col:
|
|
3478
|
-
refresh = st.button("Actualizar vozes", type="primary",
|
|
3478
|
+
refresh = st.button("Actualizar vozes", type="primary", width='stretch', key="elevenlabs_refresh_voices")
|
|
3479
3479
|
try:
|
|
3480
3480
|
voices, metadata = fetch_personal_voices(settings, force=refresh)
|
|
3481
3481
|
if metadata.get("source") == "cache":
|
|
@@ -3506,7 +3506,7 @@ def render_custom_music_voices() -> None:
|
|
|
3506
3506
|
st.caption("Categoria: personal")
|
|
3507
3507
|
st.caption("Labels: " + (", ".join(f"{key}: {value}" for key, value in labels.items()) if labels else "não disponíveis"))
|
|
3508
3508
|
with voice_cols[2]:
|
|
3509
|
-
if st.button("Testar voz", key=f"elevenlabs_test_voice_{voice['voice_id']}",
|
|
3509
|
+
if st.button("Testar voz", key=f"elevenlabs_test_voice_{voice['voice_id']}", width='stretch'):
|
|
3510
3510
|
try:
|
|
3511
3511
|
preview = synthesize_preview(test_text, "elevenlabs", voice["voice_id"], settings)
|
|
3512
3512
|
st.audio(str(preview), format="audio/mpeg")
|
|
@@ -3587,9 +3587,9 @@ def render_scripts():
|
|
|
3587
3587
|
brief = str(script_settings.get("video_script") or "").strip() or title
|
|
3588
3588
|
generate_col, clear_col = st.columns([1.4, 1])
|
|
3589
3589
|
with generate_col:
|
|
3590
|
-
generate_clicked = st.button("Gerar com IA a partir do Blueprint", type="primary",
|
|
3590
|
+
generate_clicked = st.button("Gerar com IA a partir do Blueprint", type="primary", width='stretch', key="generate_script_document")
|
|
3591
3591
|
with clear_col:
|
|
3592
|
-
clear_clicked = st.button("Limpar rascunho",
|
|
3592
|
+
clear_clicked = st.button("Limpar rascunho", width='stretch', key="clear_script_document")
|
|
3593
3593
|
if clear_clicked:
|
|
3594
3594
|
for key in ("script_draft", "script_draft_title", "script_draft_content", "script_draft_summary", "script_draft_keywords"):
|
|
3595
3595
|
st.session_state.pop(key, None)
|
|
@@ -3642,7 +3642,7 @@ def render_scripts():
|
|
|
3642
3642
|
draft_summary = st.text_input("Resumo", key="script_draft_summary")
|
|
3643
3643
|
draft_keywords = st.text_area("Palavras-chave", height=90, key="script_draft_keywords")
|
|
3644
3644
|
draft_content = st.text_area("Conteúdo guardado", height=460, key="script_draft_content")
|
|
3645
|
-
if st.button("Guardar documento no storage", type="primary",
|
|
3645
|
+
if st.button("Guardar documento no storage", type="primary", width='stretch', key="save_script_document"):
|
|
3646
3646
|
try:
|
|
3647
3647
|
record = save_script_document(
|
|
3648
3648
|
{
|
|
@@ -3716,7 +3716,7 @@ def render_niche_finder():
|
|
|
3716
3716
|
start_date = st.date_input("Data inicial", value=default_start, min_value=date(2020, 1, 1), max_value=default_end, key="niche_start_date")
|
|
3717
3717
|
end_date = st.date_input("Data final", value=default_end, min_value=date(2020, 1, 1), max_value=default_end, key="niche_end_date")
|
|
3718
3718
|
tags_text = st.text_input("Tags opcionais", key="niche_tags_text", placeholder="Ex.: history, facts, documentary")
|
|
3719
|
-
analyse = st.form_submit_button("Analisar Nichos", type="primary",
|
|
3719
|
+
analyse = st.form_submit_button("Analisar Nichos", type="primary", width='stretch')
|
|
3720
3720
|
|
|
3721
3721
|
current_parameters = {
|
|
3722
3722
|
"n_clusters": n_clusters,
|
|
@@ -3778,7 +3778,7 @@ def render_niche_finder():
|
|
|
3778
3778
|
cluster_table = cluster_table[cluster_table["palavras"].str.contains(keyword.strip(), case=False, na=False)]
|
|
3779
3779
|
tab_clusters, tab_rules, tab_data = render_localized_tabs(["Clusters encontrados", "Regras de associação", "Dados analisados"])
|
|
3780
3780
|
with tab_clusters:
|
|
3781
|
-
st.dataframe(cluster_table,
|
|
3781
|
+
st.dataframe(cluster_table, width='stretch', hide_index=True)
|
|
3782
3782
|
if not points.empty:
|
|
3783
3783
|
try:
|
|
3784
3784
|
import plotly.express as px
|
|
@@ -3786,19 +3786,19 @@ def render_niche_finder():
|
|
|
3786
3786
|
hover = [column for column in ["title", "channel_name", "country", "view_count", "engagement_rate"] if column in points.columns]
|
|
3787
3787
|
figure = px.scatter(points, x="x", y="y", color="cluster", hover_data=hover, title="Distribuição dos clusters")
|
|
3788
3788
|
figure.update_layout(legend_title_text="Cluster")
|
|
3789
|
-
st.plotly_chart(figure,
|
|
3789
|
+
st.plotly_chart(figure, width='stretch')
|
|
3790
3790
|
except ImportError:
|
|
3791
3791
|
st.error("A visualização da análise não está disponível nesta instalação.")
|
|
3792
3792
|
with tab_rules:
|
|
3793
3793
|
if rules_table.empty:
|
|
3794
3794
|
st.info("Não foram encontradas regras com os filtros actuais. Reduza o suporte mínimo ou escolha outro filtro.")
|
|
3795
3795
|
else:
|
|
3796
|
-
st.dataframe(rules_table,
|
|
3796
|
+
st.dataframe(rules_table, width='stretch', hide_index=True)
|
|
3797
3797
|
if not items_table.empty:
|
|
3798
3798
|
st.subheader("Itemsets frequentes")
|
|
3799
|
-
st.dataframe(items_table,
|
|
3799
|
+
st.dataframe(items_table, width='stretch', hide_index=True)
|
|
3800
3800
|
with tab_data:
|
|
3801
|
-
st.dataframe(results["raw_data"],
|
|
3801
|
+
st.dataframe(results["raw_data"], width='stretch', hide_index=True)
|
|
3802
3802
|
|
|
3803
3803
|
|
|
3804
3804
|
def render_niche_finder_apify():
|
|
@@ -3847,7 +3847,7 @@ def render_niche_finder_apify():
|
|
|
3847
3847
|
download_subtitles = st.checkbox("Descarregar legendas", value=True, key="niche_apify_download_subtitles")
|
|
3848
3848
|
with filter_cols[3]:
|
|
3849
3849
|
has_cc = st.checkbox("Apenas vídeos com CC", value=False, key="niche_apify_has_cc")
|
|
3850
|
-
analyse_apify = st.form_submit_button("Pesquisar no Apify", type="primary",
|
|
3850
|
+
analyse_apify = st.form_submit_button("Pesquisar no Apify", type="primary", width='stretch')
|
|
3851
3851
|
|
|
3852
3852
|
if analyse_apify:
|
|
3853
3853
|
try:
|
|
@@ -3921,17 +3921,17 @@ def render_niche_finder_apify():
|
|
|
3921
3921
|
result_frame = pd.DataFrame(visible_results)
|
|
3922
3922
|
display_columns = ["title", "channel_name", "duration", "view_count", "subscriber_count", "comments_count", "vsc_ratio", "url", "transcript_status", "summary_status", "summary"]
|
|
3923
3923
|
display_columns = [column for column in display_columns if column in result_frame.columns]
|
|
3924
|
-
st.dataframe(result_frame[display_columns],
|
|
3924
|
+
st.dataframe(result_frame[display_columns], width='stretch', hide_index=True)
|
|
3925
3925
|
export_frame = result_frame.drop(columns=["transcript"], errors="ignore")
|
|
3926
3926
|
export_json = export_frame.to_json(orient="records", force_ascii=False, indent=2)
|
|
3927
3927
|
export_csv = export_frame.to_csv(index=False).encode("utf-8")
|
|
3928
3928
|
export_cols = st.columns(2)
|
|
3929
3929
|
with export_cols[0]:
|
|
3930
|
-
st.download_button("Exportar JSON", data=export_json, file_name="niche-finder-apify-results.json", mime="application/json",
|
|
3930
|
+
st.download_button("Exportar JSON", data=export_json, file_name="niche-finder-apify-results.json", mime="application/json", width='stretch', key="niche_apify_export_json")
|
|
3931
3931
|
with export_cols[1]:
|
|
3932
|
-
st.download_button("Exportar CSV", data=export_csv, file_name="niche-finder-apify-results.csv", mime="text/csv",
|
|
3932
|
+
st.download_button("Exportar CSV", data=export_csv, file_name="niche-finder-apify-results.csv", mime="text/csv", width='stretch', key="niche_apify_export_csv")
|
|
3933
3933
|
except ImportError:
|
|
3934
|
-
st.dataframe(visible_results,
|
|
3934
|
+
st.dataframe(visible_results, width='stretch', hide_index=True)
|
|
3935
3935
|
|
|
3936
3936
|
last_run = st.session_state.get("niche_apify_last_run", {})
|
|
3937
3937
|
if last_run:
|
|
@@ -3973,7 +3973,7 @@ def render_media_download():
|
|
|
3973
3973
|
with option_cols[2]:
|
|
3974
3974
|
embed_metadata = st.checkbox("Incorporar metadados", value=False, key="media_download_embed_metadata")
|
|
3975
3975
|
st.caption("Playlist desactivada por padrão para evitar downloads acidentais em massa.")
|
|
3976
|
-
start_download = st.form_submit_button("Iniciar download", type="primary",
|
|
3976
|
+
start_download = st.form_submit_button("Iniciar download", type="primary", width='stretch')
|
|
3977
3977
|
|
|
3978
3978
|
if start_download:
|
|
3979
3979
|
progress = st.progress(0, text="A preparar o download…")
|
|
@@ -4087,7 +4087,7 @@ def render_cuts():
|
|
|
4087
4087
|
with url_tab:
|
|
4088
4088
|
url_value = st.text_input("URL directa do vídeo", placeholder="https://exemplo.com/video.mp4", key="cuts_video_url")
|
|
4089
4089
|
st.caption("A URL deve apontar directamente para um ficheiro de vídeo HTTP/HTTPS. O download só ocorre depois de clicar no botão.")
|
|
4090
|
-
if st.button("Descarregar vídeo", key="cuts_download_url",
|
|
4090
|
+
if st.button("Descarregar vídeo", key="cuts_download_url", width='stretch'):
|
|
4091
4091
|
try:
|
|
4092
4092
|
source_path = download_direct_video_url(url_value)
|
|
4093
4093
|
st.session_state["cuts_source_path"] = str(source_path)
|
|
@@ -4102,7 +4102,7 @@ def render_cuts():
|
|
|
4102
4102
|
else:
|
|
4103
4103
|
generated_labels = [f"{path.name} — {path}" for path in generated_paths]
|
|
4104
4104
|
selected_generated = st.selectbox("Vídeo gerado", range(len(generated_paths)), format_func=lambda index: generated_labels[index], key="cuts_generated_index")
|
|
4105
|
-
if st.button("Usar vídeo seleccionado", key="cuts_use_generated",
|
|
4105
|
+
if st.button("Usar vídeo seleccionado", key="cuts_use_generated", width='stretch'):
|
|
4106
4106
|
source_path = generated_paths[selected_generated]
|
|
4107
4107
|
st.session_state["cuts_source_path"] = str(source_path)
|
|
4108
4108
|
st.session_state["cuts_source_label"] = f"Pipeline · {source_path.name}"
|
|
@@ -4114,7 +4114,7 @@ def render_cuts():
|
|
|
4114
4114
|
else:
|
|
4115
4115
|
folder_labels = [f"{path.name} — {path}" for path in folder_paths]
|
|
4116
4116
|
selected_folder = st.selectbox("Vídeo da pasta", range(len(folder_paths)), format_func=lambda index: folder_labels[index], key="cuts_folder_index")
|
|
4117
|
-
if st.button("Usar vídeo da pasta", key="cuts_use_folder",
|
|
4117
|
+
if st.button("Usar vídeo da pasta", key="cuts_use_folder", width='stretch'):
|
|
4118
4118
|
source_path = folder_paths[selected_folder]
|
|
4119
4119
|
st.session_state["cuts_source_path"] = str(source_path)
|
|
4120
4120
|
st.session_state["cuts_source_label"] = f"Pasta local · {source_path.name}"
|
|
@@ -4173,7 +4173,7 @@ def render_cuts():
|
|
|
4173
4173
|
generate_button = st.button(
|
|
4174
4174
|
"Gerar Clips",
|
|
4175
4175
|
type="primary",
|
|
4176
|
-
|
|
4176
|
+
width='stretch',
|
|
4177
4177
|
disabled=not (source_path and source_path.is_file() and rights_confirmed),
|
|
4178
4178
|
key="cuts_generate_button",
|
|
4179
4179
|
)
|
|
@@ -4222,13 +4222,13 @@ def render_cuts():
|
|
|
4222
4222
|
with st.container(border=True):
|
|
4223
4223
|
st.caption(f"Clip {clip.get('index', '—')} · {float(clip.get('duration', 0)):.1f}s")
|
|
4224
4224
|
st.video(str(clip["path"]))
|
|
4225
|
-
st.download_button("Descarregar clip", data=Path(clip["path"]).read_bytes(), file_name=clip["name"], mime="video/mp4", key=f"cuts_download_{last_run['id']}_{clip['index']}",
|
|
4225
|
+
st.download_button("Descarregar clip", data=Path(clip["path"]).read_bytes(), file_name=clip["name"], mime="video/mp4", key=f"cuts_download_{last_run['id']}_{clip['index']}", width='stretch')
|
|
4226
4226
|
try:
|
|
4227
4227
|
_, archive_bytes = zip_cut_run(last_run)
|
|
4228
|
-
st.download_button("Descarregar todos os clips (ZIP)", data=archive_bytes, file_name=f"{last_run['id']}.zip", mime="application/zip", key=f"cuts_download_zip_{last_run['id']}",
|
|
4228
|
+
st.download_button("Descarregar todos os clips (ZIP)", data=archive_bytes, file_name=f"{last_run['id']}.zip", mime="application/zip", key=f"cuts_download_zip_{last_run['id']}", width='stretch')
|
|
4229
4229
|
except CutsError as exc:
|
|
4230
4230
|
st.warning(str(exc))
|
|
4231
|
-
st.download_button("Descarregar manifesto JSON", data=cut_manifest_bytes(last_run), file_name=f"{last_run['id']}.json", mime="application/json", key=f"cuts_download_manifest_{last_run['id']}",
|
|
4231
|
+
st.download_button("Descarregar manifesto JSON", data=cut_manifest_bytes(last_run), file_name=f"{last_run['id']}.json", mime="application/json", key=f"cuts_download_manifest_{last_run['id']}", width='stretch')
|
|
4232
4232
|
elif last_run.get("error"):
|
|
4233
4233
|
st.error(last_run["error"])
|
|
4234
4234
|
|
|
@@ -4237,7 +4237,7 @@ def render_cuts():
|
|
|
4237
4237
|
with st.expander("Histórico do Clip Generator"):
|
|
4238
4238
|
st.dataframe(
|
|
4239
4239
|
[{"Data": run.get("created_at", "—"), "Fonte": run.get("source_name", "—"), "Formato": run.get("output_format", "—"), "Clips": len(run.get("clips", [])), "Estado": run.get("status", "—")} for run in runs[:20]],
|
|
4240
|
-
|
|
4240
|
+
width='stretch',
|
|
4241
4241
|
hide_index=True,
|
|
4242
4242
|
)
|
|
4243
4243
|
|
|
@@ -4308,7 +4308,7 @@ def render_python_editor():
|
|
|
4308
4308
|
width = st.number_input("Largura", min_value=16, max_value=7680, value=1280, step=2, key="python_editor_width")
|
|
4309
4309
|
with resize_cols[1]:
|
|
4310
4310
|
height = st.number_input("Altura", min_value=16, max_value=7680, value=720, step=2, key="python_editor_height")
|
|
4311
|
-
apply_operation = st.form_submit_button(f"Aplicar: {operation}", type="primary",
|
|
4311
|
+
apply_operation = st.form_submit_button(f"Aplicar: {operation}", type="primary", width='stretch')
|
|
4312
4312
|
if apply_operation:
|
|
4313
4313
|
try:
|
|
4314
4314
|
ffmpeg_path = read_json("settings.json", {}).get("ffmpeg_path", "")
|
|
@@ -4344,7 +4344,7 @@ def render_python_editor():
|
|
|
4344
4344
|
records = list_python_editor_records()
|
|
4345
4345
|
if records:
|
|
4346
4346
|
with st.expander("Histórico do Editor Python"):
|
|
4347
|
-
st.dataframe([{key: record.get(key, "") for key in ("created_at", "operation", "source_name", "output_name")} for record in records[:20]],
|
|
4347
|
+
st.dataframe([{key: record.get(key, "") for key in ("created_at", "operation", "source_name", "output_name")} for record in records[:20]], width='stretch', hide_index=True)
|
|
4348
4348
|
|
|
4349
4349
|
with code_tab:
|
|
4350
4350
|
st.subheader("Scripts Python locais")
|
|
@@ -4642,7 +4642,7 @@ def render_videos():
|
|
|
4642
4642
|
file_name=video_file.name,
|
|
4643
4643
|
mime='video/mp4',
|
|
4644
4644
|
key=f"pipeline_video_download_{task['id']}",
|
|
4645
|
-
|
|
4645
|
+
width='stretch',
|
|
4646
4646
|
)
|
|
4647
4647
|
elif video_path:
|
|
4648
4648
|
st.caption(f'Vídeo registado: {video_path}')
|
|
@@ -4658,11 +4658,11 @@ def render_videos():
|
|
|
4658
4658
|
state = str(task.get("state") or "")
|
|
4659
4659
|
start_col, stop_col = st.columns(2)
|
|
4660
4660
|
with start_col:
|
|
4661
|
-
if st.button("Start", key=f"automation_start_{task['id']}",
|
|
4661
|
+
if st.button("Start", key=f"automation_start_{task['id']}", width='stretch', disabled=state not in {"to_do", "blocked", "failed"}):
|
|
4662
4662
|
transition_task(task["id"], "doing")
|
|
4663
4663
|
st.rerun()
|
|
4664
4664
|
with stop_col:
|
|
4665
|
-
if st.button("Stop", key=f"automation_stop_{task['id']}",
|
|
4665
|
+
if st.button("Stop", key=f"automation_stop_{task['id']}", width='stretch', disabled=state != "doing"):
|
|
4666
4666
|
transition_task(task["id"], "blocked")
|
|
4667
4667
|
st.rerun()
|
|
4668
4668
|
|
|
@@ -4701,7 +4701,7 @@ def render_music_backlog() -> None:
|
|
|
4701
4701
|
file_name=music_file.name,
|
|
4702
4702
|
mime="audio/mpeg",
|
|
4703
4703
|
key=f"music_backlog_download_{task['id']}",
|
|
4704
|
-
|
|
4704
|
+
width='stretch',
|
|
4705
4705
|
)
|
|
4706
4706
|
elif music_path:
|
|
4707
4707
|
st.caption(f"Música registada: {music_path}")
|
|
@@ -4725,11 +4725,11 @@ def render_music_backlog() -> None:
|
|
|
4725
4725
|
state = str(task.get("state") or "")
|
|
4726
4726
|
start_col, stop_col = st.columns(2)
|
|
4727
4727
|
with start_col:
|
|
4728
|
-
if st.button("Start", key=f"music_backlog_start_{task['id']}",
|
|
4728
|
+
if st.button("Start", key=f"music_backlog_start_{task['id']}", width='stretch', disabled=state not in {"to_do", "blocked", "failed"}):
|
|
4729
4729
|
run_music_task(str(task["id"]), read_json("settings.json", {}))
|
|
4730
4730
|
st.rerun()
|
|
4731
4731
|
with stop_col:
|
|
4732
|
-
if st.button("Stop", key=f"music_backlog_stop_{task['id']}",
|
|
4732
|
+
if st.button("Stop", key=f"music_backlog_stop_{task['id']}", width='stretch', disabled=state != "doing"):
|
|
4733
4733
|
transition_music_task(str(task["id"]), "blocked")
|
|
4734
4734
|
st.rerun()
|
|
4735
4735
|
|
|
@@ -4775,14 +4775,14 @@ def render_thumbnails():
|
|
|
4775
4775
|
with image_col:
|
|
4776
4776
|
image_path = record.get("image_path")
|
|
4777
4777
|
if image_path and image_path.is_file():
|
|
4778
|
-
st.image(str(image_path),
|
|
4778
|
+
st.image(str(image_path), width='stretch')
|
|
4779
4779
|
st.download_button(
|
|
4780
4780
|
"Descarregar thumbnail",
|
|
4781
4781
|
data=image_path.read_bytes(),
|
|
4782
4782
|
file_name=image_path.name,
|
|
4783
4783
|
mime="image/jpeg" if image_path.suffix.lower() in {".jpg", ".jpeg"} else "image/png",
|
|
4784
4784
|
key=f"thumbnail_download_{task_id}_{record['variant_index']}",
|
|
4785
|
-
|
|
4785
|
+
width='stretch',
|
|
4786
4786
|
)
|
|
4787
4787
|
else:
|
|
4788
4788
|
st.markdown("### Sem imagem")
|
|
@@ -4802,7 +4802,7 @@ def render_thumbnails():
|
|
|
4802
4802
|
"Refazer Prompt Thumb",
|
|
4803
4803
|
key=f"regenerate_thumbnail_{task_id}",
|
|
4804
4804
|
icon=":material/refresh:",
|
|
4805
|
-
|
|
4805
|
+
width='stretch',
|
|
4806
4806
|
disabled=not bool(record["title"] or record["topic"]),
|
|
4807
4807
|
):
|
|
4808
4808
|
try:
|
|
@@ -4837,7 +4837,7 @@ def render_thumbnails():
|
|
|
4837
4837
|
"Gerar Imagem",
|
|
4838
4838
|
key=f"generate_thumbnail_image_{task_id}",
|
|
4839
4839
|
icon=":material/image:",
|
|
4840
|
-
|
|
4840
|
+
width='stretch',
|
|
4841
4841
|
disabled=not bool(record["prompt"]),
|
|
4842
4842
|
):
|
|
4843
4843
|
try:
|
|
@@ -4861,7 +4861,7 @@ def render_thumbnails():
|
|
|
4861
4861
|
"Refazer Prompt e Gerar Imagem",
|
|
4862
4862
|
key=f"regenerate_thumbnail_prompt_{task_id}",
|
|
4863
4863
|
icon=":material/auto_awesome:",
|
|
4864
|
-
|
|
4864
|
+
width='stretch',
|
|
4865
4865
|
disabled=not bool(record["title"] or record["topic"]),
|
|
4866
4866
|
):
|
|
4867
4867
|
try:
|
|
@@ -4892,7 +4892,7 @@ def render_thumbnails():
|
|
|
4892
4892
|
"Refazer Lettering",
|
|
4893
4893
|
key=f"regenerate_thumbnail_lettering_{task_id}",
|
|
4894
4894
|
icon=":material/title:",
|
|
4895
|
-
|
|
4895
|
+
width='stretch',
|
|
4896
4896
|
disabled=not bool(record.get("image_path") and record["image_path"].is_file()),
|
|
4897
4897
|
):
|
|
4898
4898
|
try:
|
|
@@ -5002,7 +5002,7 @@ def render_tiktok_automation():
|
|
|
5002
5002
|
with default_cols[4]:
|
|
5003
5003
|
automation_format = st.selectbox("Formato", CHANNEL_FORMAT_OPTIONS, index=CHANNEL_FORMAT_OPTIONS.index(str(channel.get("format") or "Shorts")) if str(channel.get("format") or "Shorts") in CHANNEL_FORMAT_OPTIONS else 1, key=f"tiktok_automation_format_{channel_id}")
|
|
5004
5004
|
with default_cols[5]:
|
|
5005
|
-
if st.button("Guardar", key=f"tiktok_automation_save_{channel_id}",
|
|
5005
|
+
if st.button("Guardar", key=f"tiktok_automation_save_{channel_id}", width='stretch', type="primary"):
|
|
5006
5006
|
if not valid_hhmm(schedule_time):
|
|
5007
5007
|
st.error("Use o formato HH:MM, por exemplo 08:30.")
|
|
5008
5008
|
else:
|
|
@@ -5034,10 +5034,10 @@ def render_tiktok_automation():
|
|
|
5034
5034
|
st.caption("Portrait 9:16")
|
|
5035
5035
|
with cols[3]:
|
|
5036
5036
|
state = str(task.get("state") or "")
|
|
5037
|
-
if st.button("Start", key=f"tiktok_automation_start_{task_id}", disabled=state not in {"to_do", "blocked", "failed"},
|
|
5037
|
+
if st.button("Start", key=f"tiktok_automation_start_{task_id}", disabled=state not in {"to_do", "blocked", "failed"}, width='stretch'):
|
|
5038
5038
|
retry_task_with_current_settings(task_id) if state in {"blocked", "failed"} else transition_task(task_id, "doing")
|
|
5039
5039
|
st.rerun()
|
|
5040
|
-
if st.button("Stop", key=f"tiktok_automation_stop_{task_id}", disabled=state != "doing",
|
|
5040
|
+
if st.button("Stop", key=f"tiktok_automation_stop_{task_id}", disabled=state != "doing", width='stretch'):
|
|
5041
5041
|
stop_task_by_user(task_id)
|
|
5042
5042
|
st.rerun()
|
|
5043
5043
|
|
|
@@ -5112,7 +5112,7 @@ def render_automation():
|
|
|
5112
5112
|
key=f"automation_voice_{channel_id}",
|
|
5113
5113
|
)
|
|
5114
5114
|
with default_cols[5]:
|
|
5115
|
-
if st.button("Guardar", key=f"automation_save_{channel_id}",
|
|
5115
|
+
if st.button("Guardar", key=f"automation_save_{channel_id}", width='stretch'):
|
|
5116
5116
|
if not valid_hhmm(schedule_time):
|
|
5117
5117
|
st.error("Use o formato HH:MM, por exemplo 08:30.")
|
|
5118
5118
|
else:
|
|
@@ -5156,7 +5156,7 @@ def render_automation():
|
|
|
5156
5156
|
file_name=thumbnail_path.name if thumbnail_path else "thumbnail.png",
|
|
5157
5157
|
mime="image/png",
|
|
5158
5158
|
key=f"automation_download_thumbnail_{task['id']}",
|
|
5159
|
-
|
|
5159
|
+
width='stretch',
|
|
5160
5160
|
disabled=thumbnail_path is None,
|
|
5161
5161
|
)
|
|
5162
5162
|
with prompt_download_col:
|
|
@@ -5167,7 +5167,7 @@ def render_automation():
|
|
|
5167
5167
|
file_name=thumbnail_prompt_path.name if thumbnail_prompt_path else "thumbnail-prompt.txt",
|
|
5168
5168
|
mime="application/json" if thumbnail_prompt_path else "text/plain",
|
|
5169
5169
|
key=f"automation_download_thumbnail_prompt_{task['id']}",
|
|
5170
|
-
|
|
5170
|
+
width='stretch',
|
|
5171
5171
|
disabled=thumbnail_prompt_path is None and not thumbnail_prompt,
|
|
5172
5172
|
)
|
|
5173
5173
|
with task_cols[1]:
|
|
@@ -5179,14 +5179,14 @@ def render_automation():
|
|
|
5179
5179
|
state = str(task.get("state") or "")
|
|
5180
5180
|
start_col, stop_col, delete_col = st.columns(3)
|
|
5181
5181
|
with start_col:
|
|
5182
|
-
if st.button("Start", key=f"automation_start_{task['id']}",
|
|
5182
|
+
if st.button("Start", key=f"automation_start_{task['id']}", width='stretch', disabled=state not in {"to_do", "blocked", "failed"}):
|
|
5183
5183
|
if state in {"failed", "blocked"}:
|
|
5184
5184
|
retry_task_with_current_settings(task["id"])
|
|
5185
5185
|
else:
|
|
5186
5186
|
transition_task(task["id"], "doing")
|
|
5187
5187
|
st.rerun()
|
|
5188
5188
|
with stop_col:
|
|
5189
|
-
if st.button("Stop", key=f"automation_stop_{task['id']}",
|
|
5189
|
+
if st.button("Stop", key=f"automation_stop_{task['id']}", width='stretch', disabled=state != "doing"):
|
|
5190
5190
|
stop_task_by_user(task["id"])
|
|
5191
5191
|
st.rerun()
|
|
5192
5192
|
script_download_col, video_download_col = st.columns(2, gap="small")
|
|
@@ -5197,7 +5197,7 @@ def render_automation():
|
|
|
5197
5197
|
file_name=script_path.name if script_path else "roteiro.md",
|
|
5198
5198
|
mime="text/markdown",
|
|
5199
5199
|
key=f"automation_download_script_{task['id']}",
|
|
5200
|
-
|
|
5200
|
+
width='stretch',
|
|
5201
5201
|
disabled=script_path is None,
|
|
5202
5202
|
)
|
|
5203
5203
|
with video_download_col:
|
|
@@ -5207,19 +5207,19 @@ def render_automation():
|
|
|
5207
5207
|
file_name=video_path.name if video_path else "video.mp4",
|
|
5208
5208
|
mime="video/mp4",
|
|
5209
5209
|
key=f"automation_download_video_{task['id']}",
|
|
5210
|
-
|
|
5210
|
+
width='stretch',
|
|
5211
5211
|
disabled=video_path is None,
|
|
5212
5212
|
)
|
|
5213
5213
|
with delete_col:
|
|
5214
5214
|
confirm_delete_key = f"automation_confirm_delete_{task['id']}"
|
|
5215
|
-
if st.button("Apagar", key=f"automation_delete_{task['id']}",
|
|
5215
|
+
if st.button("Apagar", key=f"automation_delete_{task['id']}", width='stretch', disabled=state == "doing"):
|
|
5216
5216
|
st.session_state[confirm_delete_key] = True
|
|
5217
5217
|
st.rerun()
|
|
5218
5218
|
if st.session_state.get(confirm_delete_key):
|
|
5219
5219
|
st.warning("Remover este vídeo da fila? Os ficheiros de artefactos serão preservados.")
|
|
5220
5220
|
confirm_col, cancel_col = st.columns(2)
|
|
5221
5221
|
with confirm_col:
|
|
5222
|
-
if st.button("Confirmar", key=f"automation_confirm_delete_button_{task['id']}",
|
|
5222
|
+
if st.button("Confirmar", key=f"automation_confirm_delete_button_{task['id']}", width='stretch', type="primary"):
|
|
5223
5223
|
try:
|
|
5224
5224
|
delete_task(task["id"])
|
|
5225
5225
|
st.session_state.pop(confirm_delete_key, None)
|
|
@@ -5227,7 +5227,7 @@ def render_automation():
|
|
|
5227
5227
|
except ValueError as exc:
|
|
5228
5228
|
st.error(str(exc))
|
|
5229
5229
|
with cancel_col:
|
|
5230
|
-
if st.button("Cancelar", key=f"automation_cancel_delete_{task['id']}",
|
|
5230
|
+
if st.button("Cancelar", key=f"automation_cancel_delete_{task['id']}", width='stretch'):
|
|
5231
5231
|
st.session_state.pop(confirm_delete_key, None)
|
|
5232
5232
|
st.rerun()
|
|
5233
5233
|
|
|
@@ -5326,7 +5326,7 @@ def _render_music_source(prefix: str, extensions: set[str]) -> str:
|
|
|
5326
5326
|
type=sorted(extension.lstrip(".") for extension in extensions),
|
|
5327
5327
|
key=f"{prefix}_music_file_upload",
|
|
5328
5328
|
)
|
|
5329
|
-
if uploaded is not None and st.button("Guardar música no storage local", key=f"{prefix}_music_store",
|
|
5329
|
+
if uploaded is not None and st.button("Guardar música no storage local", key=f"{prefix}_music_store", width='stretch'):
|
|
5330
5330
|
try:
|
|
5331
5331
|
stored = store_music_file(uploaded.name, uploaded.getvalue())
|
|
5332
5332
|
selected_path = str(stored)
|
|
@@ -5399,7 +5399,7 @@ def _render_jewelmusic_upload_tab() -> None:
|
|
|
5399
5399
|
base_url = st.text_input("Base URL", value=str(settings.get("jewelmusic_base_url") or "https://api.jewelmusic.com"))
|
|
5400
5400
|
proxy_url = st.text_input("Proxy opcional", value=str(settings.get("jewelmusic_proxy_url") or ""), placeholder="http://127.0.0.1:8080")
|
|
5401
5401
|
timeout_seconds = st.number_input("Timeout (segundos)", min_value=5, max_value=900, value=int(settings.get("jewelmusic_timeout_seconds", 120)), step=5)
|
|
5402
|
-
save = st.form_submit_button("Guardar configuração JewelMusic", type="primary",
|
|
5402
|
+
save = st.form_submit_button("Guardar configuração JewelMusic", type="primary", width='stretch')
|
|
5403
5403
|
if save:
|
|
5404
5404
|
settings = _persist_music_upload_settings({
|
|
5405
5405
|
"jewelmusic_enabled": bool(enabled),
|
|
@@ -5410,7 +5410,7 @@ def _render_jewelmusic_upload_tab() -> None:
|
|
|
5410
5410
|
})
|
|
5411
5411
|
st.success("Configuração JewelMusic guardada no storage local.")
|
|
5412
5412
|
adapter = JewelMusicAdapter(settings)
|
|
5413
|
-
if st.button("Testar conexão JewelMusic", key="jewelmusic_test",
|
|
5413
|
+
if st.button("Testar conexão JewelMusic", key="jewelmusic_test", width='stretch'):
|
|
5414
5414
|
test_result = adapter.test_connection()
|
|
5415
5415
|
(st.success if test_result.ok else st.error)(test_result.message)
|
|
5416
5416
|
music_path = _render_music_source("jewelmusic", MUSIC_UPLOAD_EXTENSIONS)
|
|
@@ -5424,7 +5424,7 @@ def _render_jewelmusic_upload_tab() -> None:
|
|
|
5424
5424
|
with metadata_cols[3]:
|
|
5425
5425
|
year = st.text_input("Ano", key="jewelmusic_year")
|
|
5426
5426
|
genre = st.text_input("Género", key="jewelmusic_genre")
|
|
5427
|
-
if st.button("Enviar música para JewelMusic", type="primary", key="jewelmusic_upload",
|
|
5427
|
+
if st.button("Enviar música para JewelMusic", type="primary", key="jewelmusic_upload", width='stretch', disabled=not bool(music_path)):
|
|
5428
5428
|
result = JewelMusicAdapter(read_json("settings.json", {})).upload_track(music_path, title=title, artist=artist, album=album, year=year, genre=genre)
|
|
5429
5429
|
_record_music_upload("JewelMusic", result, music_path=music_path, target={"artist": artist, "title": title})
|
|
5430
5430
|
(st.success if result.ok else st.error)(result.message)
|
|
@@ -5459,7 +5459,7 @@ def _render_pushtunes_upload_tab() -> None:
|
|
|
5459
5459
|
spotify_client_secret = st.text_input("Spotify Client Secret", value=str(settings.get("pushtunes_spotify_client_secret") or ""), type="password")
|
|
5460
5460
|
spotify_redirect_uri = st.text_input("Spotify Redirect URI", value=str(settings.get("pushtunes_spotify_redirect_uri") or ""))
|
|
5461
5461
|
timeout_seconds = st.number_input("Timeout Pushtunes (segundos)", min_value=30, max_value=3600, value=int(settings.get("pushtunes_timeout_seconds", 1800)), step=30)
|
|
5462
|
-
save = st.form_submit_button("Guardar configuração Pushtunes", type="primary",
|
|
5462
|
+
save = st.form_submit_button("Guardar configuração Pushtunes", type="primary", width='stretch')
|
|
5463
5463
|
if save:
|
|
5464
5464
|
settings = _persist_music_upload_settings({
|
|
5465
5465
|
"pushtunes_enabled": bool(enabled),
|
|
@@ -5483,17 +5483,17 @@ def _render_pushtunes_upload_tab() -> None:
|
|
|
5483
5483
|
if source == "csv":
|
|
5484
5484
|
st.caption("Pode carregar o CSV nesta página e usar o caminho guardado como origem Pushtunes.")
|
|
5485
5485
|
csv_upload = st.file_uploader("Carregar CSV de origem Pushtunes", type=["csv"], key="pushtunes_csv_upload")
|
|
5486
|
-
if csv_upload is not None and st.button("Guardar CSV Pushtunes no storage", key="pushtunes_csv_store",
|
|
5486
|
+
if csv_upload is not None and st.button("Guardar CSV Pushtunes no storage", key="pushtunes_csv_store", width='stretch'):
|
|
5487
5487
|
stored_csv = _store_pushtunes_csv(csv_upload)
|
|
5488
5488
|
settings = _persist_music_upload_settings({"pushtunes_csv_file": stored_csv})
|
|
5489
5489
|
st.success(f"CSV guardado em `{stored_csv}`.")
|
|
5490
5490
|
adapter = PushtunesAdapter(read_json("settings.json", {}))
|
|
5491
5491
|
status = adapter.status()
|
|
5492
5492
|
(st.success if status.ok else st.warning)(status.message)
|
|
5493
|
-
if st.button("Validar instalação e configuração Pushtunes", key="pushtunes_test",
|
|
5493
|
+
if st.button("Validar instalação e configuração Pushtunes", key="pushtunes_test", width='stretch'):
|
|
5494
5494
|
check = PushtunesAdapter(read_json("settings.json", {})).status()
|
|
5495
5495
|
(st.success if check.ok else st.error)(check.message)
|
|
5496
|
-
if st.button("Executar sincronização Pushtunes", type="primary", key="pushtunes_sync",
|
|
5496
|
+
if st.button("Executar sincronização Pushtunes", type="primary", key="pushtunes_sync", width='stretch', disabled=not status.ok):
|
|
5497
5497
|
result = PushtunesAdapter(read_json("settings.json", {})).sync()
|
|
5498
5498
|
_record_music_upload(f"Pushtunes ({adapter.source} → {adapter.target})", result, target={"source": adapter.source, "target": adapter.target, "operation": adapter.operation})
|
|
5499
5499
|
(st.success if result.ok else st.error)(result.message)
|
|
@@ -5521,7 +5521,7 @@ def _render_ytmusicapi_upload_tab() -> None:
|
|
|
5521
5521
|
auth_file = st.text_input("Caminho do browser.json", value=str(settings.get("ytmusicapi_auth_file") or ""))
|
|
5522
5522
|
proxy_url = st.text_input("Proxy opcional", value=str(settings.get("ytmusicapi_proxy_url") or ""), placeholder="http://127.0.0.1:8080")
|
|
5523
5523
|
timeout_seconds = st.number_input("Timeout (segundos)", min_value=30, max_value=900, value=int(settings.get("ytmusicapi_timeout_seconds", 240)), step=10)
|
|
5524
|
-
save = st.form_submit_button("Guardar configuração ytmusicapi", type="primary",
|
|
5524
|
+
save = st.form_submit_button("Guardar configuração ytmusicapi", type="primary", width='stretch')
|
|
5525
5525
|
if save:
|
|
5526
5526
|
settings = _persist_music_upload_settings({
|
|
5527
5527
|
"ytmusicapi_enabled": bool(enabled),
|
|
@@ -5531,18 +5531,18 @@ def _render_ytmusicapi_upload_tab() -> None:
|
|
|
5531
5531
|
})
|
|
5532
5532
|
st.success("Configuração ytmusicapi guardada no storage local.")
|
|
5533
5533
|
auth_upload = st.file_uploader("Carregar browser.json", type=["json"], key="ytmusicapi_auth_upload")
|
|
5534
|
-
if auth_upload is not None and st.button("Guardar browser.json no storage local", key="ytmusicapi_auth_store",
|
|
5534
|
+
if auth_upload is not None and st.button("Guardar browser.json no storage local", key="ytmusicapi_auth_store", width='stretch'):
|
|
5535
5535
|
stored_auth = _store_ytmusicapi_auth(auth_upload)
|
|
5536
5536
|
settings = _persist_music_upload_settings({"ytmusicapi_auth_file": stored_auth, "ytmusicapi_enabled": True})
|
|
5537
5537
|
st.success(f"Ficheiro de autenticação guardado em `{stored_auth}`.")
|
|
5538
5538
|
adapter = YTMusicApiAdapter(read_json("settings.json", {}))
|
|
5539
5539
|
status = adapter.status()
|
|
5540
5540
|
(st.success if status.ok else st.warning)(status.message)
|
|
5541
|
-
if st.button("Testar autenticação ytmusicapi", key="ytmusicapi_test",
|
|
5541
|
+
if st.button("Testar autenticação ytmusicapi", key="ytmusicapi_test", width='stretch'):
|
|
5542
5542
|
result = YTMusicApiAdapter(read_json("settings.json", {})).test_connection()
|
|
5543
5543
|
(st.success if result.ok else st.error)(result.message)
|
|
5544
5544
|
music_path = _render_music_source("ytmusicapi", YT_MUSIC_UPLOAD_EXTENSIONS)
|
|
5545
|
-
if st.button("Enviar música para YouTube Music", type="primary", key="ytmusicapi_upload",
|
|
5545
|
+
if st.button("Enviar música para YouTube Music", type="primary", key="ytmusicapi_upload", width='stretch', disabled=not bool(music_path) or not status.ok):
|
|
5546
5546
|
result = YTMusicApiAdapter(read_json("settings.json", {})).upload_song(music_path)
|
|
5547
5547
|
_record_music_upload("ytmusicapi / YouTube Music", result, music_path=music_path, target={"service": "youtube_music"})
|
|
5548
5548
|
(st.success if result.ok else st.error)(result.message)
|
|
@@ -5574,7 +5574,7 @@ def _render_distrokid_upload_tab() -> None:
|
|
|
5574
5574
|
last_name = st.text_input("Apelido", value=str(settings.get("distrokid_last_name") or ""))
|
|
5575
5575
|
record_label = st.text_input("Record label", value=str(settings.get("distrokid_record_label") or ""))
|
|
5576
5576
|
genre = st.text_input("Género principal", value=str(settings.get("distrokid_genre") or ""), help="O género deve corresponder a uma opção aceite pelo formulário DistroKid.")
|
|
5577
|
-
save = st.form_submit_button("Guardar configuração DistroKid", type="primary",
|
|
5577
|
+
save = st.form_submit_button("Guardar configuração DistroKid", type="primary", width='stretch')
|
|
5578
5578
|
if save:
|
|
5579
5579
|
_persist_music_upload_settings({
|
|
5580
5580
|
"distrokid_enabled": bool(enabled), "distrokid_cookie": cookie.strip(), "distrokid_account": account.strip(),
|
|
@@ -5586,11 +5586,11 @@ def _render_distrokid_upload_tab() -> None:
|
|
|
5586
5586
|
adapter = DistroKidAdapter(settings)
|
|
5587
5587
|
status = adapter.status()
|
|
5588
5588
|
(st.success if status.ok else st.warning)(status.message)
|
|
5589
|
-
if st.button("Testar sessão DistroKid", key="distrokid_test",
|
|
5589
|
+
if st.button("Testar sessão DistroKid", key="distrokid_test", width='stretch'):
|
|
5590
5590
|
result = DistroKidAdapter(read_json("settings.json", {})).test_connection()
|
|
5591
5591
|
(st.success if result.ok else st.error)(result.message)
|
|
5592
5592
|
uploaded_tracks = st.file_uploader("Faixas para o lançamento", type=sorted(extension.lstrip(".") for extension in DISTROKID_AUDIO_EXTENSIONS), accept_multiple_files=True, key="distrokid_track_upload")
|
|
5593
|
-
if uploaded_tracks and st.button("Guardar faixas no storage local", key="distrokid_store_tracks",
|
|
5593
|
+
if uploaded_tracks and st.button("Guardar faixas no storage local", key="distrokid_store_tracks", width='stretch'):
|
|
5594
5594
|
stored_paths: list[str] = []
|
|
5595
5595
|
for uploaded in uploaded_tracks:
|
|
5596
5596
|
try:
|
|
@@ -5609,7 +5609,7 @@ def _render_distrokid_upload_tab() -> None:
|
|
|
5609
5609
|
instrumental = st.checkbox("Instrumental", value=False, key=f"distrokid_track_instrumental_{index}")
|
|
5610
5610
|
track_rows.append({"path": path, "title": track_title.strip() or Path(path).stem, "instrumental": instrumental})
|
|
5611
5611
|
cover_upload = st.file_uploader("Capa do lançamento (opcional)", type=sorted(extension.lstrip(".") for extension in DISTROKID_COVER_EXTENSIONS), key="distrokid_cover_upload")
|
|
5612
|
-
if cover_upload and st.button("Guardar capa no storage local", key="distrokid_store_cover",
|
|
5612
|
+
if cover_upload and st.button("Guardar capa no storage local", key="distrokid_store_cover", width='stretch'):
|
|
5613
5613
|
try:
|
|
5614
5614
|
st.session_state["distrokid_cover_path"] = _store_distrokid_cover(cover_upload)
|
|
5615
5615
|
st.success("Capa guardada no storage local.")
|
|
@@ -5619,11 +5619,11 @@ def _render_distrokid_upload_tab() -> None:
|
|
|
5619
5619
|
if cover_path and Path(cover_path).is_file():
|
|
5620
5620
|
st.caption(f"Capa seleccionada: `{cover_path}`")
|
|
5621
5621
|
session_id = str(st.session_state.get("distrokid_session_id") or "")
|
|
5622
|
-
if session_id and st.button("Fechar browser DistroKid", key="distrokid_close_browser",
|
|
5622
|
+
if session_id and st.button("Fechar browser DistroKid", key="distrokid_close_browser", width='stretch'):
|
|
5623
5623
|
result = close_distrokid_session(session_id)
|
|
5624
5624
|
(st.success if result.ok else st.warning)(result.message)
|
|
5625
5625
|
st.session_state.pop("distrokid_session_id", None)
|
|
5626
|
-
if st.button("Abrir formulário e carregar para DistroKid", type="primary", key="distrokid_prepare_upload",
|
|
5626
|
+
if st.button("Abrir formulário e carregar para DistroKid", type="primary", key="distrokid_prepare_upload", width='stretch', disabled=not status.ok or not track_rows):
|
|
5627
5627
|
current = read_json("settings.json", {})
|
|
5628
5628
|
result = DistroKidAdapter(current).prepare_upload(
|
|
5629
5629
|
track_rows, artist=str(current.get("distrokid_artist") or ""), release_title=str(current.get("distrokid_release_title") or ""),
|
|
@@ -5675,7 +5675,7 @@ def render_upload_composio():
|
|
|
5675
5675
|
return
|
|
5676
5676
|
query = st.text_input("Pesquisar ferramenta Composio", value="upload a video file", key="composio_upload_query")
|
|
5677
5677
|
toolkit_filter = st.text_input("Toolkit preferido (opcional)", value=str(settings.get("composio_toolkit") or ""), key="composio_upload_toolkit")
|
|
5678
|
-
if st.button("Descobrir ferramentas", key="composio_upload_discover",
|
|
5678
|
+
if st.button("Descobrir ferramentas", key="composio_upload_discover", width='stretch'):
|
|
5679
5679
|
try:
|
|
5680
5680
|
st.session_state["composio_upload_tools"] = discover_tools(api_key, user_id, query, toolkit_filter)
|
|
5681
5681
|
if not st.session_state["composio_upload_tools"]:
|
|
@@ -5698,7 +5698,7 @@ def render_upload_composio():
|
|
|
5698
5698
|
st.json(schema)
|
|
5699
5699
|
file_field = st.text_input("Campo que recebe o vídeo", value=str(st.session_state.get("composio_upload_file_field") or "file"), key="composio_upload_file_field")
|
|
5700
5700
|
arguments_json = st.text_area("Argumentos JSON adicionais", value=str(st.session_state.get("composio_upload_arguments") or "{}"), height=150, key="composio_upload_arguments")
|
|
5701
|
-
if st.button("Autorizar toolkit no Composio", key="composio_upload_authorize",
|
|
5701
|
+
if st.button("Autorizar toolkit no Composio", key="composio_upload_authorize", width='stretch'):
|
|
5702
5702
|
try:
|
|
5703
5703
|
auth = authorize_toolkit(api_key, user_id, str(selected_tool.get("toolkit") or ""))
|
|
5704
5704
|
if auth.get("redirect_url"):
|
|
@@ -5718,7 +5718,7 @@ def render_upload_composio():
|
|
|
5718
5718
|
video_path = str((task.get("artifacts", {}) or {}).get("video") or "")
|
|
5719
5719
|
st.caption(video_path or "Sem caminho de vídeo registado")
|
|
5720
5720
|
st.info(f"Resumo: `{selected_slug}` · toolkit `{selected_tool.get('toolkit') or 'não indicado'}` · campo `{file_field or 'não indicado'}`")
|
|
5721
|
-
if st.button("Enviar vídeo via Composio", type="primary", key=f"composio_upload_send_{selected_task_id}",
|
|
5721
|
+
if st.button("Enviar vídeo via Composio", type="primary", key=f"composio_upload_send_{selected_task_id}", width='stretch'):
|
|
5722
5722
|
try:
|
|
5723
5723
|
parse_arguments(arguments_json)
|
|
5724
5724
|
result = execute_upload(api_key, user_id, selected_slug, video_path, file_field, arguments_json)
|
|
@@ -5991,14 +5991,14 @@ def render_upload_conventional():
|
|
|
5991
5991
|
status_cols = st.columns(2)
|
|
5992
5992
|
with status_cols[0]:
|
|
5993
5993
|
(st.success if status["agent"].ok else st.warning)(f"Agente: {status['agent'].message}")
|
|
5994
|
-
if st.button("Autorizar agente YouTube", key="youtube_authorize_agent",
|
|
5994
|
+
if st.button("Autorizar agente YouTube", key="youtube_authorize_agent", width='stretch'):
|
|
5995
5995
|
result = youtube.authorize_agent()
|
|
5996
5996
|
(st.success if result.ok else st.error)(result.message)
|
|
5997
5997
|
if result.ok:
|
|
5998
5998
|
st.rerun()
|
|
5999
5999
|
with status_cols[1]:
|
|
6000
6000
|
(st.info if status["fallback"].ok else st.caption)(f"Fallback OAuth: {status['fallback'].message}")
|
|
6001
|
-
if st.button("Autorizar fallback OAuth", key="youtube_authorize_fallback",
|
|
6001
|
+
if st.button("Autorizar fallback OAuth", key="youtube_authorize_fallback", width='stretch'):
|
|
6002
6002
|
result = youtube.authorize_fallback()
|
|
6003
6003
|
(st.success if result.ok else st.error)(result.message)
|
|
6004
6004
|
if result.ok:
|
|
@@ -6051,7 +6051,7 @@ def render_upload_conventional():
|
|
|
6051
6051
|
st.session_state.pop(description_error_key, None)
|
|
6052
6052
|
|
|
6053
6053
|
description = st.text_area("Descrição", value=task.get("description", ""), key=description_state_key, height=100)
|
|
6054
|
-
st.button("Gerar descrição com IA", key=f"yt_description_ai_{task['id']}",
|
|
6054
|
+
st.button("Gerar descrição com IA", key=f"yt_description_ai_{task['id']}", width='content', on_click=generate_upload_description_callback)
|
|
6055
6055
|
if description_error := str(st.session_state.get(description_error_key) or "").strip():
|
|
6056
6056
|
st.error(f"Não foi possível gerar a descrição: {description_error}")
|
|
6057
6057
|
tags_raw = st.text_input("Tags separadas por vírgula", value=task.get("tags", "") if isinstance(task.get("tags", ""), str) else ", ".join(task.get("tags", [])), key=f"yt_tags_{task['id']}")
|
|
@@ -6219,11 +6219,11 @@ def render_tiktok_api_cards(settings: dict[str, Any]) -> None:
|
|
|
6219
6219
|
client_secret = st.text_input("TikTok Client Secret", value=card["client_secret"], type="password", key=f"tiktok_api_{card_id}_client_secret")
|
|
6220
6220
|
action_cols = st.columns(3)
|
|
6221
6221
|
with action_cols[0]:
|
|
6222
|
-
test_clicked = st.form_submit_button("Testar chamada API",
|
|
6222
|
+
test_clicked = st.form_submit_button("Testar chamada API", width='stretch')
|
|
6223
6223
|
with action_cols[1]:
|
|
6224
|
-
save_clicked = st.form_submit_button("Guardar card", type="primary",
|
|
6224
|
+
save_clicked = st.form_submit_button("Guardar card", type="primary", width='stretch')
|
|
6225
6225
|
with action_cols[2]:
|
|
6226
|
-
delete_clicked = st.form_submit_button("Apagar card",
|
|
6226
|
+
delete_clicked = st.form_submit_button("Apagar card", width='stretch')
|
|
6227
6227
|
edited = {"id": card_id, "client_id": client_id.strip(), "client_secret": client_secret.strip()}
|
|
6228
6228
|
if test_clicked:
|
|
6229
6229
|
cards[index] = edited
|
|
@@ -6245,7 +6245,7 @@ def render_tiktok_api_cards(settings: dict[str, Any]) -> None:
|
|
|
6245
6245
|
_persist_tiktok_api_cards(settings, cards)
|
|
6246
6246
|
st.success("Card TikTok apagado.")
|
|
6247
6247
|
st.rerun()
|
|
6248
|
-
if st.button("Adicionar nova API", type="primary",
|
|
6248
|
+
if st.button("Adicionar nova API", type="primary", width='stretch', key="add_tiktok_api_card"):
|
|
6249
6249
|
cards.append({"id": f"tiktok-api-{uuid.uuid4().hex[:10]}", "client_id": "", "client_secret": ""})
|
|
6250
6250
|
_persist_tiktok_api_cards(settings, cards)
|
|
6251
6251
|
st.success("Novo card TikTok criado.")
|
|
@@ -6297,11 +6297,11 @@ def render_bilibili_api_cards(settings: dict[str, Any]) -> None:
|
|
|
6297
6297
|
proxy = st.text_input("Proxy opcional", value=str(card.get("proxy") or ""), key=f"bilibili_api_{card_id}_proxy", placeholder="http://127.0.0.1:8080")
|
|
6298
6298
|
action_cols = st.columns(3)
|
|
6299
6299
|
with action_cols[0]:
|
|
6300
|
-
test_clicked = st.form_submit_button("Testar chamada API",
|
|
6300
|
+
test_clicked = st.form_submit_button("Testar chamada API", width='stretch')
|
|
6301
6301
|
with action_cols[1]:
|
|
6302
|
-
save_clicked = st.form_submit_button("Guardar card", type="primary",
|
|
6302
|
+
save_clicked = st.form_submit_button("Guardar card", type="primary", width='stretch')
|
|
6303
6303
|
with action_cols[2]:
|
|
6304
|
-
delete_clicked = st.form_submit_button("Apagar card",
|
|
6304
|
+
delete_clicked = st.form_submit_button("Apagar card", width='stretch')
|
|
6305
6305
|
edited = {"id": card_id, "label": label.strip() or f"Conta Bilibili {index + 1}", "active": bool(active), "sessdata": sessdata.strip(), "bili_jct": bili_jct.strip(), "buvid3": buvid3.strip(), "buvid4": buvid4.strip(), "dedeuserid": dedeuserid.strip(), "ac_time_value": ac_time_value.strip(), "proxy": proxy.strip()}
|
|
6306
6306
|
if test_clicked:
|
|
6307
6307
|
cards[index] = edited
|
|
@@ -6318,7 +6318,7 @@ def render_bilibili_api_cards(settings: dict[str, Any]) -> None:
|
|
|
6318
6318
|
_persist_bilibili_api_cards(settings, cards)
|
|
6319
6319
|
st.success("Card Bilibili apagado.")
|
|
6320
6320
|
st.rerun()
|
|
6321
|
-
if st.button("Adicionar nova API", type="primary",
|
|
6321
|
+
if st.button("Adicionar nova API", type="primary", width='stretch', key="add_bilibili_api_card"):
|
|
6322
6322
|
cards.append({"id": f"bilibili-api-{uuid.uuid4().hex[:10]}", "label": f"Conta Bilibili {len(cards) + 1}", "active": True, "sessdata": "", "bili_jct": "", "buvid3": "", "buvid4": "", "dedeuserid": "", "ac_time_value": "", "proxy": ""})
|
|
6323
6323
|
_persist_bilibili_api_cards(settings, cards)
|
|
6324
6324
|
st.success("Novo card Bilibili criado.")
|
|
@@ -6402,7 +6402,7 @@ def render_google_accounts(*, include_innertube: bool = True):
|
|
|
6402
6402
|
key=f"batch_session_info_captured_at_{account_id}",
|
|
6403
6403
|
help="Data em que o sessionInfo token foi capturado. É usada para calcular o alerta de expiração e não revela o token.",
|
|
6404
6404
|
)
|
|
6405
|
-
save_account = st.form_submit_button("Guardar dados da conta Google", type="primary",
|
|
6405
|
+
save_account = st.form_submit_button("Guardar dados da conta Google", type="primary", width='stretch')
|
|
6406
6406
|
|
|
6407
6407
|
st.markdown("**Documento de cookies/credenciais desta conta Google**")
|
|
6408
6408
|
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.")
|
|
@@ -6417,20 +6417,20 @@ def render_google_accounts(*, include_innertube: bool = True):
|
|
|
6417
6417
|
else:
|
|
6418
6418
|
st.success("Documento completo para a conta Google")
|
|
6419
6419
|
st.caption(f"Documento guardado em: {direct_status['document_file']}")
|
|
6420
|
-
document_save = st.button("Guardar documento nesta conta", key=f"save_direct_account_{account_id}",
|
|
6420
|
+
document_save = st.button("Guardar documento nesta conta", key=f"save_direct_account_{account_id}", width='stretch')
|
|
6421
6421
|
|
|
6422
6422
|
account_status = youtube_batch_account_status(batch_account, STORAGE)
|
|
6423
6423
|
status_cols = st.columns(3)
|
|
6424
6424
|
with status_cols[0]:
|
|
6425
6425
|
(st.success if account_status.ok else st.warning)(account_status.message)
|
|
6426
6426
|
with status_cols[1]:
|
|
6427
|
-
if st.button("Autorizar/Reautorizar", key=f"batch_authorize_settings_{account_id}",
|
|
6427
|
+
if st.button("Autorizar/Reautorizar", key=f"batch_authorize_settings_{account_id}", width='stretch'):
|
|
6428
6428
|
result = authorize_youtube_batch_account(batch_account, STORAGE)
|
|
6429
6429
|
(st.success if result.ok else st.error)(result.message)
|
|
6430
6430
|
if result.ok:
|
|
6431
6431
|
st.rerun()
|
|
6432
6432
|
with status_cols[2]:
|
|
6433
|
-
if st.button("Apagar conta", icon=":material/delete:", key=f"batch_remove_settings_{account_id}",
|
|
6433
|
+
if st.button("Apagar conta", icon=":material/delete:", key=f"batch_remove_settings_{account_id}", width='stretch'):
|
|
6434
6434
|
delete_youtube_batch_token(batch_account, STORAGE)
|
|
6435
6435
|
delete_credentials_document(STORAGE, batch_account)
|
|
6436
6436
|
remaining_accounts = [account for account in batch_accounts if str(account.get("id")) != account_id]
|
|
@@ -6531,7 +6531,7 @@ def render_google_accounts(*, include_innertube: bool = True):
|
|
|
6531
6531
|
lambda: test_innertube_api_key(innertube_api_key_value),
|
|
6532
6532
|
widget_key="api_test_innertube",
|
|
6533
6533
|
)
|
|
6534
|
-
save_innertube_api_key = st.form_submit_button("Guardar INNERTUBE_API_KEY global", type="primary",
|
|
6534
|
+
save_innertube_api_key = st.form_submit_button("Guardar INNERTUBE_API_KEY global", type="primary", width='stretch')
|
|
6535
6535
|
if save_innertube_api_key:
|
|
6536
6536
|
settings["direct_innertube_api_key"] = innertube_api_key_value.strip()
|
|
6537
6537
|
settings.pop("INNERTUBE_API_KEY", None)
|
|
@@ -6556,7 +6556,7 @@ def render_google_accounts(*, include_innertube: bool = True):
|
|
|
6556
6556
|
new_account_client_secret = st.text_input("OAuth Client Secret", type="password", key="new_batch_account_client_secret")
|
|
6557
6557
|
new_account_session_info = st.text_input("sessionInfo token desta conta Google", type="password", key="new_batch_account_session_info", help="Token sessionInfo desta conta. Os cookies e delegated_session_ids ficam no documento; a INNERTUBE_API_KEY é configurada no bloco próprio acima.")
|
|
6558
6558
|
new_account_document = st.file_uploader("Documento de cookies/credenciais opcional", type=["json"], key="new_batch_account_credentials_document", help="Pode subir agora um JSON completo ou apenas o documento de cookies. Se não subir, será criado um credentials.json padrão vazio.")
|
|
6559
|
-
add_account = st.form_submit_button("Adicionar conta Google/YouTube", type="primary",
|
|
6559
|
+
add_account = st.form_submit_button("Adicionar conta Google/YouTube", type="primary", width='stretch')
|
|
6560
6560
|
if add_account:
|
|
6561
6561
|
document_error = ""
|
|
6562
6562
|
if "@" not in new_account_email.strip():
|
|
@@ -6616,7 +6616,7 @@ def render_google_accounts(*, include_innertube: bool = True):
|
|
|
6616
6616
|
key="google_page_youtube_api_key",
|
|
6617
6617
|
help="Credencial Google Cloud separada, usada apenas para métricas oficiais da YouTube Data API.",
|
|
6618
6618
|
)
|
|
6619
|
-
save_google_global = st.form_submit_button("Guardar configuração global do YouTube", type="primary",
|
|
6619
|
+
save_google_global = st.form_submit_button("Guardar configuração global do YouTube", type="primary", width='stretch')
|
|
6620
6620
|
if save_google_global:
|
|
6621
6621
|
settings.update({
|
|
6622
6622
|
"youtube_client_id": youtube_client_id.strip(),
|
|
@@ -6693,7 +6693,7 @@ def _render_material_source_card(settings: dict[str, Any], cards: list[dict[str,
|
|
|
6693
6693
|
lambda: test_material_source_credentials(provider, api_key),
|
|
6694
6694
|
widget_key=f"api_test_material_{card_id}",
|
|
6695
6695
|
)
|
|
6696
|
-
save_card = st.form_submit_button("Salvar", type="primary",
|
|
6696
|
+
save_card = st.form_submit_button("Salvar", type="primary", width='stretch', key=f"material_card_{card_id}_save")
|
|
6697
6697
|
if save_card:
|
|
6698
6698
|
cards[index] = {
|
|
6699
6699
|
**card,
|
|
@@ -6726,7 +6726,7 @@ def render_material_source_api_keys(settings: dict[str, Any], *, embedded: bool
|
|
|
6726
6726
|
format_func=lambda value: _material_source_card_definition(value)["label"],
|
|
6727
6727
|
key="material_new_provider_choice",
|
|
6728
6728
|
)
|
|
6729
|
-
add_source_clicked = st.form_submit_button("Configurar Nova Fonte de Materiais", type="primary",
|
|
6729
|
+
add_source_clicked = st.form_submit_button("Configurar Nova Fonte de Materiais", type="primary", width='stretch', key="add_material_source_card") if embedded else st.button("Configurar Nova Fonte de Materiais", type="primary", width='stretch', key="add_material_source_card")
|
|
6730
6730
|
if add_source_clicked:
|
|
6731
6731
|
new_card = new_material_card(provider_to_add, card_id=f"material-{provider_to_add}-{uuid.uuid4().hex[:8]}")
|
|
6732
6732
|
new_card["priority"] = max(
|
|
@@ -6817,7 +6817,7 @@ def _render_api_test_control(
|
|
|
6817
6817
|
consistent: testing it must not appear successful while its values remain
|
|
6818
6818
|
only in the browser session.
|
|
6819
6819
|
"""
|
|
6820
|
-
if st.form_submit_button("Testar chamada API",
|
|
6820
|
+
if st.form_submit_button("Testar chamada API", width='stretch', key=widget_key):
|
|
6821
6821
|
with st.spinner(ui_text("A testar chamada API…", current_ui_language())):
|
|
6822
6822
|
try:
|
|
6823
6823
|
if persist_callback is not None:
|
|
@@ -6917,9 +6917,9 @@ def _render_llm_card(settings: dict[str, Any], cards: list[dict[str, Any]], inde
|
|
|
6917
6917
|
with action_col:
|
|
6918
6918
|
action_buttons = st.columns(2)
|
|
6919
6919
|
with action_buttons[0]:
|
|
6920
|
-
refresh_clicked = st.form_submit_button("Consultar modelos",
|
|
6920
|
+
refresh_clicked = st.form_submit_button("Consultar modelos", width='stretch', key=f"llm_card_{card_id}_refresh")
|
|
6921
6921
|
with action_buttons[1]:
|
|
6922
|
-
test_clicked = st.form_submit_button("Testar chamada API",
|
|
6922
|
+
test_clicked = st.form_submit_button("Testar chamada API", width='stretch', key=f"llm_card_{card_id}_test")
|
|
6923
6923
|
|
|
6924
6924
|
extra_values: dict[str, str] = {}
|
|
6925
6925
|
if definition.extra_fields:
|
|
@@ -6956,10 +6956,10 @@ def _render_llm_card(settings: dict[str, Any], cards: list[dict[str, Any]], inde
|
|
|
6956
6956
|
if telegram_llm:
|
|
6957
6957
|
st.caption("Exclusivo para Notificações de Telegram — prioridade ignorada no pool LLM.")
|
|
6958
6958
|
|
|
6959
|
-
save_clicked = st.form_submit_button("Salvar", type="primary",
|
|
6959
|
+
save_clicked = st.form_submit_button("Salvar", type="primary", width='stretch', key=f"llm_card_{card_id}_save")
|
|
6960
6960
|
remove_clicked = False
|
|
6961
6961
|
if definition.code != "openai":
|
|
6962
|
-
remove_clicked = st.form_submit_button("Remover cartão",
|
|
6962
|
+
remove_clicked = st.form_submit_button("Remover cartão", width='stretch', key=f"llm_card_{card_id}_remove")
|
|
6963
6963
|
|
|
6964
6964
|
edited = dict(card)
|
|
6965
6965
|
edited.update({"api_key": str(api_key or "").strip(), "model": str(model or "").strip(), "base_url": str(base_url or "").strip(), "enabled": bool(enabled), "priority": max(1, int(priority)), "telegram_llm": bool(telegram_llm), **extra_values})
|
|
@@ -7015,9 +7015,9 @@ def render_llm_provider_cards(settings: dict[str, Any], *, embedded: bool = Fals
|
|
|
7015
7015
|
with rpm_cols[2]:
|
|
7016
7016
|
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")
|
|
7017
7017
|
save_llm_rpm_clicked = (
|
|
7018
|
-
st.form_submit_button("Guardar limite LLM NVIDIA NIM", type="primary",
|
|
7018
|
+
st.form_submit_button("Guardar limite LLM NVIDIA NIM", type="primary", width='stretch', key="save_llm_rpm_limit")
|
|
7019
7019
|
if embedded
|
|
7020
|
-
else st.button("Guardar limite LLM NVIDIA NIM", type="primary",
|
|
7020
|
+
else st.button("Guardar limite LLM NVIDIA NIM", type="primary", width='stretch', key="save_llm_rpm_limit")
|
|
7021
7021
|
)
|
|
7022
7022
|
if save_llm_rpm_clicked:
|
|
7023
7023
|
settings.update({
|
|
@@ -7039,9 +7039,9 @@ def render_llm_provider_cards(settings: dict[str, Any], *, embedded: bool = Fals
|
|
|
7039
7039
|
key="llm_new_provider_choice",
|
|
7040
7040
|
)
|
|
7041
7041
|
add_provider_clicked = (
|
|
7042
|
-
st.form_submit_button("Configurar Novo Provedor LLM", type="primary",
|
|
7042
|
+
st.form_submit_button("Configurar Novo Provedor LLM", type="primary", width='stretch', key="add_llm_provider_card")
|
|
7043
7043
|
if embedded
|
|
7044
|
-
else st.button("Configurar Novo Provedor LLM", type="primary",
|
|
7044
|
+
else st.button("Configurar Novo Provedor LLM", type="primary", width='stretch', key="add_llm_provider_card")
|
|
7045
7045
|
)
|
|
7046
7046
|
if add_provider_clicked:
|
|
7047
7047
|
new_card = new_llm_card(provider_to_add, card_id=f"llm-{provider_to_add}-{uuid.uuid4().hex[:8]}")
|
|
@@ -7161,7 +7161,7 @@ def _render_media_provider_card(settings: dict[str, Any], cards: list[dict[str,
|
|
|
7161
7161
|
)
|
|
7162
7162
|
model = "" if selected_model == "__select_model__" else selected_model
|
|
7163
7163
|
if definition.code == "canva":
|
|
7164
|
-
refresh_clicked = st.form_submit_button("Consultar Modelos",
|
|
7164
|
+
refresh_clicked = st.form_submit_button("Consultar Modelos", width='stretch', key=f"media_card_{card_id}_refresh")
|
|
7165
7165
|
if definition.code == "canva":
|
|
7166
7166
|
base_url = definition.default_base_url
|
|
7167
7167
|
st.text_input("Base URL", value=base_url, disabled=True, key=f"media_card_{card_id}_base_url_display")
|
|
@@ -7193,7 +7193,7 @@ def _render_media_provider_card(settings: dict[str, Any], cards: list[dict[str,
|
|
|
7193
7193
|
if not has_credentials:
|
|
7194
7194
|
st.info("Preencha Client Id, Client Secret e Redirect Uri para iniciar a autorização.")
|
|
7195
7195
|
else:
|
|
7196
|
-
authorize_clicked = st.form_submit_button("Iniciar autorização",
|
|
7196
|
+
authorize_clicked = st.form_submit_button("Iniciar autorização", width='stretch', key=f"media_card_{card_id}_authorize") if embedded else st.button("Iniciar autorização", key=f"media_card_{card_id}_authorize")
|
|
7197
7197
|
if authorize_clicked:
|
|
7198
7198
|
verifier, challenge = create_pkce_pair()
|
|
7199
7199
|
state = create_state()
|
|
@@ -7221,13 +7221,13 @@ def _render_media_provider_card(settings: dict[str, Any], cards: list[dict[str,
|
|
|
7221
7221
|
action_cols = st.columns(4)
|
|
7222
7222
|
with action_cols[0]:
|
|
7223
7223
|
if definition.code != "canva":
|
|
7224
|
-
refresh_clicked = st.form_submit_button("Consultar Modelos",
|
|
7224
|
+
refresh_clicked = st.form_submit_button("Consultar Modelos", width='stretch', key=f"media_card_{card_id}_refresh")
|
|
7225
7225
|
with action_cols[1]:
|
|
7226
|
-
test_clicked = st.form_submit_button("Testar Chamada API",
|
|
7226
|
+
test_clicked = st.form_submit_button("Testar Chamada API", width='stretch', key=f"media_card_{card_id}_test")
|
|
7227
7227
|
with action_cols[2]:
|
|
7228
|
-
save_clicked = st.form_submit_button("Salvar", type="primary",
|
|
7228
|
+
save_clicked = st.form_submit_button("Salvar", type="primary", width='stretch', key=f"media_card_{card_id}_save")
|
|
7229
7229
|
with action_cols[3]:
|
|
7230
|
-
remove_clicked = st.form_submit_button("Remover provider",
|
|
7230
|
+
remove_clicked = st.form_submit_button("Remover provider", width='stretch', key=f"media_card_{card_id}_remove")
|
|
7231
7231
|
edited = dict(card)
|
|
7232
7232
|
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})
|
|
7233
7233
|
cards[index] = edited
|
|
@@ -7279,7 +7279,7 @@ def render_media_provider_cards(settings: dict[str, Any], *, embedded: bool = Fa
|
|
|
7279
7279
|
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")
|
|
7280
7280
|
with selector_cols[2]:
|
|
7281
7281
|
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")
|
|
7282
|
-
if st.button("Salvar selecção dos pools",
|
|
7282
|
+
if st.button("Salvar selecção dos pools", width='stretch', key="save_media_pool_selection") if not embedded else st.form_submit_button("Salvar selecção dos pools", width='stretch', key="save_media_pool_selection"):
|
|
7283
7283
|
settings["media_video_pool_enabled"] = bool(video_pool_enabled)
|
|
7284
7284
|
_persist_media_cards(settings, cards, image_active_id, video_active_id)
|
|
7285
7285
|
st.success("Selecção dos pools guardada.")
|
|
@@ -7290,7 +7290,7 @@ def render_media_provider_cards(settings: dict[str, Any], *, embedded: bool = Fa
|
|
|
7290
7290
|
st.markdown("**Adicionar provider de imagem/vídeo**")
|
|
7291
7291
|
provider_codes = [item["code"] for item in media_provider_catalog()]
|
|
7292
7292
|
provider_to_add = st.selectbox("Provider de media", provider_codes, format_func=lambda value: media_provider_definition(value).label, key="media_new_provider_choice")
|
|
7293
|
-
add_clicked = st.form_submit_button("Adicionar provider de imagem/vídeo",
|
|
7293
|
+
add_clicked = st.form_submit_button("Adicionar provider de imagem/vídeo", width='stretch', key="add_media_provider_card") if embedded else st.button("Adicionar provider de imagem/vídeo", width='stretch', key="add_media_provider_card")
|
|
7294
7294
|
if add_clicked:
|
|
7295
7295
|
cards.append(new_media_card(provider_to_add, card_id=f"media-{provider_to_add}-{uuid.uuid4().hex[:8]}"))
|
|
7296
7296
|
_persist_media_cards(settings, cards, image_active_id, video_active_id)
|
|
@@ -7328,7 +7328,7 @@ def render_settings():
|
|
|
7328
7328
|
for index, (key, label) in enumerate((("json", "JSON/API"), ("log", "Logs"), ("pdf", "PDF"), ("csv", "CSV"), ("diff", "Diffs"), ("code", "Código"))):
|
|
7329
7329
|
with optimizer_cols[index % 4]:
|
|
7330
7330
|
optimizer_values[f"token_optimizer_{key}_enabled"] = st.checkbox(label, value=bool(settings.get(f"token_optimizer_{key}_enabled", True)), key=f"token_optimizer_{key}_ui")
|
|
7331
|
-
if st.button("Guardar optimizador", key="save_token_optimizer",
|
|
7331
|
+
if st.button("Guardar optimizador", key="save_token_optimizer", width='stretch'):
|
|
7332
7332
|
settings.update({"token_optimizer_enabled": optimizer_enabled, **optimizer_values})
|
|
7333
7333
|
write_json("settings.json", settings)
|
|
7334
7334
|
st.success("Configuração do jusTokenMax guardada.")
|
|
@@ -7401,7 +7401,7 @@ def render_settings():
|
|
|
7401
7401
|
})
|
|
7402
7402
|
write_json("settings.json", settings)
|
|
7403
7403
|
|
|
7404
|
-
if st.form_submit_button("Guardar Azure Speech", type="primary",
|
|
7404
|
+
if st.form_submit_button("Guardar Azure Speech", type="primary", width='stretch', key="save_azure_speech"):
|
|
7405
7405
|
save_azure_speech()
|
|
7406
7406
|
st.success("Azure Speech guardado.")
|
|
7407
7407
|
_render_api_test_control(
|
|
@@ -7509,7 +7509,7 @@ def render_settings():
|
|
|
7509
7509
|
settings.update({"lyria_api_key": lyria_api_key.strip(), "lyria_model": lyria_model})
|
|
7510
7510
|
write_json("settings.json", settings)
|
|
7511
7511
|
|
|
7512
|
-
if st.form_submit_button("Guardar Google Lyria", type="primary",
|
|
7512
|
+
if st.form_submit_button("Guardar Google Lyria", type="primary", width='stretch', key="save_google_lyria"):
|
|
7513
7513
|
save_google_lyria()
|
|
7514
7514
|
st.success("Google Lyria guardado.")
|
|
7515
7515
|
_render_api_test_control(
|
|
@@ -7593,7 +7593,7 @@ def render_settings():
|
|
|
7593
7593
|
with st.form("upload_innertube_api_key_form"):
|
|
7594
7594
|
upload_innertube = st.text_input("INNERTUBE_API_KEY", value=current_upload_innertube, type="password", key="upload_api_innertube_key", help="Chave global usada pelo Upload directo para todas as contas Google/YouTube. Guarde-a na configuração global, separada dos documentos de cookies.")
|
|
7595
7595
|
_render_api_test_control(settings, "innertube_upload", lambda: test_innertube_api_key(upload_innertube), widget_key="api_test_innertube_upload")
|
|
7596
|
-
save_upload_innertube = st.form_submit_button("Guardar INNERTUBE_API_KEY global", type="primary",
|
|
7596
|
+
save_upload_innertube = st.form_submit_button("Guardar INNERTUBE_API_KEY global", type="primary", width='stretch')
|
|
7597
7597
|
if save_upload_innertube:
|
|
7598
7598
|
settings["direct_innertube_api_key"] = upload_innertube.strip()
|
|
7599
7599
|
settings.pop("INNERTUBE_API_KEY", None)
|
|
@@ -7625,9 +7625,9 @@ def render_settings():
|
|
|
7625
7625
|
_render_credential_status(composio_api_key)
|
|
7626
7626
|
composio_action_cols = st.columns(2)
|
|
7627
7627
|
with composio_action_cols[0]:
|
|
7628
|
-
save_composio = st.button("Guardar Composio", type="primary",
|
|
7628
|
+
save_composio = st.button("Guardar Composio", type="primary", width='stretch', key="upload_composio_save")
|
|
7629
7629
|
with composio_action_cols[1]:
|
|
7630
|
-
test_composio = st.button("Testar configuração",
|
|
7630
|
+
test_composio = st.button("Testar configuração", width='stretch', key="upload_composio_test")
|
|
7631
7631
|
if save_composio:
|
|
7632
7632
|
settings.update({"composio_enabled": bool(composio_enabled), "composio_auto_upload": bool(composio_auto_upload), "composio_api_key": composio_api_key.strip(), "composio_user_id": composio_user_id.strip() or "thunderbolt-local", "composio_toolkit": composio_toolkit.strip(), "composio_tool_slug": composio_tool_slug.strip(), "composio_file_field": composio_file_field.strip() or "file", "composio_channel_field": composio_channel_field.strip() or "channel_id", "composio_privacy_field": composio_privacy_field.strip() or "privacy_status", "composio_category_field": composio_category_field.strip() or "category_id", "composio_language_field": composio_language_field.strip() or "language", "composio_arguments_json": composio_arguments_json.strip() or "{}"})
|
|
7633
7633
|
write_json("settings.json", settings)
|
|
@@ -7646,7 +7646,7 @@ def render_settings():
|
|
|
7646
7646
|
upload_post_user_upload = st.text_input("Upload-Post username", value=str(settings.get("upload_post_username") or ""), key="upload_tab_upload_post_user")
|
|
7647
7647
|
upload_post_platforms_upload = st.text_input("Plataformas Upload-Post", value=str(settings.get("upload_post_platforms") or "youtube,tiktok"), key="upload_tab_upload_post_platforms")
|
|
7648
7648
|
upload_post_auto_upload = st.checkbox("Publicar automaticamente após gerar", bool(settings.get("upload_post_auto_upload", False)), key="upload_tab_upload_post_auto")
|
|
7649
|
-
if st.button("Guardar Upload-Post", type="primary",
|
|
7649
|
+
if st.button("Guardar Upload-Post", type="primary", width='stretch', key="upload_tab_save_upload_post"):
|
|
7650
7650
|
settings.update({"upload_post_enabled": bool(upload_post_enabled_upload), "upload_post_api_key": upload_post_key_upload.strip(), "upload_post_username": upload_post_user_upload.strip(), "upload_post_platforms": upload_post_platforms_upload.strip(), "upload_post_auto_upload": bool(upload_post_auto_upload)})
|
|
7651
7651
|
write_json("settings.json", settings)
|
|
7652
7652
|
st.success("Upload-Post guardado.")
|
|
@@ -7658,7 +7658,7 @@ def render_settings():
|
|
|
7658
7658
|
postiz_mcp_upload = st.text_input("Postiz MCP URL", value=str(settings.get("postiz_mcp_url") or "https://api.postiz.com/mcp"), key="upload_tab_postiz_mcp")
|
|
7659
7659
|
postiz_integration_upload = st.text_input("Postiz integração padrão", value=str(settings.get("postiz_integration_id") or ""), key="upload_tab_postiz_integration")
|
|
7660
7660
|
postiz_auto_upload = st.checkbox("Permitir publicação imediata no Postiz", bool(settings.get("postiz_auto_publish", False)), key="upload_tab_postiz_auto")
|
|
7661
|
-
if st.button("Guardar Postiz", type="primary",
|
|
7661
|
+
if st.button("Guardar Postiz", type="primary", width='stretch', key="upload_tab_save_postiz"):
|
|
7662
7662
|
settings.update({"postiz_enabled": bool(postiz_enabled_upload), "postiz_api_key": postiz_key_upload.strip(), "postiz_base_url": postiz_base_upload.strip(), "postiz_mcp_url": postiz_mcp_upload.strip(), "postiz_integration_id": postiz_integration_upload.strip(), "postiz_auto_publish": bool(postiz_auto_upload)})
|
|
7663
7663
|
write_json("settings.json", settings)
|
|
7664
7664
|
st.success("Postiz guardado.")
|
|
@@ -7684,8 +7684,8 @@ def render_settings():
|
|
|
7684
7684
|
influencer_supabase_url = text_setting("Supabase Project URL", "influencer_supabase_url", help_text="URL do projecto, por exemplo https://project-id.supabase.co")
|
|
7685
7685
|
with db_cols[1]:
|
|
7686
7686
|
influencer_supabase_key = text_setting("Supabase API key", "influencer_supabase_key", secret=True, help_text="Use uma chave com as permissões RLS adequadas. Nunca é colocada no GitHub ou nos logs.")
|
|
7687
|
-
test_backend_clicked = st.form_submit_button("Testar ligação do backend",
|
|
7688
|
-
save_backend_clicked = st.form_submit_button("Guardar configuração do backend", type="primary",
|
|
7687
|
+
test_backend_clicked = st.form_submit_button("Testar ligação do backend", width='stretch')
|
|
7688
|
+
save_backend_clicked = st.form_submit_button("Guardar configuração do backend", type="primary", width='stretch')
|
|
7689
7689
|
effective_settings = dict(settings)
|
|
7690
7690
|
effective_settings.update({
|
|
7691
7691
|
"influencer_db_backend": influencer_db_backend,
|
|
@@ -7790,7 +7790,7 @@ def _render_telegram_notification_settings() -> None:
|
|
|
7790
7790
|
lambda: test_telegram_credentials(telegram_bot_token, telegram_chat_id),
|
|
7791
7791
|
widget_key="api_test_telegram",
|
|
7792
7792
|
)
|
|
7793
|
-
if st.form_submit_button("Guardar configuração Telegram", type="primary",
|
|
7793
|
+
if st.form_submit_button("Guardar configuração Telegram", type="primary", width='stretch'):
|
|
7794
7794
|
settings.update({
|
|
7795
7795
|
"telegram_enabled": bool(telegram_enabled),
|
|
7796
7796
|
"telegram_bot_token": telegram_bot_token.strip(),
|
|
@@ -7838,7 +7838,7 @@ def render_logs():
|
|
|
7838
7838
|
|
|
7839
7839
|
action_cols = st.columns([1, 1, 3])
|
|
7840
7840
|
with action_cols[0]:
|
|
7841
|
-
if st.button("Actualizar logs",
|
|
7841
|
+
if st.button("Actualizar logs", width='stretch'):
|
|
7842
7842
|
st.rerun()
|
|
7843
7843
|
with action_cols[1]:
|
|
7844
7844
|
st.metric("Registos", len(initial_records))
|
|
@@ -7854,7 +7854,7 @@ def render_logs():
|
|
|
7854
7854
|
rows = logs_to_rows(records)
|
|
7855
7855
|
st.dataframe(
|
|
7856
7856
|
rows,
|
|
7857
|
-
|
|
7857
|
+
width='stretch',
|
|
7858
7858
|
height=520,
|
|
7859
7859
|
hide_index=True,
|
|
7860
7860
|
column_config={
|
|
@@ -7894,16 +7894,16 @@ def render_notifications():
|
|
|
7894
7894
|
|
|
7895
7895
|
action_cols = st.columns([1.4, 1.4, 2.2])
|
|
7896
7896
|
with action_cols[0]:
|
|
7897
|
-
if st.button("Marcar todas como lidas",
|
|
7897
|
+
if st.button("Marcar todas como lidas", width='stretch', disabled=unread_count == 0):
|
|
7898
7898
|
mark_all_notifications_read()
|
|
7899
7899
|
st.rerun()
|
|
7900
7900
|
with action_cols[1]:
|
|
7901
|
-
if st.button("Actualizar notificações",
|
|
7901
|
+
if st.button("Actualizar notificações", width='stretch'):
|
|
7902
7902
|
reconcile_persisted_notifications()
|
|
7903
7903
|
st.rerun()
|
|
7904
7904
|
with action_cols[2]:
|
|
7905
7905
|
confirm_clear = st.checkbox("Confirmar limpeza do histórico", key="confirm_clear_notifications")
|
|
7906
|
-
if st.button("Limpar histórico",
|
|
7906
|
+
if st.button("Limpar histórico", width='stretch', disabled=not confirm_clear):
|
|
7907
7907
|
clear_notifications()
|
|
7908
7908
|
st.session_state.pop("confirm_clear_notifications", None)
|
|
7909
7909
|
st.rerun()
|
|
@@ -7925,7 +7925,7 @@ def render_notifications():
|
|
|
7925
7925
|
help=event["description"],
|
|
7926
7926
|
key=f"notification_preference_{event['code']}",
|
|
7927
7927
|
)
|
|
7928
|
-
if st.form_submit_button("Guardar preferências", type="primary",
|
|
7928
|
+
if st.form_submit_button("Guardar preferências", type="primary", width='stretch'):
|
|
7929
7929
|
save_notification_preferences(pending_preferences)
|
|
7930
7930
|
st.success("Preferências de notificação guardadas.")
|
|
7931
7931
|
st.rerun()
|
|
@@ -7960,7 +7960,7 @@ def render_notifications():
|
|
|
7960
7960
|
st.caption(f"{item.get('category', 'Sistema')} · {item.get('created_at', '—')}")
|
|
7961
7961
|
st.write("Lida" if item.get("read") else "Não lida")
|
|
7962
7962
|
with notification_cols[2]:
|
|
7963
|
-
if not item.get("read") and st.button("Marcar como lida", key=f"mark_notification_{item.get('id')}",
|
|
7963
|
+
if not item.get("read") and st.button("Marcar como lida", key=f"mark_notification_{item.get('id')}", width='stretch'):
|
|
7964
7964
|
mark_notification_read(str(item.get("id")))
|
|
7965
7965
|
st.rerun()
|
|
7966
7966
|
|
|
@@ -8080,7 +8080,7 @@ def render_mcp():
|
|
|
8080
8080
|
st.rerun()
|
|
8081
8081
|
|
|
8082
8082
|
st.caption(integration.get("endpoint_note", "Porta editável para o serviço local."))
|
|
8083
|
-
if st.button("Guardar porta", key=f"mcp_save_port_{integration_id}",
|
|
8083
|
+
if st.button("Guardar porta", key=f"mcp_save_port_{integration_id}", width='stretch'):
|
|
8084
8084
|
update_integration(integration_id, port=int(port))
|
|
8085
8085
|
st.success(f"Porta de {integration['name']} guardada: {int(port)}")
|
|
8086
8086
|
st.rerun()
|
|
@@ -8114,7 +8114,7 @@ def render_mcp():
|
|
|
8114
8114
|
st.error("Para expor o servidor fora do computador local, defina um token de acesso MCP.")
|
|
8115
8115
|
|
|
8116
8116
|
action_label = "Guardar e iniciar Servidor MCP" if server_enabled else "Guardar e parar Servidor MCP"
|
|
8117
|
-
if st.button(action_label, type="primary", key="mcp_server_save",
|
|
8117
|
+
if st.button(action_label, type="primary", key="mcp_server_save", width='stretch'):
|
|
8118
8118
|
try:
|
|
8119
8119
|
saved = save_server_config(enabled=server_enabled, host=server_host, port=int(server_port), auth_token=server_token, write_enabled=write_enabled)
|
|
8120
8120
|
if saved["enabled"]:
|
|
@@ -8154,7 +8154,7 @@ def render_mcp():
|
|
|
8154
8154
|
st.caption("A skill anexada pode ser guardada na pasta local do Thunderbolt ou descarregada como ficheiro Markdown. Nenhum dos quatro repositórios externos é copiado para o pacote.")
|
|
8155
8155
|
skill_cols = st.columns(2)
|
|
8156
8156
|
with skill_cols[0]:
|
|
8157
|
-
if st.button("Guardar skill localmente", type="primary",
|
|
8157
|
+
if st.button("Guardar skill localmente", type="primary", width='stretch', key="mcp_install_mpt_skill"):
|
|
8158
8158
|
try:
|
|
8159
8159
|
destination = install_skill_locally()
|
|
8160
8160
|
st.success(f"Skill guardada em `{destination}`")
|
|
@@ -8171,7 +8171,7 @@ def render_mcp():
|
|
|
8171
8171
|
data=skill_data,
|
|
8172
8172
|
file_name="moneyprinterturbo-video.md",
|
|
8173
8173
|
mime="text/markdown",
|
|
8174
|
-
|
|
8174
|
+
width='stretch',
|
|
8175
8175
|
key="mcp_download_mpt_skill",
|
|
8176
8176
|
)
|
|
8177
8177
|
else:
|
|
@@ -8273,8 +8273,8 @@ def render_metadata_cleaner():
|
|
|
8273
8273
|
st.subheader("Resultado")
|
|
8274
8274
|
st.write(f"**Ficheiro limpo:** `{output.name}`")
|
|
8275
8275
|
mime = "video/mp4" if output.suffix.lower() == ".mp4" else "video/*"
|
|
8276
|
-
st.download_button("Descarregar vídeo limpo", data=output.read_bytes(), file_name=output.name, mime=mime,
|
|
8277
|
-
st.download_button("Descarregar manifesto de upload (JSON)", data=metadata_manifest(record), file_name=f"{output.stem}-metadata.json", mime="application/json",
|
|
8276
|
+
st.download_button("Descarregar vídeo limpo", data=output.read_bytes(), file_name=output.name, mime=mime, width='stretch', key="metadata_download_video")
|
|
8277
|
+
st.download_button("Descarregar manifesto de upload (JSON)", data=metadata_manifest(record), file_name=f"{output.stem}-metadata.json", mime="application/json", width='stretch', key="metadata_download_manifest")
|
|
8278
8278
|
with st.expander("Pré-visualizar metadados"):
|
|
8279
8279
|
st.json(record["metadata"])
|
|
8280
8280
|
|
|
@@ -8303,7 +8303,7 @@ def render_update_youtube_videos():
|
|
|
8303
8303
|
channel = st.selectbox("Canal YouTube", channels, format_func=lambda item: item.get("name") or item.get("url") or "Canal sem nome", key="update_youtube_channel")
|
|
8304
8304
|
updater = YouTubeVideoUpdater(settings, STORAGE)
|
|
8305
8305
|
refresh_key = f"update_youtube_loaded_{channel.get('id', '')}"
|
|
8306
|
-
if st.button("Carregar vídeos do canal", type="primary",
|
|
8306
|
+
if st.button("Carregar vídeos do canal", type="primary", width='stretch', key="update_youtube_load") or refresh_key not in st.session_state:
|
|
8307
8307
|
with st.spinner("A carregar vídeos publicados…"):
|
|
8308
8308
|
result = updater.list_videos(channel, max_results=50)
|
|
8309
8309
|
st.session_state[refresh_key] = result.data if result.ok else {"error": result.message, "videos": []}
|
|
@@ -8325,7 +8325,7 @@ def render_update_youtube_videos():
|
|
|
8325
8325
|
top = st.columns([1.2, 3.8, 1.2])
|
|
8326
8326
|
with top[0]:
|
|
8327
8327
|
if video.get("thumbnail_url"):
|
|
8328
|
-
st.image(video["thumbnail_url"],
|
|
8328
|
+
st.image(video["thumbnail_url"], width='stretch')
|
|
8329
8329
|
with top[1]:
|
|
8330
8330
|
st.markdown(f"**{video.get('title') or 'Sem título'}**")
|
|
8331
8331
|
st.caption(f"{video.get('published_at') or 'Data indisponível'} · {video.get('privacy_status') or 'estado desconhecido'} · `{video_id}`")
|
|
@@ -8340,7 +8340,7 @@ def render_update_youtube_videos():
|
|
|
8340
8340
|
ai_cols = st.columns(3)
|
|
8341
8341
|
script = next((str(task.get("script") or task.get("video_script") or "") for task in tasks if str(task.get("youtube_video_id") or task.get("video_id") or "") == video_id), "")
|
|
8342
8342
|
with ai_cols[0]:
|
|
8343
|
-
if st.button("Gerar título", key=f"{card_key}_ai_title",
|
|
8343
|
+
if st.button("Gerar título", key=f"{card_key}_ai_title", width='stretch'):
|
|
8344
8344
|
try:
|
|
8345
8345
|
generated = generate_video_update_metadata(settings, channel, video, script=script, blueprint=blueprint, mode="title")
|
|
8346
8346
|
st.session_state[title_key] = generated["title"]
|
|
@@ -8348,7 +8348,7 @@ def render_update_youtube_videos():
|
|
|
8348
8348
|
except CreativeGenerationError as exc:
|
|
8349
8349
|
st.error(str(exc))
|
|
8350
8350
|
with ai_cols[1]:
|
|
8351
|
-
if st.button("Gerar descrição", key=f"{card_key}_ai_description",
|
|
8351
|
+
if st.button("Gerar descrição", key=f"{card_key}_ai_description", width='stretch'):
|
|
8352
8352
|
try:
|
|
8353
8353
|
generated = generate_video_update_metadata(settings, channel, video, script=script, blueprint=blueprint, mode="description")
|
|
8354
8354
|
st.session_state[desc_key] = generated["description"]
|
|
@@ -8356,7 +8356,7 @@ def render_update_youtube_videos():
|
|
|
8356
8356
|
except CreativeGenerationError as exc:
|
|
8357
8357
|
st.error(str(exc))
|
|
8358
8358
|
with ai_cols[2]:
|
|
8359
|
-
if st.button("Gerar título e descrição", key=f"{card_key}_ai_both",
|
|
8359
|
+
if st.button("Gerar título e descrição", key=f"{card_key}_ai_both", width='stretch'):
|
|
8360
8360
|
try:
|
|
8361
8361
|
generated = generate_video_update_metadata(settings, channel, video, script=script, blueprint=blueprint, mode="both")
|
|
8362
8362
|
st.session_state[title_key] = generated.get("title", title)
|
|
@@ -8365,7 +8365,7 @@ def render_update_youtube_videos():
|
|
|
8365
8365
|
except CreativeGenerationError as exc:
|
|
8366
8366
|
st.error(str(exc))
|
|
8367
8367
|
thumbnail = st.file_uploader("Trocar thumbnail (opcional)", type=["jpg", "jpeg", "png"], key=f"{card_key}_thumbnail")
|
|
8368
|
-
if st.button("Actualizar no YouTube", type="primary", key=f"{card_key}_save",
|
|
8368
|
+
if st.button("Actualizar no YouTube", type="primary", key=f"{card_key}_save", width='stretch'):
|
|
8369
8369
|
thumbnail_path = None
|
|
8370
8370
|
if thumbnail is not None:
|
|
8371
8371
|
thumbnail_path = STORAGE / "youtube_update" / f"{video_id}_{thumbnail.name}"
|
|
@@ -8548,7 +8548,7 @@ def main():
|
|
|
8548
8548
|
|
|
8549
8549
|
def render_nav_button(target: str, icon: str, label: str, scope: str):
|
|
8550
8550
|
display_label = ui_text(label, ui_language)
|
|
8551
|
-
if st.button(display_label, key=f"nav_{scope}_{target}", icon=icon,
|
|
8551
|
+
if st.button(display_label, key=f"nav_{scope}_{target}", icon=icon, width='stretch', type="primary" if is_nav_item_active(target) else "secondary"):
|
|
8552
8552
|
navigate(target)
|
|
8553
8553
|
|
|
8554
8554
|
with st.sidebar:
|