@danhachuel/thunderbolt 0.4.43 → 0.4.45
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 +97 -16
- package/hermes_ui/domain.py +1 -0
- package/package.json +1 -1
package/app/main.py
CHANGED
|
@@ -1844,6 +1844,15 @@ def render_tiktok_prompt_masters():
|
|
|
1844
1844
|
st.error(str(exc))
|
|
1845
1845
|
|
|
1846
1846
|
|
|
1847
|
+
def format_metric_number(value: Any) -> str:
|
|
1848
|
+
if value in (None, ""):
|
|
1849
|
+
return "—"
|
|
1850
|
+
try:
|
|
1851
|
+
return f"{int(value):,}".replace(",", ".")
|
|
1852
|
+
except (TypeError, ValueError):
|
|
1853
|
+
return str(value)
|
|
1854
|
+
|
|
1855
|
+
|
|
1847
1856
|
def _tiktok_channel_records() -> list[dict[str, Any]]:
|
|
1848
1857
|
return [channel for channel in read_json("channels.json", []) if isinstance(channel, dict) and channel.get("platform") == "tiktok"]
|
|
1849
1858
|
|
|
@@ -1862,15 +1871,56 @@ def render_tiktok_channels():
|
|
|
1862
1871
|
import_tab, spreadsheet_tab, manual_tab = render_localized_tabs(["Importar do Tiktok", "Canais em lote Planilha", "Cadastro manual"])
|
|
1863
1872
|
prompt_ids, prompt_labels = _tiktok_prompt_options()
|
|
1864
1873
|
with import_tab:
|
|
1874
|
+
st.caption("A pesquisa consulta exclusivamente a página pública do TikTok. Não existe método alternativo nem utilização de API.")
|
|
1865
1875
|
source = st.text_input("URL pública ou @handle", placeholder="https://www.tiktok.com/@conta", key="tiktok_channel_source")
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
st.
|
|
1876
|
+
lookup_cols = st.columns([1, 1])
|
|
1877
|
+
with lookup_cols[0]:
|
|
1878
|
+
if st.button("Buscar no Tiktok", type="primary", use_container_width=True, key="tiktok_channel_lookup"):
|
|
1879
|
+
result = fetch_public_tiktok_profile(source)
|
|
1880
|
+
st.session_state["tiktok_channel_import"] = {"ok": result.ok, "message": result.message, "data": result.data}
|
|
1881
|
+
with lookup_cols[1]:
|
|
1882
|
+
if st.button("Limpar importação", use_container_width=True, key="tiktok_channel_clear"):
|
|
1883
|
+
for key in ("tiktok_channel_import", "tiktok_import_language", "tiktok_import_niche", "tiktok_import_prompt", "tiktok_import_description"):
|
|
1884
|
+
st.session_state.pop(key, None)
|
|
1885
|
+
st.rerun()
|
|
1869
1886
|
imported_state = st.session_state.get("tiktok_channel_import", {})
|
|
1870
1887
|
if imported_state.get("message"):
|
|
1871
1888
|
(st.success if imported_state.get("ok") else st.warning)(imported_state["message"])
|
|
1872
1889
|
imported = imported_state.get("data") if imported_state.get("ok") else {}
|
|
1873
1890
|
if imported:
|
|
1891
|
+
st.caption("Dados encontrados na página pública. Reveja os cartões e edite os campos antes de guardar.")
|
|
1892
|
+
with st.container(border=True):
|
|
1893
|
+
profile_cols = st.columns([0.8, 2.2, 1, 1, 1])
|
|
1894
|
+
with profile_cols[0]:
|
|
1895
|
+
avatar = imported.get("avatar_url") or imported.get("avatar")
|
|
1896
|
+
if avatar:
|
|
1897
|
+
st.image(avatar, width=82)
|
|
1898
|
+
else:
|
|
1899
|
+
st.markdown("### TT")
|
|
1900
|
+
with profile_cols[1]:
|
|
1901
|
+
st.markdown(f"### {imported.get('name') or imported.get('username') or 'Perfil TikTok'}")
|
|
1902
|
+
st.write(imported.get("handle") or f"@{imported.get('username', '')}")
|
|
1903
|
+
st.caption(imported.get("public_url") or imported.get("url") or "Página pública")
|
|
1904
|
+
with profile_cols[2]:
|
|
1905
|
+
st.metric("Seguidores", format_metric_number(imported.get("subscriber_count")))
|
|
1906
|
+
with profile_cols[3]:
|
|
1907
|
+
st.metric("Curtidas", format_metric_number(imported.get("likes_count")))
|
|
1908
|
+
with profile_cols[4]:
|
|
1909
|
+
st.metric("Vídeos", format_metric_number(imported.get("video_count")))
|
|
1910
|
+
if imported.get("bio"):
|
|
1911
|
+
st.caption(f"Descrição pública: {imported['bio']}")
|
|
1912
|
+
with st.container(border=True):
|
|
1913
|
+
st.markdown("**Dados do canal a cadastrar**")
|
|
1914
|
+
data_cols = st.columns(3)
|
|
1915
|
+
with data_cols[0]:
|
|
1916
|
+
st.write(f"**Nome:** {imported.get('name') or imported.get('username') or '—'}")
|
|
1917
|
+
st.write(f"**Handle:** {imported.get('handle') or '—'}")
|
|
1918
|
+
with data_cols[1]:
|
|
1919
|
+
st.write(f"**URL:** {imported.get('public_url') or imported.get('url') or '—'}")
|
|
1920
|
+
st.write(f"**Origem:** {imported.get('metrics_source') or 'tiktok_public_page'}")
|
|
1921
|
+
with data_cols[2]:
|
|
1922
|
+
st.write("**Formato:** Portrait 9:16")
|
|
1923
|
+
st.write("**Consulta:** Página pública")
|
|
1874
1924
|
with st.form("tiktok_channel_import_form"):
|
|
1875
1925
|
name = st.text_input("Nome canal", value=str(imported.get("name") or imported.get("username") or ""))
|
|
1876
1926
|
url = st.text_input("URL canal", value=str(imported.get("public_url") or imported.get("url") or ""))
|
|
@@ -1881,7 +1931,7 @@ def render_tiktok_channels():
|
|
|
1881
1931
|
description = st.text_area("Descrição", value=str(imported.get("bio") or ""), key="tiktok_import_description")
|
|
1882
1932
|
if st.form_submit_button("Cadastrar canal TikTok", type="primary"):
|
|
1883
1933
|
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, "style_wide": "portrait", "video_aspect_ratio": "Portrait 9:16", "metrics_source": "tiktok_public_page", "subscriber_count": imported.get("subscriber_count"), "video_count": imported.get("video_count")})
|
|
1884
|
-
update_channel(channel["id"], {"platform": "tiktok", "default_prompt_master": prompt, "prompt_master": prompt})
|
|
1934
|
+
update_channel(channel["id"], {"platform": "tiktok", "default_prompt_master": prompt, "prompt_master": prompt, "avatar_url": imported.get("avatar_url") or imported.get("avatar") or "", "thumbnail_url": imported.get("avatar_url") or imported.get("avatar") or ""})
|
|
1885
1935
|
st.session_state.pop("tiktok_channel_import", None)
|
|
1886
1936
|
st.success(f"Canal {channel['name']} cadastrado.")
|
|
1887
1937
|
st.rerun()
|
|
@@ -4539,7 +4589,13 @@ def render_thumbnails():
|
|
|
4539
4589
|
|
|
4540
4590
|
def render_tiktok_automation():
|
|
4541
4591
|
st.title("Automação Tiktok")
|
|
4542
|
-
st.caption("
|
|
4592
|
+
st.caption("Agendamento diário da geração por canal. A fila TikTok usa Prompt Master e mantém exclusivamente o formato Portrait 9:16.")
|
|
4593
|
+
worker_status = load_worker_status()
|
|
4594
|
+
local_now = datetime.now().astimezone()
|
|
4595
|
+
if worker_status.get("alive"):
|
|
4596
|
+
st.success(f"Worker activo · relógio local: {local_now.strftime('%d/%m/%Y %H:%M:%S %Z')}")
|
|
4597
|
+
else:
|
|
4598
|
+
st.warning("Worker de automação não está activo. Inicie o Thunderbolt pelo launcher (`npx.cmd --yes @danhachuel/thunderbolt`) para activar as verificações horárias.")
|
|
4543
4599
|
channels = _tiktok_channel_records()
|
|
4544
4600
|
prompt_ids, prompt_labels = _tiktok_prompt_options()
|
|
4545
4601
|
if not channels:
|
|
@@ -4548,18 +4604,43 @@ def render_tiktok_automation():
|
|
|
4548
4604
|
for channel in channels:
|
|
4549
4605
|
channel_id = str(channel["id"])
|
|
4550
4606
|
with st.container(border=True):
|
|
4551
|
-
st.
|
|
4552
|
-
|
|
4553
|
-
|
|
4554
|
-
|
|
4555
|
-
|
|
4556
|
-
if st.button("Guardar automação TikTok", key=f"tiktok_automation_save_{channel_id}", type="primary"):
|
|
4557
|
-
if not valid_hhmm(schedule_time):
|
|
4558
|
-
st.error("Use o formato HH:MM, por exemplo 08:30.")
|
|
4607
|
+
header_cols = st.columns([0.55, 2.35, 1.35, 1.5, 1.35])
|
|
4608
|
+
with header_cols[0]:
|
|
4609
|
+
profile_image = channel.get("avatar_url") or channel.get("thumbnail_url")
|
|
4610
|
+
if profile_image:
|
|
4611
|
+
st.image(profile_image, width=48)
|
|
4559
4612
|
else:
|
|
4560
|
-
|
|
4561
|
-
|
|
4562
|
-
|
|
4613
|
+
st.markdown("### TT")
|
|
4614
|
+
with header_cols[1]:
|
|
4615
|
+
st.write(f"**{channel.get('name', 'Sem nome')}**")
|
|
4616
|
+
st.caption(channel.get("handle") or channel.get("url") or "sem URL")
|
|
4617
|
+
with header_cols[2]:
|
|
4618
|
+
enabled = st.toggle("Automação ligada", value=bool(channel.get("automation_on", False)), key=f"tiktok_automation_on_{channel_id}")
|
|
4619
|
+
with header_cols[3]:
|
|
4620
|
+
schedule_time = st.text_input("Horário (HH:MM)", value=channel.get("automation_time", "00:00"), key=f"tiktok_automation_time_{channel_id}")
|
|
4621
|
+
default_cols = st.columns([1.0, 1.0, 1.35, 1.45, 1.55, 1.0], gap="small")
|
|
4622
|
+
with default_cols[0]:
|
|
4623
|
+
st.markdown("**Idioma Padrão**")
|
|
4624
|
+
st.caption(language_label(channel.get("language") or "pt"))
|
|
4625
|
+
with default_cols[1]:
|
|
4626
|
+
st.markdown("**Nicho Padrão**")
|
|
4627
|
+
st.caption(channel_niche_label(channel))
|
|
4628
|
+
with default_cols[2]:
|
|
4629
|
+
st.markdown("**Formato da fila**")
|
|
4630
|
+
st.caption("Portrait 9:16")
|
|
4631
|
+
with default_cols[3]:
|
|
4632
|
+
prompt = st.selectbox("Prompt Master Padrão", prompt_ids, index=prompt_ids.index(channel.get("default_prompt_master", "")) if channel.get("default_prompt_master", "") in prompt_ids else 0, format_func=lambda item: prompt_labels.get(item, item), key=f"tiktok_automation_prompt_{channel_id}")
|
|
4633
|
+
with default_cols[4]:
|
|
4634
|
+
st.markdown("**Pool de vídeos**")
|
|
4635
|
+
st.caption("TikTok / Shorts")
|
|
4636
|
+
with default_cols[5]:
|
|
4637
|
+
if st.button("Guardar", key=f"tiktok_automation_save_{channel_id}", use_container_width=True, type="primary"):
|
|
4638
|
+
if not valid_hhmm(schedule_time):
|
|
4639
|
+
st.error("Use o formato HH:MM, por exemplo 08:30.")
|
|
4640
|
+
else:
|
|
4641
|
+
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"})
|
|
4642
|
+
st.success("Automação TikTok guardada.")
|
|
4643
|
+
st.rerun()
|
|
4563
4644
|
|
|
4564
4645
|
|
|
4565
4646
|
def render_automation():
|
package/hermes_ui/domain.py
CHANGED
package/package.json
CHANGED