@danhachuel/thunderbolt 0.4.84 → 0.4.86
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 +95 -12
- package/package.json +1 -1
package/app/main.py
CHANGED
|
@@ -1924,6 +1924,7 @@ def render_tiktok_channels():
|
|
|
1924
1924
|
channels = _tiktok_channel_records()
|
|
1925
1925
|
import_tab, spreadsheet_tab, manual_tab = render_localized_tabs(["Importar do Tiktok", "Canais em lote Planilha", "Cadastro manual"])
|
|
1926
1926
|
prompt_ids, prompt_labels = _tiktok_prompt_options()
|
|
1927
|
+
_, _, _, voice_options, _ = channel_default_options({})
|
|
1927
1928
|
with import_tab:
|
|
1928
1929
|
st.caption("A pesquisa consulta exclusivamente a página pública do TikTok. Não existe método alternativo nem utilização de API.")
|
|
1929
1930
|
st.divider()
|
|
@@ -1984,9 +1985,15 @@ def render_tiktok_channels():
|
|
|
1984
1985
|
language = st.selectbox("Idioma", list(LANGUAGE_CODES), format_func=language_label, key="tiktok_import_language")
|
|
1985
1986
|
niche = st.text_input("Nicho", key="tiktok_import_niche")
|
|
1986
1987
|
prompt = st.selectbox("Prompt Master padrão", prompt_ids, format_func=lambda item: prompt_labels.get(item, item), key="tiktok_import_prompt")
|
|
1988
|
+
voice = st.selectbox("Narrador/Voz Padrão", voice_options, format_func=lambda item: item or "Sem voz padrão", key="tiktok_import_voice")
|
|
1989
|
+
automation_on = st.toggle("Automação ON", value=False, key="tiktok_import_automation")
|
|
1990
|
+
automation_time = st.text_input("Horário diário (HH:MM)", value="00:00", key="tiktok_import_automation_time")
|
|
1987
1991
|
description = st.text_area("Descrição", value=str(imported.get("bio") or ""), key="tiktok_import_description")
|
|
1988
1992
|
if st.form_submit_button("Cadastrar canal TikTok", type="primary"):
|
|
1989
|
-
|
|
1993
|
+
if not valid_hhmm(automation_time):
|
|
1994
|
+
st.error("O horário diário deve estar no formato HH:MM.")
|
|
1995
|
+
st.stop()
|
|
1996
|
+
channel = create_channel(name.strip(), url.strip(), {"platform": "tiktok", "handle": handle.strip(), "language": language, "niche": niche.strip(), "description": description.strip(), "default_prompt_master": prompt, "prompt_master": prompt, "default_voice": voice, "voice": voice, "style_wide": "portrait", "video_aspect_ratio": "Portrait 9:16", "automation_on": automation_on, "automation_time": automation_time.strip(), "metrics_source": "tiktok_public_page", "subscriber_count": imported.get("subscriber_count"), "video_count": imported.get("video_count")})
|
|
1990
1997
|
avatar_url = _tiktok_avatar_url(imported)
|
|
1991
1998
|
update_channel(channel["id"], {"platform": "tiktok", "default_prompt_master": prompt, "prompt_master": prompt, "avatar_url": avatar_url, "thumbnail_url": avatar_url})
|
|
1992
1999
|
st.session_state.pop("tiktok_channel_import", None)
|
|
@@ -2009,6 +2016,7 @@ def render_tiktok_channels():
|
|
|
2009
2016
|
st.markdown(f"**{channel.get('name') or 'Perfil TikTok'}**")
|
|
2010
2017
|
st.caption(f"{channel.get('handle') or 'sem handle'} · {channel.get('url') or 'sem URL'}")
|
|
2011
2018
|
st.caption(channel.get("description") or "Perfil carregado da página pública do TikTok.")
|
|
2019
|
+
st.caption(f"Nicho: {channel.get('niche') or 'não configurado'}")
|
|
2012
2020
|
with card_cols[2]:
|
|
2013
2021
|
st.metric("Seguidores", format_metric_number(channel.get("subscriber_count")))
|
|
2014
2022
|
with card_cols[3]:
|
|
@@ -2027,11 +2035,20 @@ def render_tiktok_channels():
|
|
|
2027
2035
|
if st.button("Apagar card", key=f"tiktok_import_card_delete_{channel_id}"):
|
|
2028
2036
|
st.session_state[f"tiktok_delete_{channel_id}"] = True
|
|
2029
2037
|
st.rerun()
|
|
2030
|
-
st.caption("
|
|
2038
|
+
st.caption("Estilo wide: Portrait 9:16")
|
|
2039
|
+
st.caption(f"Automação: {'ligada' if automation_on else 'desligada'} · {automation_time}")
|
|
2040
|
+
_, _, _, card_voice_options, card_current_voice = channel_default_options(channel)
|
|
2041
|
+
selected_voice = st.selectbox("Narrador/Voz Padrão", card_voice_options, index=card_voice_options.index(card_current_voice) if card_current_voice in card_voice_options else 0, format_func=lambda item: item or "Sem voz padrão", key=f"tiktok_import_card_voice_{channel_id}")
|
|
2042
|
+
automation_on = st.toggle("Automação ON", value=bool(channel.get("automation_on", False)), key=f"tiktok_import_card_automation_{channel_id}")
|
|
2043
|
+
automation_time = st.text_input("Horário diário (HH:MM)", value=str(channel.get("automation_time") or "00:00"), key=f"tiktok_import_card_time_{channel_id}")
|
|
2031
2044
|
if st.button("Guardar", key=f"tiktok_import_card_save_{channel_id}", use_container_width=True, type="primary"):
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2045
|
+
if not valid_hhmm(automation_time):
|
|
2046
|
+
st.error("O horário diário deve estar no formato HH:MM.")
|
|
2047
|
+
else:
|
|
2048
|
+
update_channel(channel_id, {"platform": "tiktok", "default_prompt_master": selected_prompt, "prompt_master": selected_prompt, "default_voice": selected_voice, "voice": selected_voice, "video_aspect_ratio": "Portrait 9:16", "style_wide": "portrait", "automation_on": automation_on, "automation_time": automation_time.strip()})
|
|
2049
|
+
st.success("Dados do canal TikTok guardados.")
|
|
2050
|
+
st.rerun()
|
|
2051
|
+
st.caption("Estilo wide: Portrait 9:16")
|
|
2035
2052
|
niche_action = st.columns([1.0, 4.0])
|
|
2036
2053
|
with niche_action[0]:
|
|
2037
2054
|
if st.button("Editar nicho", key=f"tiktok_import_card_niche_{channel_id}"):
|
|
@@ -2062,11 +2079,18 @@ def render_tiktok_channels():
|
|
|
2062
2079
|
edit_url = st.text_input("URL pública", value=str(channel.get("url") or ""))
|
|
2063
2080
|
edit_handle = st.text_input("Handle", value=str(channel.get("handle") or ""))
|
|
2064
2081
|
edit_language = st.selectbox("Idioma do canal", list(LANGUAGE_CODES), index=list(LANGUAGE_CODES).index(language_code(channel.get("language") or "pt")), format_func=language_label)
|
|
2082
|
+
edit_voice = st.selectbox("Narrador/Voz Padrão", voice_options, index=voice_options.index(str(channel.get("default_voice") or channel.get("voice") or "")) if str(channel.get("default_voice") or channel.get("voice") or "") in voice_options else 0, format_func=lambda item: item or "Sem voz padrão")
|
|
2083
|
+
edit_automation = st.toggle("Automação ON", value=bool(channel.get("automation_on", False)), key=f"tiktok_edit_automation_{channel_id}")
|
|
2084
|
+
edit_time = st.text_input("Horário diário (HH:MM)", value=str(channel.get("automation_time") or "00:00"))
|
|
2085
|
+
st.caption("Estilo wide: Portrait 9:16")
|
|
2065
2086
|
edit_description = st.text_area("Descrição", value=str(channel.get("description") or ""))
|
|
2066
2087
|
if st.form_submit_button("Guardar edição", type="primary"):
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2088
|
+
if not valid_hhmm(edit_time):
|
|
2089
|
+
st.error("O horário diário deve estar no formato HH:MM.")
|
|
2090
|
+
else:
|
|
2091
|
+
update_channel(channel_id, {"name": edit_name.strip(), "url": edit_url.strip(), "handle": edit_handle.strip(), "language": edit_language, "default_voice": edit_voice, "voice": edit_voice, "style_wide": "portrait", "video_aspect_ratio": "Portrait 9:16", "automation_on": edit_automation, "automation_time": edit_time.strip(), "description": edit_description.strip(), "platform": "tiktok"})
|
|
2092
|
+
st.session_state.pop(f"tiktok_edit_{channel_id}", None)
|
|
2093
|
+
st.rerun()
|
|
2070
2094
|
with st.expander("Últimos 10 vídeos publicados", expanded=False):
|
|
2071
2095
|
recent_videos = channel_videos_for(channel, limit=10)
|
|
2072
2096
|
if not recent_videos:
|
|
@@ -2096,7 +2120,7 @@ def render_tiktok_channels():
|
|
|
2096
2120
|
candidate = {"name": row.get("name", ""), "handle": row.get("handle", ""), "url": row.get("url", "")}
|
|
2097
2121
|
if find_duplicate_channel(candidate, channels):
|
|
2098
2122
|
continue
|
|
2099
|
-
created_channel = create_channel(str(row.get("name") or row.get("handle") or "TikTok"), str(row.get("url") or ""), {"platform": "tiktok", "handle": row.get("handle", ""), "language": language_code(row.get("language") or "pt"), "niche": row.get("niche", ""), "description": row.get("description", ""), "default_prompt_master": row.get("prompt_master", ""), "prompt_master": row.get("prompt_master", ""), "style_wide": "portrait", "video_aspect_ratio": "Portrait 9:16", "automation_time": row.get("automation_time") or "00:00"})
|
|
2123
|
+
created_channel = create_channel(str(row.get("name") or row.get("handle") or "TikTok"), str(row.get("url") or ""), {"platform": "tiktok", "handle": row.get("handle", ""), "language": language_code(row.get("language") or "pt"), "niche": row.get("niche", ""), "description": row.get("description", ""), "default_prompt_master": row.get("prompt_master", ""), "prompt_master": row.get("prompt_master", ""), "default_voice": row.get("voice", ""), "voice": row.get("voice", ""), "style_wide": "portrait", "video_aspect_ratio": "Portrait 9:16", "automation_on": bool(row.get("automation_on", False)), "automation_time": row.get("automation_time") or "00:00"})
|
|
2100
2124
|
update_channel(created_channel["id"], {"platform": "tiktok"})
|
|
2101
2125
|
created += 1
|
|
2102
2126
|
st.success(f"{created} canal(is) TikTok cadastrado(s).")
|
|
@@ -2106,7 +2130,7 @@ def render_tiktok_channels():
|
|
|
2106
2130
|
st.subheader(f"Canais TikTok cadastrados ({len(channels)})")
|
|
2107
2131
|
if channels:
|
|
2108
2132
|
st.dataframe(
|
|
2109
|
-
[{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", "Proporção": "video_aspect_ratio", "Activo": "active", "Descrição": "description"}.items()} for c in channels],
|
|
2133
|
+
[{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],
|
|
2110
2134
|
use_container_width=True,
|
|
2111
2135
|
hide_index=True,
|
|
2112
2136
|
height=360,
|
|
@@ -2121,10 +2145,17 @@ def render_tiktok_channels():
|
|
|
2121
2145
|
language = st.selectbox("Idioma", list(LANGUAGE_CODES), format_func=language_label, key="tiktok_manual_channel_language")
|
|
2122
2146
|
niche = st.text_input("Nicho", key="tiktok_manual_channel_niche")
|
|
2123
2147
|
prompt = st.selectbox("Prompt Master padrão", prompt_ids, format_func=lambda item: prompt_labels.get(item, item), key="tiktok_manual_channel_prompt")
|
|
2148
|
+
voice = st.selectbox("Narrador/Voz Padrão", voice_options, format_func=lambda item: item or "Sem voz padrão", key="tiktok_manual_channel_voice")
|
|
2149
|
+
automation_on = st.toggle("Automação ON", value=False, key="tiktok_manual_channel_automation")
|
|
2150
|
+
automation_time = st.text_input("Horário diário (HH:MM)", value="00:00", key="tiktok_manual_channel_automation_time")
|
|
2151
|
+
st.caption("Estilo wide: Portrait 9:16")
|
|
2124
2152
|
description = st.text_area("Descrição", key="tiktok_manual_channel_description")
|
|
2125
2153
|
if st.form_submit_button("Guardar canal manual", type="primary"):
|
|
2126
2154
|
reference = normalize_tiktok_reference(url or handle)
|
|
2127
|
-
|
|
2155
|
+
if not valid_hhmm(automation_time):
|
|
2156
|
+
st.error("O horário diário deve estar no formato HH:MM.")
|
|
2157
|
+
st.stop()
|
|
2158
|
+
channel = create_channel(name.strip() or reference["username"], reference["url"], {"platform": "tiktok", "handle": reference["handle"], "language": language, "niche": niche.strip(), "description": description.strip(), "default_prompt_master": prompt, "prompt_master": prompt, "default_voice": voice, "voice": voice, "style_wide": "portrait", "video_aspect_ratio": "Portrait 9:16", "automation_on": automation_on, "automation_time": automation_time.strip()})
|
|
2128
2159
|
update_channel(channel["id"], {"platform": "tiktok"})
|
|
2129
2160
|
st.success("Canal TikTok cadastrado.")
|
|
2130
2161
|
st.rerun()
|
|
@@ -4435,6 +4466,28 @@ def load_video_tasks_for_catalog() -> list[dict[str, Any]]:
|
|
|
4435
4466
|
return [task for task in saved if isinstance(task, dict) and str(task.get("id") or "").strip()]
|
|
4436
4467
|
|
|
4437
4468
|
|
|
4469
|
+
def task_platform(task: dict[str, Any]) -> str:
|
|
4470
|
+
"""Resolve a plataforma da tarefa, incluindo tarefas legadas sem platform."""
|
|
4471
|
+
explicit = str(task.get("platform") or "").strip().casefold()
|
|
4472
|
+
if explicit in {"youtube", "yt"}:
|
|
4473
|
+
return "youtube"
|
|
4474
|
+
if explicit in {"tiktok", "tik-tok", "tt"}:
|
|
4475
|
+
return "tiktok"
|
|
4476
|
+
channel_id = str(task.get("channel_id") or "")
|
|
4477
|
+
if channel_id:
|
|
4478
|
+
channel = next((item for item in read_json("channels.json", []) if isinstance(item, dict) and str(item.get("id")) == channel_id), None)
|
|
4479
|
+
if channel:
|
|
4480
|
+
return classify_channel_platform(channel)
|
|
4481
|
+
format_value = str(task.get("format") or "").strip().casefold()
|
|
4482
|
+
return "tiktok" if format_value in {"portrait", "portrait 9:16", "9:16"} else "youtube"
|
|
4483
|
+
|
|
4484
|
+
|
|
4485
|
+
def load_automation_tasks_for_platform(platform_name: str) -> list[dict[str, Any]]:
|
|
4486
|
+
"""Return only automation/catalog tasks belonging to one publishing platform."""
|
|
4487
|
+
target = str(platform_name or "").strip().casefold()
|
|
4488
|
+
return [task for task in load_video_tasks_for_catalog() if task_platform(task) == target]
|
|
4489
|
+
|
|
4490
|
+
|
|
4438
4491
|
def _task_thumbnail_path(task: dict[str, Any]) -> Path | None:
|
|
4439
4492
|
"""Resolve a generated thumbnail from current and legacy task fields."""
|
|
4440
4493
|
artifacts = task.get("artifacts") if isinstance(task.get("artifacts"), dict) else {}
|
|
@@ -4937,6 +4990,36 @@ def render_tiktok_automation():
|
|
|
4937
4990
|
update_channel(channel_id, {"automation_on": bool(enabled), "automation_time": schedule_time.strip(), "default_prompt_master": prompt, "prompt_master": prompt, "platform": "tiktok", "video_aspect_ratio": "Portrait 9:16", "style_wide": "portrait", "avatar_url": avatar_url, "thumbnail_url": avatar_url})
|
|
4938
4991
|
st.success("Automação TikTok guardada.")
|
|
4939
4992
|
st.rerun()
|
|
4993
|
+
st.divider()
|
|
4994
|
+
st.subheader("Vídeos cadastrados TikTok")
|
|
4995
|
+
st.caption("Esta fila mostra exclusivamente tarefas associadas a canais TikTok.")
|
|
4996
|
+
tiktok_tasks = load_automation_tasks_for_platform("tiktok")
|
|
4997
|
+
if not tiktok_tasks:
|
|
4998
|
+
st.info("Ainda não existem vídeos TikTok cadastrados.")
|
|
4999
|
+
for task in tiktok_tasks:
|
|
5000
|
+
task_id = str(task["id"])
|
|
5001
|
+
with st.container(border=True):
|
|
5002
|
+
cols = st.columns([2.4, 1.4, 1.1, 1.6])
|
|
5003
|
+
with cols[0]:
|
|
5004
|
+
thumbnail_path = _task_thumbnail_path(task)
|
|
5005
|
+
if thumbnail_path:
|
|
5006
|
+
st.image(str(thumbnail_path), width=150, caption="Thumbnail")
|
|
5007
|
+
st.write(f"**{task.get('title') or task.get('topic') or 'Vídeo TikTok'}**")
|
|
5008
|
+
st.caption(f"{task.get('channel_name') or 'Canal TikTok'} · {task_id}")
|
|
5009
|
+
with cols[1]:
|
|
5010
|
+
_render_video_task_state(task)
|
|
5011
|
+
with cols[2]:
|
|
5012
|
+
st.caption("Plataforma")
|
|
5013
|
+
st.write("TikTok")
|
|
5014
|
+
st.caption("Portrait 9:16")
|
|
5015
|
+
with cols[3]:
|
|
5016
|
+
state = str(task.get("state") or "")
|
|
5017
|
+
if st.button("Start", key=f"tiktok_automation_start_{task_id}", disabled=state not in {"to_do", "blocked", "failed"}, use_container_width=True):
|
|
5018
|
+
retry_task_with_current_settings(task_id) if state in {"blocked", "failed"} else transition_task(task_id, "doing")
|
|
5019
|
+
st.rerun()
|
|
5020
|
+
if st.button("Stop", key=f"tiktok_automation_stop_{task_id}", disabled=state != "doing", use_container_width=True):
|
|
5021
|
+
stop_task_by_user(task_id)
|
|
5022
|
+
st.rerun()
|
|
4940
5023
|
|
|
4941
5024
|
|
|
4942
5025
|
def render_automation():
|
|
@@ -5018,7 +5101,7 @@ def render_automation():
|
|
|
5018
5101
|
st.divider()
|
|
5019
5102
|
st.subheader("Vídeos cadastrados")
|
|
5020
5103
|
st.caption("Start retoma as etapas já concluídas e só gera novamente o que ainda não estiver pronto. Em tarefas falhadas ou bloqueadas, a nova tentativa lê as chaves, prioridades e configurações actualmente guardadas. Apagar remove o card da fila após confirmação e preserva os artefactos locais.")
|
|
5021
|
-
tasks =
|
|
5104
|
+
tasks = load_automation_tasks_for_platform("youtube")
|
|
5022
5105
|
if not tasks:
|
|
5023
5106
|
st.info("Ainda não existem vídeos cadastrados.")
|
|
5024
5107
|
for task in tasks:
|
package/package.json
CHANGED