@danhachuel/thunderbolt 0.4.87 → 0.4.89

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,12 +1,20 @@
1
1
  import os
2
2
  import sys
3
+ import io
3
4
 
4
5
  os.environ["PYTHONIOENCODING"] = "utf-8"
5
6
  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")
7
+
8
+ def _force_utf8_stream(stream: object) -> object:
9
+ buffer = getattr(stream, "buffer", None)
10
+ encoding = str(getattr(stream, "encoding", "") or "").lower().replace("-", "_")
11
+ if buffer is not None and encoding != "utf_8":
12
+ return io.TextIOWrapper(buffer, encoding="utf-8", errors="replace", line_buffering=True)
13
+ return stream
14
+
15
+ _original_stdout, _original_stderr = sys.stdout, sys.stderr
16
+ sys.stdout = _force_utf8_stream(sys.stdout)
17
+ sys.stderr = _force_utf8_stream(sys.stderr)
10
18
 
11
19
  import hashlib
12
20
  import json
@@ -130,6 +138,16 @@ AI_STYLE_OPTIONS = [
130
138
  ]
131
139
 
132
140
  WIDE_STYLE_OPTIONS = ["Pexels/Pixabay", "full_ia", "Apenas Música"]
141
+ CHANNEL_ASPECT_RATIO_OPTIONS = ["Landscape 16:9", "Portrait 9:16", "Square 1:1"]
142
+ CHANNEL_FORMAT_OPTIONS = ["wide", "Shorts", "Music"]
143
+
144
+
145
+ def channel_video_source_value(value: Any) -> str:
146
+ return {"pexels": "Pexels/Pixabay", "full_ia": "full_ia", "music": "Apenas Música"}.get(str(value or "").strip(), str(value or "Pexels/Pixabay") if str(value or "").strip() in set(WIDE_STYLE_OPTIONS) else "Pexels/Pixabay")
147
+
148
+
149
+ def channel_video_source_storage(value: str) -> str:
150
+ return {"Pexels/Pixabay": "pexels", "full_ia": "full_ia", "Apenas Música": "music"}.get(value, value)
133
151
  MATERIAL_SOURCE_OPTIONS = ["Pexels", "Pixabay"]
134
152
 
135
153
  VIDEO_LANGUAGE_OPTIONS = [
@@ -1289,10 +1307,11 @@ def render_channel_edit_form(channel: dict, youtube_account_ids: list[str], yout
1289
1307
  edited_name = st.text_input("Nome do canal", value=str(channel.get("name") or ""))
1290
1308
  edited_url = st.text_input("URL", value=str(channel.get("url") or ""))
1291
1309
  edited_handle = st.text_input("Handle", value=str(channel.get("handle") or ""))
1292
- edited_language = st.selectbox("Idioma", list(LANGUAGE_CODES), index=list(LANGUAGE_CODES).index(language_code(channel.get("language") or "pt")), format_func=language_label)
1293
- style_options = ["Pexels/Pixabay", "full_ia", "Apenas Música"]
1294
- style_value = {"pexels": "Pexels/Pixabay", "music": "Apenas Música"}.get(str(channel.get("style_wide") or "pexels"), str(channel.get("style_wide") or "Pexels/Pixabay"))
1295
- edited_style = st.selectbox("Estilo wide", style_options, index=style_options.index(style_value) if style_value in style_options else 0)
1310
+ language_options = VIDEO_LANGUAGE_SELECTION_OPTIONS
1311
+ edited_language = st.selectbox("Idioma do roteiro", language_options, index=language_options.index(normalize_video_language(channel.get("language") or "pt")) if normalize_video_language(channel.get("language") or "pt") in language_options else 0, format_func=video_language_label)
1312
+ edited_style = st.selectbox("Fonte do vídeo", WIDE_STYLE_OPTIONS, index=WIDE_STYLE_OPTIONS.index(channel_video_source_value(channel.get("style_wide"))) if channel_video_source_value(channel.get("style_wide")) in WIDE_STYLE_OPTIONS else 0)
1313
+ edited_aspect = st.selectbox("Proporção do vídeo", CHANNEL_ASPECT_RATIO_OPTIONS, index=CHANNEL_ASPECT_RATIO_OPTIONS.index(str(channel.get("video_aspect_ratio") or "Landscape 16:9")) if str(channel.get("video_aspect_ratio") or "Landscape 16:9") in CHANNEL_ASPECT_RATIO_OPTIONS else 0)
1314
+ edited_format = st.selectbox("Formato", CHANNEL_FORMAT_OPTIONS, index=CHANNEL_FORMAT_OPTIONS.index(str(channel.get("format") or "wide")) if str(channel.get("format") or "wide") in CHANNEL_FORMAT_OPTIONS else 0)
1296
1315
  edited_niche = st.text_input("Nicho", value=str(channel.get("niche") or channel_niche_label(channel) if channel_niche_label(channel) != "SEM NICHO CONFIGURADO" else "") )
1297
1316
  with edit_cols[1]:
1298
1317
  edited_blueprint = st.selectbox("Blueprint Padrão", blueprint_ids, index=blueprint_ids.index(current_blueprint) if current_blueprint in blueprint_ids else 0, format_func=lambda item: blueprint_labels.get(item, item or "Sem Blueprint padrão"))
@@ -1314,7 +1333,7 @@ def render_channel_edit_form(channel: dict, youtube_account_ids: list[str], yout
1314
1333
  else:
1315
1334
  update_channel(channel_id, {
1316
1335
  "name": edited_name.strip(), "url": edited_url.strip(), "handle": edited_handle.strip(), "language": edited_language,
1317
- "style_wide": {"Pexels/Pixabay": "pexels", "Apenas Música": "music"}.get(edited_style, edited_style),
1336
+ "style_wide": channel_video_source_storage(edited_style), "video_aspect_ratio": edited_aspect, "format": edited_format,
1318
1337
  "niche": edited_niche.strip(), "reference_channels": [item.strip() for item in re.split(r"[,|]", edited_niche) if item.strip()],
1319
1338
  "default_blueprint_id": edited_blueprint.strip(), "blueprint_id": edited_blueprint.strip(),
1320
1339
  "default_voice": edited_voice.strip(), "voice": edited_voice.strip(),
@@ -1982,7 +2001,10 @@ def render_tiktok_channels():
1982
2001
  name = st.text_input("Nome canal", value=str(imported.get("name") or imported.get("username") or ""))
1983
2002
  url = st.text_input("URL canal", value=str(imported.get("public_url") or imported.get("url") or ""))
1984
2003
  handle = st.text_input("Handle canal", value=str(imported.get("handle") or ""))
1985
- language = st.selectbox("Idioma", list(LANGUAGE_CODES), format_func=language_label, key="tiktok_import_language")
2004
+ language = st.selectbox("Idioma do roteiro", VIDEO_LANGUAGE_SELECTION_OPTIONS, format_func=video_language_label, key="tiktok_import_language")
2005
+ source_value = st.selectbox("Fonte do vídeo", WIDE_STYLE_OPTIONS, key="tiktok_import_source")
2006
+ aspect_value = st.selectbox("Proporção do vídeo", CHANNEL_ASPECT_RATIO_OPTIONS, index=1, key="tiktok_import_aspect")
2007
+ format_value = st.selectbox("Formato", CHANNEL_FORMAT_OPTIONS, index=1, key="tiktok_import_format")
1986
2008
  niche = st.text_input("Nicho", key="tiktok_import_niche")
1987
2009
  prompt = st.selectbox("Prompt Master padrão", prompt_ids, format_func=lambda item: prompt_labels.get(item, item), key="tiktok_import_prompt")
1988
2010
  voice = st.selectbox("Narrador/Voz Padrão", voice_options, format_func=lambda item: item or "Sem voz padrão", key="tiktok_import_voice")
@@ -1993,7 +2015,7 @@ def render_tiktok_channels():
1993
2015
  if not valid_hhmm(automation_time):
1994
2016
  st.error("O horário diário deve estar no formato HH:MM.")
1995
2017
  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")})
2018
+ 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": channel_video_source_storage(source_value), "video_aspect_ratio": aspect_value, "format": format_value, "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")})
1997
2019
  avatar_url = _tiktok_avatar_url(imported)
1998
2020
  update_channel(channel["id"], {"platform": "tiktok", "default_prompt_master": prompt, "prompt_master": prompt, "avatar_url": avatar_url, "thumbnail_url": avatar_url})
1999
2021
  st.session_state.pop("tiktok_channel_import", None)
@@ -2035,20 +2057,22 @@ def render_tiktok_channels():
2035
2057
  if st.button("Apagar card", key=f"tiktok_import_card_delete_{channel_id}"):
2036
2058
  st.session_state[f"tiktok_delete_{channel_id}"] = True
2037
2059
  st.rerun()
2038
- st.caption("Estilo wide: Portrait 9:16")
2039
- st.caption(f"Automação: {'ligada' if automation_on else 'desligada'} · {automation_time}")
2040
2060
  _, _, _, card_voice_options, card_current_voice = channel_default_options(channel)
2041
2061
  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}")
2062
+ selected_source = st.selectbox("Fonte do vídeo", WIDE_STYLE_OPTIONS, index=WIDE_STYLE_OPTIONS.index(channel_video_source_value(channel.get("style_wide"))) if channel_video_source_value(channel.get("style_wide")) in WIDE_STYLE_OPTIONS else 0, key=f"tiktok_import_card_source_{channel_id}")
2063
+ selected_aspect = st.selectbox("Proporção do vídeo", CHANNEL_ASPECT_RATIO_OPTIONS, index=CHANNEL_ASPECT_RATIO_OPTIONS.index(str(channel.get("video_aspect_ratio") or "Portrait 9:16")) if str(channel.get("video_aspect_ratio") or "Portrait 9:16") in CHANNEL_ASPECT_RATIO_OPTIONS else 1, key=f"tiktok_import_card_aspect_{channel_id}")
2064
+ selected_format = st.selectbox("Formato", CHANNEL_FORMAT_OPTIONS, index=CHANNEL_FORMAT_OPTIONS.index(str(channel.get("format") or "Shorts")) if str(channel.get("format") or "Shorts") in CHANNEL_FORMAT_OPTIONS else 1, key=f"tiktok_import_card_format_{channel_id}")
2065
+ selected_language = st.selectbox("Idioma do roteiro", VIDEO_LANGUAGE_SELECTION_OPTIONS, index=VIDEO_LANGUAGE_SELECTION_OPTIONS.index(normalize_video_language(channel.get("language") or "pt")) if normalize_video_language(channel.get("language") or "pt") in VIDEO_LANGUAGE_SELECTION_OPTIONS else 0, format_func=video_language_label, key=f"tiktok_import_card_language_{channel_id}")
2042
2066
  automation_on = st.toggle("Automação ON", value=bool(channel.get("automation_on", False)), key=f"tiktok_import_card_automation_{channel_id}")
2043
2067
  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}")
2044
2068
  if st.button("Guardar", key=f"tiktok_import_card_save_{channel_id}", use_container_width=True, type="primary"):
2045
2069
  if not valid_hhmm(automation_time):
2046
2070
  st.error("O horário diário deve estar no formato HH:MM.")
2047
2071
  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()})
2072
+ update_channel(channel_id, {"platform": "tiktok", "default_prompt_master": selected_prompt, "prompt_master": selected_prompt, "default_voice": selected_voice, "voice": selected_voice, "language": selected_language, "video_aspect_ratio": selected_aspect, "format": selected_format, "style_wide": channel_video_source_storage(selected_source), "automation_on": automation_on, "automation_time": automation_time.strip()})
2049
2073
  st.success("Dados do canal TikTok guardados.")
2050
2074
  st.rerun()
2051
- st.caption("Estilo wide: Portrait 9:16")
2075
+ st.caption(f"Fonte do vídeo: {channel_video_source_value(channel.get('style_wide'))}")
2052
2076
  niche_action = st.columns([1.0, 4.0])
2053
2077
  with niche_action[0]:
2054
2078
  if st.button("Editar nicho", key=f"tiktok_import_card_niche_{channel_id}"):
@@ -2078,17 +2102,20 @@ def render_tiktok_channels():
2078
2102
  edit_name = st.text_input("Nome do canal", value=str(channel.get("name") or ""))
2079
2103
  edit_url = st.text_input("URL pública", value=str(channel.get("url") or ""))
2080
2104
  edit_handle = st.text_input("Handle", value=str(channel.get("handle") or ""))
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)
2105
+ edit_language = st.selectbox("Idioma do roteiro", VIDEO_LANGUAGE_SELECTION_OPTIONS, index=VIDEO_LANGUAGE_SELECTION_OPTIONS.index(normalize_video_language(channel.get("language") or "pt")) if normalize_video_language(channel.get("language") or "pt") in VIDEO_LANGUAGE_SELECTION_OPTIONS else 0, format_func=video_language_label)
2106
+ edit_source = st.selectbox("Fonte do vídeo", WIDE_STYLE_OPTIONS, index=WIDE_STYLE_OPTIONS.index(channel_video_source_value(channel.get("style_wide"))) if channel_video_source_value(channel.get("style_wide")) in WIDE_STYLE_OPTIONS else 0)
2107
+ edit_aspect = st.selectbox("Proporção do vídeo", CHANNEL_ASPECT_RATIO_OPTIONS, index=CHANNEL_ASPECT_RATIO_OPTIONS.index(str(channel.get("video_aspect_ratio") or "Portrait 9:16")) if str(channel.get("video_aspect_ratio") or "Portrait 9:16") in CHANNEL_ASPECT_RATIO_OPTIONS else 1)
2108
+ edit_format = st.selectbox("Formato", CHANNEL_FORMAT_OPTIONS, index=CHANNEL_FORMAT_OPTIONS.index(str(channel.get("format") or "Shorts")) if str(channel.get("format") or "Shorts") in CHANNEL_FORMAT_OPTIONS else 1)
2082
2109
  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
2110
  edit_automation = st.toggle("Automação ON", value=bool(channel.get("automation_on", False)), key=f"tiktok_edit_automation_{channel_id}")
2084
2111
  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")
2112
+ st.caption(f"Fonte do vídeo: {channel_video_source_value(channel.get('style_wide'))}")
2086
2113
  edit_description = st.text_area("Descrição", value=str(channel.get("description") or ""))
2087
2114
  if st.form_submit_button("Guardar edição", type="primary"):
2088
2115
  if not valid_hhmm(edit_time):
2089
2116
  st.error("O horário diário deve estar no formato HH:MM.")
2090
2117
  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"})
2118
+ 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": channel_video_source_storage(edit_source), "video_aspect_ratio": edit_aspect, "format": edit_format, "automation_on": edit_automation, "automation_time": edit_time.strip(), "description": edit_description.strip(), "platform": "tiktok"})
2092
2119
  st.session_state.pop(f"tiktok_edit_{channel_id}", None)
2093
2120
  st.rerun()
2094
2121
  with st.expander("Últimos 10 vídeos publicados", expanded=False):
@@ -2142,7 +2169,10 @@ def render_tiktok_channels():
2142
2169
  url = st.text_input("URL canal", placeholder="https://www.tiktok.com/@conta", key="tiktok_manual_channel_url")
2143
2170
  name = st.text_input("Nome canal", key="tiktok_manual_channel_name")
2144
2171
  handle = st.text_input("Handle canal", key="tiktok_manual_channel_handle")
2145
- language = st.selectbox("Idioma", list(LANGUAGE_CODES), format_func=language_label, key="tiktok_manual_channel_language")
2172
+ language = st.selectbox("Idioma do roteiro", VIDEO_LANGUAGE_SELECTION_OPTIONS, format_func=video_language_label, key="tiktok_manual_channel_language")
2173
+ source_value = st.selectbox("Fonte do vídeo", WIDE_STYLE_OPTIONS, key="tiktok_manual_channel_source")
2174
+ aspect_value = st.selectbox("Proporção do vídeo", CHANNEL_ASPECT_RATIO_OPTIONS, index=1, key="tiktok_manual_channel_aspect")
2175
+ format_value = st.selectbox("Formato", CHANNEL_FORMAT_OPTIONS, index=1, key="tiktok_manual_channel_format")
2146
2176
  niche = st.text_input("Nicho", key="tiktok_manual_channel_niche")
2147
2177
  prompt = st.selectbox("Prompt Master padrão", prompt_ids, format_func=lambda item: prompt_labels.get(item, item), key="tiktok_manual_channel_prompt")
2148
2178
  voice = st.selectbox("Narrador/Voz Padrão", voice_options, format_func=lambda item: item or "Sem voz padrão", key="tiktok_manual_channel_voice")
@@ -2155,7 +2185,7 @@ def render_tiktok_channels():
2155
2185
  if not valid_hhmm(automation_time):
2156
2186
  st.error("O horário diário deve estar no formato HH:MM.")
2157
2187
  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()})
2188
+ 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": channel_video_source_storage(source_value), "video_aspect_ratio": aspect_value, "format": format_value, "automation_on": automation_on, "automation_time": automation_time.strip()})
2159
2189
  update_channel(channel["id"], {"platform": "tiktok"})
2160
2190
  st.success("Canal TikTok cadastrado.")
2161
2191
  st.rerun()
@@ -2231,8 +2261,10 @@ def render_channels():
2231
2261
  name = st.text_input("Nome do canal", value=imported.get("name", ""), key="yt_import_name")
2232
2262
  url = st.text_input("URL", value=imported.get("url", source if source.startswith("http") else ""), key="yt_import_url")
2233
2263
  handle = st.text_input("Handle", value=imported.get("handle", ""), key="yt_import_handle")
2234
- language = st.selectbox("Idioma", list(LANGUAGE_CODES), index=list(LANGUAGE_CODES).index(language_code(imported.get("language") or "pt")), format_func=language_label, key="yt_import_language")
2235
- style = st.selectbox("Estilo wide", ["Pexels/Pixabay", "full_ia", "Apenas Música"], index=0, key="yt_import_style")
2264
+ language = st.selectbox("Idioma do roteiro", VIDEO_LANGUAGE_SELECTION_OPTIONS, index=VIDEO_LANGUAGE_SELECTION_OPTIONS.index(normalize_video_language(imported.get("language") or "pt")) if normalize_video_language(imported.get("language") or "pt") in VIDEO_LANGUAGE_SELECTION_OPTIONS else 0, format_func=video_language_label, key="yt_import_language")
2265
+ style = st.selectbox("Fonte do vídeo", WIDE_STYLE_OPTIONS, index=0, key="yt_import_style")
2266
+ video_aspect_ratio = st.selectbox("Proporção do vídeo", CHANNEL_ASPECT_RATIO_OPTIONS, key="yt_import_aspect_ratio")
2267
+ video_format = st.selectbox("Formato", CHANNEL_FORMAT_OPTIONS, key="yt_import_format")
2236
2268
  blueprint = st.selectbox("Blueprint Padrão", blueprint_ids, index=blueprint_ids.index(imported_blueprint) if imported_blueprint in blueprint_ids else 0, format_func=lambda item: blueprint_labels.get(item, item or "Sem Blueprint padrão"), key="yt_import_blueprint")
2237
2269
  voice_options = voice_catalog(imported.get("default_voice") or imported.get("voice", ""))
2238
2270
  current_voice = imported.get("default_voice") or imported.get("voice", "")
@@ -2262,7 +2294,7 @@ def render_channels():
2262
2294
  "niche": niche.strip(),
2263
2295
  "reference_channels": [item.strip() for item in re.split(r"[,|]", niche) if item.strip()],
2264
2296
  "language": language,
2265
- "style_wide": {"Pexels/Pixabay": "pexels", "full_ia": "full_ia", "Apenas Música": "music"}.get(style, style),
2297
+ "style_wide": channel_video_source_storage(style), "video_aspect_ratio": video_aspect_ratio, "format": video_format,
2266
2298
  "blueprint_id": blueprint.strip(),
2267
2299
  "default_blueprint_id": blueprint.strip(),
2268
2300
  "default_voice": voice.strip(),
@@ -4968,14 +5000,18 @@ def render_tiktok_automation():
4968
5000
  schedule_time = st.text_input("Horário (HH:MM)", value=channel.get("automation_time", "00:00"), key=f"tiktok_automation_time_{channel_id}")
4969
5001
  default_cols = st.columns([1.0, 1.0, 1.35, 1.45, 1.55, 1.0], gap="small")
4970
5002
  with default_cols[0]:
4971
- st.markdown("**Idioma Padrão**")
4972
- st.caption(language_label(channel.get("language") or "pt"))
5003
+ st.markdown("**Idioma do roteiro**")
5004
+ st.caption(video_language_label(normalize_video_language(channel.get("language") or "pt")))
4973
5005
  with default_cols[1]:
4974
5006
  st.markdown("**Nicho Padrão**")
4975
5007
  st.caption(channel_niche_label(channel))
4976
5008
  with default_cols[2]:
4977
- st.markdown("**Formato da fila**")
4978
- st.caption("Portrait 9:16")
5009
+ st.markdown("**Fonte do vídeo**")
5010
+ st.caption(channel_video_source_value(channel.get("style_wide")))
5011
+ st.markdown("**Proporção do vídeo**")
5012
+ st.caption(str(channel.get("video_aspect_ratio") or "Portrait 9:16"))
5013
+ st.markdown("**Formato**")
5014
+ st.caption(str(channel.get("format") or "Shorts"))
4979
5015
  with default_cols[3]:
4980
5016
  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}")
4981
5017
  with default_cols[4]:
@@ -5058,8 +5094,8 @@ def render_automation():
5058
5094
  blueprint_ids, blueprint_labels, current_blueprint, voice_options, current_voice = channel_default_options(channel)
5059
5095
  default_cols = st.columns([1.0, 1.0, 1.35, 1.45, 1.55, 1.0], gap="small")
5060
5096
  with default_cols[0]:
5061
- st.markdown("**Idioma Padrão**")
5062
- st.caption(language_label(channel.get("language") or "pt"))
5097
+ st.markdown("**Idioma do roteiro**")
5098
+ st.caption(video_language_label(normalize_video_language(channel.get("language") or "pt")))
5063
5099
  with default_cols[1]:
5064
5100
  st.markdown("**Nicho Padrão**")
5065
5101
  st.caption(channel_niche_label(channel))
@@ -5073,6 +5109,12 @@ def render_automation():
5073
5109
  )
5074
5110
  with default_cols[2]:
5075
5111
  paired_thumbnail = thumbnail_blueprint_for_blueprint(automation_blueprint)
5112
+ st.markdown("**Fonte do vídeo**")
5113
+ st.caption(channel_video_source_value(channel.get("style_wide")))
5114
+ st.markdown("**Proporção do vídeo**")
5115
+ st.caption(str(channel.get("video_aspect_ratio") or "Landscape 16:9"))
5116
+ st.markdown("**Formato**")
5117
+ st.caption(str(channel.get("format") or "wide"))
5076
5118
  st.markdown("**Thumbnail Blueprint**")
5077
5119
  st.caption(str(paired_thumbnail.get("name") or "Generic_Thumbnail_Blueprint"))
5078
5120
  with default_cols[4]:
@@ -1,12 +1,14 @@
1
1
  import os
2
2
  import sys
3
+ import io
3
4
 
4
5
  os.environ["PYTHONIOENCODING"] = "utf-8"
5
6
  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")
7
+ _original_stdout, _original_stderr = sys.stdout, sys.stderr
8
+ if getattr(sys.stdout, "buffer", None) is not None and str(getattr(sys.stdout, "encoding", "") or "").lower().replace("-", "_") != "utf_8":
9
+ sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace", line_buffering=True)
10
+ if getattr(sys.stderr, "buffer", None) is not None and str(getattr(sys.stderr, "encoding", "") or "").lower().replace("-", "_") != "utf_8":
11
+ sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8", errors="replace", line_buffering=True)
10
12
 
11
13
  import argparse
12
14
  import time
@@ -1,12 +1,14 @@
1
1
  import os
2
2
  import sys
3
+ import io
3
4
 
4
5
  os.environ["PYTHONIOENCODING"] = "utf-8"
5
6
  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")
7
+ _original_stdout, _original_stderr = sys.stdout, sys.stderr
8
+ if getattr(sys.stdout, "buffer", None) is not None and str(getattr(sys.stdout, "encoding", "") or "").lower().replace("-", "_") != "utf_8":
9
+ sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace", line_buffering=True)
10
+ if getattr(sys.stderr, "buffer", None) is not None and str(getattr(sys.stderr, "encoding", "") or "").lower().replace("-", "_") != "utf_8":
11
+ sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8", errors="replace", line_buffering=True)
10
12
 
11
13
  import json
12
14
  import queue
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danhachuel/thunderbolt",
3
- "version": "0.4.87",
3
+ "version": "0.4.89",
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",
package/sitecustomize.py CHANGED
@@ -3,13 +3,22 @@ from __future__ import annotations
3
3
 
4
4
  import os
5
5
  import sys
6
+ import io
6
7
 
7
8
  os.environ["PYTHONIOENCODING"] = "utf-8"
8
9
  os.environ["PYTHONUTF8"] = "1"
9
10
 
10
11
  for stream_name in ("stdin", "stdout", "stderr"):
11
12
  stream = getattr(sys, stream_name, None)
12
- if stream is not None and hasattr(stream, "reconfigure"):
13
+ buffer = getattr(stream, "buffer", None) if stream is not None else None
14
+ encoding = str(getattr(stream, "encoding", "") or "").lower().replace("-", "_") if stream is not None else ""
15
+ if buffer is not None and encoding != "utf_8":
16
+ try:
17
+ replacement = io.TextIOWrapper(buffer, encoding="utf-8", errors="replace", line_buffering=True)
18
+ setattr(sys, stream_name, replacement)
19
+ except (OSError, ValueError):
20
+ pass
21
+ elif stream is not None and hasattr(stream, "reconfigure"):
13
22
  try:
14
23
  stream.reconfigure(encoding="utf-8", errors="replace")
15
24
  except (OSError, ValueError):