@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 +24 -2
- package/hermes_ui/elevenlabs_voices.py +6 -0
- package/hermes_ui/music.py +35 -1
- package/package.json +1 -1
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)]
|
package/hermes_ui/music.py
CHANGED
|
@@ -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