@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 +51 -13
- package/hermes_ui/automation_worker.py +9 -2
- package/hermes_ui/pipeline_worker.py +9 -2
- package/package.json +2 -1
- package/sitecustomize.py +16 -0
package/app/main.py
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
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("
|
|
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
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
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
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
],
|
package/sitecustomize.py
ADDED
|
@@ -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
|