@danhachuel/thunderbolt 0.2.59 → 0.2.60

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
@@ -120,6 +120,21 @@ VIDEO_LANGUAGE_OPTIONS = [
120
120
  "50 – Hausa",
121
121
  ]
122
122
 
123
+ # Mantemos estes valores separados para que Criação de Vídeos e Roteiros partilhem
124
+ # exactamente os mesmos selectores sem alterar a lista histórica de idiomas.
125
+ VIDEO_FORMAT_OPTIONS = ["wide", "shorts", "music"]
126
+ VIDEO_CONCATENATION_OPTIONS = ["Random Concatenation (Recommended)", "Sequential Concatenation"]
127
+ VIDEO_TRANSITION_OPTIONS = ["None", "Fade", "Dissolve"]
128
+ VIDEO_ENCODER_OPTIONS = ["Default (Recommended)", "H.264", "H.265"]
129
+ VOICEOVER_MODE_OPTIONS = ["Auto", "Upload", "None"]
130
+ VOICEOVER_SERVICE_OPTIONS = ["Azure TTS V1"]
131
+ VOICEOVER_VOLUME_OPTIONS = ["20%", "40%", "60%", "80%", "100%"]
132
+ VOICEOVER_SPEED_OPTIONS = ["0.5x", "0.75x", "1.0x", "1.25x", "1.5x", "2.0x"]
133
+ BACKGROUND_MUSIC_SOURCE_OPTIONS = ["Ficheiro existente", "Carregar ficheiro", "Criar via Suno API", "Random Background Music", "Sem música"]
134
+ BACKGROUND_MUSIC_VOLUME_OPTIONS = ["0%", "10%", "20%", "30%", "50%", "75%", "100%"]
135
+ SUBTITLE_FONT_OPTIONS = ["MicrosoftYaHeiBold.ttc", "Arial.ttf", "DejaVuSans.ttf"]
136
+ SUBTITLE_POSITION_OPTIONS = ["Bottom (Recommended)", "Top", "Center"]
137
+
123
138
  ensure_storage()
124
139
  st.set_page_config(page_title="Thunderbolt", page_icon="T", layout="wide", initial_sidebar_state="expanded")
125
140
 
@@ -300,6 +315,101 @@ def voice_catalog(current: str = "") -> list[str]:
300
315
  return voices
301
316
 
302
317
 
318
+ def render_video_generation_settings(prefix: str, *, current_language: str = "") -> dict[str, Any]:
319
+ """Render the shared MoneyPrinter-style settings and return a serializable payload."""
320
+ settings: dict[str, Any] = {}
321
+ st.markdown("### Video Subject Settings")
322
+ subject_cols = st.columns(2)
323
+ with subject_cols[0]:
324
+ settings["video_subject"] = st.text_input(
325
+ "Video Subject",
326
+ value=str(st.session_state.get(f"{prefix}_video_subject", "")),
327
+ key=f"{prefix}_video_subject",
328
+ placeholder="Ex.: How AI is changing everyday life",
329
+ )
330
+ settings["script_language"] = st.selectbox(
331
+ "Script Language",
332
+ VIDEO_LANGUAGE_OPTIONS,
333
+ index=VIDEO_LANGUAGE_OPTIONS.index(current_language) if current_language in VIDEO_LANGUAGE_OPTIONS else 0,
334
+ key=f"{prefix}_script_language",
335
+ )
336
+ with subject_cols[1]:
337
+ with st.expander("Advanced Script Settings", expanded=False):
338
+ settings["script_structure_notes"] = st.text_area(
339
+ "Estrutura e notas opcionais",
340
+ value=str(st.session_state.get(f"{prefix}_script_structure_notes", "")),
341
+ key=f"{prefix}_script_structure_notes",
342
+ height=100,
343
+ placeholder="Ex.: gancho forte, 6 cenas, narração documental…",
344
+ )
345
+ settings["generate_script_with_ai"] = st.checkbox("Generate Script & Keywords with AI", value=True, key=f"{prefix}_generate_script_with_ai")
346
+ settings["video_script"] = st.text_area(
347
+ "Video Script (Optional)",
348
+ value=str(st.session_state.get(f"{prefix}_video_script", "")),
349
+ key=f"{prefix}_video_script",
350
+ height=130,
351
+ )
352
+ settings["video_keywords"] = st.text_area(
353
+ "Video Keywords (English, Optional)",
354
+ value=str(st.session_state.get(f"{prefix}_video_keywords", "")),
355
+ key=f"{prefix}_video_keywords",
356
+ height=90,
357
+ )
358
+
359
+ st.markdown("### Video Settings")
360
+ video_cols = st.columns(2)
361
+ with video_cols[0]:
362
+ settings["video_source"] = st.selectbox("Video Source", WIDE_STYLE_OPTIONS, key=f"{prefix}_video_source")
363
+ if settings["video_source"] == "full_ia":
364
+ settings["style_ia"] = st.selectbox("Estilo IA", AI_STYLE_OPTIONS, key=f"{prefix}_style_ia")
365
+ else:
366
+ settings["style_ia"] = ""
367
+ settings["video_format"] = st.selectbox("Formato", VIDEO_FORMAT_OPTIONS, key=f"{prefix}_video_format")
368
+ settings["video_concatenation_mode"] = st.selectbox("Video Concatenation Mode", VIDEO_CONCATENATION_OPTIONS, key=f"{prefix}_video_concatenation")
369
+ settings["match_visuals_to_script_order"] = st.checkbox("Match Visuals to Script Order", value=False, key=f"{prefix}_match_visuals")
370
+ settings["video_transition_mode"] = st.selectbox("Video Transition Mode", VIDEO_TRANSITION_OPTIONS, key=f"{prefix}_video_transition")
371
+ with video_cols[1]:
372
+ settings["video_aspect_ratio"] = st.selectbox("Video Aspect Ratio", ["Portrait 9:16", "Landscape 16:9", "Square 1:1"], key=f"{prefix}_video_aspect_ratio")
373
+ settings["maximum_clip_duration"] = st.selectbox("Maximum Clip Duration (seconds)", [3, 5, 8, 10, 15], key=f"{prefix}_maximum_clip_duration")
374
+ settings["videos_per_run"] = st.selectbox("Videos per Run", list(range(1, 11)), key=f"{prefix}_videos_per_run")
375
+ settings["video_encoder"] = st.selectbox("Video Encoder", VIDEO_ENCODER_OPTIONS, key=f"{prefix}_video_encoder")
376
+
377
+ st.markdown("### Audio Settings")
378
+ audio_cols = st.columns(2)
379
+ with audio_cols[0]:
380
+ settings["voiceover_mode"] = st.radio("Voiceover Mode", VOICEOVER_MODE_OPTIONS, horizontal=True, key=f"{prefix}_voiceover_mode")
381
+ settings["voiceover_service"] = st.selectbox("Voiceover Service", VOICEOVER_SERVICE_OPTIONS, key=f"{prefix}_voiceover_service")
382
+ current_voice = str(st.session_state.get(f"{prefix}_voice", ""))
383
+ voice_options = voice_catalog(current_voice)
384
+ settings["voice"] = st.selectbox("Voice (match script language)", voice_options, format_func=lambda value: value or "Sem voz seleccionada", key=f"{prefix}_voice")
385
+ volume_speed_cols = st.columns(2)
386
+ with volume_speed_cols[0]:
387
+ settings["voiceover_volume"] = st.selectbox("Voiceover Volume", VOICEOVER_VOLUME_OPTIONS, index=VOICEOVER_VOLUME_OPTIONS.index("100%"), key=f"{prefix}_voiceover_volume")
388
+ with volume_speed_cols[1]:
389
+ settings["voiceover_speed"] = st.selectbox("Voiceover Speed", VOICEOVER_SPEED_OPTIONS, index=VOICEOVER_SPEED_OPTIONS.index("1.0x"), key=f"{prefix}_voiceover_speed")
390
+ st.button("Preview Voice", key=f"{prefix}_preview_voice", disabled=True, help="A pré-visualização de voz será ligada ao provider configurado.")
391
+ with audio_cols[1]:
392
+ settings["background_music_source"] = st.selectbox("Background Music Source", BACKGROUND_MUSIC_SOURCE_OPTIONS, index=3, key=f"{prefix}_background_music_source")
393
+ settings["background_music_volume"] = st.selectbox("Background Music Volume", BACKGROUND_MUSIC_VOLUME_OPTIONS, index=2, key=f"{prefix}_background_music_volume")
394
+
395
+ st.markdown("### Subtitle Settings")
396
+ subtitle_cols = st.columns(2)
397
+ with subtitle_cols[0]:
398
+ settings["enable_subtitles"] = st.checkbox("Enable Subtitles", value=True, key=f"{prefix}_enable_subtitles")
399
+ settings["subtitle_font"] = st.selectbox("Font", SUBTITLE_FONT_OPTIONS, key=f"{prefix}_subtitle_font")
400
+ settings["subtitle_position"] = st.selectbox("Position", SUBTITLE_POSITION_OPTIONS, key=f"{prefix}_subtitle_position")
401
+ settings["subtitle_color"] = st.color_picker("Color", "#FFFFFF", key=f"{prefix}_subtitle_color")
402
+ settings["subtitle_background"] = st.checkbox("Background", value=True, key=f"{prefix}_subtitle_background")
403
+ settings["subtitle_background_color"] = st.color_picker("Background Color", "#000000", key=f"{prefix}_subtitle_background_color")
404
+ settings["subtitle_rounded_background"] = st.checkbox("Rounded Background", value=False, key=f"{prefix}_subtitle_rounded_background")
405
+ with subtitle_cols[1]:
406
+ settings["subtitle_font_size"] = st.slider("Font Size", min_value=12, max_value=96, value=60, key=f"{prefix}_subtitle_font_size")
407
+ settings["subtitle_outline"] = st.color_picker("Outline", "#000000", key=f"{prefix}_subtitle_outline")
408
+ settings["subtitle_outline_width"] = st.slider("Outline Width", min_value=0.0, max_value=5.0, value=1.5, step=0.25, key=f"{prefix}_subtitle_outline_width")
409
+ st.button("Restore Subtitle Defaults", key=f"{prefix}_restore_subtitle_defaults", disabled=True, help="Os valores predefinidos já estão activos nesta configuração.")
410
+ return settings
411
+
412
+
303
413
  def channel_default_options(channel: dict) -> tuple[list[str], dict[str, str], str, list[str], str]:
304
414
  """Return synchronised Blueprint and voice options for a channel editor."""
305
415
  blueprint_items = blueprint_catalog()
@@ -959,6 +1069,11 @@ def render_new_video(page_title: str = "Criação de Vídeos"):
959
1069
  )
960
1070
  mode = {"Canal específico": "single", "Lote no mesmo canal": "same_channel", "Lote geral": "general"}[mode_label]
961
1071
  selected_one: dict[str, Any] | None = None
1072
+ legacy_language = st.session_state.get("video_language")
1073
+ legacy_language_map = {"Português": "36 – Português (Brasil)", "English": "01 – Inglês", "Español": "41 – Espanhol (LatAm)"}
1074
+ if legacy_language not in VIDEO_LANGUAGE_OPTIONS:
1075
+ st.session_state["video_language"] = legacy_language_map.get(legacy_language, VIDEO_LANGUAGE_OPTIONS[0])
1076
+ generation_settings: dict[str, Any] = {}
962
1077
  if mode == "general":
963
1078
  selected = [str(channel["id"]) for channel in all_channels if channel.get("id")]
964
1079
  st.info(f"**Lote geral:** será criada exactamente uma tarefa para cada um dos {len(selected)} canais cadastrados. Cada canal receberá um tema, título e thumbnail próprios; não existe selecção parcial.")
@@ -999,6 +1114,10 @@ def render_new_video(page_title: str = "Criação de Vídeos"):
999
1114
  st.write(f"**{channel.get('name', 'Canal')}**")
1000
1115
  st.caption(f"{result.get('niche', '')} · {result.get('angle', '')}")
1001
1116
  st.text_area("Briefing gerado", value=result.get("topic", ""), key=f"new_video_general_topic_{channel['id']}", height=80)
1117
+ generation_settings = render_video_generation_settings(
1118
+ "new_video",
1119
+ current_language=str(st.session_state.get("video_language") or ""),
1120
+ )
1002
1121
  else:
1003
1122
  if not active_channels:
1004
1123
  st.warning("Não existem canais activos disponíveis para os modos de canal específico.")
@@ -1006,8 +1125,12 @@ def render_new_video(page_title: str = "Criação de Vídeos"):
1006
1125
  else:
1007
1126
  selected_one = st.selectbox("Canal", active_channels, format_func=lambda c: c["name"], key="new_video_channel")
1008
1127
  selected = [selected_one["id"]]
1009
- # Intentionally sits between Canal and Estilo wide, as requested.
1128
+ # Intentionally sits between Canal and the generation settings, as requested.
1010
1129
  render_channel_blueprint_panel(selected_one)
1130
+ generation_settings = render_video_generation_settings(
1131
+ "new_video",
1132
+ current_language=str(st.session_state.get("video_language") or ""),
1133
+ )
1011
1134
  topic = st.text_area(
1012
1135
  "Tópico ou briefing",
1013
1136
  value=st.session_state.get("new_video_topic", ""),
@@ -1034,8 +1157,13 @@ def render_new_video(page_title: str = "Criação de Vídeos"):
1034
1157
  meta = st.session_state["new_video_topic_meta"]
1035
1158
  st.caption(f"Origem: IA · Nicho: {meta.get('niche', '—')} · Ângulo: {meta.get('angle', '—')}")
1036
1159
 
1037
- wide_style_label = st.selectbox("Estilo wide", WIDE_STYLE_OPTIONS, key="new_video_style_wide")
1038
- style_ia = st.selectbox("Estilo IA", AI_STYLE_OPTIONS, key="new_video_style_ia") if wide_style_label == "full_ia" else ""
1160
+ if not generation_settings:
1161
+ generation_settings = render_video_generation_settings(
1162
+ "new_video",
1163
+ current_language=str(st.session_state.get("video_language") or ""),
1164
+ )
1165
+ wide_style_label = generation_settings["video_source"]
1166
+ style_ia = generation_settings.get("style_ia", "")
1039
1167
  music_path = ""
1040
1168
  music_source = ""
1041
1169
  if wide_style_label == "Apenas Música":
@@ -1162,12 +1290,8 @@ def render_new_video(page_title: str = "Criação de Vídeos"):
1162
1290
 
1163
1291
  with st.form("new_video_form"):
1164
1292
  quantity = st.number_input("Quantidade", min_value=1, max_value=100, value=1, disabled=mode != "same_channel")
1165
- legacy_language = st.session_state.get("video_language")
1166
- legacy_language_map = {"Português": "36 – Português (Brasil)", "English": "01 – Inglês", "Español": "41 – Espanhol (LatAm)"}
1167
- if legacy_language not in VIDEO_LANGUAGE_OPTIONS:
1168
- st.session_state["video_language"] = legacy_language_map.get(legacy_language, VIDEO_LANGUAGE_OPTIONS[0])
1169
- language = st.selectbox("Idioma", VIDEO_LANGUAGE_OPTIONS, key="video_language")
1170
- fmt = st.selectbox("Formato", ["wide", "shorts", "music"], key="new_video_format")
1293
+ language = generation_settings["script_language"]
1294
+ fmt = generation_settings["video_format"]
1171
1295
  submitted = st.form_submit_button("Criar tarefas", type="primary")
1172
1296
  if submitted:
1173
1297
  style = {"Pexels/Pixabay": "pexels", "full_ia": "full_ia", "Apenas Música": "music"}[wide_style_label]
@@ -1213,8 +1337,8 @@ def render_new_video(page_title: str = "Criação de Vídeos"):
1213
1337
  st.session_state["new_video_general_topics"] = {cid: {"topic": payload["topic"], "topic_source": payload.get("topic_source", "llm")} for cid, payload in payloads.items()}
1214
1338
  if len(payloads) == len(selected):
1215
1339
  batch_topic = "Lote geral — um vídeo independente por canal"
1216
- channel_payloads = {cid: {**payload, "language": language, "format": fmt, "style_wide": style, "style_ia": style_ia, "music_mode": style == "music", "background_mode": "none" if style == "music" else ("ai" if style == "full_ia" else "stock"), "music_path": music_path, "music_source": music_source} for cid, payload in payloads.items()}
1217
- batch = create_batch("general", selected, batch_topic, 1, {"language": language, "format": fmt, "style_wide": style, "style_ia": style_ia, "music_mode": style == "music", "background_mode": "none" if style == "music" else ("ai" if style == "full_ia" else "stock"), "music_path": music_path, "music_source": music_source, "topic_source": "llm", "channel_payloads": channel_payloads})
1340
+ channel_payloads = {cid: {**payload, "language": language, "format": fmt, "style_wide": style, "style_ia": style_ia, "music_mode": style == "music", "background_mode": "none" if style == "music" else ("ai" if style == "full_ia" else "stock"), "music_path": music_path, "music_source": music_source, "generation_settings": generation_settings} for cid, payload in payloads.items()}
1341
+ batch = create_batch("general", selected, batch_topic, 1, {"language": language, "format": fmt, "style_wide": style, "style_ia": style_ia, "music_mode": style == "music", "background_mode": "none" if style == "music" else ("ai" if style == "full_ia" else "stock"), "music_path": music_path, "music_source": music_source, "generation_settings": generation_settings, "topic_source": "llm", "channel_payloads": channel_payloads})
1218
1342
  tasks = create_tasks_for_batch(batch)
1219
1343
  st.success(f"Lote geral {batch['id']} criado com {len(tasks)} tarefas independentes, uma por canal.")
1220
1344
  else:
@@ -1230,8 +1354,8 @@ def render_new_video(page_title: str = "Criação de Vídeos"):
1230
1354
  except CreativeGenerationError as exc:
1231
1355
  st.warning(f"Título/thumbnail automáticos pendentes: {exc} A tarefa será criada com o tópico como título e sem ficheiro de thumbnail.")
1232
1356
  payload = {"topic": topic_value, "title": topic_value, "topic_source": "manual", "thumbnail_status": "pending_provider", "thumbnail_variants": [], "thumbnail_variant": {}, "thumbnail_prompt": "", "thumbnail_text": ""}
1233
- payload.update({"topic": topic_value, "topic_source": payload.get("topic_source") or ("llm" if st.session_state.get("new_video_topic_meta") else "manual"), "language": language, "format": fmt, "style_wide": style, "style_ia": style_ia, "music_mode": style == "music", "background_mode": "none" if style == "music" else ("ai" if style == "full_ia" else "stock"), "music_path": music_path, "music_source": music_source})
1234
- batch = create_batch(mode, selected, topic_value, quantity_value, {"language": language, "format": fmt, "style_wide": style, "style_ia": style_ia, "music_mode": style == "music", "background_mode": "none" if style == "music" else ("ai" if style == "full_ia" else "stock"), "music_path": music_path, "music_source": music_source, "topic_source": payload.get("topic_source", "manual"), "channel_payloads": {selected[0]: payload}})
1357
+ payload.update({"topic": topic_value, "topic_source": payload.get("topic_source") or ("llm" if st.session_state.get("new_video_topic_meta") else "manual"), "language": language, "format": fmt, "style_wide": style, "style_ia": style_ia, "music_mode": style == "music", "background_mode": "none" if style == "music" else ("ai" if style == "full_ia" else "stock"), "music_path": music_path, "music_source": music_source, "generation_settings": generation_settings})
1358
+ batch = create_batch(mode, selected, topic_value, quantity_value, {"language": language, "format": fmt, "style_wide": style, "style_ia": style_ia, "music_mode": style == "music", "background_mode": "none" if style == "music" else ("ai" if style == "full_ia" else "stock"), "music_path": music_path, "music_source": music_source, "generation_settings": generation_settings, "topic_source": payload.get("topic_source", "manual"), "channel_payloads": {selected[0]: payload}})
1235
1359
  tasks = create_tasks_for_batch(batch)
1236
1360
  st.success(f"Lote {batch['id']} criado com {len(tasks)} tarefa(s). Abra a subaba Vídeos para acompanhar.")
1237
1361
  with videos_tab:
@@ -1295,14 +1419,13 @@ def render_scripts():
1295
1419
  height=120,
1296
1420
  placeholder="Descreva o tema, a mensagem, o conflito ou a ideia musical que o Blueprint deve orientar.",
1297
1421
  )
1298
- language_options = VIDEO_LANGUAGE_OPTIONS
1299
- language = st.selectbox("Idioma", language_options, key="script_language")
1300
- structure_notes = st.text_area(
1301
- "Estrutura e notas opcionais",
1302
- key="script_structure_notes",
1303
- height=90,
1304
- placeholder="Ex.: 6 cenas, narração documental, refrão repetível, atmosfera sombria…",
1422
+ legacy_script_language = str(st.session_state.get("script_language") or "")
1423
+ script_settings = render_video_generation_settings(
1424
+ "pipeline_scripts",
1425
+ current_language=legacy_script_language if legacy_script_language in VIDEO_LANGUAGE_OPTIONS else "",
1305
1426
  )
1427
+ language = script_settings["script_language"]
1428
+ structure_notes = script_settings["script_structure_notes"]
1306
1429
  generate_col, clear_col = st.columns([1.4, 1])
1307
1430
  with generate_col:
1308
1431
  generate_clicked = st.button("Gerar com IA a partir do Blueprint", type="primary", use_container_width=True, key="generate_script_document")
@@ -1324,6 +1447,7 @@ def render_scripts():
1324
1447
  channel=selected_channel or {},
1325
1448
  blueprint=selected_blueprint,
1326
1449
  structure_notes=structure_notes,
1450
+ generation_settings=script_settings,
1327
1451
  )
1328
1452
  st.session_state["script_draft"] = generated
1329
1453
  st.session_state["script_draft_title"] = generated["title"]
@@ -1356,6 +1480,7 @@ def render_scripts():
1356
1480
  "content": draft_content,
1357
1481
  "document_type": "video_script" if document_type == "Roteiro de vídeo" else "music_lyrics",
1358
1482
  "language": language,
1483
+ "generation_settings": script_settings,
1359
1484
  "channel_id": str((selected_channel or {}).get("id") or ""),
1360
1485
  "channel_name": str((selected_channel or {}).get("name") or "Documento independente"),
1361
1486
  "blueprint_id": str(selected_blueprint.get("id") or selected_blueprint_id or ""),
@@ -154,6 +154,7 @@ def create_tasks_for_batch(batch: dict[str, Any]) -> list[dict[str, Any]]:
154
154
  "music_path": payload.get("music_path", options.get("music_path", "")),
155
155
  "music_source": payload.get("music_source", options.get("music_source", "")),
156
156
  "background_mode": payload.get("background_mode", options.get("background_mode", "stock")),
157
+ "generation_settings": payload.get("generation_settings", options.get("generation_settings", {})),
157
158
  "blueprint_id": payload.get("blueprint_id") or channel.get("default_blueprint_id") or channel.get("blueprint_id", ""),
158
159
  "blueprint_name": payload.get("blueprint_name", ""),
159
160
  "voice": payload.get("voice") or channel.get("default_voice") or channel.get("voice", ""),
@@ -24,6 +24,7 @@ def generate_script_document(
24
24
  channel: dict[str, Any] | None = None,
25
25
  blueprint: dict[str, Any] | None = None,
26
26
  structure_notes: str = "",
27
+ generation_settings: dict[str, Any] | None = None,
27
28
  ) -> dict[str, Any]:
28
29
  """Generate an editable Markdown document from the configured LLM.
29
30
 
@@ -38,6 +39,7 @@ def generate_script_document(
38
39
 
39
40
  channel_context_value = channel_context(channel or {}, blueprint or {})
40
41
  blueprint_payload = blueprint or {}
42
+ generation_settings_payload = generation_settings or {}
41
43
  if normalized_type == "video_script":
42
44
  output_requirements = {
43
45
  "content": "roteiro completo em Markdown, com título, gancho, cenas, narração e indicações visuais/sonoras",
@@ -69,6 +71,7 @@ def generate_script_document(
69
71
  "channel": channel_context_value,
70
72
  "blueprint": blueprint_payload,
71
73
  "structure_notes": structure_notes.strip(),
74
+ "generation_settings": generation_settings_payload,
72
75
  "output_requirements": output_requirements,
73
76
  },
74
77
  ensure_ascii=False,
@@ -87,5 +90,6 @@ def generate_script_document(
87
90
  "blueprint_name": str(blueprint_payload.get("name") or "SEM BLUEPRINT CONFIGURADO"),
88
91
  "channel_id": str((channel or {}).get("id") or ""),
89
92
  "channel_name": str((channel or {}).get("name") or "Documento independente"),
93
+ "generation_settings": generation_settings_payload,
90
94
  "generated_by": "configured_llm",
91
95
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danhachuel/thunderbolt",
3
- "version": "0.2.59",
3
+ "version": "0.2.60",
4
4
  "description": "Thunderbolt — interface local para operação de canais faceless e motor MoneyPrinterTurbo",
5
5
  "main": "scripts/cli.mjs",
6
6
  "type": "module",