@danhachuel/thunderbolt 0.3.97 → 0.3.99
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/influencers_ui.py +32 -17
- package/app/main.py +78 -14
- package/hermes_ui/music.py +39 -4
- package/hermes_ui/music_generation.py +88 -0
- package/package.json +1 -1
package/app/influencers_ui.py
CHANGED
|
@@ -6,6 +6,7 @@ import base64
|
|
|
6
6
|
import hashlib
|
|
7
7
|
import io
|
|
8
8
|
import json
|
|
9
|
+
import mimetypes
|
|
9
10
|
from pathlib import Path
|
|
10
11
|
from typing import Any, Mapping
|
|
11
12
|
|
|
@@ -294,24 +295,38 @@ def _render_content_history(repository: Any, influencer_id: str = "") -> None:
|
|
|
294
295
|
if not records:
|
|
295
296
|
return
|
|
296
297
|
st.subheader("Conteúdos gerados")
|
|
297
|
-
rows = []
|
|
298
298
|
for item in records:
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
299
|
+
content_id = str(item.get("id") or "content")
|
|
300
|
+
content_type = str(item.get("content_type") or "").strip().lower()
|
|
301
|
+
artifact = Path(str(item.get("artifact_path") or ""))
|
|
302
|
+
state = str(item.get("state") or "")
|
|
303
|
+
type_label = "Imagem" if content_type == "image" else "Vídeo" if content_type == "video" else "Conteúdo"
|
|
304
|
+
with st.container(border=True):
|
|
305
|
+
preview_col, detail_col = st.columns([1.25, 2.75])
|
|
306
|
+
with preview_col:
|
|
307
|
+
if artifact.is_file() and content_type == "image":
|
|
308
|
+
st.image(str(artifact), caption=f"{type_label} gerada", use_container_width=True)
|
|
309
|
+
elif artifact.is_file() and content_type == "video":
|
|
310
|
+
st.video(str(artifact))
|
|
311
|
+
else:
|
|
312
|
+
st.caption("Artefacto indisponível")
|
|
313
|
+
with detail_col:
|
|
314
|
+
st.write(f"**{type_label} · {CONTENT_STATES.get(state, state or '—')}**")
|
|
315
|
+
st.caption(f"Provider: {item.get('provider') or '—'} · {item.get('model') or '—'}")
|
|
316
|
+
st.caption(f"Plataforma: {item.get('platform') or '—'} · Criado: {item.get('created_at') or '—'}")
|
|
317
|
+
if item.get("error"):
|
|
318
|
+
st.error(str(item.get("error") or "")[:700])
|
|
319
|
+
if artifact.is_file() and state == "completed" and content_type in {"image", "video"}:
|
|
320
|
+
fallback_mime = "image/png" if content_type == "image" else "video/mp4"
|
|
321
|
+
mime = mimetypes.guess_type(artifact.name)[0] or fallback_mime
|
|
322
|
+
st.download_button(
|
|
323
|
+
f"Descarregar {type_label.casefold()}",
|
|
324
|
+
data=artifact.read_bytes(),
|
|
325
|
+
file_name=artifact.name,
|
|
326
|
+
mime=mime,
|
|
327
|
+
key=f"influencer_content_download_{content_type}_{content_id}",
|
|
328
|
+
use_container_width=True,
|
|
329
|
+
)
|
|
315
330
|
|
|
316
331
|
|
|
317
332
|
def _store_uploaded_file(uploaded: Any, folder: str) -> Path:
|
package/app/main.py
CHANGED
|
@@ -43,6 +43,7 @@ from hermes_ui.material_sources import apply_material_source_cards_to_settings,
|
|
|
43
43
|
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
|
|
44
44
|
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
|
|
45
45
|
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
|
|
46
|
+
from hermes_ui.music_generation import MUSIC_GENRES, MUSIC_VOCAL_OPTIONS, generate_music_fields
|
|
46
47
|
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
|
|
47
48
|
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
|
|
48
49
|
from hermes_ui.influencers import BACKEND_OPTIONS, DOCUMENT_EXTENSIONS, IMAGE_EXTENSIONS, backend_name, backend_status, get_repository, test_backend
|
|
@@ -2651,20 +2652,75 @@ def render_music_creation():
|
|
|
2651
2652
|
st.title("Criação de Músicas")
|
|
2652
2653
|
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.")
|
|
2653
2654
|
settings = read_json("settings.json", {})
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2655
|
+
generated_fields = st.session_state.pop("music_task_generated_fields", None)
|
|
2656
|
+
if isinstance(generated_fields, dict):
|
|
2657
|
+
for state_key, generated_key in (
|
|
2658
|
+
("music_task_title", "title"),
|
|
2659
|
+
("music_task_language", "language"),
|
|
2660
|
+
("music_task_genre", "genre"),
|
|
2661
|
+
("music_task_vocal", "vocal"),
|
|
2662
|
+
("music_task_references", "references"),
|
|
2663
|
+
("music_task_prompt", "prompt"),
|
|
2664
|
+
):
|
|
2665
|
+
st.session_state[state_key] = str(generated_fields.get(generated_key) or "")
|
|
2666
|
+
provider_label = st.selectbox("Provider de geração musical", ["Suno AI", "Google Lyria"], key="music_task_provider")
|
|
2667
|
+
theme = st.text_input("Tema / assunto principal", key="music_task_theme", placeholder="Ex.: uma viagem nocturna pela costa portuguesa")
|
|
2668
|
+
title = st.text_input("Título da música", key="music_task_title")
|
|
2669
|
+
language = st.selectbox(
|
|
2670
|
+
"Idioma da letra/música",
|
|
2671
|
+
list(LANGUAGE_CODES),
|
|
2672
|
+
index=list(LANGUAGE_CODES).index(str(st.session_state.get("music_task_language") or "pt")) if str(st.session_state.get("music_task_language") or "pt") in LANGUAGE_CODES else list(LANGUAGE_CODES).index("pt"),
|
|
2673
|
+
format_func=language_label,
|
|
2674
|
+
key="music_task_language",
|
|
2675
|
+
)
|
|
2676
|
+
genre = st.selectbox("Género musical", list(MUSIC_GENRES), key="music_task_genre")
|
|
2677
|
+
vocal = st.selectbox("Vocal", list(MUSIC_VOCAL_OPTIONS), key="music_task_vocal")
|
|
2678
|
+
references = st.text_area(
|
|
2679
|
+
"Referências culturais, paisagens, clima ou artistas similares (opcional)",
|
|
2680
|
+
key="music_task_references",
|
|
2681
|
+
placeholder="Ex.: pôr do sol mediterrânico, estrada molhada, nostalgia suave e arranjos acústicos contemporâneos",
|
|
2682
|
+
height=90,
|
|
2683
|
+
)
|
|
2684
|
+
if st.button("Gerar campos musicais com IA", key="music_task_generate_fields", use_container_width=True, icon=":material/auto_awesome:"):
|
|
2685
|
+
try:
|
|
2686
|
+
with st.spinner("A criar título, letra e prompt musical originais…"):
|
|
2687
|
+
generated = generate_music_fields(
|
|
2688
|
+
settings,
|
|
2689
|
+
theme=theme,
|
|
2690
|
+
language=language,
|
|
2691
|
+
genre=genre,
|
|
2692
|
+
vocal=vocal,
|
|
2693
|
+
references=references,
|
|
2694
|
+
)
|
|
2695
|
+
st.session_state["music_task_generated_fields"] = generated
|
|
2696
|
+
st.rerun()
|
|
2697
|
+
except CreativeGenerationError as exc:
|
|
2698
|
+
st.error(str(exc))
|
|
2699
|
+
prompt = st.text_area(
|
|
2700
|
+
"Prompt musical",
|
|
2701
|
+
placeholder="Use Gerar campos musicais com IA para criar letra e estilo completos, ou escreva um prompt próprio.",
|
|
2702
|
+
key="music_task_prompt",
|
|
2703
|
+
height=300,
|
|
2704
|
+
)
|
|
2705
|
+
lyria_model = ""
|
|
2706
|
+
if provider_label == "Google Lyria":
|
|
2707
|
+
models = ["lyria-3-clip-preview", "lyria-3-pro-preview"]
|
|
2708
|
+
configured = str(settings.get("lyria_model") or models[0])
|
|
2709
|
+
lyria_model = st.selectbox("Modelo Lyria", models, index=models.index(configured) if configured in models else 0, key="music_task_lyria_model")
|
|
2710
|
+
if st.button("Gerar Música", key="music_task_submit", type="primary", use_container_width=True, icon=":material/music_note:"):
|
|
2665
2711
|
try:
|
|
2666
|
-
task = create_music_task(
|
|
2667
|
-
|
|
2712
|
+
task = create_music_task(
|
|
2713
|
+
provider_label,
|
|
2714
|
+
prompt,
|
|
2715
|
+
title,
|
|
2716
|
+
lyria_model,
|
|
2717
|
+
language=language,
|
|
2718
|
+
genre=genre,
|
|
2719
|
+
vocal=vocal,
|
|
2720
|
+
references=references,
|
|
2721
|
+
theme=theme,
|
|
2722
|
+
)
|
|
2723
|
+
st.success(f"Música criada no Music Backlog: {task['title']}")
|
|
2668
2724
|
st.rerun()
|
|
2669
2725
|
except ValueError as exc:
|
|
2670
2726
|
st.error(str(exc))
|
|
@@ -3788,7 +3844,7 @@ def render_music_backlog() -> None:
|
|
|
3788
3844
|
music_file = Path(music_path)
|
|
3789
3845
|
st.success("Música pronta; pode continuar para o destino configurado.")
|
|
3790
3846
|
st.download_button(
|
|
3791
|
-
"Descarregar música
|
|
3847
|
+
"Descarregar música",
|
|
3792
3848
|
data=music_file.read_bytes(),
|
|
3793
3849
|
file_name=music_file.name,
|
|
3794
3850
|
mime="audio/mpeg",
|
|
@@ -3865,6 +3921,14 @@ def render_thumbnails():
|
|
|
3865
3921
|
image_path = record.get("image_path")
|
|
3866
3922
|
if image_path and image_path.is_file():
|
|
3867
3923
|
st.image(str(image_path), use_container_width=True)
|
|
3924
|
+
st.download_button(
|
|
3925
|
+
"Descarregar thumbnail",
|
|
3926
|
+
data=image_path.read_bytes(),
|
|
3927
|
+
file_name=image_path.name,
|
|
3928
|
+
mime="image/jpeg" if image_path.suffix.lower() in {".jpg", ".jpeg"} else "image/png",
|
|
3929
|
+
key=f"thumbnail_download_{task_id}_{record['variant_index']}",
|
|
3930
|
+
use_container_width=True,
|
|
3931
|
+
)
|
|
3868
3932
|
else:
|
|
3869
3933
|
st.markdown("### Sem imagem")
|
|
3870
3934
|
st.caption("Imagem ainda não gerada")
|
package/hermes_ui/music.py
CHANGED
|
@@ -97,7 +97,14 @@ def materialize_suno_audio(data: dict[str, Any], title: str = "suno-generated.mp
|
|
|
97
97
|
return store_music_file(title, response.content)
|
|
98
98
|
|
|
99
99
|
|
|
100
|
-
def request_suno_generation(
|
|
100
|
+
def request_suno_generation(
|
|
101
|
+
settings: dict[str, Any],
|
|
102
|
+
prompt: str,
|
|
103
|
+
title: str = "",
|
|
104
|
+
duration_seconds: int = 120,
|
|
105
|
+
*,
|
|
106
|
+
make_instrumental: bool = True,
|
|
107
|
+
) -> dict[str, Any]:
|
|
101
108
|
"""Request music from a configured Suno-compatible endpoint.
|
|
102
109
|
|
|
103
110
|
Suno-compatible deployments expose different endpoint paths; the UI therefore
|
|
@@ -109,7 +116,12 @@ def request_suno_generation(settings: dict[str, Any], prompt: str, title: str =
|
|
|
109
116
|
if not api_key or not base_url:
|
|
110
117
|
return {"ok": False, "message": "Configure Suno API Key e Suno API Base URL em Configurações antes de solicitar uma música.", "data": {}}
|
|
111
118
|
url = endpoint if endpoint.startswith("http") else f"{base_url}/{endpoint.lstrip('/')}"
|
|
112
|
-
payload = {
|
|
119
|
+
payload = {
|
|
120
|
+
"prompt": prompt.strip(),
|
|
121
|
+
"title": title.strip(),
|
|
122
|
+
"duration": max(120, int(duration_seconds)),
|
|
123
|
+
"make_instrumental": bool(make_instrumental),
|
|
124
|
+
}
|
|
113
125
|
try:
|
|
114
126
|
response = requests.post(url, json=payload, headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}, timeout=30)
|
|
115
127
|
if response.status_code >= 400:
|
|
@@ -132,7 +144,18 @@ def _save_music_tasks(tasks: list[dict[str, Any]]) -> None:
|
|
|
132
144
|
storage.write_json("music_tasks.json", tasks)
|
|
133
145
|
|
|
134
146
|
|
|
135
|
-
def create_music_task(
|
|
147
|
+
def create_music_task(
|
|
148
|
+
provider: str,
|
|
149
|
+
prompt: str,
|
|
150
|
+
title: str,
|
|
151
|
+
model: str = "",
|
|
152
|
+
*,
|
|
153
|
+
language: str = "",
|
|
154
|
+
genre: str = "",
|
|
155
|
+
vocal: str = "",
|
|
156
|
+
references: str = "",
|
|
157
|
+
theme: str = "",
|
|
158
|
+
) -> dict[str, Any]:
|
|
136
159
|
"""Enqueue one audio-only generation; no video task or video worker is used."""
|
|
137
160
|
cleaned_prompt = str(prompt or "").strip()
|
|
138
161
|
if not cleaned_prompt:
|
|
@@ -145,6 +168,12 @@ def create_music_task(provider: str, prompt: str, title: str, model: str = "") -
|
|
|
145
168
|
"model": str(model or "").strip(),
|
|
146
169
|
"title": str(title or "Música sem título").strip() or "Música sem título",
|
|
147
170
|
"prompt": cleaned_prompt,
|
|
171
|
+
"language": str(language or "").strip(),
|
|
172
|
+
"genre": str(genre or "").strip(),
|
|
173
|
+
"vocal": str(vocal or "").strip(),
|
|
174
|
+
"references": str(references or "").strip(),
|
|
175
|
+
"theme": str(theme or "").strip(),
|
|
176
|
+
"duration_seconds": 120,
|
|
148
177
|
"state": "to_do",
|
|
149
178
|
"stage": "music_generation",
|
|
150
179
|
"progress": 0,
|
|
@@ -240,7 +269,13 @@ def run_music_task(task_id: str, settings: dict[str, Any]) -> dict[str, Any] | N
|
|
|
240
269
|
result = request_lyria_generation(settings, str(task.get("prompt") or ""), str(task.get("title") or ""), str(task.get("model") or ""))
|
|
241
270
|
audio_path = str((result.get("data") or {}).get("audio_path") or "")
|
|
242
271
|
else:
|
|
243
|
-
result = request_suno_generation(
|
|
272
|
+
result = request_suno_generation(
|
|
273
|
+
settings,
|
|
274
|
+
str(task.get("prompt") or ""),
|
|
275
|
+
str(task.get("title") or ""),
|
|
276
|
+
int(task.get("duration_seconds") or 120),
|
|
277
|
+
make_instrumental=not bool(str(task.get("vocal") or "").strip()),
|
|
278
|
+
)
|
|
244
279
|
try:
|
|
245
280
|
output = materialize_suno_audio(result.get("data") or {}, str(task.get("title") or "suno-generated.mp3")) if result.get("ok") else None
|
|
246
281
|
audio_path = str(output or "")
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Structured, original music briefs generated through the configured LLM pool."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from .creative_generation import CreativeGenerationError, _chat_json
|
|
9
|
+
from .languages import language_code
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
MUSIC_GENRES = (
|
|
13
|
+
"Pop", "Rock", "Hip Hop", "EDM (Eletrônica)", "Techno", "Country", "Folk", "Indie", "K-pop", "Jazz",
|
|
14
|
+
"Reggae", "Classical", "Metal", "Punk", "Afrobeat (Nigéria)", "Ambient (Reino Unido)", "Anime (Japão)",
|
|
15
|
+
"Arabic Pop / Khaleeji (Mundo Árabe)", "Austropop (Áustria)", "Ballad (Internacional)", "Banda (México)",
|
|
16
|
+
"Blues (EUA)", "Bollywood / Indian (Índia)", "City Pop (Japão)", "Corridos Tumbados (México)", "C-pop (China)",
|
|
17
|
+
"Cumbia (Colômbia)", "Dangdut (Indonésia)", "Dansband (Suécia)", "Deutschrap (Alemanha)",
|
|
18
|
+
"Disco Polo (Polônia)", "Drum & Bass (Reino Unido)", "Entechno (Grécia)", "Fado (Portugal)",
|
|
19
|
+
"Flamenco (Espanha)", "Forró (Brasil)", "Funk (Brasil)", "Fusion (Internacional)", "Gamolan (Indonésia)",
|
|
20
|
+
"Gospel (EUA)", "Grime (Reino Unido)", "Hardstyle (Países Baixos)", "J-Pop (Japão)", "J-Rock (Japão)",
|
|
21
|
+
"K-Hip Hop (Coreia do Sul)", "Kizomba (Angola)", "Klapa (Croácia)", "Klezmer (Judeu / Europa Oriental)",
|
|
22
|
+
"Laïko (Grécia)", "Latin (América Latina)", "Mandopop (China / Taiwan)", "Māori (Nova Zelândia)", "MPB (Brasil)",
|
|
23
|
+
"Mundart (Dialect Pop) (Suíça)", "Pasifika (Pacífico)", "Pimba (Portugal)", "Pop Sueco (Suécia)", "R&B (EUA)",
|
|
24
|
+
"Rap Francês (França)", "Reggaeton (Porto Rico)", "Regional Mexicano (México)", "Samba / Pagode (Brasil)",
|
|
25
|
+
"Schlager (Alemanha)", "Sertanejo (Brasil)", "Trot (Coreia do Sul)", "Urbano / Trap (América Latina)",
|
|
26
|
+
"Variété (França)", "Vocaloid (Japão)",
|
|
27
|
+
)
|
|
28
|
+
MUSIC_VOCAL_OPTIONS = ("Masculino", "Feminina", "Coral")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def generate_music_fields(
|
|
32
|
+
settings: dict[str, Any],
|
|
33
|
+
*,
|
|
34
|
+
theme: str,
|
|
35
|
+
language: str,
|
|
36
|
+
genre: str,
|
|
37
|
+
vocal: str,
|
|
38
|
+
references: str = "",
|
|
39
|
+
) -> dict[str, str]:
|
|
40
|
+
"""Generate original, ready-to-use music fields through the active LLM pool."""
|
|
41
|
+
cleaned_theme = str(theme or "").strip()
|
|
42
|
+
if not cleaned_theme:
|
|
43
|
+
raise CreativeGenerationError("Escreva o tema ou assunto principal antes de gerar os campos musicais com IA.")
|
|
44
|
+
selected_genre = str(genre or "").strip()
|
|
45
|
+
selected_vocal = str(vocal or "").strip()
|
|
46
|
+
selected_language = language_code(language)
|
|
47
|
+
system = (
|
|
48
|
+
"És um compositor e produtor musical. Cria uma canção inteiramente original, sem reproduzir letras, melodias, "
|
|
49
|
+
"frases distintivas ou a identidade de artistas existentes. As referências fornecidas servem apenas para atributos "
|
|
50
|
+
"de alto nível, como instrumentação, clima, contexto cultural e energia. Responde apenas com JSON válido contendo "
|
|
51
|
+
"as chaves title, language, genre, vocal, cultural_references e music_prompt. "
|
|
52
|
+
"music_prompt deve ser Markdown legível, pronto para uma ferramenta de geração musical, com exactamente estas secções: "
|
|
53
|
+
"# 1. Nome da Música, # 2. Idioma da Música, # 3. Letra / Lyrics, # 4. Estilo / Style Prompt. "
|
|
54
|
+
"A letra deve ser original, incluir [Intro], [Verse 1], [Pre-Chorus], [Chorus], [Verse 2], [Bridge], "
|
|
55
|
+
"[Instrumental Solo], [Final Chorus] e [Outro], ter uma estrutura que vise pelo menos dois minutos, e o estilo deve "
|
|
56
|
+
"informar subgénero, vocal, instrumentação, BPM, clima, produção e estrutura."
|
|
57
|
+
)
|
|
58
|
+
user = json.dumps(
|
|
59
|
+
{
|
|
60
|
+
"theme": cleaned_theme,
|
|
61
|
+
"selected_language_code": selected_language,
|
|
62
|
+
"selected_genre": selected_genre,
|
|
63
|
+
"selected_vocal": selected_vocal,
|
|
64
|
+
"cultural_landscape_weather_or_artist_references": str(references or "").strip(),
|
|
65
|
+
"available_genres": list(MUSIC_GENRES),
|
|
66
|
+
"available_vocals": list(MUSIC_VOCAL_OPTIONS),
|
|
67
|
+
"requirements": {
|
|
68
|
+
"minimum_target_duration_seconds": 120,
|
|
69
|
+
"preserve_selected_language_genre_and_vocal_when_present": True,
|
|
70
|
+
"output_original_content_only": True,
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
ensure_ascii=False,
|
|
74
|
+
)
|
|
75
|
+
result = _chat_json(settings, system, user)
|
|
76
|
+
music_prompt = str(result.get("music_prompt") or "").strip()
|
|
77
|
+
if not music_prompt:
|
|
78
|
+
raise CreativeGenerationError("O provider LLM não devolveu um prompt musical válido.")
|
|
79
|
+
generated_genre = str(result.get("genre") or selected_genre).strip()
|
|
80
|
+
generated_vocal = str(result.get("vocal") or selected_vocal).strip()
|
|
81
|
+
return {
|
|
82
|
+
"title": str(result.get("title") or cleaned_theme).strip()[:180],
|
|
83
|
+
"language": language_code(result.get("language"), default=selected_language),
|
|
84
|
+
"genre": generated_genre if generated_genre in MUSIC_GENRES else selected_genre,
|
|
85
|
+
"vocal": generated_vocal if generated_vocal in MUSIC_VOCAL_OPTIONS else selected_vocal,
|
|
86
|
+
"references": str(result.get("cultural_references") or references or "").strip()[:800],
|
|
87
|
+
"prompt": music_prompt[:16000],
|
|
88
|
+
}
|
package/package.json
CHANGED