@danhachuel/thunderbolt 0.6.97 → 0.6.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 +12 -12
- package/app/main.py +214 -214
- package/app/social_networks_ui.py +19 -19
- package/hermes_ui/domain.py +21 -8
- package/hermes_ui/growth_bilibili.py +1 -1
- package/hermes_ui/growth_facebook_pages.py +3 -3
- package/hermes_ui/growth_instagram.py +3 -3
- package/hermes_ui/growth_tiktok.py +3 -3
- package/package.json +1 -1
package/app/main.py
CHANGED
|
@@ -639,9 +639,9 @@ def _render_library_name_editor(kind: str, path: Path, current_name: str) -> str
|
|
|
639
639
|
edited_name = st.text_input("Nome de apresentação", value=current_name, max_chars=120, key=f"{edit_key}_input")
|
|
640
640
|
save_col, cancel_col = st.columns(2)
|
|
641
641
|
with save_col:
|
|
642
|
-
save_name = st.form_submit_button("Guardar nome", type="primary",
|
|
642
|
+
save_name = st.form_submit_button("Guardar nome", type="primary", width="stretch")
|
|
643
643
|
with cancel_col:
|
|
644
|
-
cancel_name = st.form_submit_button("Cancelar",
|
|
644
|
+
cancel_name = st.form_submit_button("Cancelar", width="stretch")
|
|
645
645
|
if save_name:
|
|
646
646
|
try:
|
|
647
647
|
set_display_name(kind, path, edited_name)
|
|
@@ -657,7 +657,7 @@ def _render_library_name_editor(kind: str, path: Path, current_name: str) -> str
|
|
|
657
657
|
|
|
658
658
|
|
|
659
659
|
def _render_card_pencil(edit_key: str) -> None:
|
|
660
|
-
if st.button("✏️", help="Editar nome de apresentação", key=f"pencil_{edit_key}", type="tertiary",
|
|
660
|
+
if st.button("✏️", help="Editar nome de apresentação", key=f"pencil_{edit_key}", type="tertiary", width="stretch"):
|
|
661
661
|
st.session_state[edit_key] = True
|
|
662
662
|
st.rerun()
|
|
663
663
|
|
|
@@ -1153,7 +1153,7 @@ def render_video_generation_settings(
|
|
|
1153
1153
|
st.button(
|
|
1154
1154
|
"Gerar tópico, roteiro e palavras-chave com IA",
|
|
1155
1155
|
key=f"{prefix}_generate_video_content",
|
|
1156
|
-
|
|
1156
|
+
width="stretch",
|
|
1157
1157
|
type="secondary",
|
|
1158
1158
|
icon=":material/auto_awesome:",
|
|
1159
1159
|
on_click=generate_content_callback,
|
|
@@ -1166,7 +1166,7 @@ def render_video_generation_settings(
|
|
|
1166
1166
|
st.button(
|
|
1167
1167
|
"Salvar rascunho",
|
|
1168
1168
|
key=f"{prefix}_save_draft",
|
|
1169
|
-
|
|
1169
|
+
width="stretch",
|
|
1170
1170
|
type="secondary",
|
|
1171
1171
|
icon=":material/save:",
|
|
1172
1172
|
on_click=save_draft_callback,
|
|
@@ -1225,7 +1225,7 @@ def render_video_generation_settings(
|
|
|
1225
1225
|
type=["mp3", "mpeg", "wav", "m4a", "aac", "flac", "ogg"],
|
|
1226
1226
|
key=f"{prefix}_voiceover_upload",
|
|
1227
1227
|
)
|
|
1228
|
-
if uploaded_voiceover is not None and st.button("Guardar áudio de narração", key=f"{prefix}_voiceover_store",
|
|
1228
|
+
if uploaded_voiceover is not None and st.button("Guardar áudio de narração", key=f"{prefix}_voiceover_store", width="stretch"):
|
|
1229
1229
|
try:
|
|
1230
1230
|
stored_voiceover = store_voiceover_file(uploaded_voiceover.name, uploaded_voiceover.getvalue())
|
|
1231
1231
|
st.session_state[f"{prefix}_voiceover_file"] = str(stored_voiceover)
|
|
@@ -1357,7 +1357,7 @@ def channel_videos_for(channel: dict, limit: int = 10) -> list[dict[str, Any]]:
|
|
|
1357
1357
|
def render_channel_video_editor(video: dict, channel_id: str) -> None:
|
|
1358
1358
|
video_id = str(video.get("id") or "")
|
|
1359
1359
|
edit_key = f"channel_video_edit_{video_id}"
|
|
1360
|
-
if st.button("Editar vídeo", key=f"edit_channel_video_{video_id}",
|
|
1360
|
+
if st.button("Editar vídeo", key=f"edit_channel_video_{video_id}", width="stretch"):
|
|
1361
1361
|
st.session_state[edit_key] = True
|
|
1362
1362
|
st.rerun()
|
|
1363
1363
|
if st.session_state.get(edit_key):
|
|
@@ -1381,7 +1381,7 @@ def render_channel_videos(channel: dict) -> None:
|
|
|
1381
1381
|
st.caption("A lista usa o feed público do YouTube, sem Data API Key. Pode actualizar manualmente e editar os metadados locais apresentados.")
|
|
1382
1382
|
refresh_col = st.columns(1)[0]
|
|
1383
1383
|
with refresh_col:
|
|
1384
|
-
if st.button("Actualizar últimos 10 vídeos", key=f"refresh_channel_videos_{channel_id}",
|
|
1384
|
+
if st.button("Actualizar últimos 10 vídeos", key=f"refresh_channel_videos_{channel_id}", width="stretch"):
|
|
1385
1385
|
result = fetch_channel_videos_public(channel, limit=10)
|
|
1386
1386
|
if result.ok:
|
|
1387
1387
|
videos = _merge_channel_videos(channel_id, result.data.get("videos", []))
|
|
@@ -1432,7 +1432,7 @@ def render_channel_audio_subtitle_defaults(channel: dict) -> None:
|
|
|
1432
1432
|
subtitle_font_size = st.slider("Font Size", min_value=12, max_value=96, value=int(channel.get("default_subtitle_font_size") or 60), key=f"channel_default_subtitle_font_size_{channel_id}")
|
|
1433
1433
|
subtitle_outline = st.color_picker("Outline", str(channel.get("default_subtitle_outline") or "#000000"), key=f"channel_default_subtitle_outline_{channel_id}")
|
|
1434
1434
|
subtitle_outline_width = st.slider("Outline Width", min_value=0.0, max_value=5.0, value=float(channel.get("default_subtitle_outline_width") or 1.5), step=0.25, key=f"channel_default_subtitle_outline_width_{channel_id}")
|
|
1435
|
-
if st.button("Guardar configurações de legendas", type="primary", key=f"save_channel_subtitle_defaults_{channel_id}",
|
|
1435
|
+
if st.button("Guardar configurações de legendas", type="primary", key=f"save_channel_subtitle_defaults_{channel_id}", width="stretch"):
|
|
1436
1436
|
update_channel(channel_id, {
|
|
1437
1437
|
"default_enable_subtitles": bool(enable_subtitles), "default_subtitle_font": selected_font, "default_subtitle_position": selected_position,
|
|
1438
1438
|
"default_subtitle_color": subtitle_color, "default_subtitle_background": bool(subtitle_background), "default_subtitle_background_color": subtitle_background_color,
|
|
@@ -1444,7 +1444,7 @@ def render_channel_audio_subtitle_defaults(channel: dict) -> None:
|
|
|
1444
1444
|
with st.expander("Configurações de Audio", expanded=False):
|
|
1445
1445
|
selected_music_source = st.selectbox("Fonte da música de fundo", BACKGROUND_MUSIC_SOURCE_OPTIONS, index=BACKGROUND_MUSIC_SOURCE_OPTIONS.index(music_source) if music_source in BACKGROUND_MUSIC_SOURCE_OPTIONS else BACKGROUND_MUSIC_SOURCE_OPTIONS.index("Sem música"), key=f"channel_default_music_source_{channel_id}")
|
|
1446
1446
|
selected_music_volume = st.selectbox("Volume da música de fundo", BACKGROUND_MUSIC_VOLUME_OPTIONS, index=BACKGROUND_MUSIC_VOLUME_OPTIONS.index(music_volume) if music_volume in BACKGROUND_MUSIC_VOLUME_OPTIONS else 2, key=f"channel_default_music_volume_{channel_id}")
|
|
1447
|
-
if st.button("Guardar configurações de Audio", type="primary", key=f"save_channel_audio_defaults_{channel_id}",
|
|
1447
|
+
if st.button("Guardar configurações de Audio", type="primary", key=f"save_channel_audio_defaults_{channel_id}", width="stretch"):
|
|
1448
1448
|
update_channel(channel_id, {"default_background_music_source": selected_music_source, "default_background_music_volume": selected_music_volume})
|
|
1449
1449
|
st.success("Default de Audio guardado para este canal.")
|
|
1450
1450
|
st.rerun()
|
|
@@ -1482,7 +1482,7 @@ def render_channel_edit_form(channel: dict, youtube_account_ids: list[str], yout
|
|
|
1482
1482
|
edited_description = st.text_area("Descrição", value=str(channel.get("description") or ""), height=100)
|
|
1483
1483
|
edited_automation = st.toggle("Automação ON", value=bool(channel.get("automation_on", False)), key=f"edit_automation_{channel_id}")
|
|
1484
1484
|
edited_time = st.text_input("Horário diário (HH:MM)", value=str(channel.get("automation_time") or "00:00"))
|
|
1485
|
-
save_channel = st.form_submit_button("Guardar alterações", type="primary",
|
|
1485
|
+
save_channel = st.form_submit_button("Guardar alterações", type="primary", width="stretch")
|
|
1486
1486
|
cancel_edit = st.form_submit_button("Cancelar edição")
|
|
1487
1487
|
if cancel_edit:
|
|
1488
1488
|
st.session_state.pop(f"edit_channel_{channel_id}", None)
|
|
@@ -1650,7 +1650,7 @@ def render_growth_youtube():
|
|
|
1650
1650
|
action_button_col, action_status_col = st.columns([1.15, 0.85], gap="small")
|
|
1651
1651
|
with action_button_col:
|
|
1652
1652
|
st.write("")
|
|
1653
|
-
analyse_clicked = st.button("ANALISAR CANAL", type="primary",
|
|
1653
|
+
analyse_clicked = st.button("ANALISAR CANAL", type="primary", width="stretch", key="growth_youtube_analyse")
|
|
1654
1654
|
with action_status_col:
|
|
1655
1655
|
if st.session_state.get("growth_analysis_status") == "complete":
|
|
1656
1656
|
st.markdown('<div style="padding-top:9px;color:#22c55e;font-weight:700;white-space:nowrap;">✓ Análise concluída</div>', unsafe_allow_html=True)
|
|
@@ -1709,9 +1709,9 @@ def render_growth_youtube():
|
|
|
1709
1709
|
if selected_record and selected_record.get("report_path"):
|
|
1710
1710
|
report_path = Path(str(selected_record["report_path"]))
|
|
1711
1711
|
if report_path.is_file():
|
|
1712
|
-
st.download_button("BAIXAR ANALISE COMPLETA", report_path.read_bytes(), file_name=report_path.name, mime="text/markdown",
|
|
1712
|
+
st.download_button("BAIXAR ANALISE COMPLETA", report_path.read_bytes(), file_name=report_path.name, mime="text/markdown", width="stretch", key=f"growth_download_{selected_record['code']}")
|
|
1713
1713
|
else:
|
|
1714
|
-
st.button("BAIXAR ANALISE COMPLETA", disabled=True,
|
|
1714
|
+
st.button("BAIXAR ANALISE COMPLETA", disabled=True, width="stretch", key="growth_download_disabled")
|
|
1715
1715
|
st.subheader("Dashboard de Growth")
|
|
1716
1716
|
st.caption("Visão híbrida: os 3 pilares críticos em destaque e os 5 pilares operacionais abaixo. Vermelho: 0–30 · Amarelo: 31–69 · Verde: 70–100.")
|
|
1717
1717
|
source_names = {"public": "PÚBLICO", "paligemma": "PALIGEMMA", "estimated": "DADO ESTIMADO", "unavailable": "INDISPONÍVEL", "youtube_analytics_oauth": "ANALYTICS OAUTH"}
|
|
@@ -2029,7 +2029,7 @@ def render_thumbnail_blueprints():
|
|
|
2029
2029
|
with cols[1]:
|
|
2030
2030
|
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}")
|
|
2031
2031
|
with cols[2]:
|
|
2032
|
-
if st.button("Guardar par", key=f"thumbnail_blueprint_save_{channel_id}",
|
|
2032
|
+
if st.button("Guardar par", key=f"thumbnail_blueprint_save_{channel_id}", width="stretch"):
|
|
2033
2033
|
if thumb == "Generic_Thumbnail_Blueprint" and script:
|
|
2034
2034
|
st.error("Not Allowed to Associate, System Use Only")
|
|
2035
2035
|
else:
|
|
@@ -2102,7 +2102,7 @@ def render_tiktok_accounts():
|
|
|
2102
2102
|
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.")
|
|
2103
2103
|
with st.form("tiktok_public_lookup_form"):
|
|
2104
2104
|
lookup_source = st.text_input("URL pública ou @handle", placeholder="https://www.tiktok.com/@conta ou @conta", key="tiktok_public_lookup_source")
|
|
2105
|
-
lookup_submitted = st.form_submit_button("Pesquisar perfil público", type="primary",
|
|
2105
|
+
lookup_submitted = st.form_submit_button("Pesquisar perfil público", type="primary", width="stretch")
|
|
2106
2106
|
if lookup_submitted:
|
|
2107
2107
|
result = fetch_public_tiktok_profile(lookup_source)
|
|
2108
2108
|
st.session_state["tiktok_public_lookup"] = {"ok": result.ok, "message": result.message, "data": result.data}
|
|
@@ -2122,7 +2122,7 @@ def render_tiktok_accounts():
|
|
|
2122
2122
|
with metric_cols[3]: st.metric("Vídeos", lookup_data.get("video_count") if lookup_data.get("video_count") is not None else "—")
|
|
2123
2123
|
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")
|
|
2124
2124
|
notes = st.text_area("Observações internas", value=str(lookup_data.get("notes") or ""), key="tiktok_lookup_notes", height=80)
|
|
2125
|
-
if st.button("Cadastrar conta TikTok", type="primary",
|
|
2125
|
+
if st.button("Cadastrar conta TikTok", type="primary", width="stretch", key="tiktok_register_public_account"):
|
|
2126
2126
|
try:
|
|
2127
2127
|
stored = {**lookup_data, "name": display_name.strip() or lookup_data.get("username", ""), "notes": notes.strip(), "source": "public_lookup"}
|
|
2128
2128
|
_upsert_tiktok_account(stored)
|
|
@@ -2138,7 +2138,7 @@ def render_tiktok_accounts():
|
|
|
2138
2138
|
manual_source = st.text_input("@handle ou URL pública", placeholder="@minhaconta", key="tiktok_manual_source")
|
|
2139
2139
|
manual_name = st.text_input("Nome da conta", placeholder="Nome de apresentação", key="tiktok_manual_name")
|
|
2140
2140
|
manual_notes = st.text_area("Observações internas", height=90, key="tiktok_manual_notes")
|
|
2141
|
-
manual_submitted = st.form_submit_button("Guardar cadastro manual", type="primary",
|
|
2141
|
+
manual_submitted = st.form_submit_button("Guardar cadastro manual", type="primary", width="stretch")
|
|
2142
2142
|
if manual_submitted:
|
|
2143
2143
|
try:
|
|
2144
2144
|
reference = normalize_tiktok_reference(manual_source)
|
|
@@ -2177,7 +2177,7 @@ def render_tiktok_accounts():
|
|
|
2177
2177
|
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}")
|
|
2178
2178
|
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}")
|
|
2179
2179
|
edited_notes = st.text_area("Observações internas", value=str(account.get("notes") or ""), key=f"edit_tiktok_notes_{account_id}", height=80)
|
|
2180
|
-
edit_submitted = st.form_submit_button("Guardar conta", type="primary",
|
|
2180
|
+
edit_submitted = st.form_submit_button("Guardar conta", type="primary", width="stretch")
|
|
2181
2181
|
if edit_submitted:
|
|
2182
2182
|
try:
|
|
2183
2183
|
reference = normalize_tiktok_reference(edited_source)
|
|
@@ -2202,7 +2202,7 @@ def render_tiktok_prompt_masters():
|
|
|
2202
2202
|
uploaded_name = Path(uploaded_prompt.name).stem
|
|
2203
2203
|
prompt_name = st.text_input("Nome do Prompt Master", value=uploaded_name, key="tiktok_prompt_master_name")
|
|
2204
2204
|
replace_prompt = st.checkbox("Permitir substituir um ficheiro existente", key="tiktok_prompt_master_replace")
|
|
2205
|
-
if st.button("Guardar Prompt Master", type="primary",
|
|
2205
|
+
if st.button("Guardar Prompt Master", type="primary", width="stretch", key="save_tiktok_prompt_master"):
|
|
2206
2206
|
safe_stem = re.sub(r"[^A-Za-z0-9À-ÿ._-]+", "-", prompt_name.strip() or uploaded_name).strip(".-") or "prompt-master"
|
|
2207
2207
|
destination = TIKTOK_PROMPT_MASTERS / f"{safe_stem}.md"
|
|
2208
2208
|
if destination.exists() and not replace_prompt:
|
|
@@ -2248,14 +2248,14 @@ def render_tiktok_prompt_masters():
|
|
|
2248
2248
|
edited_content = st.text_area("Conteúdo Markdown", value=content, height=360, key=f"tiktok_prompt_master_editor_{path.stem}")
|
|
2249
2249
|
prompt_cols = st.columns(3)
|
|
2250
2250
|
with prompt_cols[0]:
|
|
2251
|
-
if st.button("Guardar alterações", type="primary",
|
|
2251
|
+
if st.button("Guardar alterações", type="primary", width="stretch", key=f"save_prompt_master_{path.stem}"):
|
|
2252
2252
|
path.write_text(edited_content.rstrip() + "\n", encoding="utf-8")
|
|
2253
2253
|
st.success("Prompt Master actualizado.")
|
|
2254
2254
|
st.rerun()
|
|
2255
2255
|
with prompt_cols[1]:
|
|
2256
|
-
st.download_button("Descarregar", data=content.encode("utf-8"), file_name=path.name, mime="text/markdown",
|
|
2256
|
+
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}")
|
|
2257
2257
|
with prompt_cols[2]:
|
|
2258
|
-
if st.button("Apagar",
|
|
2258
|
+
if st.button("Apagar", width="stretch", key=f"delete_prompt_master_{path.stem}"):
|
|
2259
2259
|
path.unlink(missing_ok=True)
|
|
2260
2260
|
st.success("Prompt Master removido da biblioteca TikTok.")
|
|
2261
2261
|
st.rerun()
|
|
@@ -2334,11 +2334,11 @@ def render_tiktok_channels():
|
|
|
2334
2334
|
source = st.text_input("URL pública ou @handle", placeholder="https://www.tiktok.com/@conta", key="tiktok_channel_source")
|
|
2335
2335
|
lookup_cols = st.columns([1, 1])
|
|
2336
2336
|
with lookup_cols[0]:
|
|
2337
|
-
if st.button("Buscar no Tiktok", type="primary",
|
|
2337
|
+
if st.button("Buscar no Tiktok", type="primary", width="stretch", key="tiktok_channel_lookup"):
|
|
2338
2338
|
result = fetch_public_tiktok_profile(source)
|
|
2339
2339
|
st.session_state["tiktok_channel_import"] = {"ok": result.ok, "message": result.message, "data": result.data}
|
|
2340
2340
|
with lookup_cols[1]:
|
|
2341
|
-
if st.button("Limpar importação",
|
|
2341
|
+
if st.button("Limpar importação", width="stretch", key="tiktok_channel_clear"):
|
|
2342
2342
|
for key in ("tiktok_channel_import", "tiktok_import_language", "tiktok_import_niche", "tiktok_import_prompt", "tiktok_import_description"):
|
|
2343
2343
|
st.session_state.pop(key, None)
|
|
2344
2344
|
st.rerun()
|
|
@@ -2432,7 +2432,7 @@ def render_tiktok_channels():
|
|
|
2432
2432
|
prompt_content = load_prompt_master_file(prompt_file) if prompt_file.is_file() else ""
|
|
2433
2433
|
calculated_time, calculated_source, calculated_words = _channel_average_video_time(channel, prompt_master=prompt_content)
|
|
2434
2434
|
average_video_time = st.text_input("Tempo Medio de Video", value=channel_video_time_value(channel), key=f"tiktok_channel_average_video_time_{channel_id}", help=f"Referência: {calculated_words or 0} palavras · {calculated_source}.")
|
|
2435
|
-
if st.button("Guardar tempo", key=f"save_tiktok_channel_average_video_time_{channel_id}",
|
|
2435
|
+
if st.button("Guardar tempo", key=f"save_tiktok_channel_average_video_time_{channel_id}", width="stretch"):
|
|
2436
2436
|
if not valid_hhmm(average_video_time):
|
|
2437
2437
|
st.error("Use o formato MM:SS, por exemplo 12:00.")
|
|
2438
2438
|
else:
|
|
@@ -2440,7 +2440,7 @@ def render_tiktok_channels():
|
|
|
2440
2440
|
st.success("Tempo médio do canal TikTok guardado.")
|
|
2441
2441
|
st.rerun()
|
|
2442
2442
|
with card_cols[6]:
|
|
2443
|
-
if st.button("↻", key=f"refresh_tiktok_metrics_{channel_id}", help="Actualizar Seguidores, Vídeos e Curtidas",
|
|
2443
|
+
if st.button("↻", key=f"refresh_tiktok_metrics_{channel_id}", help="Actualizar Seguidores, Vídeos e Curtidas", width="stretch"):
|
|
2444
2444
|
with st.spinner("A actualizar métricas TikTok…"):
|
|
2445
2445
|
refreshed, message = _refresh_tiktok_channel_metrics(channel)
|
|
2446
2446
|
(st.success if refreshed else st.warning)(message)
|
|
@@ -2544,7 +2544,7 @@ def render_tiktok_channels():
|
|
|
2544
2544
|
if channels:
|
|
2545
2545
|
st.dataframe(
|
|
2546
2546
|
[{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],
|
|
2547
|
-
|
|
2547
|
+
width="stretch",
|
|
2548
2548
|
hide_index=True,
|
|
2549
2549
|
height=360,
|
|
2550
2550
|
)
|
|
@@ -2664,7 +2664,7 @@ def render_channels():
|
|
|
2664
2664
|
st.caption("Os canais cadastrados são apresentados apenas em lista.")
|
|
2665
2665
|
col1, col2 = st.columns([1, 1])
|
|
2666
2666
|
with col1:
|
|
2667
|
-
if st.button("Buscar no YouTube", type="primary",
|
|
2667
|
+
if st.button("Buscar no YouTube", type="primary", width="stretch", key="youtube_channel_lookup"):
|
|
2668
2668
|
search_source = str(source or "").strip()
|
|
2669
2669
|
st.session_state.pop("yt_import", None)
|
|
2670
2670
|
st.session_state.pop("yt_message", None)
|
|
@@ -2689,7 +2689,7 @@ def render_channels():
|
|
|
2689
2689
|
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]}"
|
|
2690
2690
|
st.session_state["yt_ok"] = False
|
|
2691
2691
|
with col2:
|
|
2692
|
-
if st.button("Limpar importação",
|
|
2692
|
+
if st.button("Limpar importação", width="stretch", key="youtube_channel_clear"):
|
|
2693
2693
|
for key in ("yt_import", "yt_message", "yt_ok"):
|
|
2694
2694
|
st.session_state.pop(key, None)
|
|
2695
2695
|
st.rerun()
|
|
@@ -2771,7 +2771,7 @@ def render_channels():
|
|
|
2771
2771
|
data=build_channel_template_xlsx(),
|
|
2772
2772
|
file_name="modelo_canais_youtube.xlsx",
|
|
2773
2773
|
mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
2774
|
-
|
|
2774
|
+
width="stretch",
|
|
2775
2775
|
key="download_channel_spreadsheet_template",
|
|
2776
2776
|
)
|
|
2777
2777
|
with help_col:
|
|
@@ -2783,7 +2783,7 @@ def render_channels():
|
|
|
2783
2783
|
key="channel_spreadsheet_upload",
|
|
2784
2784
|
help="Apenas a primeira aba do arquivo será importada. Linhas sem URL, nome e handle são ignoradas.",
|
|
2785
2785
|
)
|
|
2786
|
-
if st.button("Ler e preparar planilha", type="primary",
|
|
2786
|
+
if st.button("Ler e preparar planilha", type="primary", width="stretch", key="read_channel_spreadsheet"):
|
|
2787
2787
|
if uploaded_sheet is None:
|
|
2788
2788
|
st.error("Selecione um arquivo Excel antes de continuar.")
|
|
2789
2789
|
else:
|
|
@@ -2833,9 +2833,9 @@ def render_channels():
|
|
|
2833
2833
|
"Descrição": "Preencher via YouTube" if not row.get("description") else "Da planilha",
|
|
2834
2834
|
"Estado": "Já cadastrado" if find_duplicate_channel({"name": row.get("name"), "handle": row.get("handle"), "url": row.get("url")}, existing_spreadsheet_channels) else "Novo",
|
|
2835
2835
|
})
|
|
2836
|
-
st.dataframe(preview_rows,
|
|
2836
|
+
st.dataframe(preview_rows, width="stretch", hide_index=True, height=min(420, 86 + 38 * len(preview_rows)))
|
|
2837
2837
|
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.")
|
|
2838
|
-
if st.button("Cadastrar canais da planilha", type="primary",
|
|
2838
|
+
if st.button("Cadastrar canais da planilha", type="primary", width="stretch", key="import_channel_spreadsheet"):
|
|
2839
2839
|
created_names: list[str] = []
|
|
2840
2840
|
skipped_names: list[str] = []
|
|
2841
2841
|
spreadsheet_errors: list[str] = []
|
|
@@ -2933,7 +2933,7 @@ def render_channels():
|
|
|
2933
2933
|
]
|
|
2934
2934
|
st.dataframe(
|
|
2935
2935
|
registered_rows,
|
|
2936
|
-
|
|
2936
|
+
width="stretch",
|
|
2937
2937
|
hide_index=True,
|
|
2938
2938
|
height=420,
|
|
2939
2939
|
column_config={
|
|
@@ -2974,7 +2974,7 @@ def render_channels():
|
|
|
2974
2974
|
(st.success if account_status.ok else st.warning)(account_status.message)
|
|
2975
2975
|
auth_cols = st.columns([1, 2])
|
|
2976
2976
|
with auth_cols[0]:
|
|
2977
|
-
if st.button("Autorizar conta Google", type="secondary",
|
|
2977
|
+
if st.button("Autorizar conta Google", type="secondary", width="stretch", key="batch_authorize_account"):
|
|
2978
2978
|
result = authorize_youtube_batch_account(selected_account, STORAGE)
|
|
2979
2979
|
(st.success if result.ok else st.error)(result.message)
|
|
2980
2980
|
if result.ok:
|
|
@@ -2982,7 +2982,7 @@ def render_channels():
|
|
|
2982
2982
|
with auth_cols[1]:
|
|
2983
2983
|
st.caption("A autorização é feita no browser do sistema e fica guardada separadamente para esta conta.")
|
|
2984
2984
|
batch_key = f"youtube_batch_channels_{selected_account_id}"
|
|
2985
|
-
if st.button("Listar canais desta conta", type="primary",
|
|
2985
|
+
if st.button("Listar canais desta conta", type="primary", width="stretch", key="batch_list_channels"):
|
|
2986
2986
|
result = list_youtube_batch_channels(selected_account, STORAGE)
|
|
2987
2987
|
st.session_state[batch_key] = result.data.get("channels", []) if result.ok else []
|
|
2988
2988
|
st.session_state[f"{batch_key}_message"] = result.message
|
|
@@ -3012,7 +3012,7 @@ def render_channels():
|
|
|
3012
3012
|
batch_language = st.selectbox("Idioma", list(LANGUAGE_CODES), index=list(LANGUAGE_CODES).index("pt"), format_func=language_label, key="batch_channel_language")
|
|
3013
3013
|
with defaults_cols[3]:
|
|
3014
3014
|
batch_style = st.selectbox("Estilo wide", ["Pexels/Pixabay", "full_ia", "Apenas Música"], key="batch_channel_style")
|
|
3015
|
-
import_selected = st.form_submit_button("Cadastrar canais seleccionados", type="primary",
|
|
3015
|
+
import_selected = st.form_submit_button("Cadastrar canais seleccionados", type="primary", width="stretch")
|
|
3016
3016
|
if import_selected:
|
|
3017
3017
|
created_names = []
|
|
3018
3018
|
skipped_names = []
|
|
@@ -3131,7 +3131,7 @@ def render_channels():
|
|
|
3131
3131
|
with header_cols[5]:
|
|
3132
3132
|
calculated_time, calculated_source, calculated_words = _channel_average_video_time(channel)
|
|
3133
3133
|
average_video_time = st.text_input("Tempo Medio de Video", value=channel_video_time_value(channel), key=f"youtube_channel_average_video_time_{channel_id}", help=f"Referência: {calculated_words or 0} palavras · {calculated_source}.")
|
|
3134
|
-
if st.button("Guardar tempo", key=f"save_youtube_channel_average_video_time_{channel_id}",
|
|
3134
|
+
if st.button("Guardar tempo", key=f"save_youtube_channel_average_video_time_{channel_id}", width="stretch"):
|
|
3135
3135
|
if not valid_hhmm(average_video_time):
|
|
3136
3136
|
st.error("Use o formato MM:SS, por exemplo 12:00.")
|
|
3137
3137
|
else:
|
|
@@ -3139,7 +3139,7 @@ def render_channels():
|
|
|
3139
3139
|
st.success("Tempo médio do canal YouTube guardado.")
|
|
3140
3140
|
st.rerun()
|
|
3141
3141
|
with header_cols[6]:
|
|
3142
|
-
if st.button("↻", key=f"refresh_youtube_metrics_{channel_id}", help="Actualizar Inscritos, Vídeos e Visualizações",
|
|
3142
|
+
if st.button("↻", key=f"refresh_youtube_metrics_{channel_id}", help="Actualizar Inscritos, Vídeos e Visualizações", width="stretch"):
|
|
3143
3143
|
with st.spinner("A actualizar métricas YouTube…"):
|
|
3144
3144
|
refreshed, message = _refresh_youtube_channel_metrics(channel, youtube)
|
|
3145
3145
|
(st.success if refreshed else st.warning)(message)
|
|
@@ -3151,12 +3151,12 @@ def render_channels():
|
|
|
3151
3151
|
st.rerun()
|
|
3152
3152
|
action_cols = st.columns(2)
|
|
3153
3153
|
with action_cols[0]:
|
|
3154
|
-
if st.button("Editar", key=f"edit_channel_button_{channel_id}",
|
|
3154
|
+
if st.button("Editar", key=f"edit_channel_button_{channel_id}", width="stretch"):
|
|
3155
3155
|
st.session_state[edit_key] = True
|
|
3156
3156
|
st.rerun()
|
|
3157
3157
|
with action_cols[1]:
|
|
3158
3158
|
delete_key = f"delete_pending_{channel_id}"
|
|
3159
|
-
if st.button("Apagar", key=f"delete_{channel_id}",
|
|
3159
|
+
if st.button("Apagar", key=f"delete_{channel_id}", width="stretch"):
|
|
3160
3160
|
st.session_state[delete_key] = True
|
|
3161
3161
|
st.rerun()
|
|
3162
3162
|
if st.session_state.get(delete_key):
|
|
@@ -3184,17 +3184,17 @@ def render_channels():
|
|
|
3184
3184
|
block_cols = st.columns(4, gap="small")
|
|
3185
3185
|
with block_cols[0]:
|
|
3186
3186
|
st.markdown(f"**Blueprint Padrão**\n\n{summary['name']}")
|
|
3187
|
-
if st.button("Editar Blueprint", key=f"edit_prompts_{channel_id}",
|
|
3187
|
+
if st.button("Editar Blueprint", key=f"edit_prompts_{channel_id}", width="stretch"):
|
|
3188
3188
|
st.session_state[edit_key] = True
|
|
3189
3189
|
st.rerun()
|
|
3190
3190
|
with block_cols[1]:
|
|
3191
3191
|
st.markdown(f"**Nicho**\n\n{channel_niche_label(channel)}")
|
|
3192
|
-
if st.button("Editar Nicho", key=f"edit_niche_{channel_id}",
|
|
3192
|
+
if st.button("Editar Nicho", key=f"edit_niche_{channel_id}", width="stretch"):
|
|
3193
3193
|
st.session_state[edit_key] = True
|
|
3194
3194
|
st.rerun()
|
|
3195
3195
|
with block_cols[2]:
|
|
3196
3196
|
st.markdown(f"**Narrador/Voz Padrão**\n\n{summary['voice'] or 'Sem voz padrão'}")
|
|
3197
|
-
if st.button("Configurar Narrador/Voz", key=f"edit_voice_{channel_id}",
|
|
3197
|
+
if st.button("Configurar Narrador/Voz", key=f"edit_voice_{channel_id}", width="stretch"):
|
|
3198
3198
|
st.session_state[edit_key] = True
|
|
3199
3199
|
st.rerun()
|
|
3200
3200
|
with block_cols[3]:
|
|
@@ -3216,7 +3216,7 @@ def render_channels():
|
|
|
3216
3216
|
key=f"channel_delegated_session_id_{channel_id}",
|
|
3217
3217
|
help="Identificador individual usado pelo Upload directo deste canal. Não é partilhado com outros canais nem mostrado nos diagnósticos.",
|
|
3218
3218
|
)
|
|
3219
|
-
save_channel_direct_credentials = st.form_submit_button("Guardar conta Google e DELEGATED_SESSION_ID", type="primary",
|
|
3219
|
+
save_channel_direct_credentials = st.form_submit_button("Guardar conta Google e DELEGATED_SESSION_ID", type="primary", width="stretch")
|
|
3220
3220
|
selected_channel_account = youtube_accounts_by_id.get(channel_account_id)
|
|
3221
3221
|
if selected_channel_account:
|
|
3222
3222
|
selected_account_status = document_status(STORAGE, selected_channel_account, channel, settings, channels)
|
|
@@ -3421,7 +3421,7 @@ def render_video_from_draft() -> None:
|
|
|
3421
3421
|
st.caption(f"Faltam: {', '.join(still_missing)}")
|
|
3422
3422
|
elif not missing_sections:
|
|
3423
3423
|
st.caption("Este roteiro será usado directamente, sem regenerar o conteúdo editorial guardado.")
|
|
3424
|
-
if st.button(action_label, type="primary",
|
|
3424
|
+
if st.button(action_label, type="primary", width="stretch", key="new_video_resume_submit", icon=":material/movie:"):
|
|
3425
3425
|
if still_missing:
|
|
3426
3426
|
st.error(f"Complete as configurações seleccionadas: {', '.join(still_missing)}.")
|
|
3427
3427
|
elif not selected_channel.get("id"):
|
|
@@ -3476,7 +3476,7 @@ def render_new_video(page_title: str = "Criação de Vídeos", prefix: str = "ne
|
|
|
3476
3476
|
key=f"{prefix}_general_context",
|
|
3477
3477
|
placeholder="Opcional: campanha, época, evento ou restrição editorial comum. O tema final será individual por canal.",
|
|
3478
3478
|
)
|
|
3479
|
-
if st.button("Gerar tópicos individuais para todos os canais", key=f"{prefix}_generate_general_topics",
|
|
3479
|
+
if st.button("Gerar tópicos individuais para todos os canais", key=f"{prefix}_generate_general_topics", width="stretch"):
|
|
3480
3480
|
settings = read_json("settings.json", {})
|
|
3481
3481
|
generated_topics: dict[str, dict[str, Any]] = {}
|
|
3482
3482
|
errors: list[str] = []
|
|
@@ -3580,7 +3580,7 @@ def render_new_video(page_title: str = "Criação de Vídeos", prefix: str = "ne
|
|
|
3580
3580
|
st.warning("Ainda não existem músicas em storage/music. Escolha Carregar ficheiro ou Criar via Suno API.")
|
|
3581
3581
|
elif music_source == "Carregar ficheiro":
|
|
3582
3582
|
uploaded_music = st.file_uploader("Carregar música", type=["mp3", "mpeg", "wav", "m4a", "aac", "flac", "ogg"], key=f"{prefix}_music_upload")
|
|
3583
|
-
if uploaded_music and st.button("Guardar música local", key=f"{prefix}_music_store",
|
|
3583
|
+
if uploaded_music and st.button("Guardar música local", key=f"{prefix}_music_store", width="stretch"):
|
|
3584
3584
|
try:
|
|
3585
3585
|
stored_music = store_music_file(uploaded_music.name, uploaded_music.getvalue())
|
|
3586
3586
|
st.session_state[f"{prefix}_music_path"] = str(stored_music)
|
|
@@ -3591,7 +3591,7 @@ def render_new_video(page_title: str = "Criação de Vídeos", prefix: str = "ne
|
|
|
3591
3591
|
else:
|
|
3592
3592
|
suno_prompt = st.text_area("Prompt musical Suno", placeholder="Instrumental cinematográfico, calmo, sem voz...", key=f"{prefix}_suno_prompt")
|
|
3593
3593
|
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")
|
|
3594
|
-
if st.button("Solicitar música no Suno", key=f"{prefix}_suno_request",
|
|
3594
|
+
if st.button("Solicitar música no Suno", key=f"{prefix}_suno_request", width="stretch"):
|
|
3595
3595
|
suno_result = request_suno_generation(read_json("settings.json", {}), suno_prompt, suno_title)
|
|
3596
3596
|
(st.success if suno_result["ok"] else st.error)(suno_result["message"])
|
|
3597
3597
|
if suno_result["ok"]:
|
|
@@ -3612,7 +3612,7 @@ def render_new_video(page_title: str = "Criação de Vídeos", prefix: str = "ne
|
|
|
3612
3612
|
existing_topics = st.session_state.get(f"{prefix}_general_topics", {})
|
|
3613
3613
|
payloads = dict(st.session_state.get(f"{prefix}_general_payloads", {}))
|
|
3614
3614
|
with st.expander("Gerar Thumbnail com IA", expanded=False):
|
|
3615
|
-
if st.button("Gerar Thumbnail com IA para todos os vídeos", key=f"{prefix}_generate_general_creative",
|
|
3615
|
+
if st.button("Gerar Thumbnail com IA para todos os vídeos", key=f"{prefix}_generate_general_creative", width="stretch"):
|
|
3616
3616
|
settings = read_json("settings.json", {})
|
|
3617
3617
|
new_payloads: dict[str, dict[str, Any]] = {}
|
|
3618
3618
|
errors: list[str] = []
|
|
@@ -3653,7 +3653,7 @@ def render_new_video(page_title: str = "Criação de Vídeos", prefix: str = "ne
|
|
|
3653
3653
|
payload["thumbnail_text"] = variant.get("overlay_text", "")
|
|
3654
3654
|
st.caption(f"{variant.get('composition', '')} · {variant.get('color_palette', '')}")
|
|
3655
3655
|
thumbnail_path = str(variant.get("image_path") or payload.get("thumbnail_path") or "").strip()
|
|
3656
|
-
if st.button("Gerar imagem com Nano Banana", key=f"{prefix}_general_generate_thumbnail_{channel['id']}",
|
|
3656
|
+
if st.button("Gerar imagem com Nano Banana", key=f"{prefix}_general_generate_thumbnail_{channel['id']}", width="stretch"):
|
|
3657
3657
|
try:
|
|
3658
3658
|
thumbnail_path = str(
|
|
3659
3659
|
generate_thumbnail_image(
|
|
@@ -3675,7 +3675,7 @@ def render_new_video(page_title: str = "Criação de Vídeos", prefix: str = "ne
|
|
|
3675
3675
|
except ThumbnailGenerationError as exc:
|
|
3676
3676
|
st.error(str(exc))
|
|
3677
3677
|
if thumbnail_path and Path(thumbnail_path).is_file():
|
|
3678
|
-
st.image(thumbnail_path, caption="Thumbnail gerada pelo Nano Banana",
|
|
3678
|
+
st.image(thumbnail_path, caption="Thumbnail gerada pelo Nano Banana", width="stretch")
|
|
3679
3679
|
payload["thumbnail_path"] = thumbnail_path
|
|
3680
3680
|
payload["thumbnail_status"] = "generated"
|
|
3681
3681
|
else:
|
|
@@ -3684,7 +3684,7 @@ def render_new_video(page_title: str = "Criação de Vídeos", prefix: str = "ne
|
|
|
3684
3684
|
else:
|
|
3685
3685
|
with st.expander("Gerar Thumbnail com IA", expanded=False):
|
|
3686
3686
|
topic_for_thumbnail = str(generation_settings.get("video_subject") or "").strip()
|
|
3687
|
-
if st.button("Gerar Thumbnail com IA", key=f"{prefix}_generate_creative",
|
|
3687
|
+
if st.button("Gerar Thumbnail com IA", key=f"{prefix}_generate_creative", width="stretch"):
|
|
3688
3688
|
if selected_one is None:
|
|
3689
3689
|
st.error("Seleccione primeiro um canal.")
|
|
3690
3690
|
elif not topic_for_thumbnail:
|
|
@@ -3719,7 +3719,7 @@ def render_new_video(page_title: str = "Criação de Vídeos", prefix: str = "ne
|
|
|
3719
3719
|
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")
|
|
3720
3720
|
payload["title"] = selected_title
|
|
3721
3721
|
with st.expander(f"Ver {len(title_options)} candidatos de título"):
|
|
3722
|
-
st.dataframe(payload.get("title_candidates", []),
|
|
3722
|
+
st.dataframe(payload.get("title_candidates", []), width="stretch", hide_index=True)
|
|
3723
3723
|
variants = payload.get("thumbnail_variants", [])
|
|
3724
3724
|
if variants:
|
|
3725
3725
|
labels = [f"{idx + 1}. {item.get('concept', 'Variante')}" for idx, item in enumerate(variants)]
|
|
@@ -3732,7 +3732,7 @@ def render_new_video(page_title: str = "Criação de Vídeos", prefix: str = "ne
|
|
|
3732
3732
|
st.caption(f"Composição: {variant.get('composition', '')} · Cores: {variant.get('color_palette', '')}")
|
|
3733
3733
|
st.code(variant.get("image_prompt", ""), language="text")
|
|
3734
3734
|
thumbnail_path = str(variant.get("image_path") or payload.get("thumbnail_path") or "").strip()
|
|
3735
|
-
if st.button("Gerar imagem da thumbnail com Nano Banana", key=f"{prefix}_generate_thumbnail_image",
|
|
3735
|
+
if st.button("Gerar imagem da thumbnail com Nano Banana", key=f"{prefix}_generate_thumbnail_image", width="stretch"):
|
|
3736
3736
|
try:
|
|
3737
3737
|
thumbnail_path = str(
|
|
3738
3738
|
generate_thumbnail_image(
|
|
@@ -3754,7 +3754,7 @@ def render_new_video(page_title: str = "Criação de Vídeos", prefix: str = "ne
|
|
|
3754
3754
|
except ThumbnailGenerationError as exc:
|
|
3755
3755
|
st.error(str(exc))
|
|
3756
3756
|
if thumbnail_path and Path(thumbnail_path).is_file():
|
|
3757
|
-
st.image(thumbnail_path, caption="Thumbnail gerada pelo Nano Banana",
|
|
3757
|
+
st.image(thumbnail_path, caption="Thumbnail gerada pelo Nano Banana", width="stretch")
|
|
3758
3758
|
payload["thumbnail_path"] = thumbnail_path
|
|
3759
3759
|
payload["thumbnail_status"] = "generated"
|
|
3760
3760
|
else:
|
|
@@ -3911,7 +3911,7 @@ def render_music_creation():
|
|
|
3911
3911
|
key="music_task_prompt",
|
|
3912
3912
|
height=300,
|
|
3913
3913
|
)
|
|
3914
|
-
if st.button("Gerar campos musicais com IA", key="music_task_generate_fields",
|
|
3914
|
+
if st.button("Gerar campos musicais com IA", key="music_task_generate_fields", width="stretch", icon=":material/auto_awesome:"):
|
|
3915
3915
|
try:
|
|
3916
3916
|
with st.spinner("A criar título, letra e prompt musical originais…"):
|
|
3917
3917
|
generated = generate_music_fields(
|
|
@@ -3931,7 +3931,7 @@ def render_music_creation():
|
|
|
3931
3931
|
models = ["lyria-3-clip-preview", "lyria-3-pro-preview"]
|
|
3932
3932
|
configured = str(settings.get("lyria_model") or models[0])
|
|
3933
3933
|
lyria_model = st.selectbox("Modelo Lyria", models, index=models.index(configured) if configured in models else 0, key="music_task_lyria_model")
|
|
3934
|
-
if st.button("Gerar Música", key="music_task_submit", type="primary",
|
|
3934
|
+
if st.button("Gerar Música", key="music_task_submit", type="primary", width="stretch", icon=":material/music_note:"):
|
|
3935
3935
|
try:
|
|
3936
3936
|
task = create_music_task(
|
|
3937
3937
|
provider_label,
|
|
@@ -3959,7 +3959,7 @@ def render_custom_music_voices() -> None:
|
|
|
3959
3959
|
settings = read_json("settings.json", {})
|
|
3960
3960
|
refresh_col, cache_col = st.columns([1, 2])
|
|
3961
3961
|
with refresh_col:
|
|
3962
|
-
refresh = st.button("Actualizar vozes", type="primary",
|
|
3962
|
+
refresh = st.button("Actualizar vozes", type="primary", width="stretch", key="elevenlabs_refresh_voices")
|
|
3963
3963
|
try:
|
|
3964
3964
|
voices, metadata = fetch_personal_voices(settings, force=refresh)
|
|
3965
3965
|
if metadata.get("source") == "cache":
|
|
@@ -3990,7 +3990,7 @@ def render_custom_music_voices() -> None:
|
|
|
3990
3990
|
st.caption("Categoria: personal")
|
|
3991
3991
|
st.caption("Labels: " + (", ".join(f"{key}: {value}" for key, value in labels.items()) if labels else "não disponíveis"))
|
|
3992
3992
|
with voice_cols[2]:
|
|
3993
|
-
if st.button("Testar voz", key=f"elevenlabs_test_voice_{voice['voice_id']}",
|
|
3993
|
+
if st.button("Testar voz", key=f"elevenlabs_test_voice_{voice['voice_id']}", width="stretch"):
|
|
3994
3994
|
try:
|
|
3995
3995
|
preview = synthesize_preview(test_text, "elevenlabs", voice["voice_id"], settings)
|
|
3996
3996
|
st.audio(str(preview), format="audio/mpeg")
|
|
@@ -4071,9 +4071,9 @@ def render_scripts():
|
|
|
4071
4071
|
brief = str(script_settings.get("video_script") or "").strip() or title
|
|
4072
4072
|
generate_col, clear_col = st.columns([1.4, 1])
|
|
4073
4073
|
with generate_col:
|
|
4074
|
-
generate_clicked = st.button("Gerar com IA a partir do Blueprint", type="primary",
|
|
4074
|
+
generate_clicked = st.button("Gerar com IA a partir do Blueprint", type="primary", width="stretch", key="generate_script_document")
|
|
4075
4075
|
with clear_col:
|
|
4076
|
-
clear_clicked = st.button("Limpar rascunho",
|
|
4076
|
+
clear_clicked = st.button("Limpar rascunho", width="stretch", key="clear_script_document")
|
|
4077
4077
|
if clear_clicked:
|
|
4078
4078
|
for key in ("script_draft", "script_draft_title", "script_draft_content", "script_draft_summary", "script_draft_keywords"):
|
|
4079
4079
|
st.session_state.pop(key, None)
|
|
@@ -4126,7 +4126,7 @@ def render_scripts():
|
|
|
4126
4126
|
draft_summary = st.text_input("Resumo", key="script_draft_summary")
|
|
4127
4127
|
draft_keywords = st.text_area("Palavras-chave", height=90, key="script_draft_keywords")
|
|
4128
4128
|
draft_content = st.text_area("Conteúdo guardado", height=460, key="script_draft_content")
|
|
4129
|
-
if st.button("Guardar documento no storage", type="primary",
|
|
4129
|
+
if st.button("Guardar documento no storage", type="primary", width="stretch", key="save_script_document"):
|
|
4130
4130
|
try:
|
|
4131
4131
|
record = save_script_document(
|
|
4132
4132
|
{
|
|
@@ -4200,7 +4200,7 @@ def render_niche_finder():
|
|
|
4200
4200
|
start_date = st.date_input("Data inicial", value=default_start, min_value=date(2020, 1, 1), max_value=default_end, key="niche_start_date")
|
|
4201
4201
|
end_date = st.date_input("Data final", value=default_end, min_value=date(2020, 1, 1), max_value=default_end, key="niche_end_date")
|
|
4202
4202
|
tags_text = st.text_input("Tags opcionais", key="niche_tags_text", placeholder="Ex.: history, facts, documentary")
|
|
4203
|
-
analyse = st.form_submit_button("Analisar Nichos", type="primary",
|
|
4203
|
+
analyse = st.form_submit_button("Analisar Nichos", type="primary", width="stretch")
|
|
4204
4204
|
|
|
4205
4205
|
current_parameters = {
|
|
4206
4206
|
"n_clusters": n_clusters,
|
|
@@ -4262,7 +4262,7 @@ def render_niche_finder():
|
|
|
4262
4262
|
cluster_table = cluster_table[cluster_table["palavras"].str.contains(keyword.strip(), case=False, na=False)]
|
|
4263
4263
|
tab_clusters, tab_rules, tab_data = render_localized_tabs(["Clusters encontrados", "Regras de associação", "Dados analisados"])
|
|
4264
4264
|
with tab_clusters:
|
|
4265
|
-
st.dataframe(cluster_table,
|
|
4265
|
+
st.dataframe(cluster_table, width="stretch", hide_index=True)
|
|
4266
4266
|
if not points.empty:
|
|
4267
4267
|
try:
|
|
4268
4268
|
import plotly.express as px
|
|
@@ -4270,19 +4270,19 @@ def render_niche_finder():
|
|
|
4270
4270
|
hover = [column for column in ["title", "channel_name", "country", "view_count", "engagement_rate"] if column in points.columns]
|
|
4271
4271
|
figure = px.scatter(points, x="x", y="y", color="cluster", hover_data=hover, title="Distribuição dos clusters")
|
|
4272
4272
|
figure.update_layout(legend_title_text="Cluster")
|
|
4273
|
-
st.plotly_chart(figure,
|
|
4273
|
+
st.plotly_chart(figure, width="stretch")
|
|
4274
4274
|
except ImportError:
|
|
4275
4275
|
st.error("A visualização da análise não está disponível nesta instalação.")
|
|
4276
4276
|
with tab_rules:
|
|
4277
4277
|
if rules_table.empty:
|
|
4278
4278
|
st.info("Não foram encontradas regras com os filtros actuais. Reduza o suporte mínimo ou escolha outro filtro.")
|
|
4279
4279
|
else:
|
|
4280
|
-
st.dataframe(rules_table,
|
|
4280
|
+
st.dataframe(rules_table, width="stretch", hide_index=True)
|
|
4281
4281
|
if not items_table.empty:
|
|
4282
4282
|
st.subheader("Itemsets frequentes")
|
|
4283
|
-
st.dataframe(items_table,
|
|
4283
|
+
st.dataframe(items_table, width="stretch", hide_index=True)
|
|
4284
4284
|
with tab_data:
|
|
4285
|
-
st.dataframe(results["raw_data"],
|
|
4285
|
+
st.dataframe(results["raw_data"], width="stretch", hide_index=True)
|
|
4286
4286
|
|
|
4287
4287
|
|
|
4288
4288
|
def render_niche_finder_apify():
|
|
@@ -4331,7 +4331,7 @@ def render_niche_finder_apify():
|
|
|
4331
4331
|
download_subtitles = st.checkbox("Descarregar legendas", value=True, key="niche_apify_download_subtitles")
|
|
4332
4332
|
with filter_cols[3]:
|
|
4333
4333
|
has_cc = st.checkbox("Apenas vídeos com CC", value=False, key="niche_apify_has_cc")
|
|
4334
|
-
analyse_apify = st.form_submit_button("Pesquisar no Apify", type="primary",
|
|
4334
|
+
analyse_apify = st.form_submit_button("Pesquisar no Apify", type="primary", width="stretch")
|
|
4335
4335
|
|
|
4336
4336
|
if analyse_apify:
|
|
4337
4337
|
try:
|
|
@@ -4405,17 +4405,17 @@ def render_niche_finder_apify():
|
|
|
4405
4405
|
result_frame = pd.DataFrame(visible_results)
|
|
4406
4406
|
display_columns = ["title", "channel_name", "duration", "view_count", "subscriber_count", "comments_count", "vsc_ratio", "url", "transcript_status", "summary_status", "summary"]
|
|
4407
4407
|
display_columns = [column for column in display_columns if column in result_frame.columns]
|
|
4408
|
-
st.dataframe(result_frame[display_columns],
|
|
4408
|
+
st.dataframe(result_frame[display_columns], width="stretch", hide_index=True)
|
|
4409
4409
|
export_frame = result_frame.drop(columns=["transcript"], errors="ignore")
|
|
4410
4410
|
export_json = export_frame.to_json(orient="records", force_ascii=False, indent=2)
|
|
4411
4411
|
export_csv = export_frame.to_csv(index=False).encode("utf-8")
|
|
4412
4412
|
export_cols = st.columns(2)
|
|
4413
4413
|
with export_cols[0]:
|
|
4414
|
-
st.download_button("Exportar JSON", data=export_json, file_name="niche-finder-apify-results.json", mime="application/json",
|
|
4414
|
+
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")
|
|
4415
4415
|
with export_cols[1]:
|
|
4416
|
-
st.download_button("Exportar CSV", data=export_csv, file_name="niche-finder-apify-results.csv", mime="text/csv",
|
|
4416
|
+
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")
|
|
4417
4417
|
except ImportError:
|
|
4418
|
-
st.dataframe(visible_results,
|
|
4418
|
+
st.dataframe(visible_results, width="stretch", hide_index=True)
|
|
4419
4419
|
|
|
4420
4420
|
last_run = st.session_state.get("niche_apify_last_run", {})
|
|
4421
4421
|
if last_run:
|
|
@@ -4457,7 +4457,7 @@ def render_media_download():
|
|
|
4457
4457
|
with option_cols[2]:
|
|
4458
4458
|
embed_metadata = st.checkbox("Incorporar metadados", value=False, key="media_download_embed_metadata")
|
|
4459
4459
|
st.caption("Playlist desactivada por padrão para evitar downloads acidentais em massa.")
|
|
4460
|
-
start_download = st.form_submit_button("Iniciar download", type="primary",
|
|
4460
|
+
start_download = st.form_submit_button("Iniciar download", type="primary", width="stretch")
|
|
4461
4461
|
|
|
4462
4462
|
if start_download:
|
|
4463
4463
|
progress = st.progress(0, text="A preparar o download…")
|
|
@@ -4571,7 +4571,7 @@ def render_cuts():
|
|
|
4571
4571
|
with url_tab:
|
|
4572
4572
|
url_value = st.text_input("URL directa do vídeo", placeholder="https://exemplo.com/video.mp4", key="cuts_video_url")
|
|
4573
4573
|
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.")
|
|
4574
|
-
if st.button("Descarregar vídeo", key="cuts_download_url",
|
|
4574
|
+
if st.button("Descarregar vídeo", key="cuts_download_url", width="stretch"):
|
|
4575
4575
|
try:
|
|
4576
4576
|
source_path = download_direct_video_url(url_value)
|
|
4577
4577
|
st.session_state["cuts_source_path"] = str(source_path)
|
|
@@ -4586,7 +4586,7 @@ def render_cuts():
|
|
|
4586
4586
|
else:
|
|
4587
4587
|
generated_labels = [f"{path.name} — {path}" for path in generated_paths]
|
|
4588
4588
|
selected_generated = st.selectbox("Vídeo gerado", range(len(generated_paths)), format_func=lambda index: generated_labels[index], key="cuts_generated_index")
|
|
4589
|
-
if st.button("Usar vídeo seleccionado", key="cuts_use_generated",
|
|
4589
|
+
if st.button("Usar vídeo seleccionado", key="cuts_use_generated", width="stretch"):
|
|
4590
4590
|
source_path = generated_paths[selected_generated]
|
|
4591
4591
|
st.session_state["cuts_source_path"] = str(source_path)
|
|
4592
4592
|
st.session_state["cuts_source_label"] = f"Pipeline · {source_path.name}"
|
|
@@ -4598,7 +4598,7 @@ def render_cuts():
|
|
|
4598
4598
|
else:
|
|
4599
4599
|
folder_labels = [f"{path.name} — {path}" for path in folder_paths]
|
|
4600
4600
|
selected_folder = st.selectbox("Vídeo da pasta", range(len(folder_paths)), format_func=lambda index: folder_labels[index], key="cuts_folder_index")
|
|
4601
|
-
if st.button("Usar vídeo da pasta", key="cuts_use_folder",
|
|
4601
|
+
if st.button("Usar vídeo da pasta", key="cuts_use_folder", width="stretch"):
|
|
4602
4602
|
source_path = folder_paths[selected_folder]
|
|
4603
4603
|
st.session_state["cuts_source_path"] = str(source_path)
|
|
4604
4604
|
st.session_state["cuts_source_label"] = f"Pasta local · {source_path.name}"
|
|
@@ -4657,7 +4657,7 @@ def render_cuts():
|
|
|
4657
4657
|
generate_button = st.button(
|
|
4658
4658
|
"Gerar Clips",
|
|
4659
4659
|
type="primary",
|
|
4660
|
-
|
|
4660
|
+
width="stretch",
|
|
4661
4661
|
disabled=not (source_path and source_path.is_file() and rights_confirmed),
|
|
4662
4662
|
key="cuts_generate_button",
|
|
4663
4663
|
)
|
|
@@ -4706,13 +4706,13 @@ def render_cuts():
|
|
|
4706
4706
|
with st.container(border=True):
|
|
4707
4707
|
st.caption(f"Clip {clip.get('index', '—')} · {float(clip.get('duration', 0)):.1f}s")
|
|
4708
4708
|
st.video(str(clip["path"]))
|
|
4709
|
-
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']}",
|
|
4709
|
+
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")
|
|
4710
4710
|
try:
|
|
4711
4711
|
_, archive_bytes = zip_cut_run(last_run)
|
|
4712
|
-
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']}",
|
|
4712
|
+
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")
|
|
4713
4713
|
except CutsError as exc:
|
|
4714
4714
|
st.warning(str(exc))
|
|
4715
|
-
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']}",
|
|
4715
|
+
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")
|
|
4716
4716
|
elif last_run.get("error"):
|
|
4717
4717
|
st.error(last_run["error"])
|
|
4718
4718
|
|
|
@@ -4721,7 +4721,7 @@ def render_cuts():
|
|
|
4721
4721
|
with st.expander("Histórico do Clip Generator"):
|
|
4722
4722
|
st.dataframe(
|
|
4723
4723
|
[{"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]],
|
|
4724
|
-
|
|
4724
|
+
width="stretch",
|
|
4725
4725
|
hide_index=True,
|
|
4726
4726
|
)
|
|
4727
4727
|
|
|
@@ -4792,7 +4792,7 @@ def render_python_editor():
|
|
|
4792
4792
|
width = st.number_input("Largura", min_value=16, max_value=7680, value=1280, step=2, key="python_editor_width")
|
|
4793
4793
|
with resize_cols[1]:
|
|
4794
4794
|
height = st.number_input("Altura", min_value=16, max_value=7680, value=720, step=2, key="python_editor_height")
|
|
4795
|
-
apply_operation = st.form_submit_button(f"Aplicar: {operation}", type="primary",
|
|
4795
|
+
apply_operation = st.form_submit_button(f"Aplicar: {operation}", type="primary", width="stretch")
|
|
4796
4796
|
if apply_operation:
|
|
4797
4797
|
try:
|
|
4798
4798
|
ffmpeg_path = read_json("settings.json", {}).get("ffmpeg_path", "")
|
|
@@ -4828,7 +4828,7 @@ def render_python_editor():
|
|
|
4828
4828
|
records = list_python_editor_records()
|
|
4829
4829
|
if records:
|
|
4830
4830
|
with st.expander("Histórico do Editor Python"):
|
|
4831
|
-
st.dataframe([{key: record.get(key, "") for key in ("created_at", "operation", "source_name", "output_name")} for record in records[:20]],
|
|
4831
|
+
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)
|
|
4832
4832
|
|
|
4833
4833
|
with code_tab:
|
|
4834
4834
|
st.subheader("Scripts Python locais")
|
|
@@ -5168,7 +5168,7 @@ def render_videos():
|
|
|
5168
5168
|
file_name=video_file.name,
|
|
5169
5169
|
mime='video/mp4',
|
|
5170
5170
|
key=f"pipeline_video_download_{task['id']}",
|
|
5171
|
-
|
|
5171
|
+
width="stretch",
|
|
5172
5172
|
)
|
|
5173
5173
|
elif video_path:
|
|
5174
5174
|
st.caption(f'Vídeo registado: {video_path}')
|
|
@@ -5184,11 +5184,11 @@ def render_videos():
|
|
|
5184
5184
|
state = str(task.get("state") or "")
|
|
5185
5185
|
start_col, stop_col = st.columns(2)
|
|
5186
5186
|
with start_col:
|
|
5187
|
-
if st.button("Start", key=f"automation_start_{task['id']}",
|
|
5187
|
+
if st.button("Start", key=f"automation_start_{task['id']}", width="stretch", disabled=state not in {"to_do", "blocked", "failed"}):
|
|
5188
5188
|
_start_pipeline_task(str(task["id"]), state)
|
|
5189
5189
|
st.rerun()
|
|
5190
5190
|
with stop_col:
|
|
5191
|
-
if st.button("Stop", key=f"automation_stop_{task['id']}",
|
|
5191
|
+
if st.button("Stop", key=f"automation_stop_{task['id']}", width="stretch", disabled=state != "doing"):
|
|
5192
5192
|
transition_task(task["id"], "blocked")
|
|
5193
5193
|
st.rerun()
|
|
5194
5194
|
|
|
@@ -5255,7 +5255,7 @@ def _render_music_file_card(record: dict[str, Any]) -> None:
|
|
|
5255
5255
|
file_name=music_path.name,
|
|
5256
5256
|
mime=mimetypes.guess_type(music_path.name)[0] or "audio/mpeg",
|
|
5257
5257
|
key=f"music_backlog_download_{record_id}",
|
|
5258
|
-
|
|
5258
|
+
width="stretch",
|
|
5259
5259
|
)
|
|
5260
5260
|
edit_key = _render_library_name_editor("music", music_path, title)
|
|
5261
5261
|
with st.columns([0.2, 0.8])[0]:
|
|
@@ -5282,11 +5282,11 @@ def _render_music_file_card(record: dict[str, Any]) -> None:
|
|
|
5282
5282
|
state = str(record.get("state") or "")
|
|
5283
5283
|
start_col, stop_col = st.columns(2)
|
|
5284
5284
|
with start_col:
|
|
5285
|
-
if st.button("Start", key=f"music_backlog_start_{record_id}",
|
|
5285
|
+
if st.button("Start", key=f"music_backlog_start_{record_id}", width="stretch", disabled=state not in {"to_do", "blocked", "failed"}):
|
|
5286
5286
|
run_music_task(record_id, read_json("settings.json", {}))
|
|
5287
5287
|
st.rerun()
|
|
5288
5288
|
with stop_col:
|
|
5289
|
-
if st.button("Stop", key=f"music_backlog_stop_{record_id}",
|
|
5289
|
+
if st.button("Stop", key=f"music_backlog_stop_{record_id}", width="stretch", disabled=state != "doing"):
|
|
5290
5290
|
transition_music_task(record_id, "blocked")
|
|
5291
5291
|
st.rerun()
|
|
5292
5292
|
|
|
@@ -5304,7 +5304,7 @@ def _render_lyrics_card(record: dict[str, Any]) -> None:
|
|
|
5304
5304
|
st.write(f"**{title}**")
|
|
5305
5305
|
st.caption("Lyrics")
|
|
5306
5306
|
with download_col:
|
|
5307
|
-
st.download_button("Descarregar lyrics", data=path.read_bytes(), file_name=path.name, mime="text/markdown", key=f"music_lyrics_download_{record_id}",
|
|
5307
|
+
st.download_button("Descarregar lyrics", data=path.read_bytes(), file_name=path.name, mime="text/markdown", key=f"music_lyrics_download_{record_id}", width="stretch")
|
|
5308
5308
|
with edit_col:
|
|
5309
5309
|
edit_key = _render_library_name_editor("music_lyrics", path, title)
|
|
5310
5310
|
_render_card_pencil(edit_key)
|
|
@@ -5321,7 +5321,7 @@ def render_music_backlog() -> None:
|
|
|
5321
5321
|
with music_tab:
|
|
5322
5322
|
with st.form("music_backlog_upload_form", clear_on_submit=True):
|
|
5323
5323
|
uploaded_music = st.file_uploader("Adicionar músicas à pasta acima", type=["mp3", "mpeg", "wav", "m4a", "aac", "flac", "ogg"], accept_multiple_files=True, key="music_backlog_upload")
|
|
5324
|
-
upload_music = st.form_submit_button("Guardar músicas", type="primary",
|
|
5324
|
+
upload_music = st.form_submit_button("Guardar músicas", type="primary", width="stretch")
|
|
5325
5325
|
if upload_music and uploaded_music:
|
|
5326
5326
|
for uploaded in uploaded_music:
|
|
5327
5327
|
store_music_file(uploaded.name, uploaded.getvalue())
|
|
@@ -5345,7 +5345,7 @@ def render_music_backlog() -> None:
|
|
|
5345
5345
|
st.caption(f"As lyrics são guardadas em `{script_storage_path()}` no formato Markdown local do Thunderbolt.")
|
|
5346
5346
|
with st.form("music_backlog_lyrics_upload_form", clear_on_submit=True):
|
|
5347
5347
|
uploaded_lyrics = st.file_uploader("Adicionar lyrics", type=["md", "txt"], key="music_backlog_lyrics_upload")
|
|
5348
|
-
upload_lyrics = st.form_submit_button("Guardar lyrics",
|
|
5348
|
+
upload_lyrics = st.form_submit_button("Guardar lyrics", width="stretch")
|
|
5349
5349
|
if upload_lyrics and uploaded_lyrics:
|
|
5350
5350
|
content = uploaded_lyrics.getvalue().decode("utf-8", errors="replace").strip()
|
|
5351
5351
|
if content:
|
|
@@ -5403,7 +5403,7 @@ def render_thumbnails():
|
|
|
5403
5403
|
image_path = record.get("image_path")
|
|
5404
5404
|
if image_path and image_path.is_file():
|
|
5405
5405
|
# Pass bytes to Streamlit and avoid the deprecated
|
|
5406
|
-
#
|
|
5406
|
+
# legacy full-width argument. On Windows the
|
|
5407
5407
|
# deprecation warning can be emitted after the bootstrap
|
|
5408
5408
|
# stream has been closed, which masks the real page with
|
|
5409
5409
|
# ``ValueError: I/O operation on closed file``.
|
|
@@ -5415,7 +5415,7 @@ def render_thumbnails():
|
|
|
5415
5415
|
file_name=image_path.name,
|
|
5416
5416
|
mime="image/jpeg" if image_path.suffix.lower() in {".jpg", ".jpeg"} else "image/png",
|
|
5417
5417
|
key=f"thumbnail_download_{task_id}_{record['variant_index']}",
|
|
5418
|
-
|
|
5418
|
+
width="stretch",
|
|
5419
5419
|
)
|
|
5420
5420
|
else:
|
|
5421
5421
|
st.markdown("### Sem imagem")
|
|
@@ -5435,7 +5435,7 @@ def render_thumbnails():
|
|
|
5435
5435
|
"Refazer Prompt Thumb",
|
|
5436
5436
|
key=f"regenerate_thumbnail_{task_id}",
|
|
5437
5437
|
icon=":material/refresh:",
|
|
5438
|
-
|
|
5438
|
+
width="stretch",
|
|
5439
5439
|
disabled=not bool(record["title"] or record["topic"]),
|
|
5440
5440
|
):
|
|
5441
5441
|
try:
|
|
@@ -5470,7 +5470,7 @@ def render_thumbnails():
|
|
|
5470
5470
|
"Gerar Imagem",
|
|
5471
5471
|
key=f"generate_thumbnail_image_{task_id}",
|
|
5472
5472
|
icon=":material/image:",
|
|
5473
|
-
|
|
5473
|
+
width="stretch",
|
|
5474
5474
|
disabled=not bool(record["prompt"]),
|
|
5475
5475
|
):
|
|
5476
5476
|
try:
|
|
@@ -5494,7 +5494,7 @@ def render_thumbnails():
|
|
|
5494
5494
|
"Refazer Prompt e Gerar Imagem",
|
|
5495
5495
|
key=f"regenerate_thumbnail_prompt_{task_id}",
|
|
5496
5496
|
icon=":material/auto_awesome:",
|
|
5497
|
-
|
|
5497
|
+
width="stretch",
|
|
5498
5498
|
disabled=not bool(record["title"] or record["topic"]),
|
|
5499
5499
|
):
|
|
5500
5500
|
try:
|
|
@@ -5525,7 +5525,7 @@ def render_thumbnails():
|
|
|
5525
5525
|
"Refazer Lettering",
|
|
5526
5526
|
key=f"regenerate_thumbnail_lettering_{task_id}",
|
|
5527
5527
|
icon=":material/title:",
|
|
5528
|
-
|
|
5528
|
+
width="stretch",
|
|
5529
5529
|
disabled=not bool(record.get("image_path") and record["image_path"].is_file()),
|
|
5530
5530
|
):
|
|
5531
5531
|
try:
|
|
@@ -5617,7 +5617,7 @@ def _render_tiktok_automation_cards():
|
|
|
5617
5617
|
file_name=_automation_download_name("Thumbnail9:16", task, thumbnail_path, ".png"),
|
|
5618
5618
|
mime="image/png",
|
|
5619
5619
|
key=f"tiktok_automation_download_thumbnail_{task_id}",
|
|
5620
|
-
|
|
5620
|
+
width="stretch",
|
|
5621
5621
|
disabled=thumbnail_path is None,
|
|
5622
5622
|
)
|
|
5623
5623
|
with prompt_download_col:
|
|
@@ -5628,7 +5628,7 @@ def _render_tiktok_automation_cards():
|
|
|
5628
5628
|
file_name=_automation_download_name("Thumbnail-Prompt", task, thumbnail_prompt_path, ".txt"),
|
|
5629
5629
|
mime="application/json" if thumbnail_prompt_path else "text/plain",
|
|
5630
5630
|
key=f"tiktok_automation_download_thumbnail_prompt_{task_id}",
|
|
5631
|
-
|
|
5631
|
+
width="stretch",
|
|
5632
5632
|
disabled=thumbnail_prompt_path is None and not thumbnail_prompt,
|
|
5633
5633
|
)
|
|
5634
5634
|
with task_cols[1]:
|
|
@@ -5641,11 +5641,11 @@ def _render_tiktok_automation_cards():
|
|
|
5641
5641
|
state = str(task.get("state") or "")
|
|
5642
5642
|
start_col, stop_col, delete_col = st.columns(3)
|
|
5643
5643
|
with start_col:
|
|
5644
|
-
if st.button("Start", key=f"tiktok_automation_start_{task_id}",
|
|
5644
|
+
if st.button("Start", key=f"tiktok_automation_start_{task_id}", width="stretch", disabled=state not in {"to_do", "blocked", "failed"}):
|
|
5645
5645
|
if _start_pipeline_task(task_id, state):
|
|
5646
5646
|
st.rerun()
|
|
5647
5647
|
with stop_col:
|
|
5648
|
-
if st.button("Stop", key=f"tiktok_automation_stop_{task_id}",
|
|
5648
|
+
if st.button("Stop", key=f"tiktok_automation_stop_{task_id}", width="stretch", disabled=state != "doing"):
|
|
5649
5649
|
stop_task_by_user(task_id)
|
|
5650
5650
|
st.rerun()
|
|
5651
5651
|
script_download_col, video_download_col = st.columns(2, gap="small")
|
|
@@ -5656,7 +5656,7 @@ def _render_tiktok_automation_cards():
|
|
|
5656
5656
|
file_name=_automation_download_name("Script", task, script_path, ".md"),
|
|
5657
5657
|
mime="text/markdown",
|
|
5658
5658
|
key=f"tiktok_automation_download_script_{task_id}",
|
|
5659
|
-
|
|
5659
|
+
width="stretch",
|
|
5660
5660
|
disabled=script_path is None,
|
|
5661
5661
|
)
|
|
5662
5662
|
with video_download_col:
|
|
@@ -5666,7 +5666,7 @@ def _render_tiktok_automation_cards():
|
|
|
5666
5666
|
file_name=_automation_download_name("Vídeo9:16", task, video_path, ".mp4"),
|
|
5667
5667
|
mime="video/mp4",
|
|
5668
5668
|
key=f"tiktok_automation_download_video_{task_id}",
|
|
5669
|
-
|
|
5669
|
+
width="stretch",
|
|
5670
5670
|
disabled=video_path is None,
|
|
5671
5671
|
)
|
|
5672
5672
|
if st.button(
|
|
@@ -5674,7 +5674,7 @@ def _render_tiktok_automation_cards():
|
|
|
5674
5674
|
key=f"tiktok_automation_remake_video_{task_id}",
|
|
5675
5675
|
icon=":material/refresh:",
|
|
5676
5676
|
type="primary",
|
|
5677
|
-
|
|
5677
|
+
width="stretch",
|
|
5678
5678
|
disabled=state == "doing",
|
|
5679
5679
|
help="Remonta apenas o vídeo, mantendo o mesmo roteiro, Blueprint/Prompt Master, tags, thumbnail, voz e artefactos já baixados.",
|
|
5680
5680
|
):
|
|
@@ -5682,14 +5682,14 @@ def _render_tiktok_automation_cards():
|
|
|
5682
5682
|
st.rerun()
|
|
5683
5683
|
with delete_col:
|
|
5684
5684
|
confirm_delete_key = f"tiktok_automation_confirm_delete_{task_id}"
|
|
5685
|
-
if st.button("Apagar", key=f"tiktok_automation_delete_{task_id}",
|
|
5685
|
+
if st.button("Apagar", key=f"tiktok_automation_delete_{task_id}", width="stretch", disabled=state == "doing"):
|
|
5686
5686
|
st.session_state[confirm_delete_key] = True
|
|
5687
5687
|
st.rerun()
|
|
5688
5688
|
if st.session_state.get(confirm_delete_key):
|
|
5689
5689
|
st.warning("Remover este vídeo da fila? Os ficheiros de artefactos serão preservados.")
|
|
5690
5690
|
confirm_col, cancel_col = st.columns(2)
|
|
5691
5691
|
with confirm_col:
|
|
5692
|
-
if st.button("Confirmar", key=f"tiktok_automation_confirm_delete_button_{task_id}",
|
|
5692
|
+
if st.button("Confirmar", key=f"tiktok_automation_confirm_delete_button_{task_id}", width="stretch", type="primary"):
|
|
5693
5693
|
try:
|
|
5694
5694
|
delete_task(task["id"])
|
|
5695
5695
|
st.session_state.pop(confirm_delete_key, None)
|
|
@@ -5697,7 +5697,7 @@ def _render_tiktok_automation_cards():
|
|
|
5697
5697
|
except ValueError as exc:
|
|
5698
5698
|
st.error(str(exc))
|
|
5699
5699
|
with cancel_col:
|
|
5700
|
-
if st.button("Cancelar", key=f"tiktok_automation_cancel_delete_{task_id}",
|
|
5700
|
+
if st.button("Cancelar", key=f"tiktok_automation_cancel_delete_{task_id}", width="stretch"):
|
|
5701
5701
|
st.session_state.pop(confirm_delete_key, None)
|
|
5702
5702
|
st.rerun()
|
|
5703
5703
|
|
|
@@ -5758,7 +5758,7 @@ def render_tiktok_automation():
|
|
|
5758
5758
|
with default_cols[4]:
|
|
5759
5759
|
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}")
|
|
5760
5760
|
with default_cols[5]:
|
|
5761
|
-
if st.button("Guardar", key=f"tiktok_automation_save_{channel_id}",
|
|
5761
|
+
if st.button("Guardar", key=f"tiktok_automation_save_{channel_id}", width="stretch", type="primary"):
|
|
5762
5762
|
if not valid_hhmm(schedule_time):
|
|
5763
5763
|
st.error("Use o formato HH:MM, por exemplo 08:30.")
|
|
5764
5764
|
elif not valid_hhmm(average_video_time):
|
|
@@ -5801,7 +5801,7 @@ def _render_youtube_automation_cards():
|
|
|
5801
5801
|
file_name=_automation_download_name("Thumbnail", task, thumbnail_path, ".png"),
|
|
5802
5802
|
mime="image/png",
|
|
5803
5803
|
key=f"automation_download_thumbnail_{task['id']}",
|
|
5804
|
-
|
|
5804
|
+
width="stretch",
|
|
5805
5805
|
disabled=thumbnail_path is None,
|
|
5806
5806
|
)
|
|
5807
5807
|
with prompt_download_col:
|
|
@@ -5812,7 +5812,7 @@ def _render_youtube_automation_cards():
|
|
|
5812
5812
|
file_name=_automation_download_name("Thumbnail-Prompt", task, thumbnail_prompt_path, ".txt"),
|
|
5813
5813
|
mime="application/json" if thumbnail_prompt_path else "text/plain",
|
|
5814
5814
|
key=f"automation_download_thumbnail_prompt_{task['id']}",
|
|
5815
|
-
|
|
5815
|
+
width="stretch",
|
|
5816
5816
|
disabled=thumbnail_prompt_path is None and not thumbnail_prompt,
|
|
5817
5817
|
)
|
|
5818
5818
|
with task_cols[1]:
|
|
@@ -5824,11 +5824,11 @@ def _render_youtube_automation_cards():
|
|
|
5824
5824
|
state = str(task.get("state") or "")
|
|
5825
5825
|
start_col, stop_col, delete_col = st.columns(3)
|
|
5826
5826
|
with start_col:
|
|
5827
|
-
if st.button("Start", key=f"automation_start_{task['id']}",
|
|
5827
|
+
if st.button("Start", key=f"automation_start_{task['id']}", width="stretch", disabled=state not in {"to_do", "blocked", "failed"}):
|
|
5828
5828
|
if _start_pipeline_task(str(task["id"]), state):
|
|
5829
5829
|
st.rerun()
|
|
5830
5830
|
with stop_col:
|
|
5831
|
-
if st.button("Stop", key=f"automation_stop_{task['id']}",
|
|
5831
|
+
if st.button("Stop", key=f"automation_stop_{task['id']}", width="stretch", disabled=state != "doing"):
|
|
5832
5832
|
stop_task_by_user(task["id"])
|
|
5833
5833
|
st.rerun()
|
|
5834
5834
|
script_download_col, video_download_col = st.columns(2, gap="small")
|
|
@@ -5839,7 +5839,7 @@ def _render_youtube_automation_cards():
|
|
|
5839
5839
|
file_name=_automation_download_name("Script", task, script_path, ".md"),
|
|
5840
5840
|
mime="text/markdown",
|
|
5841
5841
|
key=f"automation_download_script_{task['id']}",
|
|
5842
|
-
|
|
5842
|
+
width="stretch",
|
|
5843
5843
|
disabled=script_path is None,
|
|
5844
5844
|
)
|
|
5845
5845
|
with video_download_col:
|
|
@@ -5849,7 +5849,7 @@ def _render_youtube_automation_cards():
|
|
|
5849
5849
|
file_name=_automation_download_name("Vídeo", task, video_path, ".mp4"),
|
|
5850
5850
|
mime="video/mp4",
|
|
5851
5851
|
key=f"automation_download_video_{task['id']}",
|
|
5852
|
-
|
|
5852
|
+
width="stretch",
|
|
5853
5853
|
disabled=video_path is None,
|
|
5854
5854
|
)
|
|
5855
5855
|
if st.button(
|
|
@@ -5857,7 +5857,7 @@ def _render_youtube_automation_cards():
|
|
|
5857
5857
|
key=f"automation_remake_video_{task['id']}",
|
|
5858
5858
|
icon=":material/refresh:",
|
|
5859
5859
|
type="primary",
|
|
5860
|
-
|
|
5860
|
+
width="stretch",
|
|
5861
5861
|
disabled=state == "doing",
|
|
5862
5862
|
help="Remonta apenas o vídeo, mantendo o mesmo roteiro, Blueprint/Prompt Master, tags, thumbnail, voz e artefactos já baixados.",
|
|
5863
5863
|
):
|
|
@@ -5865,14 +5865,14 @@ def _render_youtube_automation_cards():
|
|
|
5865
5865
|
st.rerun()
|
|
5866
5866
|
with delete_col:
|
|
5867
5867
|
confirm_delete_key = f"automation_confirm_delete_{task['id']}"
|
|
5868
|
-
if st.button("Apagar", key=f"automation_delete_{task['id']}",
|
|
5868
|
+
if st.button("Apagar", key=f"automation_delete_{task['id']}", width="stretch", disabled=state == "doing"):
|
|
5869
5869
|
st.session_state[confirm_delete_key] = True
|
|
5870
5870
|
st.rerun()
|
|
5871
5871
|
if st.session_state.get(confirm_delete_key):
|
|
5872
5872
|
st.warning("Remover este vídeo da fila? Os ficheiros de artefactos serão preservados.")
|
|
5873
5873
|
confirm_col, cancel_col = st.columns(2)
|
|
5874
5874
|
with confirm_col:
|
|
5875
|
-
if st.button("Confirmar", key=f"automation_confirm_delete_button_{task['id']}",
|
|
5875
|
+
if st.button("Confirmar", key=f"automation_confirm_delete_button_{task['id']}", width="stretch", type="primary"):
|
|
5876
5876
|
try:
|
|
5877
5877
|
delete_task(task["id"])
|
|
5878
5878
|
st.session_state.pop(confirm_delete_key, None)
|
|
@@ -5880,7 +5880,7 @@ def _render_youtube_automation_cards():
|
|
|
5880
5880
|
except ValueError as exc:
|
|
5881
5881
|
st.error(str(exc))
|
|
5882
5882
|
with cancel_col:
|
|
5883
|
-
if st.button("Cancelar", key=f"automation_cancel_delete_{task['id']}",
|
|
5883
|
+
if st.button("Cancelar", key=f"automation_cancel_delete_{task['id']}", width="stretch"):
|
|
5884
5884
|
st.session_state.pop(confirm_delete_key, None)
|
|
5885
5885
|
st.rerun()
|
|
5886
5886
|
|
|
@@ -5961,7 +5961,7 @@ def render_automation():
|
|
|
5961
5961
|
with control_cols[2]:
|
|
5962
5962
|
enabled = st.toggle("Automação ligada", value=bool(channel.get("automation_on", False)), key=f"automation_on_{channel_id}")
|
|
5963
5963
|
with control_cols[3]:
|
|
5964
|
-
if st.button("Guardar", key=f"automation_save_{channel_id}",
|
|
5964
|
+
if st.button("Guardar", key=f"automation_save_{channel_id}", width="stretch", type="primary"):
|
|
5965
5965
|
if not valid_hhmm(schedule_time):
|
|
5966
5966
|
st.error("Use o formato HH:MM, por exemplo 08:30.")
|
|
5967
5967
|
elif not valid_hhmm(average_video_time):
|
|
@@ -6076,7 +6076,7 @@ def _render_music_source(prefix: str, extensions: set[str]) -> str:
|
|
|
6076
6076
|
type=sorted(extension.lstrip(".") for extension in extensions),
|
|
6077
6077
|
key=f"{prefix}_music_file_upload",
|
|
6078
6078
|
)
|
|
6079
|
-
if uploaded is not None and st.button("Guardar música no storage local", key=f"{prefix}_music_store",
|
|
6079
|
+
if uploaded is not None and st.button("Guardar música no storage local", key=f"{prefix}_music_store", width="stretch"):
|
|
6080
6080
|
try:
|
|
6081
6081
|
stored = store_music_file(uploaded.name, uploaded.getvalue())
|
|
6082
6082
|
selected_path = str(stored)
|
|
@@ -6149,7 +6149,7 @@ def _render_jewelmusic_upload_tab() -> None:
|
|
|
6149
6149
|
base_url = st.text_input("Base URL", value=str(settings.get("jewelmusic_base_url") or "https://api.jewelmusic.com"))
|
|
6150
6150
|
proxy_url = st.text_input("Proxy opcional", value=str(settings.get("jewelmusic_proxy_url") or ""), placeholder="http://127.0.0.1:8080")
|
|
6151
6151
|
timeout_seconds = st.number_input("Timeout (segundos)", min_value=5, max_value=900, value=int(settings.get("jewelmusic_timeout_seconds", 120)), step=5)
|
|
6152
|
-
save = st.form_submit_button("Guardar configuração JewelMusic", type="primary",
|
|
6152
|
+
save = st.form_submit_button("Guardar configuração JewelMusic", type="primary", width="stretch")
|
|
6153
6153
|
if save:
|
|
6154
6154
|
settings = _persist_music_upload_settings({
|
|
6155
6155
|
"jewelmusic_enabled": bool(enabled),
|
|
@@ -6160,7 +6160,7 @@ def _render_jewelmusic_upload_tab() -> None:
|
|
|
6160
6160
|
})
|
|
6161
6161
|
st.success("Configuração JewelMusic guardada no storage local.")
|
|
6162
6162
|
adapter = JewelMusicAdapter(settings)
|
|
6163
|
-
if st.button("Testar conexão JewelMusic", key="jewelmusic_test",
|
|
6163
|
+
if st.button("Testar conexão JewelMusic", key="jewelmusic_test", width="stretch"):
|
|
6164
6164
|
test_result = adapter.test_connection()
|
|
6165
6165
|
(st.success if test_result.ok else st.error)(test_result.message)
|
|
6166
6166
|
music_path = _render_music_source("jewelmusic", MUSIC_UPLOAD_EXTENSIONS)
|
|
@@ -6174,7 +6174,7 @@ def _render_jewelmusic_upload_tab() -> None:
|
|
|
6174
6174
|
with metadata_cols[3]:
|
|
6175
6175
|
year = st.text_input("Ano", key="jewelmusic_year")
|
|
6176
6176
|
genre = st.text_input("Género", key="jewelmusic_genre")
|
|
6177
|
-
if st.button("Enviar música para JewelMusic", type="primary", key="jewelmusic_upload",
|
|
6177
|
+
if st.button("Enviar música para JewelMusic", type="primary", key="jewelmusic_upload", width="stretch", disabled=not bool(music_path)):
|
|
6178
6178
|
result = JewelMusicAdapter(read_json("settings.json", {})).upload_track(music_path, title=title, artist=artist, album=album, year=year, genre=genre)
|
|
6179
6179
|
_record_music_upload("JewelMusic", result, music_path=music_path, target={"artist": artist, "title": title})
|
|
6180
6180
|
(st.success if result.ok else st.error)(result.message)
|
|
@@ -6209,7 +6209,7 @@ def _render_pushtunes_upload_tab() -> None:
|
|
|
6209
6209
|
spotify_client_secret = st.text_input("Spotify Client Secret", value=str(settings.get("pushtunes_spotify_client_secret") or ""), type="password")
|
|
6210
6210
|
spotify_redirect_uri = st.text_input("Spotify Redirect URI", value=str(settings.get("pushtunes_spotify_redirect_uri") or ""))
|
|
6211
6211
|
timeout_seconds = st.number_input("Timeout Pushtunes (segundos)", min_value=30, max_value=3600, value=int(settings.get("pushtunes_timeout_seconds", 1800)), step=30)
|
|
6212
|
-
save = st.form_submit_button("Guardar configuração Pushtunes", type="primary",
|
|
6212
|
+
save = st.form_submit_button("Guardar configuração Pushtunes", type="primary", width="stretch")
|
|
6213
6213
|
if save:
|
|
6214
6214
|
settings = _persist_music_upload_settings({
|
|
6215
6215
|
"pushtunes_enabled": bool(enabled),
|
|
@@ -6233,17 +6233,17 @@ def _render_pushtunes_upload_tab() -> None:
|
|
|
6233
6233
|
if source == "csv":
|
|
6234
6234
|
st.caption("Pode carregar o CSV nesta página e usar o caminho guardado como origem Pushtunes.")
|
|
6235
6235
|
csv_upload = st.file_uploader("Carregar CSV de origem Pushtunes", type=["csv"], key="pushtunes_csv_upload")
|
|
6236
|
-
if csv_upload is not None and st.button("Guardar CSV Pushtunes no storage", key="pushtunes_csv_store",
|
|
6236
|
+
if csv_upload is not None and st.button("Guardar CSV Pushtunes no storage", key="pushtunes_csv_store", width="stretch"):
|
|
6237
6237
|
stored_csv = _store_pushtunes_csv(csv_upload)
|
|
6238
6238
|
settings = _persist_music_upload_settings({"pushtunes_csv_file": stored_csv})
|
|
6239
6239
|
st.success(f"CSV guardado em `{stored_csv}`.")
|
|
6240
6240
|
adapter = PushtunesAdapter(read_json("settings.json", {}))
|
|
6241
6241
|
status = adapter.status()
|
|
6242
6242
|
(st.success if status.ok else st.warning)(status.message)
|
|
6243
|
-
if st.button("Validar instalação e configuração Pushtunes", key="pushtunes_test",
|
|
6243
|
+
if st.button("Validar instalação e configuração Pushtunes", key="pushtunes_test", width="stretch"):
|
|
6244
6244
|
check = PushtunesAdapter(read_json("settings.json", {})).status()
|
|
6245
6245
|
(st.success if check.ok else st.error)(check.message)
|
|
6246
|
-
if st.button("Executar sincronização Pushtunes", type="primary", key="pushtunes_sync",
|
|
6246
|
+
if st.button("Executar sincronização Pushtunes", type="primary", key="pushtunes_sync", width="stretch", disabled=not status.ok):
|
|
6247
6247
|
result = PushtunesAdapter(read_json("settings.json", {})).sync()
|
|
6248
6248
|
_record_music_upload(f"Pushtunes ({adapter.source} → {adapter.target})", result, target={"source": adapter.source, "target": adapter.target, "operation": adapter.operation})
|
|
6249
6249
|
(st.success if result.ok else st.error)(result.message)
|
|
@@ -6271,7 +6271,7 @@ def _render_ytmusicapi_upload_tab() -> None:
|
|
|
6271
6271
|
auth_file = st.text_input("Caminho do browser.json", value=str(settings.get("ytmusicapi_auth_file") or ""))
|
|
6272
6272
|
proxy_url = st.text_input("Proxy opcional", value=str(settings.get("ytmusicapi_proxy_url") or ""), placeholder="http://127.0.0.1:8080")
|
|
6273
6273
|
timeout_seconds = st.number_input("Timeout (segundos)", min_value=30, max_value=900, value=int(settings.get("ytmusicapi_timeout_seconds", 240)), step=10)
|
|
6274
|
-
save = st.form_submit_button("Guardar configuração ytmusicapi", type="primary",
|
|
6274
|
+
save = st.form_submit_button("Guardar configuração ytmusicapi", type="primary", width="stretch")
|
|
6275
6275
|
if save:
|
|
6276
6276
|
settings = _persist_music_upload_settings({
|
|
6277
6277
|
"ytmusicapi_enabled": bool(enabled),
|
|
@@ -6281,18 +6281,18 @@ def _render_ytmusicapi_upload_tab() -> None:
|
|
|
6281
6281
|
})
|
|
6282
6282
|
st.success("Configuração ytmusicapi guardada no storage local.")
|
|
6283
6283
|
auth_upload = st.file_uploader("Carregar browser.json", type=["json"], key="ytmusicapi_auth_upload")
|
|
6284
|
-
if auth_upload is not None and st.button("Guardar browser.json no storage local", key="ytmusicapi_auth_store",
|
|
6284
|
+
if auth_upload is not None and st.button("Guardar browser.json no storage local", key="ytmusicapi_auth_store", width="stretch"):
|
|
6285
6285
|
stored_auth = _store_ytmusicapi_auth(auth_upload)
|
|
6286
6286
|
settings = _persist_music_upload_settings({"ytmusicapi_auth_file": stored_auth, "ytmusicapi_enabled": True})
|
|
6287
6287
|
st.success(f"Ficheiro de autenticação guardado em `{stored_auth}`.")
|
|
6288
6288
|
adapter = YTMusicApiAdapter(read_json("settings.json", {}))
|
|
6289
6289
|
status = adapter.status()
|
|
6290
6290
|
(st.success if status.ok else st.warning)(status.message)
|
|
6291
|
-
if st.button("Testar autenticação ytmusicapi", key="ytmusicapi_test",
|
|
6291
|
+
if st.button("Testar autenticação ytmusicapi", key="ytmusicapi_test", width="stretch"):
|
|
6292
6292
|
result = YTMusicApiAdapter(read_json("settings.json", {})).test_connection()
|
|
6293
6293
|
(st.success if result.ok else st.error)(result.message)
|
|
6294
6294
|
music_path = _render_music_source("ytmusicapi", YT_MUSIC_UPLOAD_EXTENSIONS)
|
|
6295
|
-
if st.button("Enviar música para YouTube Music", type="primary", key="ytmusicapi_upload",
|
|
6295
|
+
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):
|
|
6296
6296
|
result = YTMusicApiAdapter(read_json("settings.json", {})).upload_song(music_path)
|
|
6297
6297
|
_record_music_upload("ytmusicapi / YouTube Music", result, music_path=music_path, target={"service": "youtube_music"})
|
|
6298
6298
|
(st.success if result.ok else st.error)(result.message)
|
|
@@ -6324,7 +6324,7 @@ def _render_distrokid_upload_tab() -> None:
|
|
|
6324
6324
|
last_name = st.text_input("Apelido", value=str(settings.get("distrokid_last_name") or ""))
|
|
6325
6325
|
record_label = st.text_input("Record label", value=str(settings.get("distrokid_record_label") or ""))
|
|
6326
6326
|
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.")
|
|
6327
|
-
save = st.form_submit_button("Guardar configuração DistroKid", type="primary",
|
|
6327
|
+
save = st.form_submit_button("Guardar configuração DistroKid", type="primary", width="stretch")
|
|
6328
6328
|
if save:
|
|
6329
6329
|
_persist_music_upload_settings({
|
|
6330
6330
|
"distrokid_enabled": bool(enabled), "distrokid_cookie": cookie.strip(), "distrokid_account": account.strip(),
|
|
@@ -6336,11 +6336,11 @@ def _render_distrokid_upload_tab() -> None:
|
|
|
6336
6336
|
adapter = DistroKidAdapter(settings)
|
|
6337
6337
|
status = adapter.status()
|
|
6338
6338
|
(st.success if status.ok else st.warning)(status.message)
|
|
6339
|
-
if st.button("Testar sessão DistroKid", key="distrokid_test",
|
|
6339
|
+
if st.button("Testar sessão DistroKid", key="distrokid_test", width="stretch"):
|
|
6340
6340
|
result = DistroKidAdapter(read_json("settings.json", {})).test_connection()
|
|
6341
6341
|
(st.success if result.ok else st.error)(result.message)
|
|
6342
6342
|
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")
|
|
6343
|
-
if uploaded_tracks and st.button("Guardar faixas no storage local", key="distrokid_store_tracks",
|
|
6343
|
+
if uploaded_tracks and st.button("Guardar faixas no storage local", key="distrokid_store_tracks", width="stretch"):
|
|
6344
6344
|
stored_paths: list[str] = []
|
|
6345
6345
|
for uploaded in uploaded_tracks:
|
|
6346
6346
|
try:
|
|
@@ -6359,7 +6359,7 @@ def _render_distrokid_upload_tab() -> None:
|
|
|
6359
6359
|
instrumental = st.checkbox("Instrumental", value=False, key=f"distrokid_track_instrumental_{index}")
|
|
6360
6360
|
track_rows.append({"path": path, "title": track_title.strip() or Path(path).stem, "instrumental": instrumental})
|
|
6361
6361
|
cover_upload = st.file_uploader("Capa do lançamento (opcional)", type=sorted(extension.lstrip(".") for extension in DISTROKID_COVER_EXTENSIONS), key="distrokid_cover_upload")
|
|
6362
|
-
if cover_upload and st.button("Guardar capa no storage local", key="distrokid_store_cover",
|
|
6362
|
+
if cover_upload and st.button("Guardar capa no storage local", key="distrokid_store_cover", width="stretch"):
|
|
6363
6363
|
try:
|
|
6364
6364
|
st.session_state["distrokid_cover_path"] = _store_distrokid_cover(cover_upload)
|
|
6365
6365
|
st.success("Capa guardada no storage local.")
|
|
@@ -6369,11 +6369,11 @@ def _render_distrokid_upload_tab() -> None:
|
|
|
6369
6369
|
if cover_path and Path(cover_path).is_file():
|
|
6370
6370
|
st.caption(f"Capa seleccionada: `{cover_path}`")
|
|
6371
6371
|
session_id = str(st.session_state.get("distrokid_session_id") or "")
|
|
6372
|
-
if session_id and st.button("Fechar browser DistroKid", key="distrokid_close_browser",
|
|
6372
|
+
if session_id and st.button("Fechar browser DistroKid", key="distrokid_close_browser", width="stretch"):
|
|
6373
6373
|
result = close_distrokid_session(session_id)
|
|
6374
6374
|
(st.success if result.ok else st.warning)(result.message)
|
|
6375
6375
|
st.session_state.pop("distrokid_session_id", None)
|
|
6376
|
-
if st.button("Abrir formulário e carregar para DistroKid", type="primary", key="distrokid_prepare_upload",
|
|
6376
|
+
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):
|
|
6377
6377
|
current = read_json("settings.json", {})
|
|
6378
6378
|
result = DistroKidAdapter(current).prepare_upload(
|
|
6379
6379
|
track_rows, artist=str(current.get("distrokid_artist") or ""), release_title=str(current.get("distrokid_release_title") or ""),
|
|
@@ -6426,7 +6426,7 @@ def render_upload_composio():
|
|
|
6426
6426
|
return
|
|
6427
6427
|
query = st.text_input("Pesquisar ferramenta Composio", value="upload a video file", key="composio_upload_query")
|
|
6428
6428
|
toolkit_filter = st.text_input("Toolkit preferido (opcional)", value=str(settings.get("composio_toolkit") or ""), key="composio_upload_toolkit")
|
|
6429
|
-
if st.button("Descobrir ferramentas", key="composio_upload_discover",
|
|
6429
|
+
if st.button("Descobrir ferramentas", key="composio_upload_discover", width="stretch"):
|
|
6430
6430
|
try:
|
|
6431
6431
|
st.session_state["composio_upload_tools"] = discover_tools(api_key, user_id, query, toolkit_filter)
|
|
6432
6432
|
if not st.session_state["composio_upload_tools"]:
|
|
@@ -6449,7 +6449,7 @@ def render_upload_composio():
|
|
|
6449
6449
|
st.json(schema)
|
|
6450
6450
|
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")
|
|
6451
6451
|
arguments_json = st.text_area("Argumentos JSON adicionais", value=str(st.session_state.get("composio_upload_arguments") or "{}"), height=150, key="composio_upload_arguments")
|
|
6452
|
-
if st.button("Autorizar toolkit no Composio", key="composio_upload_authorize",
|
|
6452
|
+
if st.button("Autorizar toolkit no Composio", key="composio_upload_authorize", width="stretch"):
|
|
6453
6453
|
try:
|
|
6454
6454
|
auth = authorize_toolkit(api_key, user_id, str(selected_tool.get("toolkit") or ""))
|
|
6455
6455
|
if auth.get("redirect_url"):
|
|
@@ -6469,7 +6469,7 @@ def render_upload_composio():
|
|
|
6469
6469
|
video_path = str((task.get("artifacts", {}) or {}).get("video") or "")
|
|
6470
6470
|
st.caption(video_path or "Sem caminho de vídeo registado")
|
|
6471
6471
|
st.info(f"Resumo: `{selected_slug}` · toolkit `{selected_tool.get('toolkit') or 'não indicado'}` · campo `{file_field or 'não indicado'}`")
|
|
6472
|
-
if st.button("Enviar vídeo via Composio", type="primary", key=f"composio_upload_send_{selected_task_id}",
|
|
6472
|
+
if st.button("Enviar vídeo via Composio", type="primary", key=f"composio_upload_send_{selected_task_id}", width="stretch"):
|
|
6473
6473
|
try:
|
|
6474
6474
|
parse_arguments(arguments_json)
|
|
6475
6475
|
result = execute_upload(api_key, user_id, selected_slug, video_path, file_field, arguments_json, connected_account_id)
|
|
@@ -6742,14 +6742,14 @@ def render_upload_conventional():
|
|
|
6742
6742
|
status_cols = st.columns(2)
|
|
6743
6743
|
with status_cols[0]:
|
|
6744
6744
|
(st.success if status["agent"].ok else st.warning)(f"Agente: {status['agent'].message}")
|
|
6745
|
-
if st.button("Autorizar agente YouTube", key="youtube_authorize_agent",
|
|
6745
|
+
if st.button("Autorizar agente YouTube", key="youtube_authorize_agent", width="stretch"):
|
|
6746
6746
|
result = youtube.authorize_agent()
|
|
6747
6747
|
(st.success if result.ok else st.error)(result.message)
|
|
6748
6748
|
if result.ok:
|
|
6749
6749
|
st.rerun()
|
|
6750
6750
|
with status_cols[1]:
|
|
6751
6751
|
(st.info if status["fallback"].ok else st.caption)(f"Fallback OAuth: {status['fallback'].message}")
|
|
6752
|
-
if st.button("Autorizar fallback OAuth", key="youtube_authorize_fallback",
|
|
6752
|
+
if st.button("Autorizar fallback OAuth", key="youtube_authorize_fallback", width="stretch"):
|
|
6753
6753
|
result = youtube.authorize_fallback()
|
|
6754
6754
|
(st.success if result.ok else st.error)(result.message)
|
|
6755
6755
|
if result.ok:
|
|
@@ -6802,7 +6802,7 @@ def render_upload_conventional():
|
|
|
6802
6802
|
st.session_state.pop(description_error_key, None)
|
|
6803
6803
|
|
|
6804
6804
|
description = st.text_area("Descrição", value=task.get("description", ""), key=description_state_key, height=100)
|
|
6805
|
-
st.button("Gerar descrição com IA", key=f"yt_description_ai_{task['id']}",
|
|
6805
|
+
st.button("Gerar descrição com IA", key=f"yt_description_ai_{task['id']}", width="content", on_click=generate_upload_description_callback)
|
|
6806
6806
|
if description_error := str(st.session_state.get(description_error_key) or "").strip():
|
|
6807
6807
|
st.error(f"Não foi possível gerar a descrição: {description_error}")
|
|
6808
6808
|
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']}")
|
|
@@ -6970,11 +6970,11 @@ def render_tiktok_api_cards(settings: dict[str, Any]) -> None:
|
|
|
6970
6970
|
client_secret = st.text_input("TikTok Client Secret", value=card["client_secret"], type="password", key=f"tiktok_api_{card_id}_client_secret")
|
|
6971
6971
|
action_cols = st.columns(3)
|
|
6972
6972
|
with action_cols[0]:
|
|
6973
|
-
test_clicked = st.form_submit_button("Testar chamada API",
|
|
6973
|
+
test_clicked = st.form_submit_button("Testar chamada API", width="stretch")
|
|
6974
6974
|
with action_cols[1]:
|
|
6975
|
-
save_clicked = st.form_submit_button("Guardar card", type="primary",
|
|
6975
|
+
save_clicked = st.form_submit_button("Guardar card", type="primary", width="stretch")
|
|
6976
6976
|
with action_cols[2]:
|
|
6977
|
-
delete_clicked = st.form_submit_button("Apagar card",
|
|
6977
|
+
delete_clicked = st.form_submit_button("Apagar card", width="stretch")
|
|
6978
6978
|
edited = {"id": card_id, "client_id": client_id.strip(), "client_secret": client_secret.strip()}
|
|
6979
6979
|
if test_clicked:
|
|
6980
6980
|
cards[index] = edited
|
|
@@ -6996,7 +6996,7 @@ def render_tiktok_api_cards(settings: dict[str, Any]) -> None:
|
|
|
6996
6996
|
_persist_tiktok_api_cards(settings, cards)
|
|
6997
6997
|
st.success("Card TikTok apagado.")
|
|
6998
6998
|
st.rerun()
|
|
6999
|
-
if st.button("Adicionar nova API", type="primary",
|
|
6999
|
+
if st.button("Adicionar nova API", type="primary", width="stretch", key="add_tiktok_api_card"):
|
|
7000
7000
|
cards.append({"id": f"tiktok-api-{uuid.uuid4().hex[:10]}", "client_id": "", "client_secret": ""})
|
|
7001
7001
|
_persist_tiktok_api_cards(settings, cards)
|
|
7002
7002
|
st.success("Novo card TikTok criado.")
|
|
@@ -7048,11 +7048,11 @@ def render_bilibili_api_cards(settings: dict[str, Any]) -> None:
|
|
|
7048
7048
|
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")
|
|
7049
7049
|
action_cols = st.columns(3)
|
|
7050
7050
|
with action_cols[0]:
|
|
7051
|
-
test_clicked = st.form_submit_button("Testar chamada API",
|
|
7051
|
+
test_clicked = st.form_submit_button("Testar chamada API", width="stretch")
|
|
7052
7052
|
with action_cols[1]:
|
|
7053
|
-
save_clicked = st.form_submit_button("Guardar card", type="primary",
|
|
7053
|
+
save_clicked = st.form_submit_button("Guardar card", type="primary", width="stretch")
|
|
7054
7054
|
with action_cols[2]:
|
|
7055
|
-
delete_clicked = st.form_submit_button("Apagar card",
|
|
7055
|
+
delete_clicked = st.form_submit_button("Apagar card", width="stretch")
|
|
7056
7056
|
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()}
|
|
7057
7057
|
if test_clicked:
|
|
7058
7058
|
cards[index] = edited
|
|
@@ -7069,7 +7069,7 @@ def render_bilibili_api_cards(settings: dict[str, Any]) -> None:
|
|
|
7069
7069
|
_persist_bilibili_api_cards(settings, cards)
|
|
7070
7070
|
st.success("Card Bilibili apagado.")
|
|
7071
7071
|
st.rerun()
|
|
7072
|
-
if st.button("Adicionar nova API", type="primary",
|
|
7072
|
+
if st.button("Adicionar nova API", type="primary", width="stretch", key="add_bilibili_api_card"):
|
|
7073
7073
|
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": ""})
|
|
7074
7074
|
_persist_bilibili_api_cards(settings, cards)
|
|
7075
7075
|
st.success("Novo card Bilibili criado.")
|
|
@@ -7155,7 +7155,7 @@ def render_google_accounts(*, include_innertube: bool = True):
|
|
|
7155
7155
|
key=f"batch_session_info_captured_at_{account_id}",
|
|
7156
7156
|
help="Data em que o sessionInfo token foi capturado. É usada para calcular o alerta de expiração e não revela o token.",
|
|
7157
7157
|
)
|
|
7158
|
-
save_account = st.form_submit_button("Guardar dados da conta Google", type="primary",
|
|
7158
|
+
save_account = st.form_submit_button("Guardar dados da conta Google", type="primary", width="stretch")
|
|
7159
7159
|
|
|
7160
7160
|
st.markdown("**Documento de cookies/credenciais desta conta Google**")
|
|
7161
7161
|
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.")
|
|
@@ -7170,26 +7170,26 @@ def render_google_accounts(*, include_innertube: bool = True):
|
|
|
7170
7170
|
else:
|
|
7171
7171
|
st.success("Documento completo para a conta Google")
|
|
7172
7172
|
st.caption(f"Documento guardado em: {direct_status['document_file']}")
|
|
7173
|
-
document_save = st.button("Guardar documento nesta conta", key=f"save_direct_account_{account_id}",
|
|
7173
|
+
document_save = st.button("Guardar documento nesta conta", key=f"save_direct_account_{account_id}", width="stretch")
|
|
7174
7174
|
|
|
7175
7175
|
account_status = youtube_batch_account_status(batch_account, STORAGE)
|
|
7176
7176
|
status_cols = st.columns(4)
|
|
7177
7177
|
with status_cols[0]:
|
|
7178
7178
|
(st.success if account_status.ok else st.warning)(account_status.message)
|
|
7179
7179
|
with status_cols[1]:
|
|
7180
|
-
if st.button("Autorizar/Reautorizar", key=f"batch_authorize_settings_{account_id}",
|
|
7180
|
+
if st.button("Autorizar/Reautorizar", key=f"batch_authorize_settings_{account_id}", width="stretch"):
|
|
7181
7181
|
result = authorize_youtube_batch_account(batch_account, STORAGE)
|
|
7182
7182
|
(st.success if result.ok else st.error)(result.message)
|
|
7183
7183
|
if result.ok:
|
|
7184
7184
|
st.rerun()
|
|
7185
7185
|
with status_cols[2]:
|
|
7186
|
-
if st.button("Autorizar upload YouTube", key=f"batch_authorize_upload_{account_id}",
|
|
7186
|
+
if st.button("Autorizar upload YouTube", key=f"batch_authorize_upload_{account_id}", width="stretch", help="Autoriza esta conta com o escopo de publicação YouTube; é separado da autorização de leitura/listagem dos canais." ):
|
|
7187
7187
|
result = authorize_youtube_agent(settings, STORAGE, account=batch_account)
|
|
7188
7188
|
(st.success if result.ok else st.error)(result.message)
|
|
7189
7189
|
if result.ok:
|
|
7190
7190
|
st.rerun()
|
|
7191
7191
|
with status_cols[3]:
|
|
7192
|
-
if st.button("Apagar conta", icon=":material/delete:", key=f"batch_remove_settings_{account_id}",
|
|
7192
|
+
if st.button("Apagar conta", icon=":material/delete:", key=f"batch_remove_settings_{account_id}", width="stretch"):
|
|
7193
7193
|
delete_youtube_batch_token(batch_account, STORAGE)
|
|
7194
7194
|
delete_credentials_document(STORAGE, batch_account)
|
|
7195
7195
|
remaining_accounts = [account for account in batch_accounts if str(account.get("id")) != account_id]
|
|
@@ -7254,7 +7254,7 @@ def render_google_accounts(*, include_innertube: bool = True):
|
|
|
7254
7254
|
value=bool(batch_account.get("auto_renew_before_upload", False)),
|
|
7255
7255
|
key=f"auto_renew_before_upload_{account_id}",
|
|
7256
7256
|
)
|
|
7257
|
-
if renewal_cols[1].button("Renovar agora", key=f"renew_session_info_{account_id}", type="primary",
|
|
7257
|
+
if renewal_cols[1].button("Renovar agora", key=f"renew_session_info_{account_id}", type="primary", width="stretch"):
|
|
7258
7258
|
result = renew_account_session(STORAGE, batch_account, settings, force=True, wait_seconds=10)
|
|
7259
7259
|
(st.success if result.ok else st.error)(result.message)
|
|
7260
7260
|
if result.ok:
|
|
@@ -7314,7 +7314,7 @@ def render_google_accounts(*, include_innertube: bool = True):
|
|
|
7314
7314
|
lambda: test_innertube_api_key(innertube_api_key_value),
|
|
7315
7315
|
widget_key="api_test_innertube",
|
|
7316
7316
|
)
|
|
7317
|
-
save_innertube_api_key = st.form_submit_button("Guardar INNERTUBE_API_KEY global", type="primary",
|
|
7317
|
+
save_innertube_api_key = st.form_submit_button("Guardar INNERTUBE_API_KEY global", type="primary", width="stretch")
|
|
7318
7318
|
if save_innertube_api_key:
|
|
7319
7319
|
settings["direct_innertube_api_key"] = innertube_api_key_value.strip()
|
|
7320
7320
|
settings.pop("INNERTUBE_API_KEY", None)
|
|
@@ -7329,7 +7329,7 @@ def render_google_accounts(*, include_innertube: bool = True):
|
|
|
7329
7329
|
st.divider()
|
|
7330
7330
|
add_account = False
|
|
7331
7331
|
add_form_open = bool(st.session_state.get("add_batch_account_form_open", False))
|
|
7332
|
-
if not add_form_open and st.button("Adicionar conta Google/YouTube", type="primary",
|
|
7332
|
+
if not add_form_open and st.button("Adicionar conta Google/YouTube", type="primary", width="stretch", key="open_add_batch_account_form"):
|
|
7333
7333
|
st.session_state["add_batch_account_form_open"] = True
|
|
7334
7334
|
st.rerun()
|
|
7335
7335
|
if add_form_open:
|
|
@@ -7345,7 +7345,7 @@ def render_google_accounts(*, include_innertube: bool = True):
|
|
|
7345
7345
|
new_account_client_secret = st.text_input("OAuth Client Secret", type="password", key="new_batch_account_client_secret")
|
|
7346
7346
|
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.")
|
|
7347
7347
|
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.")
|
|
7348
|
-
add_account = st.form_submit_button("Guardar conta Google/YouTube", type="primary",
|
|
7348
|
+
add_account = st.form_submit_button("Guardar conta Google/YouTube", type="primary", width="stretch")
|
|
7349
7349
|
if st.button("Cancelar", key="cancel_add_batch_account_form"):
|
|
7350
7350
|
st.session_state["add_batch_account_form_open"] = False
|
|
7351
7351
|
st.rerun()
|
|
@@ -7397,7 +7397,7 @@ def render_google_accounts(*, include_innertube: bool = True):
|
|
|
7397
7397
|
key="google_page_youtube_api_key",
|
|
7398
7398
|
help="Credencial Google Cloud separada, usada apenas para métricas oficiais da YouTube Data API.",
|
|
7399
7399
|
)
|
|
7400
|
-
save_google_global = st.form_submit_button("Guardar configuração global do YouTube", type="primary",
|
|
7400
|
+
save_google_global = st.form_submit_button("Guardar configuração global do YouTube", type="primary", width="stretch")
|
|
7401
7401
|
if save_google_global:
|
|
7402
7402
|
settings.update({
|
|
7403
7403
|
"youtube_api_key": youtube_api_key.strip(),
|
|
@@ -7474,7 +7474,7 @@ def _render_material_source_card(settings: dict[str, Any], cards: list[dict[str,
|
|
|
7474
7474
|
lambda: test_material_source_credentials(provider, api_key),
|
|
7475
7475
|
widget_key=f"api_test_material_{card_id}",
|
|
7476
7476
|
)
|
|
7477
|
-
save_card = st.form_submit_button("Salvar", type="primary",
|
|
7477
|
+
save_card = st.form_submit_button("Salvar", type="primary", width="stretch", key=f"material_card_{card_id}_save")
|
|
7478
7478
|
if save_card:
|
|
7479
7479
|
cards[index] = {
|
|
7480
7480
|
**card,
|
|
@@ -7507,7 +7507,7 @@ def render_material_source_api_keys(settings: dict[str, Any], *, embedded: bool
|
|
|
7507
7507
|
format_func=lambda value: _material_source_card_definition(value)["label"],
|
|
7508
7508
|
key="material_new_provider_choice",
|
|
7509
7509
|
)
|
|
7510
|
-
add_source_clicked = st.form_submit_button("Configurar Nova Fonte de Materiais", type="primary",
|
|
7510
|
+
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")
|
|
7511
7511
|
if add_source_clicked:
|
|
7512
7512
|
new_card = new_material_card(provider_to_add, card_id=f"material-{provider_to_add}-{uuid.uuid4().hex[:8]}")
|
|
7513
7513
|
new_card["priority"] = max(
|
|
@@ -7598,7 +7598,7 @@ def _render_api_test_control(
|
|
|
7598
7598
|
consistent: testing it must not appear successful while its values remain
|
|
7599
7599
|
only in the browser session.
|
|
7600
7600
|
"""
|
|
7601
|
-
if st.form_submit_button("Testar chamada API",
|
|
7601
|
+
if st.form_submit_button("Testar chamada API", width="stretch", key=widget_key):
|
|
7602
7602
|
with st.spinner(ui_text("A testar chamada API…", current_ui_language())):
|
|
7603
7603
|
try:
|
|
7604
7604
|
if persist_callback is not None:
|
|
@@ -7698,9 +7698,9 @@ def _render_llm_card(settings: dict[str, Any], cards: list[dict[str, Any]], inde
|
|
|
7698
7698
|
with action_col:
|
|
7699
7699
|
action_buttons = st.columns(2)
|
|
7700
7700
|
with action_buttons[0]:
|
|
7701
|
-
refresh_clicked = st.form_submit_button("Consultar modelos",
|
|
7701
|
+
refresh_clicked = st.form_submit_button("Consultar modelos", width="stretch", key=f"llm_card_{card_id}_refresh")
|
|
7702
7702
|
with action_buttons[1]:
|
|
7703
|
-
test_clicked = st.form_submit_button("Testar chamada API",
|
|
7703
|
+
test_clicked = st.form_submit_button("Testar chamada API", width="stretch", key=f"llm_card_{card_id}_test")
|
|
7704
7704
|
|
|
7705
7705
|
extra_values: dict[str, str] = {}
|
|
7706
7706
|
if definition.extra_fields:
|
|
@@ -7737,10 +7737,10 @@ def _render_llm_card(settings: dict[str, Any], cards: list[dict[str, Any]], inde
|
|
|
7737
7737
|
if telegram_llm:
|
|
7738
7738
|
st.caption("Exclusivo para Notificações de Telegram — prioridade ignorada no pool LLM.")
|
|
7739
7739
|
|
|
7740
|
-
save_clicked = st.form_submit_button("Salvar", type="primary",
|
|
7740
|
+
save_clicked = st.form_submit_button("Salvar", type="primary", width="stretch", key=f"llm_card_{card_id}_save")
|
|
7741
7741
|
remove_clicked = False
|
|
7742
7742
|
if definition.code != "openai":
|
|
7743
|
-
remove_clicked = st.form_submit_button("Remover cartão",
|
|
7743
|
+
remove_clicked = st.form_submit_button("Remover cartão", width="stretch", key=f"llm_card_{card_id}_remove")
|
|
7744
7744
|
|
|
7745
7745
|
edited = dict(card)
|
|
7746
7746
|
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})
|
|
@@ -7802,9 +7802,9 @@ def render_llm_provider_cards(settings: dict[str, Any], *, embedded: bool = Fals
|
|
|
7802
7802
|
with rpm_cols[2]:
|
|
7803
7803
|
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")
|
|
7804
7804
|
save_llm_rpm_clicked = (
|
|
7805
|
-
st.form_submit_button("Guardar limite LLM NVIDIA NIM", type="primary",
|
|
7805
|
+
st.form_submit_button("Guardar limite LLM NVIDIA NIM", type="primary", width="stretch", key="save_llm_rpm_limit")
|
|
7806
7806
|
if embedded
|
|
7807
|
-
else st.button("Guardar limite LLM NVIDIA NIM", type="primary",
|
|
7807
|
+
else st.button("Guardar limite LLM NVIDIA NIM", type="primary", width="stretch", key="save_llm_rpm_limit")
|
|
7808
7808
|
)
|
|
7809
7809
|
if save_llm_rpm_clicked:
|
|
7810
7810
|
settings.update({
|
|
@@ -7826,9 +7826,9 @@ def render_llm_provider_cards(settings: dict[str, Any], *, embedded: bool = Fals
|
|
|
7826
7826
|
key="llm_new_provider_choice",
|
|
7827
7827
|
)
|
|
7828
7828
|
add_provider_clicked = (
|
|
7829
|
-
st.form_submit_button("Configurar Novo Provedor LLM", type="primary",
|
|
7829
|
+
st.form_submit_button("Configurar Novo Provedor LLM", type="primary", width="stretch", key="add_llm_provider_card")
|
|
7830
7830
|
if embedded
|
|
7831
|
-
else st.button("Configurar Novo Provedor LLM", type="primary",
|
|
7831
|
+
else st.button("Configurar Novo Provedor LLM", type="primary", width="stretch", key="add_llm_provider_card")
|
|
7832
7832
|
)
|
|
7833
7833
|
if add_provider_clicked:
|
|
7834
7834
|
new_card = new_llm_card(provider_to_add, card_id=f"llm-{provider_to_add}-{uuid.uuid4().hex[:8]}")
|
|
@@ -7998,7 +7998,7 @@ def _render_media_provider_card(settings: dict[str, Any], cards: list[dict[str,
|
|
|
7998
7998
|
)
|
|
7999
7999
|
model = "" if selected_model == "__select_model__" else selected_model
|
|
8000
8000
|
if definition.code == "canva":
|
|
8001
|
-
refresh_clicked = st.form_submit_button("Consultar Modelos",
|
|
8001
|
+
refresh_clicked = st.form_submit_button("Consultar Modelos", width="stretch", key=f"media_card_{card_id}_refresh")
|
|
8002
8002
|
if definition.code == "canva":
|
|
8003
8003
|
base_url = definition.default_base_url
|
|
8004
8004
|
st.text_input("Base URL", value=base_url, disabled=True, key=f"media_card_{card_id}_base_url_display")
|
|
@@ -8031,7 +8031,7 @@ def _render_media_provider_card(settings: dict[str, Any], cards: list[dict[str,
|
|
|
8031
8031
|
if not has_credentials:
|
|
8032
8032
|
st.info("Preencha Client Id, Client Secret e Redirect Uri para iniciar a autorização.")
|
|
8033
8033
|
else:
|
|
8034
|
-
authorize_clicked = st.form_submit_button("Iniciar autorização",
|
|
8034
|
+
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")
|
|
8035
8035
|
if authorize_clicked:
|
|
8036
8036
|
verifier, challenge = create_pkce_pair()
|
|
8037
8037
|
state = create_state()
|
|
@@ -8060,13 +8060,13 @@ def _render_media_provider_card(settings: dict[str, Any], cards: list[dict[str,
|
|
|
8060
8060
|
action_cols = st.columns(4)
|
|
8061
8061
|
with action_cols[0]:
|
|
8062
8062
|
if definition.code != "canva":
|
|
8063
|
-
refresh_clicked = st.form_submit_button("Consultar Modelos",
|
|
8063
|
+
refresh_clicked = st.form_submit_button("Consultar Modelos", width="stretch", key=f"media_card_{card_id}_refresh")
|
|
8064
8064
|
with action_cols[1]:
|
|
8065
|
-
test_clicked = st.form_submit_button("Testar Chamada API",
|
|
8065
|
+
test_clicked = st.form_submit_button("Testar Chamada API", width="stretch", key=f"media_card_{card_id}_test")
|
|
8066
8066
|
with action_cols[2]:
|
|
8067
|
-
save_clicked = st.form_submit_button("Salvar", type="primary",
|
|
8067
|
+
save_clicked = st.form_submit_button("Salvar", type="primary", width="stretch", key=f"media_card_{card_id}_save")
|
|
8068
8068
|
with action_cols[3]:
|
|
8069
|
-
remove_clicked = st.form_submit_button("Remover provider",
|
|
8069
|
+
remove_clicked = st.form_submit_button("Remover provider", width="stretch", key=f"media_card_{card_id}_remove")
|
|
8070
8070
|
edited = dict(card)
|
|
8071
8071
|
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})
|
|
8072
8072
|
cards[index] = edited
|
|
@@ -8118,7 +8118,7 @@ def render_media_provider_cards(settings: dict[str, Any], *, embedded: bool = Fa
|
|
|
8118
8118
|
st.markdown("**Adicionar provider de imagem/vídeo**")
|
|
8119
8119
|
provider_codes = [item["code"] for item in media_provider_catalog()]
|
|
8120
8120
|
provider_to_add = st.selectbox("Provider de media", provider_codes, format_func=lambda value: media_provider_definition(value).label, key="media_new_provider_choice")
|
|
8121
|
-
add_clicked = st.form_submit_button("Adicionar provider de imagem/vídeo",
|
|
8121
|
+
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")
|
|
8122
8122
|
if add_clicked:
|
|
8123
8123
|
cards.append(new_media_card(provider_to_add, card_id=f"media-{provider_to_add}-{uuid.uuid4().hex[:8]}"))
|
|
8124
8124
|
_persist_media_cards(settings, cards)
|
|
@@ -8156,7 +8156,7 @@ def render_settings():
|
|
|
8156
8156
|
for index, (key, label) in enumerate((("json", "JSON/API"), ("log", "Logs"), ("pdf", "PDF"), ("csv", "CSV"), ("diff", "Diffs"), ("code", "Código"))):
|
|
8157
8157
|
with optimizer_cols[index % 4]:
|
|
8158
8158
|
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")
|
|
8159
|
-
if st.button("Guardar optimizador", key="save_token_optimizer",
|
|
8159
|
+
if st.button("Guardar optimizador", key="save_token_optimizer", width="stretch"):
|
|
8160
8160
|
settings.update({"token_optimizer_enabled": optimizer_enabled, **optimizer_values})
|
|
8161
8161
|
write_json("settings.json", settings)
|
|
8162
8162
|
st.success("Configuração do jusTokenMax guardada.")
|
|
@@ -8229,7 +8229,7 @@ def render_settings():
|
|
|
8229
8229
|
})
|
|
8230
8230
|
write_json("settings.json", settings)
|
|
8231
8231
|
|
|
8232
|
-
if st.form_submit_button("Guardar Azure Speech", type="primary",
|
|
8232
|
+
if st.form_submit_button("Guardar Azure Speech", type="primary", width="stretch", key="save_azure_speech"):
|
|
8233
8233
|
save_azure_speech()
|
|
8234
8234
|
st.success("Azure Speech guardado.")
|
|
8235
8235
|
_render_api_test_control(
|
|
@@ -8337,7 +8337,7 @@ def render_settings():
|
|
|
8337
8337
|
settings.update({"lyria_api_key": lyria_api_key.strip(), "lyria_model": lyria_model})
|
|
8338
8338
|
write_json("settings.json", settings)
|
|
8339
8339
|
|
|
8340
|
-
if st.form_submit_button("Guardar Google Lyria", type="primary",
|
|
8340
|
+
if st.form_submit_button("Guardar Google Lyria", type="primary", width="stretch", key="save_google_lyria"):
|
|
8341
8341
|
save_google_lyria()
|
|
8342
8342
|
st.success("Google Lyria guardado.")
|
|
8343
8343
|
_render_api_test_control(
|
|
@@ -8421,7 +8421,7 @@ def render_settings():
|
|
|
8421
8421
|
with st.form("upload_innertube_api_key_form"):
|
|
8422
8422
|
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.")
|
|
8423
8423
|
_render_api_test_control(settings, "innertube_upload", lambda: test_innertube_api_key(upload_innertube), widget_key="api_test_innertube_upload")
|
|
8424
|
-
save_upload_innertube = st.form_submit_button("Guardar INNERTUBE_API_KEY global", type="primary",
|
|
8424
|
+
save_upload_innertube = st.form_submit_button("Guardar INNERTUBE_API_KEY global", type="primary", width="stretch")
|
|
8425
8425
|
if save_upload_innertube:
|
|
8426
8426
|
settings["direct_innertube_api_key"] = upload_innertube.strip()
|
|
8427
8427
|
settings.pop("INNERTUBE_API_KEY", None)
|
|
@@ -8481,9 +8481,9 @@ def render_settings():
|
|
|
8481
8481
|
_render_credential_status(composio_api_key)
|
|
8482
8482
|
composio_action_cols = st.columns(2)
|
|
8483
8483
|
with composio_action_cols[0]:
|
|
8484
|
-
save_composio = st.button("Guardar Composio", type="primary",
|
|
8484
|
+
save_composio = st.button("Guardar Composio", type="primary", width="stretch", key="upload_composio_save")
|
|
8485
8485
|
with composio_action_cols[1]:
|
|
8486
|
-
test_composio = st.button("Testar configuração",
|
|
8486
|
+
test_composio = st.button("Testar configuração", width="stretch", key="upload_composio_test")
|
|
8487
8487
|
if save_composio:
|
|
8488
8488
|
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_connected_account_id": composio_connected_account_id.strip(), "composio_toolkit": composio_toolkit, "composio_tool_slug": composio_tool_slug, "composio_file_field": "videoFilePath", "composio_channel_field": composio_channel_field, "composio_privacy_status": composio_privacy_status, "composio_category_id": composio_category_id, "composio_language": composio_language, "composio_privacy_field": "privacyStatus", "composio_category_field": "categoryId", "composio_language_field": "defaultLanguage", "composio_arguments_json": composio_arguments_json or "{}"})
|
|
8489
8489
|
write_json("settings.json", settings)
|
|
@@ -8503,7 +8503,7 @@ def render_settings():
|
|
|
8503
8503
|
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")
|
|
8504
8504
|
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")
|
|
8505
8505
|
_render_credential_status(upload_post_key_upload, required=True)
|
|
8506
|
-
if st.button("Guardar Upload-Post", type="primary",
|
|
8506
|
+
if st.button("Guardar Upload-Post", type="primary", width="stretch", key="upload_tab_save_upload_post"):
|
|
8507
8507
|
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)})
|
|
8508
8508
|
write_json("settings.json", settings)
|
|
8509
8509
|
st.success("Upload-Post guardado.")
|
|
@@ -8516,7 +8516,7 @@ def render_settings():
|
|
|
8516
8516
|
postiz_integration_upload = st.text_input("Postiz integração padrão", value=str(settings.get("postiz_integration_id") or ""), key="upload_tab_postiz_integration")
|
|
8517
8517
|
postiz_auto_upload = st.checkbox("Permitir publicação imediata no Postiz", bool(settings.get("postiz_auto_publish", False)), key="upload_tab_postiz_auto")
|
|
8518
8518
|
_render_credential_status(postiz_key_upload, required=True)
|
|
8519
|
-
if st.button("Guardar Postiz", type="primary",
|
|
8519
|
+
if st.button("Guardar Postiz", type="primary", width="stretch", key="upload_tab_save_postiz"):
|
|
8520
8520
|
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)})
|
|
8521
8521
|
write_json("settings.json", settings)
|
|
8522
8522
|
st.success("Postiz guardado.")
|
|
@@ -8542,8 +8542,8 @@ def render_settings():
|
|
|
8542
8542
|
influencer_supabase_url = text_setting("Supabase Project URL", "influencer_supabase_url", help_text="URL do projecto, por exemplo https://project-id.supabase.co")
|
|
8543
8543
|
with db_cols[1]:
|
|
8544
8544
|
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.")
|
|
8545
|
-
test_backend_clicked = st.form_submit_button("Testar ligação do backend",
|
|
8546
|
-
save_backend_clicked = st.form_submit_button("Guardar configuração do backend", type="primary",
|
|
8545
|
+
test_backend_clicked = st.form_submit_button("Testar ligação do backend", width="stretch")
|
|
8546
|
+
save_backend_clicked = st.form_submit_button("Guardar configuração do backend", type="primary", width="stretch")
|
|
8547
8547
|
effective_settings = dict(settings)
|
|
8548
8548
|
effective_settings.update({
|
|
8549
8549
|
"influencer_db_backend": influencer_db_backend,
|
|
@@ -8648,7 +8648,7 @@ def _render_telegram_notification_settings() -> None:
|
|
|
8648
8648
|
lambda: test_telegram_credentials(telegram_bot_token, telegram_chat_id),
|
|
8649
8649
|
widget_key="api_test_telegram",
|
|
8650
8650
|
)
|
|
8651
|
-
if st.form_submit_button("Guardar configuração Telegram", type="primary",
|
|
8651
|
+
if st.form_submit_button("Guardar configuração Telegram", type="primary", width="stretch"):
|
|
8652
8652
|
settings.update({
|
|
8653
8653
|
"telegram_enabled": bool(telegram_enabled),
|
|
8654
8654
|
"telegram_bot_token": telegram_bot_token.strip(),
|
|
@@ -8696,7 +8696,7 @@ def render_logs():
|
|
|
8696
8696
|
|
|
8697
8697
|
action_cols = st.columns([1, 1, 3])
|
|
8698
8698
|
with action_cols[0]:
|
|
8699
|
-
if st.button("Actualizar logs",
|
|
8699
|
+
if st.button("Actualizar logs", width="stretch"):
|
|
8700
8700
|
st.rerun()
|
|
8701
8701
|
with action_cols[1]:
|
|
8702
8702
|
st.metric("Registos", len(initial_records))
|
|
@@ -8712,7 +8712,7 @@ def render_logs():
|
|
|
8712
8712
|
rows = logs_to_rows(records)
|
|
8713
8713
|
st.dataframe(
|
|
8714
8714
|
rows,
|
|
8715
|
-
|
|
8715
|
+
width="stretch",
|
|
8716
8716
|
height=520,
|
|
8717
8717
|
hide_index=True,
|
|
8718
8718
|
column_config={
|
|
@@ -8752,16 +8752,16 @@ def render_notifications():
|
|
|
8752
8752
|
|
|
8753
8753
|
action_cols = st.columns([1.4, 1.4, 2.2])
|
|
8754
8754
|
with action_cols[0]:
|
|
8755
|
-
if st.button("Marcar todas como lidas",
|
|
8755
|
+
if st.button("Marcar todas como lidas", width="stretch", disabled=unread_count == 0):
|
|
8756
8756
|
mark_all_notifications_read()
|
|
8757
8757
|
st.rerun()
|
|
8758
8758
|
with action_cols[1]:
|
|
8759
|
-
if st.button("Actualizar notificações",
|
|
8759
|
+
if st.button("Actualizar notificações", width="stretch"):
|
|
8760
8760
|
reconcile_persisted_notifications()
|
|
8761
8761
|
st.rerun()
|
|
8762
8762
|
with action_cols[2]:
|
|
8763
8763
|
confirm_clear = st.checkbox("Confirmar limpeza do histórico", key="confirm_clear_notifications")
|
|
8764
|
-
if st.button("Limpar histórico",
|
|
8764
|
+
if st.button("Limpar histórico", width="stretch", disabled=not confirm_clear):
|
|
8765
8765
|
clear_notifications()
|
|
8766
8766
|
st.session_state.pop("confirm_clear_notifications", None)
|
|
8767
8767
|
st.rerun()
|
|
@@ -8783,7 +8783,7 @@ def render_notifications():
|
|
|
8783
8783
|
help=event["description"],
|
|
8784
8784
|
key=f"notification_preference_{event['code']}",
|
|
8785
8785
|
)
|
|
8786
|
-
if st.form_submit_button("Guardar preferências", type="primary",
|
|
8786
|
+
if st.form_submit_button("Guardar preferências", type="primary", width="stretch"):
|
|
8787
8787
|
save_notification_preferences(pending_preferences)
|
|
8788
8788
|
st.success("Preferências de notificação guardadas.")
|
|
8789
8789
|
st.rerun()
|
|
@@ -8818,7 +8818,7 @@ def render_notifications():
|
|
|
8818
8818
|
st.caption(f"{item.get('category', 'Sistema')} · {item.get('created_at', '—')}")
|
|
8819
8819
|
st.write("Lida" if item.get("read") else "Não lida")
|
|
8820
8820
|
with notification_cols[2]:
|
|
8821
|
-
if not item.get("read") and st.button("Marcar como lida", key=f"mark_notification_{item.get('id')}",
|
|
8821
|
+
if not item.get("read") and st.button("Marcar como lida", key=f"mark_notification_{item.get('id')}", width="stretch"):
|
|
8822
8822
|
mark_notification_read(str(item.get("id")))
|
|
8823
8823
|
st.rerun()
|
|
8824
8824
|
|
|
@@ -8947,7 +8947,7 @@ def render_mcp():
|
|
|
8947
8947
|
st.rerun()
|
|
8948
8948
|
|
|
8949
8949
|
st.caption(integration.get("endpoint_note", "Porta editável para o serviço local."))
|
|
8950
|
-
if st.button("Guardar porta", key=f"mcp_save_port_{integration_id}",
|
|
8950
|
+
if st.button("Guardar porta", key=f"mcp_save_port_{integration_id}", width="stretch"):
|
|
8951
8951
|
update_integration(integration_id, port=int(port))
|
|
8952
8952
|
st.success(f"Porta de {integration['name']} guardada: {int(port)}")
|
|
8953
8953
|
st.rerun()
|
|
@@ -8981,7 +8981,7 @@ def render_mcp():
|
|
|
8981
8981
|
st.error("Para expor o servidor fora do computador local, defina um token de acesso MCP.")
|
|
8982
8982
|
|
|
8983
8983
|
action_label = "Guardar e iniciar Servidor MCP" if server_enabled else "Guardar e parar Servidor MCP"
|
|
8984
|
-
if st.button(action_label, type="primary", key="mcp_server_save",
|
|
8984
|
+
if st.button(action_label, type="primary", key="mcp_server_save", width="stretch"):
|
|
8985
8985
|
try:
|
|
8986
8986
|
saved = save_server_config(enabled=server_enabled, host=server_host, port=int(server_port), auth_token=server_token, write_enabled=write_enabled)
|
|
8987
8987
|
if saved["enabled"]:
|
|
@@ -9021,7 +9021,7 @@ def render_mcp():
|
|
|
9021
9021
|
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.")
|
|
9022
9022
|
skill_cols = st.columns(2)
|
|
9023
9023
|
with skill_cols[0]:
|
|
9024
|
-
if st.button("Guardar skill localmente", type="primary",
|
|
9024
|
+
if st.button("Guardar skill localmente", type="primary", width="stretch", key="mcp_install_mpt_skill"):
|
|
9025
9025
|
try:
|
|
9026
9026
|
destination = install_skill_locally()
|
|
9027
9027
|
st.success(f"Skill guardada em `{destination}`")
|
|
@@ -9038,7 +9038,7 @@ def render_mcp():
|
|
|
9038
9038
|
data=skill_data,
|
|
9039
9039
|
file_name="moneyprinterturbo-video.md",
|
|
9040
9040
|
mime="text/markdown",
|
|
9041
|
-
|
|
9041
|
+
width="stretch",
|
|
9042
9042
|
key="mcp_download_mpt_skill",
|
|
9043
9043
|
)
|
|
9044
9044
|
else:
|
|
@@ -9140,8 +9140,8 @@ def render_metadata_cleaner():
|
|
|
9140
9140
|
st.subheader("Resultado")
|
|
9141
9141
|
st.write(f"**Ficheiro limpo:** `{output.name}`")
|
|
9142
9142
|
mime = "video/mp4" if output.suffix.lower() == ".mp4" else "video/*"
|
|
9143
|
-
st.download_button("Descarregar vídeo limpo", data=output.read_bytes(), file_name=output.name, mime=mime,
|
|
9144
|
-
st.download_button("Descarregar manifesto de upload (JSON)", data=metadata_manifest(record), file_name=f"{output.stem}-metadata.json", mime="application/json",
|
|
9143
|
+
st.download_button("Descarregar vídeo limpo", data=output.read_bytes(), file_name=output.name, mime=mime, width="stretch", key="metadata_download_video")
|
|
9144
|
+
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")
|
|
9145
9145
|
with st.expander("Pré-visualizar metadados"):
|
|
9146
9146
|
st.json(record["metadata"])
|
|
9147
9147
|
|
|
@@ -9170,7 +9170,7 @@ def render_update_youtube_videos():
|
|
|
9170
9170
|
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")
|
|
9171
9171
|
updater = YouTubeVideoUpdater(settings, STORAGE)
|
|
9172
9172
|
refresh_key = f"update_youtube_loaded_{channel.get('id', '')}"
|
|
9173
|
-
if st.button("Carregar vídeos do canal", type="primary",
|
|
9173
|
+
if st.button("Carregar vídeos do canal", type="primary", width="stretch", key="update_youtube_load") or refresh_key not in st.session_state:
|
|
9174
9174
|
with st.spinner("A carregar vídeos publicados…"):
|
|
9175
9175
|
result = updater.list_videos(channel, max_results=50)
|
|
9176
9176
|
st.session_state[refresh_key] = result.data if result.ok else {"error": result.message, "videos": []}
|
|
@@ -9192,7 +9192,7 @@ def render_update_youtube_videos():
|
|
|
9192
9192
|
top = st.columns([1.2, 3.8, 1.2])
|
|
9193
9193
|
with top[0]:
|
|
9194
9194
|
if video.get("thumbnail_url"):
|
|
9195
|
-
st.image(video["thumbnail_url"],
|
|
9195
|
+
st.image(video["thumbnail_url"], width="stretch")
|
|
9196
9196
|
with top[1]:
|
|
9197
9197
|
st.markdown(f"**{video.get('title') or 'Sem título'}**")
|
|
9198
9198
|
st.caption(f"{video.get('published_at') or 'Data indisponível'} · {video.get('privacy_status') or 'estado desconhecido'} · `{video_id}`")
|
|
@@ -9207,7 +9207,7 @@ def render_update_youtube_videos():
|
|
|
9207
9207
|
ai_cols = st.columns(3)
|
|
9208
9208
|
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), "")
|
|
9209
9209
|
with ai_cols[0]:
|
|
9210
|
-
if st.button("Gerar título", key=f"{card_key}_ai_title",
|
|
9210
|
+
if st.button("Gerar título", key=f"{card_key}_ai_title", width="stretch"):
|
|
9211
9211
|
try:
|
|
9212
9212
|
generated = generate_video_update_metadata(settings, channel, video, script=script, blueprint=blueprint, mode="title")
|
|
9213
9213
|
st.session_state[title_key] = generated["title"]
|
|
@@ -9215,7 +9215,7 @@ def render_update_youtube_videos():
|
|
|
9215
9215
|
except CreativeGenerationError as exc:
|
|
9216
9216
|
st.error(str(exc))
|
|
9217
9217
|
with ai_cols[1]:
|
|
9218
|
-
if st.button("Gerar descrição", key=f"{card_key}_ai_description",
|
|
9218
|
+
if st.button("Gerar descrição", key=f"{card_key}_ai_description", width="stretch"):
|
|
9219
9219
|
try:
|
|
9220
9220
|
generated = generate_video_update_metadata(settings, channel, video, script=script, blueprint=blueprint, mode="description")
|
|
9221
9221
|
st.session_state[desc_key] = generated["description"]
|
|
@@ -9223,7 +9223,7 @@ def render_update_youtube_videos():
|
|
|
9223
9223
|
except CreativeGenerationError as exc:
|
|
9224
9224
|
st.error(str(exc))
|
|
9225
9225
|
with ai_cols[2]:
|
|
9226
|
-
if st.button("Gerar título e descrição", key=f"{card_key}_ai_both",
|
|
9226
|
+
if st.button("Gerar título e descrição", key=f"{card_key}_ai_both", width="stretch"):
|
|
9227
9227
|
try:
|
|
9228
9228
|
generated = generate_video_update_metadata(settings, channel, video, script=script, blueprint=blueprint, mode="both")
|
|
9229
9229
|
st.session_state[title_key] = generated.get("title", title)
|
|
@@ -9232,7 +9232,7 @@ def render_update_youtube_videos():
|
|
|
9232
9232
|
except CreativeGenerationError as exc:
|
|
9233
9233
|
st.error(str(exc))
|
|
9234
9234
|
thumbnail = st.file_uploader("Trocar thumbnail (opcional)", type=["jpg", "jpeg", "png"], key=f"{card_key}_thumbnail")
|
|
9235
|
-
if st.button("Actualizar no YouTube", type="primary", key=f"{card_key}_save",
|
|
9235
|
+
if st.button("Actualizar no YouTube", type="primary", key=f"{card_key}_save", width="stretch"):
|
|
9236
9236
|
thumbnail_path = None
|
|
9237
9237
|
if thumbnail is not None:
|
|
9238
9238
|
thumbnail_path = STORAGE / "youtube_update" / f"{video_id}_{thumbnail.name}"
|
|
@@ -9416,7 +9416,7 @@ def main():
|
|
|
9416
9416
|
|
|
9417
9417
|
def render_nav_button(target: str, icon: str, label: str, scope: str):
|
|
9418
9418
|
display_label = ui_text(label, ui_language)
|
|
9419
|
-
if st.button(display_label, key=f"nav_{scope}_{target}", icon=icon,
|
|
9419
|
+
if st.button(display_label, key=f"nav_{scope}_{target}", icon=icon, width="stretch", type="primary" if is_nav_item_active(target) else "secondary"):
|
|
9420
9420
|
navigate(target)
|
|
9421
9421
|
|
|
9422
9422
|
with st.sidebar:
|