@danhachuel/thunderbolt 0.4.89 → 0.4.91

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.
Files changed (2) hide show
  1. package/app/main.py +25 -44
  2. package/package.json +1 -1
package/app/main.py CHANGED
@@ -1311,7 +1311,6 @@ def render_channel_edit_form(channel: dict, youtube_account_ids: list[str], yout
1311
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
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
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)
1315
1314
  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 "") )
1316
1315
  with edit_cols[1]:
1317
1316
  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"))
@@ -1333,7 +1332,7 @@ def render_channel_edit_form(channel: dict, youtube_account_ids: list[str], yout
1333
1332
  else:
1334
1333
  update_channel(channel_id, {
1335
1334
  "name": edited_name.strip(), "url": edited_url.strip(), "handle": edited_handle.strip(), "language": edited_language,
1336
- "style_wide": channel_video_source_storage(edited_style), "video_aspect_ratio": edited_aspect, "format": edited_format,
1335
+ "style_wide": channel_video_source_storage(edited_style), "video_aspect_ratio": edited_aspect,
1337
1336
  "niche": edited_niche.strip(), "reference_channels": [item.strip() for item in re.split(r"[,|]", edited_niche) if item.strip()],
1338
1337
  "default_blueprint_id": edited_blueprint.strip(), "blueprint_id": edited_blueprint.strip(),
1339
1338
  "default_voice": edited_voice.strip(), "voice": edited_voice.strip(),
@@ -2004,7 +2003,6 @@ def render_tiktok_channels():
2004
2003
  language = st.selectbox("Idioma do roteiro", VIDEO_LANGUAGE_SELECTION_OPTIONS, format_func=video_language_label, key="tiktok_import_language")
2005
2004
  source_value = st.selectbox("Fonte do vídeo", WIDE_STYLE_OPTIONS, key="tiktok_import_source")
2006
2005
  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")
2008
2006
  niche = st.text_input("Nicho", key="tiktok_import_niche")
2009
2007
  prompt = st.selectbox("Prompt Master padrão", prompt_ids, format_func=lambda item: prompt_labels.get(item, item), key="tiktok_import_prompt")
2010
2008
  voice = st.selectbox("Narrador/Voz Padrão", voice_options, format_func=lambda item: item or "Sem voz padrão", key="tiktok_import_voice")
@@ -2015,7 +2013,7 @@ def render_tiktok_channels():
2015
2013
  if not valid_hhmm(automation_time):
2016
2014
  st.error("O horário diário deve estar no formato HH:MM.")
2017
2015
  st.stop()
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")})
2016
+ 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": "Shorts", "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")})
2019
2017
  avatar_url = _tiktok_avatar_url(imported)
2020
2018
  update_channel(channel["id"], {"platform": "tiktok", "default_prompt_master": prompt, "prompt_master": prompt, "avatar_url": avatar_url, "thumbnail_url": avatar_url})
2021
2019
  st.session_state.pop("tiktok_channel_import", None)
@@ -2027,7 +2025,7 @@ def render_tiktok_channels():
2027
2025
  for channel in channels:
2028
2026
  channel_id = str(channel.get("id") or "")
2029
2027
  with st.container(border=True):
2030
- card_cols = st.columns([0.7, 2.3, 1.0, 1.0, 1.35, 1.25])
2028
+ card_cols = st.columns([0.7, 3.45, 1.15, 1.15, 1.45, 1.55], gap="small")
2031
2029
  with card_cols[0]:
2032
2030
  avatar_url = _tiktok_avatar_url(channel)
2033
2031
  if avatar_url:
@@ -2043,9 +2041,6 @@ def render_tiktok_channels():
2043
2041
  st.metric("Seguidores", format_metric_number(channel.get("subscriber_count")))
2044
2042
  with card_cols[3]:
2045
2043
  st.metric("Curtidas", format_metric_number(channel.get("likes_count")))
2046
- with card_cols[4]:
2047
- current_prompt = str(channel.get("default_prompt_master") or channel.get("prompt_master") or "")
2048
- selected_prompt = st.selectbox("Prompt Master", prompt_ids, index=prompt_ids.index(current_prompt) if current_prompt in prompt_ids else 0, format_func=lambda item: prompt_labels.get(item, item), key=f"tiktok_import_card_prompt_{channel_id}")
2049
2044
  with card_cols[5]:
2050
2045
  active = st.toggle("Activo", value=bool(channel.get("active", True)), key=f"tiktok_import_card_active_{channel_id}")
2051
2046
  if active != bool(channel.get("active", True)):
@@ -2057,27 +2052,20 @@ def render_tiktok_channels():
2057
2052
  if st.button("Apagar card", key=f"tiktok_import_card_delete_{channel_id}"):
2058
2053
  st.session_state[f"tiktok_delete_{channel_id}"] = True
2059
2054
  st.rerun()
2060
- _, _, _, card_voice_options, card_current_voice = channel_default_options(channel)
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}")
2066
- automation_on = st.toggle("Automação ON", value=bool(channel.get("automation_on", False)), key=f"tiktok_import_card_automation_{channel_id}")
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}")
2068
- if st.button("Guardar", key=f"tiktok_import_card_save_{channel_id}", use_container_width=True, type="primary"):
2069
- if not valid_hhmm(automation_time):
2070
- st.error("O horário diário deve estar no formato HH:MM.")
2071
- else:
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()})
2073
- st.success("Dados do canal TikTok guardados.")
2074
- st.rerun()
2075
- st.caption(f"Fonte do vídeo: {channel_video_source_value(channel.get('style_wide'))}")
2076
- niche_action = st.columns([1.0, 4.0])
2077
- with niche_action[0]:
2078
- if st.button("Editar nicho", key=f"tiktok_import_card_niche_{channel_id}"):
2079
- st.session_state[f"tiktok_niche_{channel_id}"] = not st.session_state.get(f"tiktok_niche_{channel_id}", False)
2080
- st.rerun()
2055
+ summary_cols = st.columns(4, gap="medium")
2056
+ with summary_cols[0]:
2057
+ st.markdown("**Blueprint Padrão**")
2058
+ st.caption(prompt_labels.get(str(channel.get("default_prompt_master") or channel.get("prompt_master") or ""), "Sem Blueprint padrão"))
2059
+ with summary_cols[1]:
2060
+ st.markdown("**Nicho**")
2061
+ st.caption(channel_niche_label(channel))
2062
+ with summary_cols[2]:
2063
+ st.markdown("**Narrador/Voz Padrão**")
2064
+ st.caption(str(channel.get("default_voice") or channel.get("voice") or "Sem voz padrão"))
2065
+ with summary_cols[3]:
2066
+ st.markdown("**Idioma**")
2067
+ st.caption(video_language_label(normalize_video_language(channel.get("language") or "pt")))
2068
+ st.caption(f"Fonte do vídeo: {channel_video_source_value(channel.get('style_wide'))} · Proporção: {channel.get('video_aspect_ratio') or 'Portrait 9:16'}")
2081
2069
  if st.session_state.get(f"tiktok_delete_{channel_id}"):
2082
2070
  st.warning("Apagar este canal TikTok e os dados locais associados?")
2083
2071
  confirm_cols = st.columns(2)
@@ -2090,13 +2078,6 @@ def render_tiktok_channels():
2090
2078
  if st.button("Cancelar", key=f"tiktok_cancel_delete_{channel_id}"):
2091
2079
  st.session_state.pop(f"tiktok_delete_{channel_id}", None)
2092
2080
  st.rerun()
2093
- if st.session_state.get(f"tiktok_niche_{channel_id}"):
2094
- with st.form(f"tiktok_niche_form_{channel_id}"):
2095
- niche_value = st.text_input("Nicho do canal", value=str(channel.get("niche") or ""))
2096
- if st.form_submit_button("Guardar nicho", type="primary"):
2097
- update_channel(channel_id, {"niche": niche_value.strip(), "reference_channels": [niche_value.strip()] if niche_value.strip() else []})
2098
- st.session_state.pop(f"tiktok_niche_{channel_id}", None)
2099
- st.rerun()
2100
2081
  if st.session_state.get(f"tiktok_edit_{channel_id}"):
2101
2082
  with st.form(f"tiktok_edit_form_{channel_id}"):
2102
2083
  edit_name = st.text_input("Nome do canal", value=str(channel.get("name") or ""))
@@ -2105,7 +2086,7 @@ def render_tiktok_channels():
2105
2086
  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
2087
  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
2088
  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)
2089
+ edit_niche = st.text_input("Nicho", value=str(channel.get("niche") or ""))
2109
2090
  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")
2110
2091
  edit_automation = st.toggle("Automação ON", value=bool(channel.get("automation_on", False)), key=f"tiktok_edit_automation_{channel_id}")
2111
2092
  edit_time = st.text_input("Horário diário (HH:MM)", value=str(channel.get("automation_time") or "00:00"))
@@ -2115,7 +2096,7 @@ def render_tiktok_channels():
2115
2096
  if not valid_hhmm(edit_time):
2116
2097
  st.error("O horário diário deve estar no formato HH:MM.")
2117
2098
  else:
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"})
2099
+ update_channel(channel_id, {"name": edit_name.strip(), "url": edit_url.strip(), "handle": edit_handle.strip(), "language": edit_language, "niche": edit_niche.strip(), "reference_channels": [edit_niche.strip()] if edit_niche.strip() else [], "default_voice": edit_voice, "voice": edit_voice, "style_wide": channel_video_source_storage(edit_source), "video_aspect_ratio": edit_aspect, "automation_on": edit_automation, "automation_time": edit_time.strip(), "description": edit_description.strip(), "platform": "tiktok"})
2119
2100
  st.session_state.pop(f"tiktok_edit_{channel_id}", None)
2120
2101
  st.rerun()
2121
2102
  with st.expander("Últimos 10 vídeos publicados", expanded=False):
@@ -2172,7 +2153,6 @@ def render_tiktok_channels():
2172
2153
  language = st.selectbox("Idioma do roteiro", VIDEO_LANGUAGE_SELECTION_OPTIONS, format_func=video_language_label, key="tiktok_manual_channel_language")
2173
2154
  source_value = st.selectbox("Fonte do vídeo", WIDE_STYLE_OPTIONS, key="tiktok_manual_channel_source")
2174
2155
  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")
2176
2156
  niche = st.text_input("Nicho", key="tiktok_manual_channel_niche")
2177
2157
  prompt = st.selectbox("Prompt Master padrão", prompt_ids, format_func=lambda item: prompt_labels.get(item, item), key="tiktok_manual_channel_prompt")
2178
2158
  voice = st.selectbox("Narrador/Voz Padrão", voice_options, format_func=lambda item: item or "Sem voz padrão", key="tiktok_manual_channel_voice")
@@ -2264,7 +2244,6 @@ def render_channels():
2264
2244
  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
2245
  style = st.selectbox("Fonte do vídeo", WIDE_STYLE_OPTIONS, index=0, key="yt_import_style")
2266
2246
  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")
2268
2247
  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")
2269
2248
  voice_options = voice_catalog(imported.get("default_voice") or imported.get("voice", ""))
2270
2249
  current_voice = imported.get("default_voice") or imported.get("voice", "")
@@ -2294,7 +2273,7 @@ def render_channels():
2294
2273
  "niche": niche.strip(),
2295
2274
  "reference_channels": [item.strip() for item in re.split(r"[,|]", niche) if item.strip()],
2296
2275
  "language": language,
2297
- "style_wide": channel_video_source_storage(style), "video_aspect_ratio": video_aspect_ratio, "format": video_format,
2276
+ "style_wide": channel_video_source_storage(style), "video_aspect_ratio": video_aspect_ratio, "format": "wide",
2298
2277
  "blueprint_id": blueprint.strip(),
2299
2278
  "default_blueprint_id": blueprint.strip(),
2300
2279
  "default_voice": voice.strip(),
@@ -5015,15 +4994,14 @@ def render_tiktok_automation():
5015
4994
  with default_cols[3]:
5016
4995
  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}")
5017
4996
  with default_cols[4]:
5018
- st.markdown("**Pool de vídeos**")
5019
- st.caption("TikTok / Shorts")
4997
+ automation_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_automation_format_{channel_id}")
5020
4998
  with default_cols[5]:
5021
4999
  if st.button("Guardar", key=f"tiktok_automation_save_{channel_id}", use_container_width=True, type="primary"):
5022
5000
  if not valid_hhmm(schedule_time):
5023
5001
  st.error("Use o formato HH:MM, por exemplo 08:30.")
5024
5002
  else:
5025
5003
  avatar_url = _tiktok_avatar_url(channel)
5026
- 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", "style_wide": "portrait", "avatar_url": avatar_url, "thumbnail_url": avatar_url})
5004
+ update_channel(channel_id, {"automation_on": bool(enabled), "automation_time": schedule_time.strip(), "default_prompt_master": prompt, "prompt_master": prompt, "platform": "tiktok", "format": automation_format, "video_aspect_ratio": "Portrait 9:16", "style_wide": "portrait", "avatar_url": avatar_url, "thumbnail_url": avatar_url})
5027
5005
  st.success("Automação TikTok guardada.")
5028
5006
  st.rerun()
5029
5007
  st.divider()
@@ -5118,6 +5096,8 @@ def render_automation():
5118
5096
  st.markdown("**Thumbnail Blueprint**")
5119
5097
  st.caption(str(paired_thumbnail.get("name") or "Generic_Thumbnail_Blueprint"))
5120
5098
  with default_cols[4]:
5099
+ automation_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, key=f"automation_format_{channel_id}")
5100
+ with default_cols[5]:
5121
5101
  automation_voice = st.selectbox(
5122
5102
  "Narrador/Voz Padrão",
5123
5103
  voice_options,
@@ -5133,6 +5113,7 @@ def render_automation():
5133
5113
  update_channel(channel_id, {
5134
5114
  "automation_on": bool(enabled),
5135
5115
  "automation_time": schedule_time.strip(),
5116
+ "format": automation_format,
5136
5117
  })
5137
5118
  set_channel_defaults(channel_id, automation_blueprint, automation_voice)
5138
5119
  paired_id = str(paired_thumbnail.get("id") or "Generic_Thumbnail_Blueprint")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danhachuel/thunderbolt",
3
- "version": "0.4.89",
3
+ "version": "0.4.91",
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",