@danhachuel/thunderbolt 0.3.78 → 0.3.80

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
@@ -5397,11 +5397,25 @@ def _render_api_test_feedback(settings: dict[str, Any], test_key: str, result: d
5397
5397
  st.error(ui_text("Último teste: chamada falhou", language) + suffix)
5398
5398
 
5399
5399
 
5400
- def _render_api_test_control(settings: dict[str, Any], test_key: str, callback: Any, *, widget_key: str) -> None:
5401
- """Render a form-safe diagnostic button and persist its redacted result."""
5400
+ def _render_api_test_control(
5401
+ settings: dict[str, Any],
5402
+ test_key: str,
5403
+ callback: Any,
5404
+ *,
5405
+ widget_key: str,
5406
+ persist_callback: Any = None,
5407
+ ) -> None:
5408
+ """Render a form-safe diagnostic button and persist its redacted result.
5409
+
5410
+ ``persist_callback`` keeps a card whose fields live in the global form
5411
+ consistent: testing it must not appear successful while its values remain
5412
+ only in the browser session.
5413
+ """
5402
5414
  if st.form_submit_button("Testar chamada API", use_container_width=True, key=widget_key):
5403
5415
  with st.spinner(ui_text("A testar chamada API…", current_ui_language())):
5404
5416
  try:
5417
+ if persist_callback is not None:
5418
+ persist_callback()
5405
5419
  result = callback()
5406
5420
  except Exception:
5407
5421
  result = {"status": "error", "message": "A chamada de diagnóstico falhou."}
@@ -5832,11 +5846,23 @@ def render_settings():
5832
5846
  azure_speech_key = text_setting("Azure Speech key", "azure_speech_key", secret=True)
5833
5847
  _render_credential_status(azure_speech_key)
5834
5848
  azure_speech_region = text_setting("Azure Speech region", "azure_speech_region")
5849
+
5850
+ def save_azure_speech() -> None:
5851
+ settings.update({
5852
+ "azure_speech_key": azure_speech_key.strip(),
5853
+ "azure_speech_region": azure_speech_region.strip(),
5854
+ })
5855
+ write_json("settings.json", settings)
5856
+
5857
+ if st.form_submit_button("Guardar Azure Speech", type="primary", use_container_width=True, key="save_azure_speech"):
5858
+ save_azure_speech()
5859
+ st.success("Azure Speech guardado.")
5835
5860
  _render_api_test_control(
5836
5861
  settings,
5837
5862
  "voice:azure_speech",
5838
5863
  lambda: test_voice_provider("azure_speech", {"azure_speech_key": azure_speech_key, "azure_speech_region": azure_speech_region}),
5839
5864
  widget_key="api_test_voice_azure",
5865
+ persist_callback=save_azure_speech,
5840
5866
  )
5841
5867
 
5842
5868
  with st.container(border=True):
@@ -807,6 +807,8 @@ def _run_video_helper(task: dict[str, Any]) -> Path:
807
807
  "MPT_PEXELS_API_KEYS": json.dumps(source_keys, ensure_ascii=False) if route == "pexels" and source_keys else "",
808
808
  "MPT_PIXABAY_API_KEY": source_keys[0] if route == "pixabay" and source_keys else "",
809
809
  "MPT_PIXABAY_API_KEYS": json.dumps(source_keys, ensure_ascii=False) if route == "pixabay" and source_keys else "",
810
+ "MPT_AZURE_SPEECH_KEY": str(settings.get("azure_speech_key") or "").strip(),
811
+ "MPT_AZURE_SPEECH_REGION": str(settings.get("azure_speech_region") or "").strip(),
810
812
  }
811
813
  for key, value in env_values.items():
812
814
  if value:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danhachuel/thunderbolt",
3
- "version": "0.3.78",
3
+ "version": "0.3.80",
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/scripts/cli.mjs CHANGED
@@ -193,6 +193,11 @@ const proxy = http.createServer((request, response) => {
193
193
  });
194
194
 
195
195
  proxy.on("upgrade", (request, clientSocket, head) => {
196
+ // Clientes remotos podem encerrar o WebSocket antes de o Streamlit concluir
197
+ // a resposta. Tratar ECONNRESET impede que o Node termine o launcher.
198
+ clientSocket.on("error", () => {
199
+ if (!clientSocket.destroyed) clientSocket.destroy();
200
+ });
196
201
  const upstreamSocket = net.connect(backendPort, "127.0.0.1", () => {
197
202
  const headers = Object.entries(request.headers)
198
203
  .map(([name, value]) => `${name}: ${Array.isArray(value) ? value.join(", ") : value}`)
@@ -201,10 +206,14 @@ proxy.on("upgrade", (request, clientSocket, head) => {
201
206
  if (head.length) upstreamSocket.write(head);
202
207
  clientSocket.pipe(upstreamSocket).pipe(clientSocket);
203
208
  });
204
- upstreamSocket.on("error", () => clientSocket.destroy());
209
+ upstreamSocket.on("error", () => {
210
+ if (!clientSocket.destroyed) clientSocket.destroy();
211
+ });
205
212
  });
206
213
 
207
- proxy.listen(publicPort, "127.0.0.1", () => {
214
+ // O backend Streamlit fica em 127.0.0.1; apenas o proxy escuta em todas as
215
+ // interfaces para que o encaminhamento seguro do ambiente consiga alcançá-lo.
216
+ proxy.listen(publicPort, () => {
208
217
  console.log(`Thunderbolt: interface disponível em http://localhost:${publicPort}/`);
209
218
  });
210
219
  const worker = spawn(python, ["-m", "hermes_ui.automation_worker"], {
@@ -203,6 +203,27 @@ def _replace_config_value(text: str, key: str, value: object) -> str:
203
203
  return pattern.sub(lambda match: f"{match.group(1)}{encoded}", text, count=1)
204
204
 
205
205
 
206
+ def _replace_toml_section_value(text: str, section: str, key: str, value: object) -> str:
207
+ """Replace or append a scalar value inside one TOML section."""
208
+ encoded = json.dumps(value, ensure_ascii=False)
209
+ section_pattern = re.compile(
210
+ rf"(?ms)^\[{re.escape(section)}\][ \t]*(?:\n|$)(.*?)(?=^\[|\Z)"
211
+ )
212
+ section_match = section_pattern.search(text)
213
+ if section_match is None:
214
+ separator = "" if not text.strip() else "\n\n"
215
+ return f"{text.rstrip()}{separator}[{section}]\n{key} = {encoded}\n"
216
+ body = section_match.group(1)
217
+ value_pattern = re.compile(rf"(?m)^({re.escape(key)}\s*=\s*).*$")
218
+ if value_pattern.search(body):
219
+ updated_body = value_pattern.sub(
220
+ lambda match: f"{match.group(1)}{encoded}", body, count=1
221
+ )
222
+ else:
223
+ updated_body = f"{body.rstrip()}\n{key} = {encoded}\n"
224
+ return f"{text[:section_match.start(1)]}{updated_body}{text[section_match.end(1):]}"
225
+
226
+
206
227
  def _has_configured_value(value: str) -> bool:
207
228
  """Treat empty strings and whitespace-only key arrays as unconfigured."""
208
229
  if not value:
@@ -237,13 +258,15 @@ def apply_environment_config(config_path: Path) -> None:
237
258
  model_name = os.environ.get("MPT_LLM_MODEL_NAME", "").strip()
238
259
  pexels_key = os.environ.get("MPT_PEXELS_API_KEY", "").strip()
239
260
  pixabay_key = os.environ.get("MPT_PIXABAY_API_KEY", "").strip()
261
+ azure_speech_key = os.environ.get("MPT_AZURE_SPEECH_KEY", "").strip()
262
+ azure_speech_region = os.environ.get("MPT_AZURE_SPEECH_REGION", "").strip()
240
263
  pexels_keys = _parse_string_list(os.environ.get("MPT_PEXELS_API_KEYS", ""))
241
264
  pixabay_keys = _parse_string_list(os.environ.get("MPT_PIXABAY_API_KEYS", ""))
242
265
  if not pexels_keys and pexels_key:
243
266
  pexels_keys = [pexels_key]
244
267
  if not pixabay_keys and pixabay_key:
245
268
  pixabay_keys = [pixabay_key]
246
- if not any((provider, llm_key, base_url, model_name, pexels_keys, pixabay_keys)):
269
+ if not any((provider, llm_key, base_url, model_name, pexels_keys, pixabay_keys, azure_speech_key, azure_speech_region)):
247
270
  return
248
271
 
249
272
  text = config_path.read_text(encoding="utf-8")
@@ -268,6 +291,12 @@ def apply_environment_config(config_path: Path) -> None:
268
291
  if pixabay_keys:
269
292
  text = _replace_config_value(text, "pixabay_api_keys", pixabay_keys)
270
293
  changes.append("pixabay_api_keys")
294
+ if azure_speech_key:
295
+ text = _replace_toml_section_value(text, "azure", "speech_key", azure_speech_key)
296
+ changes.append("azure.speech_key")
297
+ if azure_speech_region:
298
+ text = _replace_toml_section_value(text, "azure", "speech_region", azure_speech_region)
299
+ changes.append("azure.speech_region")
271
300
  _atomic_write_text(config_path, text)
272
301
  log("updated configuration fields: " + ", ".join(changes))
273
302