@danhachuel/thunderbolt 0.3.97 → 0.3.98

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
@@ -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
- with st.form("create_music_task_form"):
2655
- provider_label = st.selectbox("Provider de geração musical", ["Suno AI", "Google Lyria"], key="music_task_provider")
2656
- title = st.text_input("Título da música", key="music_task_title")
2657
- prompt = st.text_area("Prompt musical", placeholder="Instrumental cinematográfico, calmo, sem voz...", key="music_task_prompt", height=150)
2658
- lyria_model = ""
2659
- if provider_label == "Google Lyria":
2660
- models = ["lyria-3-clip-preview", "lyria-3-pro-preview"]
2661
- configured = str(settings.get("lyria_model") or models[0])
2662
- lyria_model = st.selectbox("Modelo Lyria", models, index=models.index(configured) if configured in models else 0, key="music_task_lyria_model")
2663
- submitted = st.form_submit_button("Adicionar ao Music Backlog", type="primary", use_container_width=True)
2664
- if submitted:
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:"):
2665
2685
  try:
2666
- task = create_music_task(provider_label, prompt, title, lyria_model)
2667
- st.success(f"Música adicionada ao Music Backlog: {task['title']}")
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:"):
2711
+ try:
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))
@@ -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(settings: dict[str, Any], prompt: str, title: str = "", duration_seconds: int = 120) -> dict[str, Any]:
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 = {"prompt": prompt.strip(), "title": title.strip(), "duration": max(120, int(duration_seconds)), "make_instrumental": True}
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(provider: str, prompt: str, title: str, model: str = "") -> dict[str, Any]:
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(settings, str(task.get("prompt") or ""), str(task.get("title") or ""))
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danhachuel/thunderbolt",
3
- "version": "0.3.97",
3
+ "version": "0.3.98",
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",