@danhachuel/thunderbolt 0.4.77 → 0.4.78

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
@@ -69,7 +69,7 @@ from hermes_ui.update_manager import check_version, restart_current_process, upd
69
69
  from hermes_ui.script_documents import list_script_documents, read_script_document, save_script_document, script_storage_path
70
70
  from hermes_ui.script_generation import generate_script_document
71
71
  from hermes_ui.voice_preview import DEFAULT_SAMPLE, load_preview_file, synthesize_preview
72
- from hermes_ui.elevenlabs_voices import ElevenLabsVoicesError, fetch_personal_voices, personal_voice_options
72
+ from hermes_ui.elevenlabs_voices import ElevenLabsVoicesError, cached_personal_voices, fetch_personal_voices, personal_voice_options
73
73
  from hermes_ui.thumbnail_generation import ThumbnailGenerationError, generate_thumbnail_image
74
74
  from hermes_ui.thumbnails import (
75
75
  generate_thumbnail_for_task,
@@ -3324,7 +3324,7 @@ def render_music_creation():
3324
3324
  ("music_task_prompt", "prompt"),
3325
3325
  ):
3326
3326
  st.session_state[state_key] = str(generated_fields.get(generated_key) or "")
3327
- provider_label = st.selectbox("Provider de geração musical", ["Suno AI", "Google Lyria"], key="music_task_provider")
3327
+ provider_label = st.selectbox("Provider de geração musical", ["Suno AI", "Google Lyria", "Eleven Music"], key="music_task_provider")
3328
3328
  theme = st.text_input("Tema / assunto principal", key="music_task_theme", placeholder="Ex.: uma viagem nocturna pela costa portuguesa")
3329
3329
  title = st.text_input("Título da música", key="music_task_title")
3330
3330
  language = st.selectbox(
@@ -3336,6 +3336,26 @@ def render_music_creation():
3336
3336
  )
3337
3337
  genre = st.selectbox("Género musical", list(MUSIC_GENRES), key="music_task_genre")
3338
3338
  vocal = st.selectbox("Vocal", list(MUSIC_VOCAL_OPTIONS), key="music_task_vocal")
3339
+ eleven_voice_id = ""
3340
+ eleven_voice_gender = ""
3341
+ if provider_label == "Eleven Music":
3342
+ cached_voices = cached_personal_voices()
3343
+ if not cached_voices:
3344
+ st.info("Sincronize primeiro as vozes pessoais em Vozes Personalizadas. O Eleven Music não disponibiliza voz de coral para este selector.")
3345
+ else:
3346
+ voice_options = {str(item.get("voice_id")): item for item in cached_voices if item.get("voice_id")}
3347
+ selected_voice_id = st.selectbox(
3348
+ "Voz personalizada (Eleven Music)",
3349
+ [""] + list(voice_options),
3350
+ format_func=lambda item: "Sem voz personalizada" if not item else str(voice_options[item].get("name") or item),
3351
+ key="music_task_eleven_voice",
3352
+ )
3353
+ eleven_voice_id = selected_voice_id
3354
+ selected_voice = voice_options.get(selected_voice_id, {})
3355
+ labels = selected_voice.get("labels") if isinstance(selected_voice.get("labels"), dict) else {}
3356
+ raw_gender = str(labels.get("gender") or labels.get("sex") or "").strip().casefold()
3357
+ eleven_voice_gender = "female" if raw_gender in {"female", "feminino", "woman", "mulher"} else ("male" if raw_gender in {"male", "masculino", "man", "homem"} else "")
3358
+ st.caption(f"Tipo de voz detectado automaticamente: {eleven_voice_gender or 'não identificado'}. Vozes de coral não estão disponíveis no Eleven Music.")
3339
3359
  references = st.text_area(
3340
3360
  "Referências culturais, paisagens, clima ou artistas similares (opcional)",
3341
3361
  key="music_task_references",
@@ -3380,6 +3400,8 @@ def render_music_creation():
3380
3400
  vocal=vocal,
3381
3401
  references=references,
3382
3402
  theme=theme,
3403
+ voice_id=eleven_voice_id,
3404
+ voice_gender=eleven_voice_gender,
3383
3405
  )
3384
3406
  st.success(f"Música criada no Music Backlog: {task['title']}")
3385
3407
  st.rerun()
@@ -126,3 +126,9 @@ def personal_voice_options(settings: dict[str, Any]) -> dict[str, str]:
126
126
  for voice in cache.get("voices", [])
127
127
  if isinstance(voice, dict) and voice.get("voice_id")
128
128
  }
129
+
130
+
131
+ def cached_personal_voices() -> list[dict[str, Any]]:
132
+ """Return the locally cached personal voices without making a network call."""
133
+ cache = _read_cache()
134
+ return [voice for voice in (cache or {}).get("voices", []) if isinstance(voice, dict)]
@@ -155,6 +155,8 @@ def create_music_task(
155
155
  vocal: str = "",
156
156
  references: str = "",
157
157
  theme: str = "",
158
+ voice_id: str = "",
159
+ voice_gender: str = "",
158
160
  ) -> dict[str, Any]:
159
161
  """Enqueue one audio-only generation; no video task or video worker is used."""
160
162
  cleaned_prompt = str(prompt or "").strip()
@@ -164,7 +166,7 @@ def create_music_task(
164
166
  task = {
165
167
  "id": f"music_{uuid.uuid4().hex[:12]}",
166
168
  "kind": "audio_generation",
167
- "provider": "lyria" if str(provider).strip().casefold() == "google lyria" else "suno",
169
+ "provider": "lyria" if str(provider).strip().casefold() == "google lyria" else ("eleven_music" if str(provider).strip().casefold() == "eleven music" else "suno"),
168
170
  "model": str(model or "").strip(),
169
171
  "title": str(title or "Música sem título").strip() or "Música sem título",
170
172
  "prompt": cleaned_prompt,
@@ -173,6 +175,8 @@ def create_music_task(
173
175
  "vocal": str(vocal or "").strip(),
174
176
  "references": str(references or "").strip(),
175
177
  "theme": str(theme or "").strip(),
178
+ "voice_id": str(voice_id or "").strip(),
179
+ "voice_gender": str(voice_gender or "").strip().casefold(),
176
180
  "duration_seconds": 120,
177
181
  "state": "to_do",
178
182
  "stage": "music_generation",
@@ -258,6 +262,33 @@ def request_lyria_generation(settings: dict[str, Any], prompt: str, title: str =
258
262
  return {"ok": False, "message": "Não foi possível contactar Google Lyria.", "data": {}}
259
263
 
260
264
 
265
+ def request_eleven_music_generation(settings: dict[str, Any], prompt: str, title: str = "", model: str = "music_v2", *, voice_id: str = "", voice_gender: str = "") -> dict[str, Any]:
266
+ """Generate music through Eleven Music's synchronous Music API."""
267
+ api_key = str(settings.get("elevenlabs_api_key") or "").strip()
268
+ if not api_key:
269
+ return {"ok": False, "message": "Configure a API Key do ElevenLabs em Configuração API antes de solicitar Eleven Music.", "data": {}}
270
+ clean_prompt = str(prompt or "").strip()
271
+ if voice_id:
272
+ gender = voice_gender if voice_gender in {"male", "female"} else "unspecified"
273
+ clean_prompt = f"{clean_prompt}\nVocal principal: voz personalizada ElevenLabs seleccionada ({gender}). Não usar coro."
274
+ try:
275
+ response = requests.post(
276
+ "https://api.elevenlabs.io/v1/music",
277
+ params={"output_format": "mp3_44100_128"},
278
+ headers={"xi-api-key": api_key, "Content-Type": "application/json"},
279
+ json={"prompt": clean_prompt[:4100], "model_id": model or "music_v2", "force_instrumental": False},
280
+ timeout=300,
281
+ )
282
+ if response.status_code >= 400:
283
+ return {"ok": False, "message": f"Eleven Music devolveu HTTP {response.status_code}.", "data": {"status_code": response.status_code}}
284
+ if not response.content:
285
+ return {"ok": False, "message": "Eleven Music não devolveu áudio.", "data": {}}
286
+ output = store_music_file(f"{title or 'eleven-music-generated'}.mp3", response.content)
287
+ return {"ok": True, "message": "Música gerada por Eleven Music.", "data": {"audio_path": str(output), "model": model or "music_v2", "voice_id": voice_id, "voice_gender": voice_gender}}
288
+ except (requests.RequestException, OSError, ValueError) as exc:
289
+ return {"ok": False, "message": f"Não foi possível contactar Eleven Music: {exc}", "data": {}}
290
+
291
+
261
292
  def run_music_task(task_id: str, settings: dict[str, Any]) -> dict[str, Any] | None:
262
293
  """Run exactly one queued audio generation and persist only its audio artefact."""
263
294
  task = next((record for record in list_music_tasks() if str(record.get("id") or "") == str(task_id)), None)
@@ -268,6 +299,9 @@ def run_music_task(task_id: str, settings: dict[str, Any]) -> dict[str, Any] | N
268
299
  if provider == "lyria":
269
300
  result = request_lyria_generation(settings, str(task.get("prompt") or ""), str(task.get("title") or ""), str(task.get("model") or ""))
270
301
  audio_path = str((result.get("data") or {}).get("audio_path") or "")
302
+ elif provider == "eleven_music":
303
+ result = request_eleven_music_generation(settings, str(task.get("prompt") or ""), str(task.get("title") or ""), str(task.get("model") or "music_v2"), voice_id=str(task.get("voice_id") or ""), voice_gender=str(task.get("voice_gender") or ""))
304
+ audio_path = str((result.get("data") or {}).get("audio_path") or "")
271
305
  else:
272
306
  result = request_suno_generation(
273
307
  settings,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danhachuel/thunderbolt",
3
- "version": "0.4.77",
3
+ "version": "0.4.78",
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",