@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 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
- if st.button("Buscar no Tiktok", type="primary", use_container_width=True, key="tiktok_channel_lookup"):
1867
- result = fetch_public_tiktok_profile(source)
1868
- st.session_state["tiktok_channel_import"] = {"ok": result.ok, "message": result.message, "data": result.data}
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("Fila exclusiva de Shorts TikTok em Portrait 9:16, usando Prompt Master em vez de Blueprint.")
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.write(f"**{channel.get('name', 'Sem nome')}** · {channel.get('handle', '')}")
4552
- enabled = st.toggle("Automação ligada", value=bool(channel.get("automation_on", False)), key=f"tiktok_automation_on_{channel_id}")
4553
- schedule_time = st.text_input("Horário diário (HH:MM)", value=channel.get("automation_time", "00:00"), key=f"tiktok_automation_time_{channel_id}")
4554
- 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}")
4555
- st.caption("Formato da fila: Portrait 9:16 · Pool de vídeo: TikTok")
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
- 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"})
4561
- st.success("Automação TikTok guardada.")
4562
- st.rerun()
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():
@@ -39,6 +39,7 @@ def create_channel(name: str, url: str = "", metadata: dict[str, Any] | None = N
39
39
  "niche": "",
40
40
  "reference_channels": [],
41
41
  "thumbnail_url": "",
42
+ "avatar_url": "",
42
43
  "subscriber_count": None,
43
44
  "video_count": None,
44
45
  "view_count": None,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danhachuel/thunderbolt",
3
- "version": "0.4.43",
3
+ "version": "0.4.45",
4
4
  "description": "Thunderbolt — interface local para operação de canais faceless e motor MoneyPrinterTurbo",
5
5
  "license": "MIT",
6
6
  "main": "scripts/cli.mjs",