@danhachuel/thunderbolt 0.3.92 → 0.3.94
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/app/main.py
CHANGED
|
@@ -1969,10 +1969,17 @@ def render_channels():
|
|
|
1969
1969
|
channel_account_ids.append(current_channel_account_id)
|
|
1970
1970
|
youtube_account_labels[current_channel_account_id] = "Conta Google não configurada"
|
|
1971
1971
|
with st.expander("Upload directo — documento da conta deste canal", expanded=False):
|
|
1972
|
-
st.caption("O DELEGATED_SESSION_ID deste canal
|
|
1972
|
+
st.caption("O DELEGATED_SESSION_ID é individual deste canal. Guarde-o aqui; o valor fica no registo local do canal e não é copiado para o documento JSON partilhado da conta Google.")
|
|
1973
1973
|
with st.form(f"channel_direct_credentials_{channel_id}"):
|
|
1974
1974
|
channel_account_id = st.selectbox("Conta Google do documento deste canal", channel_account_ids, index=channel_account_ids.index(current_channel_account_id) if current_channel_account_id in channel_account_ids else 0, format_func=lambda item: youtube_account_labels.get(item, item or "Sem conta Google associada"), key=f"channel_account_{channel_id}")
|
|
1975
|
-
|
|
1975
|
+
channel_delegated_session_id = st.text_input(
|
|
1976
|
+
"DELEGATED_SESSION_ID deste canal",
|
|
1977
|
+
value=str(channel.get("delegated_session_id") or ""),
|
|
1978
|
+
type="password",
|
|
1979
|
+
key=f"channel_delegated_session_id_{channel_id}",
|
|
1980
|
+
help="Identificador individual usado pelo Upload directo deste canal. Não é partilhado com outros canais nem mostrado nos diagnósticos.",
|
|
1981
|
+
)
|
|
1982
|
+
save_channel_direct_credentials = st.form_submit_button("Guardar conta Google e DELEGATED_SESSION_ID", type="primary", use_container_width=True)
|
|
1976
1983
|
selected_channel_account = youtube_accounts_by_id.get(channel_account_id)
|
|
1977
1984
|
if selected_channel_account:
|
|
1978
1985
|
selected_account_status = document_status(STORAGE, selected_channel_account, channel, settings, channels)
|
|
@@ -1992,8 +1999,15 @@ def render_channels():
|
|
|
1992
1999
|
else:
|
|
1993
2000
|
st.info("Associe este canal a uma conta Google para validar o documento de credenciais.")
|
|
1994
2001
|
if save_channel_direct_credentials:
|
|
1995
|
-
update_channel(
|
|
1996
|
-
|
|
2002
|
+
update_channel(
|
|
2003
|
+
channel_id,
|
|
2004
|
+
{
|
|
2005
|
+
"google_account_id": channel_account_id.strip(),
|
|
2006
|
+
"google_account_email": str(youtube_accounts_by_id.get(channel_account_id, {}).get("email", "")),
|
|
2007
|
+
"delegated_session_id": channel_delegated_session_id.strip(),
|
|
2008
|
+
},
|
|
2009
|
+
)
|
|
2010
|
+
st.success("Conta Google e DELEGATED_SESSION_ID individual do canal guardados.")
|
|
1997
2011
|
st.rerun()
|
|
1998
2012
|
|
|
1999
2013
|
render_channel_videos(channel)
|
|
@@ -3562,6 +3576,21 @@ def load_video_tasks_for_catalog() -> list[dict[str, Any]]:
|
|
|
3562
3576
|
return [task for task in saved if isinstance(task, dict) and str(task.get("id") or "").strip()]
|
|
3563
3577
|
|
|
3564
3578
|
|
|
3579
|
+
def _is_music_task(task: dict[str, Any]) -> bool:
|
|
3580
|
+
"""Identify music pipeline tasks without conflating them with ordinary video tasks."""
|
|
3581
|
+
return bool(task.get("music_mode")) or str(task.get("style_wide") or task.get("style") or "").strip().casefold() in {"music", "música"}
|
|
3582
|
+
|
|
3583
|
+
|
|
3584
|
+
def load_music_tasks_for_catalog() -> list[dict[str, Any]]:
|
|
3585
|
+
"""Return persisted tasks that were explicitly created through a music route."""
|
|
3586
|
+
return [task for task in load_video_tasks_for_catalog() if _is_music_task(task)]
|
|
3587
|
+
|
|
3588
|
+
|
|
3589
|
+
def load_standard_video_tasks_for_catalog() -> list[dict[str, Any]]:
|
|
3590
|
+
"""Keep video backlog free of tasks that belong to the dedicated music queue."""
|
|
3591
|
+
return [task for task in load_video_tasks_for_catalog() if not _is_music_task(task)]
|
|
3592
|
+
|
|
3593
|
+
|
|
3565
3594
|
def _video_task_format(task: dict[str, Any]) -> str:
|
|
3566
3595
|
value = task.get("format") or task.get("style_wide") or task.get("style") or "wide"
|
|
3567
3596
|
return str(value).strip() or "wide"
|
|
@@ -3608,7 +3637,7 @@ def render_videos():
|
|
|
3608
3637
|
st.caption("Acompanhamento dos vídeos criados, estados da pipeline e controlos de execução.")
|
|
3609
3638
|
st.caption(f"Os vídeos são guardados em `{STORAGE / 'videos'}`.")
|
|
3610
3639
|
_render_pipeline_progress_panel()
|
|
3611
|
-
tasks =
|
|
3640
|
+
tasks = load_standard_video_tasks_for_catalog()
|
|
3612
3641
|
if not tasks:
|
|
3613
3642
|
st.info("Nenhum vídeo criado.")
|
|
3614
3643
|
return
|
|
@@ -3667,6 +3696,64 @@ def render_videos():
|
|
|
3667
3696
|
st.rerun()
|
|
3668
3697
|
|
|
3669
3698
|
|
|
3699
|
+
def render_music_backlog() -> None:
|
|
3700
|
+
"""Render the music-only counterpart of Backlog Videos with the same controls."""
|
|
3701
|
+
st.subheader("Music Backlog")
|
|
3702
|
+
st.caption("Acompanhamento das músicas criadas, estados da pipeline e controlos de execução.")
|
|
3703
|
+
st.caption(f"As músicas são guardadas em `{STORAGE / 'music'}`.")
|
|
3704
|
+
_render_pipeline_progress_panel()
|
|
3705
|
+
tasks = load_music_tasks_for_catalog()
|
|
3706
|
+
if not tasks:
|
|
3707
|
+
st.info("Nenhuma música criada.")
|
|
3708
|
+
return
|
|
3709
|
+
known_states = ["to_do", "doing", "blocked", "done", "failed", "cancelled"]
|
|
3710
|
+
extra_states = sorted({str(task.get("state") or "unknown") for task in tasks if str(task.get("state") or "unknown") not in known_states})
|
|
3711
|
+
state_filter = st.selectbox("Filtrar por estado", ["Todos", *known_states, *extra_states], key="music_backlog_state_filter")
|
|
3712
|
+
for task in tasks:
|
|
3713
|
+
if state_filter != "Todos" and task.get("state") != state_filter:
|
|
3714
|
+
continue
|
|
3715
|
+
with st.container(border=True):
|
|
3716
|
+
cols = st.columns([2.2, 1, 1, 1.2, 1.8])
|
|
3717
|
+
with cols[0]:
|
|
3718
|
+
st.write(f"**{task.get('title') or task.get('topic', 'Sem título')}**")
|
|
3719
|
+
st.caption(f"Música: {task.get('music_source') or 'rota musical'}")
|
|
3720
|
+
st.caption(f"{task.get('channel_name')} · {task.get('id')}")
|
|
3721
|
+
music_path = str(task.get("music_path") or (task.get("artifacts") or {}).get("music") or "").strip()
|
|
3722
|
+
if music_path and Path(music_path).is_file():
|
|
3723
|
+
music_file = Path(music_path)
|
|
3724
|
+
st.success("Música pronta; pode continuar para o destino configurado.")
|
|
3725
|
+
st.download_button(
|
|
3726
|
+
"Descarregar música pronta",
|
|
3727
|
+
data=music_file.read_bytes(),
|
|
3728
|
+
file_name=music_file.name,
|
|
3729
|
+
mime="audio/mpeg",
|
|
3730
|
+
key=f"music_backlog_download_{task['id']}",
|
|
3731
|
+
use_container_width=True,
|
|
3732
|
+
)
|
|
3733
|
+
elif music_path:
|
|
3734
|
+
st.caption(f"Música registada: {music_path}")
|
|
3735
|
+
else:
|
|
3736
|
+
st.caption("A música será disponibilizada quando a etapa de geração terminar.")
|
|
3737
|
+
with cols[1]:
|
|
3738
|
+
st.caption("Formato")
|
|
3739
|
+
st.write(_video_task_format(task))
|
|
3740
|
+
with cols[2]:
|
|
3741
|
+
st.write(_pipeline_stage_label(task))
|
|
3742
|
+
with cols[3]:
|
|
3743
|
+
_render_video_task_state(task)
|
|
3744
|
+
with cols[4]:
|
|
3745
|
+
state = str(task.get("state") or "")
|
|
3746
|
+
start_col, stop_col = st.columns(2)
|
|
3747
|
+
with start_col:
|
|
3748
|
+
if st.button("Start", key=f"music_backlog_start_{task['id']}", use_container_width=True, disabled=state not in {"to_do", "blocked", "failed"}):
|
|
3749
|
+
transition_task(task["id"], "doing")
|
|
3750
|
+
st.rerun()
|
|
3751
|
+
with stop_col:
|
|
3752
|
+
if st.button("Stop", key=f"music_backlog_stop_{task['id']}", use_container_width=True, disabled=state != "doing"):
|
|
3753
|
+
transition_task(task["id"], "blocked")
|
|
3754
|
+
st.rerun()
|
|
3755
|
+
|
|
3756
|
+
|
|
3670
3757
|
def _thumbnail_editor_context(record: dict[str, Any]) -> tuple[dict[str, Any], dict[str, Any]]:
|
|
3671
3758
|
"""Resolve the persisted task channel/Blueprint without requiring either one to remain registered."""
|
|
3672
3759
|
channel_id = str(record.get("channel_id") or "").strip()
|
|
@@ -6731,6 +6818,7 @@ def main():
|
|
|
6731
6818
|
]
|
|
6732
6819
|
music_items = [
|
|
6733
6820
|
("Criação de Músicas", ":material/music_note:", "Criação de Músicas"),
|
|
6821
|
+
("Music Backlog", ":material/queue_music:", "Music Backlog"),
|
|
6734
6822
|
("Upload Música", ":material/library_music:", "Upload Música"),
|
|
6735
6823
|
]
|
|
6736
6824
|
models_ai_items = [
|
|
@@ -6853,6 +6941,7 @@ def main():
|
|
|
6853
6941
|
"Criação de Vídeos": render_new_video,
|
|
6854
6942
|
"Backlog Vídeos": render_videos,
|
|
6855
6943
|
"Criação de Músicas": render_music_creation,
|
|
6944
|
+
"Music Backlog": render_music_backlog,
|
|
6856
6945
|
"Upload Música": render_music_upload,
|
|
6857
6946
|
"Roteiros": render_scripts,
|
|
6858
6947
|
"Thumbnails": render_thumbnails,
|
|
@@ -363,6 +363,9 @@ def merge_credentials_document(
|
|
|
363
363
|
|
|
364
364
|
|
|
365
365
|
def delegated_session_id(document: dict[str, Any], channel: dict[str, Any]) -> str:
|
|
366
|
+
channel_value = str(channel.get("delegated_session_id") or "").strip() if isinstance(channel, dict) else ""
|
|
367
|
+
if channel_value:
|
|
368
|
+
return channel_value
|
|
366
369
|
mapping = document.get("delegated_session_ids", {}) if isinstance(document, dict) else {}
|
|
367
370
|
if not isinstance(mapping, dict):
|
|
368
371
|
return ""
|
package/package.json
CHANGED