@danhachuel/thunderbolt 0.4.83 → 0.4.85

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
@@ -1,4 +1,12 @@
1
- from __future__ import annotations
1
+ import os
2
+ import sys
3
+
4
+ os.environ["PYTHONIOENCODING"] = "utf-8"
5
+ os.environ["PYTHONUTF8"] = "1"
6
+ if hasattr(sys.stdout, "reconfigure"):
7
+ sys.stdout.reconfigure(encoding="utf-8", errors="replace")
8
+ if hasattr(sys.stderr, "reconfigure"):
9
+ sys.stderr.reconfigure(encoding="utf-8", errors="replace")
2
10
 
3
11
  import hashlib
4
12
  import json
@@ -7,7 +15,6 @@ import re
7
15
  import time
8
16
  from contextlib import nullcontext
9
17
  from datetime import date, datetime, timezone
10
- import sys
11
18
  import uuid
12
19
  from pathlib import Path
13
20
  from typing import Any
@@ -1917,6 +1924,7 @@ def render_tiktok_channels():
1917
1924
  channels = _tiktok_channel_records()
1918
1925
  import_tab, spreadsheet_tab, manual_tab = render_localized_tabs(["Importar do Tiktok", "Canais em lote Planilha", "Cadastro manual"])
1919
1926
  prompt_ids, prompt_labels = _tiktok_prompt_options()
1927
+ _, _, _, voice_options, _ = channel_default_options({})
1920
1928
  with import_tab:
1921
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.")
1922
1930
  st.divider()
@@ -1977,9 +1985,15 @@ def render_tiktok_channels():
1977
1985
  language = st.selectbox("Idioma", list(LANGUAGE_CODES), format_func=language_label, key="tiktok_import_language")
1978
1986
  niche = st.text_input("Nicho", key="tiktok_import_niche")
1979
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")
1980
1991
  description = st.text_area("Descrição", value=str(imported.get("bio") or ""), key="tiktok_import_description")
1981
1992
  if st.form_submit_button("Cadastrar canal TikTok", type="primary"):
1982
- 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")})
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")})
1983
1997
  avatar_url = _tiktok_avatar_url(imported)
1984
1998
  update_channel(channel["id"], {"platform": "tiktok", "default_prompt_master": prompt, "prompt_master": prompt, "avatar_url": avatar_url, "thumbnail_url": avatar_url})
1985
1999
  st.session_state.pop("tiktok_channel_import", None)
@@ -2002,6 +2016,7 @@ def render_tiktok_channels():
2002
2016
  st.markdown(f"**{channel.get('name') or 'Perfil TikTok'}**")
2003
2017
  st.caption(f"{channel.get('handle') or 'sem handle'} · {channel.get('url') or 'sem URL'}")
2004
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'}")
2005
2020
  with card_cols[2]:
2006
2021
  st.metric("Seguidores", format_metric_number(channel.get("subscriber_count")))
2007
2022
  with card_cols[3]:
@@ -2020,11 +2035,20 @@ def render_tiktok_channels():
2020
2035
  if st.button("Apagar card", key=f"tiktok_import_card_delete_{channel_id}"):
2021
2036
  st.session_state[f"tiktok_delete_{channel_id}"] = True
2022
2037
  st.rerun()
2023
- st.caption("Formato: Portrait 9:16")
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}")
2024
2044
  if st.button("Guardar", key=f"tiktok_import_card_save_{channel_id}", use_container_width=True, type="primary"):
2025
- update_channel(channel_id, {"platform": "tiktok", "default_prompt_master": selected_prompt, "prompt_master": selected_prompt, "video_aspect_ratio": "Portrait 9:16", "style_wide": "portrait"})
2026
- st.success("Prompt Master do canal guardado.")
2027
- st.rerun()
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")
2028
2052
  niche_action = st.columns([1.0, 4.0])
2029
2053
  with niche_action[0]:
2030
2054
  if st.button("Editar nicho", key=f"tiktok_import_card_niche_{channel_id}"):
@@ -2055,11 +2079,18 @@ def render_tiktok_channels():
2055
2079
  edit_url = st.text_input("URL pública", value=str(channel.get("url") or ""))
2056
2080
  edit_handle = st.text_input("Handle", value=str(channel.get("handle") or ""))
2057
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")
2058
2086
  edit_description = st.text_area("Descrição", value=str(channel.get("description") or ""))
2059
2087
  if st.form_submit_button("Guardar edição", type="primary"):
2060
- update_channel(channel_id, {"name": edit_name.strip(), "url": edit_url.strip(), "handle": edit_handle.strip(), "language": edit_language, "description": edit_description.strip(), "platform": "tiktok"})
2061
- st.session_state.pop(f"tiktok_edit_{channel_id}", None)
2062
- st.rerun()
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()
2063
2094
  with st.expander("Últimos 10 vídeos publicados", expanded=False):
2064
2095
  recent_videos = channel_videos_for(channel, limit=10)
2065
2096
  if not recent_videos:
@@ -2089,7 +2120,7 @@ def render_tiktok_channels():
2089
2120
  candidate = {"name": row.get("name", ""), "handle": row.get("handle", ""), "url": row.get("url", "")}
2090
2121
  if find_duplicate_channel(candidate, channels):
2091
2122
  continue
2092
- 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"})
2093
2124
  update_channel(created_channel["id"], {"platform": "tiktok"})
2094
2125
  created += 1
2095
2126
  st.success(f"{created} canal(is) TikTok cadastrado(s).")
@@ -2099,7 +2130,7 @@ def render_tiktok_channels():
2099
2130
  st.subheader(f"Canais TikTok cadastrados ({len(channels)})")
2100
2131
  if channels:
2101
2132
  st.dataframe(
2102
- [{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],
2103
2134
  use_container_width=True,
2104
2135
  hide_index=True,
2105
2136
  height=360,
@@ -2114,10 +2145,17 @@ def render_tiktok_channels():
2114
2145
  language = st.selectbox("Idioma", list(LANGUAGE_CODES), format_func=language_label, key="tiktok_manual_channel_language")
2115
2146
  niche = st.text_input("Nicho", key="tiktok_manual_channel_niche")
2116
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")
2117
2152
  description = st.text_area("Descrição", key="tiktok_manual_channel_description")
2118
2153
  if st.form_submit_button("Guardar canal manual", type="primary"):
2119
2154
  reference = normalize_tiktok_reference(url or handle)
2120
- 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, "style_wide": "portrait", "video_aspect_ratio": "Portrait 9:16"})
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()})
2121
2159
  update_channel(channel["id"], {"platform": "tiktok"})
2122
2160
  st.success("Canal TikTok cadastrado.")
2123
2161
  st.rerun()
@@ -1,7 +1,14 @@
1
- from __future__ import annotations
1
+ import os
2
+ import sys
3
+
4
+ os.environ["PYTHONIOENCODING"] = "utf-8"
5
+ os.environ["PYTHONUTF8"] = "1"
6
+ if hasattr(sys.stdout, "reconfigure"):
7
+ sys.stdout.reconfigure(encoding="utf-8", errors="replace")
8
+ if hasattr(sys.stderr, "reconfigure"):
9
+ sys.stderr.reconfigure(encoding="utf-8", errors="replace")
2
10
 
3
11
  import argparse
4
- import os
5
12
  import time
6
13
  from datetime import datetime, tzinfo
7
14
  from pathlib import Path
@@ -1,7 +1,14 @@
1
- from __future__ import annotations
1
+ import os
2
+ import sys
3
+
4
+ os.environ["PYTHONIOENCODING"] = "utf-8"
5
+ os.environ["PYTHONUTF8"] = "1"
6
+ if hasattr(sys.stdout, "reconfigure"):
7
+ sys.stdout.reconfigure(encoding="utf-8", errors="replace")
8
+ if hasattr(sys.stderr, "reconfigure"):
9
+ sys.stderr.reconfigure(encoding="utf-8", errors="replace")
2
10
 
3
11
  import json
4
- import os
5
12
  import queue
6
13
  import re
7
14
  import signal
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danhachuel/thunderbolt",
3
- "version": "0.4.83",
3
+ "version": "0.4.85",
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",
@@ -27,6 +27,7 @@
27
27
  "MANUAL-INSTALACAO.md",
28
28
  "THIRD-PARTY-NOTICES.md",
29
29
  "requirements.txt",
30
+ "sitecustomize.py",
30
31
  "package.json",
31
32
  ".streamlit/config.toml"
32
33
  ],
@@ -0,0 +1,16 @@
1
+ """Process-wide UTF-8 protection for Windows terminals and MobaXterm."""
2
+ from __future__ import annotations
3
+
4
+ import os
5
+ import sys
6
+
7
+ os.environ["PYTHONIOENCODING"] = "utf-8"
8
+ os.environ["PYTHONUTF8"] = "1"
9
+
10
+ for stream_name in ("stdin", "stdout", "stderr"):
11
+ stream = getattr(sys, stream_name, None)
12
+ if stream is not None and hasattr(stream, "reconfigure"):
13
+ try:
14
+ stream.reconfigure(encoding="utf-8", errors="replace")
15
+ except (OSError, ValueError):
16
+ pass