@danhachuel/thunderbolt 0.3.39 → 0.3.40
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/MANUAL-INSTALACAO.md +4 -0
- package/README.md +7 -1
- package/app/main.py +55 -15
- package/package.json +1 -1
package/MANUAL-INSTALACAO.md
CHANGED
|
@@ -396,6 +396,10 @@ Uma execução da etapa Vídeo tem limite de 20 minutos. Se o processo externo t
|
|
|
396
396
|
|
|
397
397
|
Se o launcher ou o worker for encerrado abruptamente, uma tarefa `doing` sem actualização durante 25 minutos é recuperada e marcada como `failed`, evitando estados indefinidos eternos. Para processar novas tarefas, deixe a aplicação iniciada com o comando normal; o painel avisa quando não existe heartbeat recente do worker.
|
|
398
398
|
|
|
399
|
+
O **Backlog Vídeos** e a lista **Automação > Automação Youtube > Vídeos cadastrados** usam o mesmo catálogo completo de `storage/state/tasks.json`. Por isso, tarefas criadas manualmente e tarefas criadas pelo worker diário aparecem nos dois locais, sem filtragem implícita por origem. O filtro do Backlog mostra os estados conhecidos e inclui automaticamente estados adicionais encontrados nos dados.
|
|
400
|
+
|
|
401
|
+
Nos dois cards, o bloco de estado apresenta o valor técnico, o rótulo legível, a barra de progresso e a mensagem de erro quando existir. O bloco **Formato** resolve nesta ordem `format`, `style_wide`, `style` e, por fim, `wide`, mantendo a apresentação de formatos como `wide`, `shorts`, `music` ou `full_ia` mesmo em tarefas antigas.
|
|
402
|
+
|
|
399
403
|
### Upload Música — JewelMusic, Pushtunes e ytmusicapi
|
|
400
404
|
|
|
401
405
|
A área **Pipeline Música > Upload Música** separa três métodos com contratos diferentes. Em **JewelMusic**, active a integração, introduza a API Key fornecida pelo dashboard da JewelMusic e confirme a Base URL oficial `https://api.jewelmusic.com` e, se necessário, configure proxy e timeout. Carregue ou seleccione um ficheiro de música, indique artista e título e clique em **Enviar música para JewelMusic**. O teste de ligação consulta `/v1/ping`; o upload envia `multipart/form-data` para `/v1/tracks/upload` com os metadados preenchidos.
|
package/README.md
CHANGED
|
@@ -69,7 +69,7 @@ No topo da área principal da aplicação existe o menu nativo de idioma no padr
|
|
|
69
69
|
|
|
70
70
|
A UI suporta os temas **Dark** e **Light** através do menu nativo de três pontos do Streamlit, no local original do toolbar. Não existe um selector Theme adicional dentro da página. A configuração distribuída em `.streamlit/config.toml` disponibiliza as variantes nomeadas **Dark** e **Light**, e o menu nativo continua responsável por alternar entre os modos, seguindo o padrão do [MoneyPrinterTurbo](https://github.com/harry0703/MoneyPrinterTurbo). O CSS próprio do Thunderbolt usa cores semânticas, `currentColor` e `color-mix` para acompanhar o tema activo, sem alterar a posição nem a funcionalidade do toolbar, do botão Deploy e do menu principal.
|
|
71
71
|
|
|
72
|
-
## Navegação da UI 0.3.
|
|
72
|
+
## Navegação da UI 0.3.40
|
|
73
73
|
|
|
74
74
|
A barra lateral mantém os níveis principais, nesta ordem: **Início**, **Niche Finder**, **Pipeline**, **Pipeline TikTok**, **Automação**, **Edição**, **AI Influencers** e **Configurações**. **Pipeline Vídeos** é expansível e contém **Criação de Vídeos**, **Backlog Vídeos**, **Roteiros**, **Thumbnails** e **Upload**. **Pipeline Música** é expansível e contém **Criação de Músicas** e **Upload Música**. **Automação** também é expansível e contém **Automação Youtube**. **Edição** é expansível e contém **Limpador de Metadados**, **Cortes**, **Editor Python** e **Download Mídia**, nessa ordem. **AI Influencers** é expansível e contém **Personagens**, **Redes Sociais**, **Tutorial Meta** e **Tutorial Supabase**, nessa ordem. **Niche Finder** é expansível e contém **Niche Finder Kaggle** e **Niche Finder Apify**. **Configurações** é expansível e contém **Canais Youtube**, **Blueprints Youtube**, **MCP**, **Contas Google**, **Notificações**, **Logs** e **Configuração API**, nessa ordem. O Início reúne o dashboard e as filas do Pipeline, sem botões de acções rápidas.
|
|
75
75
|
|
|
@@ -89,6 +89,12 @@ A página **AI Influencers > Tutorial Meta** apresenta o guia de configuração
|
|
|
89
89
|
|
|
90
90
|
A página **Edição > Download Mídia** utiliza a API Python do [yt-dlp](https://github.com/yt-dlp/yt-dlp) para descarregar vídeos e áudio de URLs públicas, com qualidade, contentor, formato de áudio, legendas, metadados, playlists, progresso e histórico local em `storage/downloads/` e `storage/state/media_downloads.json`. Conversão e combinação de streams podem exigir FFmpeg.
|
|
91
91
|
|
|
92
|
+
## Catálogo comum do Backlog e da Automação
|
|
93
|
+
|
|
94
|
+
**Backlog Vídeos** e **Automação Youtube > Vídeos cadastrados** usam o mesmo catálogo completo persistido em `storage/state/tasks.json`. Assim, todas as tarefas criadas manualmente ou pela automação diária aparecem nos dois fluxos, sem duplicação nem filtros implícitos por origem. O filtro de estado do Backlog mantém os estados conhecidos e acrescenta estados novos encontrados no storage.
|
|
95
|
+
|
|
96
|
+
Os dois cards mostram de forma consistente o estado técnico, o rótulo legível, a barra de progresso, a etapa e o formato do vídeo. São suportados estados como `to_do`, `doing`, `blocked`, `done`, `failed` e `cancelled`; formatos ausentes usam `format`, `style_wide` ou `wide` como fallback.
|
|
97
|
+
|
|
92
98
|
## Canais Youtube — edição por cartão e vídeos recentes
|
|
93
99
|
|
|
94
100
|
A página **Canais Youtube** mantém o cadastro e a importação existentes, mas cada cartão agora tem o botão **Editar**. O editor permite alterar nome, URL, handle, idioma, estilo wide, **Nicho**, **Blueprint Padrão**, **Narrador/Voz Padrão**, conta Google do Upload directo, descrição e Automação ON/horário. O nicho aparece imediatamente abaixo do nome do canal no cartão; quando não existe, a UI mostra **SEM NICHO CONFIGURADO**.
|
package/app/main.py
CHANGED
|
@@ -3293,16 +3293,60 @@ def _render_pipeline_progress_panel() -> None:
|
|
|
3293
3293
|
_render_pipeline_progress_live()
|
|
3294
3294
|
|
|
3295
3295
|
|
|
3296
|
+
VIDEO_TASK_STATE_LABELS = {
|
|
3297
|
+
"to_do": "Pendente",
|
|
3298
|
+
"doing": "Em execução",
|
|
3299
|
+
"blocked": "Bloqueado",
|
|
3300
|
+
"done": "Concluído",
|
|
3301
|
+
"failed": "Falha",
|
|
3302
|
+
"cancelled": "Cancelado",
|
|
3303
|
+
}
|
|
3304
|
+
|
|
3305
|
+
|
|
3306
|
+
def load_video_tasks_for_catalog() -> list[dict[str, Any]]:
|
|
3307
|
+
"""Return the complete persisted task catalog shared by Backlog and Automation."""
|
|
3308
|
+
saved = read_json("tasks.json", [])
|
|
3309
|
+
if not isinstance(saved, list):
|
|
3310
|
+
return []
|
|
3311
|
+
return [task for task in saved if isinstance(task, dict) and str(task.get("id") or "").strip()]
|
|
3312
|
+
|
|
3313
|
+
|
|
3314
|
+
def _video_task_format(task: dict[str, Any]) -> str:
|
|
3315
|
+
value = task.get("format") or task.get("style_wide") or task.get("style") or "wide"
|
|
3316
|
+
return str(value).strip() or "wide"
|
|
3317
|
+
|
|
3318
|
+
|
|
3319
|
+
def _video_task_progress(task: dict[str, Any]) -> int:
|
|
3320
|
+
try:
|
|
3321
|
+
return max(0, min(100, int(task.get("progress") or 0)))
|
|
3322
|
+
except (TypeError, ValueError):
|
|
3323
|
+
return 0
|
|
3324
|
+
|
|
3325
|
+
|
|
3326
|
+
def _render_video_task_state(task: dict[str, Any]) -> None:
|
|
3327
|
+
"""Show the raw state, readable label and progress consistently in both views."""
|
|
3328
|
+
state = str(task.get("state") or "unknown").strip().lower()
|
|
3329
|
+
progress = _video_task_progress(task)
|
|
3330
|
+
st.caption("Estado")
|
|
3331
|
+
st.write(state or "—")
|
|
3332
|
+
st.caption(VIDEO_TASK_STATE_LABELS.get(state, state.replace("_", " ").capitalize() or "Desconhecido"))
|
|
3333
|
+
st.progress(progress, text=f"{progress}%")
|
|
3334
|
+
if task.get("error"):
|
|
3335
|
+
st.caption(str(task.get("error"))[:240])
|
|
3336
|
+
|
|
3337
|
+
|
|
3296
3338
|
def render_videos():
|
|
3297
3339
|
st.subheader("Backlog Videos")
|
|
3298
3340
|
st.caption("Acompanhamento dos vídeos criados, estados da pipeline e controlos de execução.")
|
|
3299
3341
|
st.caption(f"Os vídeos são guardados em `{STORAGE / 'videos'}`.")
|
|
3300
3342
|
_render_pipeline_progress_panel()
|
|
3301
|
-
tasks =
|
|
3343
|
+
tasks = load_video_tasks_for_catalog()
|
|
3302
3344
|
if not tasks:
|
|
3303
3345
|
st.info("Nenhum vídeo criado.")
|
|
3304
3346
|
return
|
|
3305
|
-
|
|
3347
|
+
known_states = ["to_do", "doing", "blocked", "done", "failed", "cancelled"]
|
|
3348
|
+
extra_states = sorted({str(task.get("state") or "unknown") for task in tasks if str(task.get("state") or "unknown") not in known_states})
|
|
3349
|
+
state_filter = st.selectbox("Filtrar por estado", ["Todos", *known_states, *extra_states], key="videos_state_filter")
|
|
3306
3350
|
for task in tasks:
|
|
3307
3351
|
if state_filter != "Todos" and task.get("state") != state_filter:
|
|
3308
3352
|
continue
|
|
@@ -3319,16 +3363,13 @@ def render_videos():
|
|
|
3319
3363
|
status = task.get('thumbnail_status', 'not_generated')
|
|
3320
3364
|
prompt_note = ' · prompt pronto' if task.get('thumbnail_prompt') else ''
|
|
3321
3365
|
st.caption(f"Thumbnail: {status}{prompt_note}")
|
|
3322
|
-
with cols[1]:
|
|
3366
|
+
with cols[1]:
|
|
3367
|
+
st.caption("Formato")
|
|
3368
|
+
st.write(_video_task_format(task))
|
|
3323
3369
|
with cols[2]:
|
|
3324
3370
|
st.write(_pipeline_stage_label(task))
|
|
3325
|
-
if str(task.get("state") or "") in {"to_do", "doing", "blocked"}:
|
|
3326
|
-
progress = _pipeline_progress_value(task)
|
|
3327
|
-
st.progress(progress, text=f"{progress}%")
|
|
3328
3371
|
with cols[3]:
|
|
3329
|
-
|
|
3330
|
-
if task.get("error"):
|
|
3331
|
-
st.caption(str(task.get("error"))[:240])
|
|
3372
|
+
_render_video_task_state(task)
|
|
3332
3373
|
|
|
3333
3374
|
with cols[4]:
|
|
3334
3375
|
state = str(task.get("state") or "")
|
|
@@ -3615,21 +3656,20 @@ def render_automation():
|
|
|
3615
3656
|
|
|
3616
3657
|
st.divider()
|
|
3617
3658
|
st.subheader("Vídeos cadastrados")
|
|
3618
|
-
tasks =
|
|
3659
|
+
tasks = load_video_tasks_for_catalog()
|
|
3619
3660
|
if not tasks:
|
|
3620
3661
|
st.info("Ainda não existem vídeos cadastrados.")
|
|
3621
3662
|
for task in tasks:
|
|
3622
3663
|
with st.container(border=True):
|
|
3623
|
-
task_cols = st.columns([2.
|
|
3664
|
+
task_cols = st.columns([2.25, 1.65, 1.15, 1.85])
|
|
3624
3665
|
with task_cols[0]:
|
|
3625
3666
|
st.write(f"**{task.get('topic', 'Sem tópico')}**")
|
|
3626
3667
|
st.caption(f"{task.get('channel_name', 'Canal')} · {task.get('id', '')}")
|
|
3627
3668
|
with task_cols[1]:
|
|
3628
|
-
|
|
3629
|
-
st.write(task.get("state", "—"))
|
|
3669
|
+
_render_video_task_state(task)
|
|
3630
3670
|
with task_cols[2]:
|
|
3631
|
-
st.caption("
|
|
3632
|
-
st.write(task
|
|
3671
|
+
st.caption("Formato")
|
|
3672
|
+
st.write(_video_task_format(task))
|
|
3633
3673
|
with task_cols[3]:
|
|
3634
3674
|
state = str(task.get("state") or "")
|
|
3635
3675
|
start_col, stop_col = st.columns(2)
|
package/package.json
CHANGED