@danhachuel/thunderbolt 0.3.94 → 0.3.96
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 +62 -21
- package/hermes_ui/domain.py +31 -0
- package/hermes_ui/music.py +137 -0
- package/hermes_ui/storage.py +3 -0
- package/package.json +1 -1
package/app/main.py
CHANGED
|
@@ -22,7 +22,7 @@ try:
|
|
|
22
22
|
except (OSError, json.JSONDecodeError):
|
|
23
23
|
APP_VERSION = ""
|
|
24
24
|
|
|
25
|
-
from hermes_ui.domain import STAGES, create_batch, create_channel, create_tasks_for_batch, delete_channel, delete_task, pipeline_summary, set_channel_defaults, transition_task, update_channel, update_channel_video
|
|
25
|
+
from hermes_ui.domain import STAGES, create_batch, create_channel, create_tasks_for_batch, delete_channel, delete_task, pipeline_summary, retry_task_with_current_settings, set_channel_defaults, transition_task, update_channel, update_channel_video
|
|
26
26
|
from hermes_ui.drafts import list_drafts, save_draft
|
|
27
27
|
from hermes_ui.automation_worker import load_worker_status
|
|
28
28
|
from hermes_ui.pipeline_worker import load_pipeline_worker_status, recover_stale_tasks, STALE_TASK_SECONDS, WORKER_HEARTBEAT_TIMEOUT_SECONDS
|
|
@@ -41,7 +41,7 @@ from hermes_ui.mcp_server import server_status, start_server, stop_server
|
|
|
41
41
|
from hermes_ui.material_sources import apply_material_source_cards_to_settings, ensure_material_source_cards, material_source_catalog, material_source_definition, new_material_card, normalize_material_card, selected_material_source
|
|
42
42
|
from hermes_ui.llm_providers import LLM_CARDS_KEY, LLM_PROVIDER_CATALOG, apply_llm_cards_to_settings, ensure_llm_provider_cards, new_llm_card, normalize_llm_card, provider_definition, test_llm_provider_card, stamp_test_result
|
|
43
43
|
from hermes_ui.media_providers import FULL_IA_VIDEO_PROVIDER_CODES, MEDIA_CARDS_KEY, MEDIA_IMAGE_ACTIVE_CARD_KEY, MEDIA_VIDEO_ACTIVE_CARD_KEY, apply_media_provider_cards_to_settings, ensure_media_provider_cards, media_cards_for_pool, media_provider_catalog, media_provider_definition, new_media_card, normalize_media_card
|
|
44
|
-
from hermes_ui.music import list_music_files, materialize_suno_audio, request_suno_generation, store_music_file, store_voiceover_file
|
|
44
|
+
from hermes_ui.music import create_music_task, list_music_files, list_music_tasks, materialize_suno_audio, request_suno_generation, run_music_task, store_music_file, store_voiceover_file, transition_music_task
|
|
45
45
|
from hermes_ui.media_downloader import AUDIO_FORMATS, VIDEO_CONTAINERS, VIDEO_QUALITY_OPTIONS, MediaDownloadError, build_download_options, clear_media_download_history, dependency_status, download_media, list_media_downloads, media_download_file
|
|
46
46
|
from hermes_ui.notifications import clear_notifications, list_notifications, mark_all_notifications_read, mark_notification_read, notification_event_catalog, notification_preferences, record_notification, reconcile_persisted_notifications, save_notification_preferences, unread_notification_count
|
|
47
47
|
from hermes_ui.influencers import BACKEND_OPTIONS, DOCUMENT_EXTENSIONS, IMAGE_EXTENSIONS, backend_name, backend_status, get_repository, test_backend
|
|
@@ -2603,8 +2603,27 @@ def render_new_video(page_title: str = "Criação de Vídeos", prefix: str = "ne
|
|
|
2603
2603
|
|
|
2604
2604
|
|
|
2605
2605
|
def render_music_creation():
|
|
2606
|
-
"""
|
|
2607
|
-
|
|
2606
|
+
"""Create audio-only items for the independent Suno/Lyria Music Backlog."""
|
|
2607
|
+
st.title("Criação de Músicas")
|
|
2608
|
+
st.caption("Crie apenas áudio. Os pedidos desta página não criam vídeo, não usam o MoneyPrinterTurbo e não entram no Backlog Vídeos.")
|
|
2609
|
+
settings = read_json("settings.json", {})
|
|
2610
|
+
with st.form("create_music_task_form"):
|
|
2611
|
+
provider_label = st.selectbox("Provider de geração musical", ["Suno AI", "Google Lyria"], key="music_task_provider")
|
|
2612
|
+
title = st.text_input("Título da música", key="music_task_title")
|
|
2613
|
+
prompt = st.text_area("Prompt musical", placeholder="Instrumental cinematográfico, calmo, sem voz...", key="music_task_prompt", height=150)
|
|
2614
|
+
lyria_model = ""
|
|
2615
|
+
if provider_label == "Google Lyria":
|
|
2616
|
+
models = ["lyria-3-clip-preview", "lyria-3-pro-preview"]
|
|
2617
|
+
configured = str(settings.get("lyria_model") or models[0])
|
|
2618
|
+
lyria_model = st.selectbox("Modelo Lyria", models, index=models.index(configured) if configured in models else 0, key="music_task_lyria_model")
|
|
2619
|
+
submitted = st.form_submit_button("Adicionar ao Music Backlog", type="primary", use_container_width=True)
|
|
2620
|
+
if submitted:
|
|
2621
|
+
try:
|
|
2622
|
+
task = create_music_task(provider_label, prompt, title, lyria_model)
|
|
2623
|
+
st.success(f"Música adicionada ao Music Backlog: {task['title']}")
|
|
2624
|
+
st.rerun()
|
|
2625
|
+
except ValueError as exc:
|
|
2626
|
+
st.error(str(exc))
|
|
2608
2627
|
|
|
2609
2628
|
|
|
2610
2629
|
def render_scripts():
|
|
@@ -3637,7 +3656,7 @@ def render_videos():
|
|
|
3637
3656
|
st.caption("Acompanhamento dos vídeos criados, estados da pipeline e controlos de execução.")
|
|
3638
3657
|
st.caption(f"Os vídeos são guardados em `{STORAGE / 'videos'}`.")
|
|
3639
3658
|
_render_pipeline_progress_panel()
|
|
3640
|
-
tasks =
|
|
3659
|
+
tasks = load_video_tasks_for_catalog()
|
|
3641
3660
|
if not tasks:
|
|
3642
3661
|
st.info("Nenhum vídeo criado.")
|
|
3643
3662
|
return
|
|
@@ -3697,12 +3716,14 @@ def render_videos():
|
|
|
3697
3716
|
|
|
3698
3717
|
|
|
3699
3718
|
def render_music_backlog() -> None:
|
|
3700
|
-
"""Render the
|
|
3719
|
+
"""Render the independent audio-only queue; it never reads the video pipeline."""
|
|
3701
3720
|
st.subheader("Music Backlog")
|
|
3702
|
-
st.caption("
|
|
3721
|
+
st.caption("Fila independente de geração de áudio por Suno AI ou Google Lyria. Não inclui tarefas, worker ou progresso de vídeo.")
|
|
3703
3722
|
st.caption(f"As músicas são guardadas em `{STORAGE / 'music'}`.")
|
|
3704
|
-
|
|
3705
|
-
|
|
3723
|
+
tasks = list_music_tasks()
|
|
3724
|
+
active = [task for task in tasks if str(task.get("state") or "") == "doing"]
|
|
3725
|
+
if active:
|
|
3726
|
+
st.info(f"Geração musical em execução · {len(active)} tarefa(s) de áudio.")
|
|
3706
3727
|
if not tasks:
|
|
3707
3728
|
st.info("Nenhuma música criada.")
|
|
3708
3729
|
return
|
|
@@ -3715,10 +3736,10 @@ def render_music_backlog() -> None:
|
|
|
3715
3736
|
with st.container(border=True):
|
|
3716
3737
|
cols = st.columns([2.2, 1, 1, 1.2, 1.8])
|
|
3717
3738
|
with cols[0]:
|
|
3718
|
-
st.write(f"**{task.get('title') or
|
|
3719
|
-
st.caption(f"
|
|
3720
|
-
st.caption(
|
|
3721
|
-
music_path = str(task.get("
|
|
3739
|
+
st.write(f"**{task.get('title') or 'Música sem título'}**")
|
|
3740
|
+
st.caption(f"Provider: {'Google Lyria' if str(task.get('provider') or '').casefold() == 'lyria' else 'Suno AI'}")
|
|
3741
|
+
st.caption(str(task.get('id') or ''))
|
|
3742
|
+
music_path = str(task.get("audio_path") or "").strip()
|
|
3722
3743
|
if music_path and Path(music_path).is_file():
|
|
3723
3744
|
music_file = Path(music_path)
|
|
3724
3745
|
st.success("Música pronta; pode continuar para o destino configurado.")
|
|
@@ -3735,22 +3756,29 @@ def render_music_backlog() -> None:
|
|
|
3735
3756
|
else:
|
|
3736
3757
|
st.caption("A música será disponibilizada quando a etapa de geração terminar.")
|
|
3737
3758
|
with cols[1]:
|
|
3738
|
-
st.caption("
|
|
3739
|
-
st.write(
|
|
3759
|
+
st.caption("Tipo")
|
|
3760
|
+
st.write("Áudio")
|
|
3740
3761
|
with cols[2]:
|
|
3741
|
-
st.write(
|
|
3762
|
+
st.write({"music_generation": "Geração musical", "completed": "Concluída", "failed": "Falha"}.get(str(task.get("stage") or ""), "Na fila"))
|
|
3742
3763
|
with cols[3]:
|
|
3743
|
-
|
|
3764
|
+
state = str(task.get("state") or "unknown").strip().lower()
|
|
3765
|
+
progress = _video_task_progress(task)
|
|
3766
|
+
st.caption("Estado")
|
|
3767
|
+
st.write(state or "—")
|
|
3768
|
+
st.caption(VIDEO_TASK_STATE_LABELS.get(state, state.replace("_", " ").capitalize() or "Desconhecido"))
|
|
3769
|
+
st.progress(progress, text=f"{progress}%")
|
|
3770
|
+
if task.get("error"):
|
|
3771
|
+
st.error(str(task.get("error") or "")[:500])
|
|
3744
3772
|
with cols[4]:
|
|
3745
3773
|
state = str(task.get("state") or "")
|
|
3746
3774
|
start_col, stop_col = st.columns(2)
|
|
3747
3775
|
with start_col:
|
|
3748
3776
|
if st.button("Start", key=f"music_backlog_start_{task['id']}", use_container_width=True, disabled=state not in {"to_do", "blocked", "failed"}):
|
|
3749
|
-
|
|
3777
|
+
run_music_task(str(task["id"]), read_json("settings.json", {}))
|
|
3750
3778
|
st.rerun()
|
|
3751
3779
|
with stop_col:
|
|
3752
3780
|
if st.button("Stop", key=f"music_backlog_stop_{task['id']}", use_container_width=True, disabled=state != "doing"):
|
|
3753
|
-
|
|
3781
|
+
transition_music_task(str(task["id"]), "blocked")
|
|
3754
3782
|
st.rerun()
|
|
3755
3783
|
|
|
3756
3784
|
|
|
@@ -4026,7 +4054,7 @@ def render_automation():
|
|
|
4026
4054
|
|
|
4027
4055
|
st.divider()
|
|
4028
4056
|
st.subheader("Vídeos cadastrados")
|
|
4029
|
-
st.caption("Start retoma as etapas já concluídas e só gera novamente o que ainda não estiver pronto. Apagar remove o card da fila após confirmação e preserva os artefactos locais.")
|
|
4057
|
+
st.caption("Start retoma as etapas já concluídas e só gera novamente o que ainda não estiver pronto. Em tarefas falhadas ou bloqueadas, a nova tentativa lê as chaves, prioridades e configurações actualmente guardadas. Apagar remove o card da fila após confirmação e preserva os artefactos locais.")
|
|
4030
4058
|
tasks = load_video_tasks_for_catalog()
|
|
4031
4059
|
if not tasks:
|
|
4032
4060
|
st.info("Ainda não existem vídeos cadastrados.")
|
|
@@ -4046,7 +4074,10 @@ def render_automation():
|
|
|
4046
4074
|
start_col, stop_col, delete_col = st.columns(3)
|
|
4047
4075
|
with start_col:
|
|
4048
4076
|
if st.button("Start", key=f"automation_start_{task['id']}", use_container_width=True, disabled=state not in {"to_do", "blocked", "failed"}):
|
|
4049
|
-
|
|
4077
|
+
if state in {"failed", "blocked"}:
|
|
4078
|
+
retry_task_with_current_settings(task["id"])
|
|
4079
|
+
else:
|
|
4080
|
+
transition_task(task["id"], "doing")
|
|
4050
4081
|
st.rerun()
|
|
4051
4082
|
with stop_col:
|
|
4052
4083
|
if st.button("Stop", key=f"automation_stop_{task['id']}", use_container_width=True, disabled=state != "doing"):
|
|
@@ -6102,6 +6133,15 @@ def render_settings():
|
|
|
6102
6133
|
widget_key="api_test_voice_suno",
|
|
6103
6134
|
)
|
|
6104
6135
|
|
|
6136
|
+
with st.container(border=True):
|
|
6137
|
+
st.markdown("#### Google Lyria — geração musical")
|
|
6138
|
+
st.caption("Google Lyria gera apenas áudio através da API Gemini Interactions; não usa a pipeline de vídeo.")
|
|
6139
|
+
lyria_api_key = text_setting("Google Lyria API key", "lyria_api_key", secret=True, help_text="Chave da Gemini API com acesso ao modelo Lyria. O valor é guardado apenas localmente.")
|
|
6140
|
+
_render_credential_status(lyria_api_key)
|
|
6141
|
+
lyria_models = ["lyria-3-clip-preview", "lyria-3-pro-preview"]
|
|
6142
|
+
saved_lyria_model = str(settings.get("lyria_model") or lyria_models[0])
|
|
6143
|
+
lyria_model = st.selectbox("Modelo Google Lyria", lyria_models, index=lyria_models.index(saved_lyria_model) if saved_lyria_model in lyria_models else 0, key="settings_lyria_model")
|
|
6144
|
+
|
|
6105
6145
|
with st.expander("Publicação através do Upload-Post", expanded=False):
|
|
6106
6146
|
upload_post_enabled = st.checkbox("Activar Upload-Post", bool(settings.get("upload_post_enabled", False)))
|
|
6107
6147
|
upload_post_api_key = text_setting("Upload-Post API key", "upload_post_api_key", secret=True)
|
|
@@ -6150,6 +6190,7 @@ def render_settings():
|
|
|
6150
6190
|
"elevenlabs_api_key": elevenlabs_api_key, "elevenlabs_model_id": elevenlabs_model_id,
|
|
6151
6191
|
"chatterbox_base_url": chatterbox_base_url, "chatterbox_api_key": chatterbox_api_key, "chatterbox_model_id": chatterbox_model_id,
|
|
6152
6192
|
"sonilo_api_key": sonilo_api_key, "sonilo_base_url": sonilo_base_url, "suno_api_key": suno_api_key, "suno_api_base_url": suno_api_base_url, "suno_api_endpoint": suno_api_endpoint,
|
|
6193
|
+
"lyria_api_key": lyria_api_key, "lyria_model": lyria_model,
|
|
6153
6194
|
"upload_post_enabled": upload_post_enabled, "upload_post_api_key": upload_post_api_key,
|
|
6154
6195
|
"upload_post_username": upload_post_username, "upload_post_platforms": upload_post_platforms,
|
|
6155
6196
|
"upload_post_auto_upload": upload_post_auto_upload,
|
package/hermes_ui/domain.py
CHANGED
|
@@ -328,6 +328,37 @@ def transition_task(task_id: str, state: str | None = None, stage: str | None =
|
|
|
328
328
|
return None
|
|
329
329
|
|
|
330
330
|
|
|
331
|
+
def retry_task_with_current_settings(task_id: str) -> dict[str, Any] | None:
|
|
332
|
+
"""Queue a failed or blocked task without persisting API credentials or provider snapshots.
|
|
333
|
+
|
|
334
|
+
The pipeline reloads settings.json immediately before every execution, so a
|
|
335
|
+
retry always uses the currently saved API keys, active provider priorities,
|
|
336
|
+
and provider endpoints while retaining the task's completed artefacts.
|
|
337
|
+
"""
|
|
338
|
+
tasks = read_json("tasks.json", [])
|
|
339
|
+
for task in tasks:
|
|
340
|
+
if task.get("id") != task_id:
|
|
341
|
+
continue
|
|
342
|
+
previous_state = str(task.get("state") or "")
|
|
343
|
+
if previous_state not in {"failed", "blocked"}:
|
|
344
|
+
raise ValueError("Apenas tarefas falhadas ou bloqueadas podem ser retomadas.")
|
|
345
|
+
try:
|
|
346
|
+
retry_count = int(task.get("retry_count") or 0)
|
|
347
|
+
except (TypeError, ValueError):
|
|
348
|
+
retry_count = 0
|
|
349
|
+
task["state"] = "to_do"
|
|
350
|
+
task["error"] = None
|
|
351
|
+
task["retry_count"] = retry_count + 1
|
|
352
|
+
task["retry_requested_at"] = now()
|
|
353
|
+
task["retry_config_source"] = "settings.json_at_execution"
|
|
354
|
+
for field in ("failure_api", "failure_provider", "failure_service", "failure_config_fields"):
|
|
355
|
+
task.pop(field, None)
|
|
356
|
+
task["updated_at"] = now()
|
|
357
|
+
write_json("tasks.json", tasks)
|
|
358
|
+
return task
|
|
359
|
+
return None
|
|
360
|
+
|
|
361
|
+
|
|
331
362
|
def pipeline_summary() -> dict[str, Any]:
|
|
332
363
|
tasks = read_json("tasks.json", [])
|
|
333
364
|
channels = read_json("channels.json", [])
|
package/hermes_ui/music.py
CHANGED
|
@@ -2,6 +2,9 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
import re
|
|
5
|
+
import base64
|
|
6
|
+
import uuid
|
|
7
|
+
from datetime import datetime, timezone
|
|
5
8
|
from pathlib import Path
|
|
6
9
|
from typing import Any
|
|
7
10
|
|
|
@@ -115,3 +118,137 @@ def request_suno_generation(settings: dict[str, Any], prompt: str, title: str =
|
|
|
115
118
|
return {"ok": True, "message": "Pedido de música enviado ao endpoint Suno configurado.", "data": body if isinstance(body, dict) else {"response": body}}
|
|
116
119
|
except (requests.RequestException, ValueError) as exc:
|
|
117
120
|
return {"ok": False, "message": f"Não foi possível contactar o endpoint Suno configurado: {exc}", "data": {}}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def list_music_tasks() -> list[dict[str, Any]]:
|
|
124
|
+
"""Return only tasks created for the independent audio-generation queue."""
|
|
125
|
+
records = storage.read_json("music_tasks.json", [])
|
|
126
|
+
if not isinstance(records, list):
|
|
127
|
+
return []
|
|
128
|
+
return [dict(record) for record in records if isinstance(record, dict) and str(record.get("id") or "").strip()]
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _save_music_tasks(tasks: list[dict[str, Any]]) -> None:
|
|
132
|
+
storage.write_json("music_tasks.json", tasks)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def create_music_task(provider: str, prompt: str, title: str, model: str = "") -> dict[str, Any]:
|
|
136
|
+
"""Enqueue one audio-only generation; no video task or video worker is used."""
|
|
137
|
+
cleaned_prompt = str(prompt or "").strip()
|
|
138
|
+
if not cleaned_prompt:
|
|
139
|
+
raise ValueError("Escreva um prompt musical antes de adicionar à fila.")
|
|
140
|
+
now = datetime.now(timezone.utc).isoformat(timespec="seconds")
|
|
141
|
+
task = {
|
|
142
|
+
"id": f"music_{uuid.uuid4().hex[:12]}",
|
|
143
|
+
"kind": "audio_generation",
|
|
144
|
+
"provider": "lyria" if str(provider).strip().casefold() == "google lyria" else "suno",
|
|
145
|
+
"model": str(model or "").strip(),
|
|
146
|
+
"title": str(title or "Música sem título").strip() or "Música sem título",
|
|
147
|
+
"prompt": cleaned_prompt,
|
|
148
|
+
"state": "to_do",
|
|
149
|
+
"stage": "music_generation",
|
|
150
|
+
"progress": 0,
|
|
151
|
+
"audio_path": "",
|
|
152
|
+
"error": "",
|
|
153
|
+
"created_at": now,
|
|
154
|
+
"updated_at": now,
|
|
155
|
+
}
|
|
156
|
+
tasks = list_music_tasks()
|
|
157
|
+
tasks.insert(0, task)
|
|
158
|
+
_save_music_tasks(tasks)
|
|
159
|
+
return task
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _update_music_task(task_id: str, **changes: Any) -> dict[str, Any] | None:
|
|
163
|
+
tasks = list_music_tasks()
|
|
164
|
+
updated: dict[str, Any] | None = None
|
|
165
|
+
for task in tasks:
|
|
166
|
+
if str(task.get("id") or "") == str(task_id):
|
|
167
|
+
task.update(changes)
|
|
168
|
+
task["updated_at"] = datetime.now(timezone.utc).isoformat(timespec="seconds")
|
|
169
|
+
updated = dict(task)
|
|
170
|
+
break
|
|
171
|
+
if updated is not None:
|
|
172
|
+
_save_music_tasks(tasks)
|
|
173
|
+
return updated
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def transition_music_task(task_id: str, state: str) -> dict[str, Any] | None:
|
|
177
|
+
"""Change only an independent audio queue state."""
|
|
178
|
+
if state not in {"to_do", "doing", "blocked", "done", "failed", "cancelled"}:
|
|
179
|
+
raise ValueError("Estado de música inválido.")
|
|
180
|
+
return _update_music_task(task_id, state=state)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _extract_lyria_audio(payload: dict[str, Any]) -> bytes:
|
|
184
|
+
"""Extract a base64 audio block from Gemini Interactions without logging the payload."""
|
|
185
|
+
candidates: list[Any] = []
|
|
186
|
+
output_audio = payload.get("output_audio") if isinstance(payload, dict) else None
|
|
187
|
+
if isinstance(output_audio, dict):
|
|
188
|
+
candidates.append(output_audio.get("data"))
|
|
189
|
+
for step in payload.get("steps", []) if isinstance(payload, dict) else []:
|
|
190
|
+
if not isinstance(step, dict):
|
|
191
|
+
continue
|
|
192
|
+
content = step.get("content")
|
|
193
|
+
for block in content if isinstance(content, list) else []:
|
|
194
|
+
if isinstance(block, dict) and str(block.get("type") or "").casefold() == "audio":
|
|
195
|
+
candidates.append(block.get("data"))
|
|
196
|
+
for candidate in candidates:
|
|
197
|
+
if isinstance(candidate, str) and candidate.strip():
|
|
198
|
+
try:
|
|
199
|
+
decoded = base64.b64decode(candidate, validate=True)
|
|
200
|
+
except (ValueError, TypeError):
|
|
201
|
+
continue
|
|
202
|
+
if decoded:
|
|
203
|
+
return decoded
|
|
204
|
+
return b""
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def request_lyria_generation(settings: dict[str, Any], prompt: str, title: str = "", model: str = "") -> dict[str, Any]:
|
|
208
|
+
"""Generate audio through Google Lyria's Interactions API without invoking video tooling."""
|
|
209
|
+
api_key = str(settings.get("lyria_api_key") or settings.get("gemini_api_key") or "").strip()
|
|
210
|
+
selected_model = str(model or settings.get("lyria_model") or "lyria-3-clip-preview").strip()
|
|
211
|
+
if not api_key:
|
|
212
|
+
return {"ok": False, "message": "Configure a Google Lyria API key em Configurações antes de solicitar uma música.", "data": {}}
|
|
213
|
+
try:
|
|
214
|
+
response = requests.post(
|
|
215
|
+
"https://generativelanguage.googleapis.com/v1beta/interactions",
|
|
216
|
+
headers={"x-goog-api-key": api_key, "Content-Type": "application/json"},
|
|
217
|
+
json={"model": selected_model, "input": str(prompt or "").strip()},
|
|
218
|
+
timeout=180,
|
|
219
|
+
)
|
|
220
|
+
if response.status_code >= 400:
|
|
221
|
+
return {"ok": False, "message": f"Google Lyria devolveu HTTP {response.status_code}.", "data": {"status_code": response.status_code}}
|
|
222
|
+
body = response.json() if response.content else {}
|
|
223
|
+
audio = _extract_lyria_audio(body if isinstance(body, dict) else {})
|
|
224
|
+
if not audio:
|
|
225
|
+
return {"ok": False, "message": "Google Lyria não devolveu áudio na resposta.", "data": {}}
|
|
226
|
+
output = store_music_file(f"{title or 'lyria-generated'}.mp3", audio)
|
|
227
|
+
return {"ok": True, "message": "Música gerada por Google Lyria.", "data": {"audio_path": str(output), "model": selected_model}}
|
|
228
|
+
except (requests.RequestException, ValueError):
|
|
229
|
+
return {"ok": False, "message": "Não foi possível contactar Google Lyria.", "data": {}}
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def run_music_task(task_id: str, settings: dict[str, Any]) -> dict[str, Any] | None:
|
|
233
|
+
"""Run exactly one queued audio generation and persist only its audio artefact."""
|
|
234
|
+
task = next((record for record in list_music_tasks() if str(record.get("id") or "") == str(task_id)), None)
|
|
235
|
+
if not task:
|
|
236
|
+
return None
|
|
237
|
+
_update_music_task(task_id, state="doing", stage="music_generation", progress=15, error="")
|
|
238
|
+
provider = str(task.get("provider") or "suno").casefold()
|
|
239
|
+
if provider == "lyria":
|
|
240
|
+
result = request_lyria_generation(settings, str(task.get("prompt") or ""), str(task.get("title") or ""), str(task.get("model") or ""))
|
|
241
|
+
audio_path = str((result.get("data") or {}).get("audio_path") or "")
|
|
242
|
+
else:
|
|
243
|
+
result = request_suno_generation(settings, str(task.get("prompt") or ""), str(task.get("title") or ""))
|
|
244
|
+
try:
|
|
245
|
+
output = materialize_suno_audio(result.get("data") or {}, str(task.get("title") or "suno-generated.mp3")) if result.get("ok") else None
|
|
246
|
+
audio_path = str(output or "")
|
|
247
|
+
except (OSError, requests.RequestException, ValueError):
|
|
248
|
+
audio_path = ""
|
|
249
|
+
result = {"ok": False, "message": "A música Suno foi solicitada, mas não foi possível guardar o áudio devolvido.", "data": {}}
|
|
250
|
+
if result.get("ok") and audio_path:
|
|
251
|
+
completed = _update_music_task(task_id, state="done", stage="completed", progress=100, audio_path=audio_path, error="")
|
|
252
|
+
record_notification("music_completed", f"Música concluída: {task.get('title') or 'Música'}", "Áudio guardado no Music Backlog.", metadata={"task_id": task_id, "provider": provider, "filename": Path(audio_path).name}, dedupe_key=f"music-task:{task_id}:{audio_path}")
|
|
253
|
+
return completed
|
|
254
|
+
return _update_music_task(task_id, state="failed", stage="failed", progress=100, error=str(result.get("message") or "A geração musical falhou.")[:500])
|
package/hermes_ui/storage.py
CHANGED
|
@@ -24,6 +24,7 @@ DEFAULTS: dict[str, Any] = {
|
|
|
24
24
|
"channels.json": [],
|
|
25
25
|
"channel_videos.json": [],
|
|
26
26
|
"tasks.json": [],
|
|
27
|
+
"music_tasks.json": [],
|
|
27
28
|
"queues.json": {"niche": [], "blueprint": [], "brand": [], "script": [], "title": [], "thumbnail": [], "video": [], "edit": [], "upload": []},
|
|
28
29
|
"batches.json": [],
|
|
29
30
|
"uploads.json": [],
|
|
@@ -300,6 +301,8 @@ DEFAULTS: dict[str, Any] = {
|
|
|
300
301
|
"suno_api_key": "",
|
|
301
302
|
"suno_api_base_url": "",
|
|
302
303
|
"suno_api_endpoint": "/api/generate",
|
|
304
|
+
"lyria_api_key": "",
|
|
305
|
+
"lyria_model": "lyria-3-clip-preview",
|
|
303
306
|
"jewelmusic_enabled": False,
|
|
304
307
|
"jewelmusic_api_key": "",
|
|
305
308
|
"jewelmusic_base_url": "https://api.jewelmusic.com",
|
package/package.json
CHANGED