AndrianBalanescu commited on
Commit
fbb5a31
·
1 Parent(s): e1cf104

feat(music-studio): wire Ion King Kong persona and voice cloner into AI Music Studio Pro

Browse files
moldovan-qwen/data/zerogpu_creative_history.json CHANGED
@@ -1,222 +1,222 @@
1
  {
2
  "recent_generations": [
3
- {
4
- "title": "Ploaie pe Ștefan cel Mare",
5
- "category": "DIVERSE_MUSIC_ART",
6
- "persona": "lyricist_poet",
7
- "genre": "Doină Lo-Fi Melancolică",
8
- "timestamp": 1787957496.964109
9
- },
10
- {
11
- "title": "Primul Congres Mondial al Șoferilor de Rutiere",
12
- "category": "SATIRICAL_NEWS",
13
- "persona": "protv_anchor",
14
- "genre": "Absurdist News",
15
- "timestamp": 1787957496.969292
16
- },
17
- {
18
- "title": "Programatorul pe 3000€ care mănâncă harbuz pe ziar",
19
- "category": "TIKTOK_REELS_SHORT",
20
- "persona": "it_chisinau",
21
- "genre": "Tech Satire Skit",
22
- "timestamp": 1787957496.974603
23
- },
24
- {
25
- "title": "Birocrația la Pașapoarte și Cozile de la 5 Dimineața",
26
- "category": "PARODY_STANDUP",
27
- "persona": "vames",
28
- "genre": "Bureaucracy Satire",
29
- "timestamp": 1787957496.979158
30
- },
31
- {
32
- "title": "Mătușa Ileana vs Senior IT-ist: Ce înseamnă cu adevărat 'muncă'?",
33
- "category": "PERSONA_DUEL",
34
- "persona": "babusca",
35
- "genre": "Philosophical Roast Duel",
36
- "timestamp": 1787957496.9839108
37
- },
38
- {
39
- "title": "Misterul Plăcintelor Dispărute la Piața Centrală",
40
- "category": "SATIRICAL_NEWS",
41
- "persona": "protv_anchor",
42
- "genre": "Investigative Comedy Report",
43
- "timestamp": 1787957496.992373
44
- },
45
- {
46
- "title": "Scrisoare din Verona (Dor de Casă)",
47
- "category": "DIVERSE_MUSIC_ART",
48
- "persona": "lyricist_poet",
49
- "genre": "Baladă Acustică despre Dor & Diasporă",
50
- "timestamp": 1787977080.513729
51
- },
52
  {
53
  "title": "Scanarea cu Raze X a Plăcintelor la Vama Leușeni",
54
  "category": "TIKTOK_REELS_SHORT",
55
  "persona": "vames",
56
  "genre": "Customs Comedy Skit",
57
- "timestamp": 1787977080.553169
 
 
 
 
 
 
 
58
  },
59
  {
60
  "title": "Mirosul de Pâine Coaptă în Cuptorul de Lut",
61
  "category": "INSPIRING_CULTURE",
62
  "persona": "babusca",
63
  "genre": "Memoir & Cultural Essay",
64
- "timestamp": 1787977080.557594
65
  },
66
  {
67
- "title": "Nea Vasile Taximetristul vs Sergiu de la Vamă: Teoriile Geopolitice Supreme",
68
- "category": "PERSONA_DUEL",
69
- "persona": "taximetrist",
70
- "genre": "Street Debate Duel",
71
- "timestamp": 1787977080.561755
72
  },
73
  {
74
  "title": "Primul Congres Mondial al Șoferilor de Rutiere",
75
  "category": "SATIRICAL_NEWS",
76
  "persona": "protv_anchor",
77
  "genre": "Absurdist News",
78
- "timestamp": 1787977080.5657952
 
 
 
 
 
 
 
79
  },
80
  {
81
  "title": "Mâinile Bătătorite care au Clădit O Țară",
82
  "category": "INSPIRING_CULTURE",
83
  "persona": "lyricist_poet",
84
  "genre": "Tribute & Historical Respect",
85
- "timestamp": 1787977080.570102
86
  },
87
  {
88
- "title": "Mătușa Ileana vs Senior IT-ist: Ce înseamnă cu adevărat 'muncă'?",
89
- "category": "PERSONA_DUEL",
 
 
 
 
 
 
 
90
  "persona": "babusca",
91
- "genre": "Philosophical Roast Duel",
92
- "timestamp": 1787977080.574239
93
  },
94
  {
95
  "title": "Misterul Plăcintelor Dispărute la Piața Centrală",
96
  "category": "SATIRICAL_NEWS",
97
  "persona": "protv_anchor",
98
  "genre": "Investigative Comedy Report",
99
- "timestamp": 1787977080.580103
100
  },
101
  {
102
  "title": "Scanarea cu Raze X a Plăcintelor la Vama Leușeni",
103
  "category": "TIKTOK_REELS_SHORT",
104
  "persona": "vames",
105
  "genre": "Customs Comedy Skit",
106
- "timestamp": 1787977124.511012
107
  },
108
  {
109
- "title": "Misterul Plăcintelor Dispărute la Piața Centrală",
110
- "category": "SATIRICAL_NEWS",
111
- "persona": "protv_anchor",
112
- "genre": "Investigative Comedy Report",
113
- "timestamp": 1787977124.552153
114
- },
115
- {
116
- "title": "Mătușa Ileana dă sfaturi de relații pe TikTok Live",
117
- "category": "TIKTOK_REELS_SHORT",
118
- "persona": "babusca",
119
- "genre": "Modern vs Traditional Skit",
120
- "timestamp": 1787977124.556492
121
  },
122
  {
123
  "title": "De ce Ne Întoarcem Mereu Acasă",
124
  "category": "INSPIRING_CULTURE",
125
  "persona": "lyricist_poet",
126
  "genre": "Diaspora Reflection",
127
- "timestamp": 1787977124.561013
128
  },
129
  {
130
  "title": "Primul Congres Mondial al Șoferilor de Rutiere",
131
  "category": "SATIRICAL_NEWS",
132
  "persona": "protv_anchor",
133
  "genre": "Absurdist News",
134
- "timestamp": 1787977124.565442
135
  },
136
  {
137
- "title": "Dating la Valea Morilor vs Tinder Botanica",
138
  "category": "PARODY_STANDUP",
139
- "persona": "standup",
140
- "genre": "Urban Comedy Skit",
141
- "timestamp": 1787977124.5695732
142
- },
143
- {
144
- "title": "Primul Congres Mondial al Șoferilor de Rutiere",
145
- "category": "SATIRICAL_NEWS",
146
- "persona": "protv_anchor",
147
- "genre": "Absurdist News",
148
- "timestamp": 1787977124.5738502
149
- },
150
- {
151
- "title": "Misterul Plăcintelor Dispărute la Piața Centrală",
152
- "category": "SATIRICAL_NEWS",
153
- "persona": "protv_anchor",
154
- "genre": "Investigative Comedy Report",
155
- "timestamp": 1787977124.582141
156
  },
157
  {
158
  "title": "Nea Vasile Taximetristul vs Sergiu de la Vamă: Teoriile Geopolitice Supreme",
159
  "category": "PERSONA_DUEL",
160
  "persona": "taximetrist",
161
  "genre": "Street Debate Duel",
162
- "timestamp": 1787977194.601278
163
  },
164
  {
165
- "title": "Nunta cu 300 de nuni și colaci giganți",
166
- "category": "PARODY_STANDUP",
167
- "persona": "standup",
168
- "genre": "Standup Roast",
169
- "timestamp": 1787977194.64315
170
  },
171
  {
172
- "title": "Mătușa Ileana vs Senior IT-ist: Ce înseamnă cu adevărat 'muncă'?",
173
- "category": "PERSONA_DUEL",
174
- "persona": "babusca",
175
- "genre": "Philosophical Roast Duel",
176
- "timestamp": 1787977194.647868
177
  },
178
  {
179
  "title": "Programatorul pe 3000€ care mănâncă harbuz pe ziar",
180
  "category": "TIKTOK_REELS_SHORT",
181
  "persona": "it_chisinau",
182
  "genre": "Tech Satire Skit",
183
- "timestamp": 1787977194.652249
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
184
  },
185
  {
186
  "title": "Nea Vasile Taximetristul vs Sergiu de la Vamă: Teoriile Geopolitice Supreme",
187
  "category": "PERSONA_DUEL",
188
  "persona": "taximetrist",
189
  "genre": "Street Debate Duel",
190
- "timestamp": 1787977194.656759
191
  },
192
  {
193
  "title": "Misterul Plăcintelor Dispărute la Piața Centrală",
194
  "category": "SATIRICAL_NEWS",
195
  "persona": "protv_anchor",
196
  "genre": "Investigative Comedy Report",
197
- "timestamp": 1787977194.661379
198
  },
199
  {
200
- "title": "Birocrația la Pașapoarte și Cozile de la 5 Dimineața",
201
- "category": "PARODY_STANDUP",
202
- "persona": "vames",
203
- "genre": "Bureaucracy Satire",
204
- "timestamp": 1787977194.666362
 
 
 
 
 
 
 
 
 
 
 
 
 
 
205
  },
206
  {
207
  "title": "Primul Congres Mondial al Șoferilor de Rutiere",
208
  "category": "SATIRICAL_NEWS",
209
  "persona": "protv_anchor",
210
  "genre": "Absurdist News",
211
- "timestamp": 1787977194.6721961
212
  }
213
  ],
214
  "category_counts": {
215
- "INSPIRING_CULTURE": 13,
216
- "TIKTOK_REELS_SHORT": 15,
217
- "PARODY_STANDUP": 13,
218
- "PERSONA_DUEL": 14,
219
- "SATIRICAL_NEWS": 21,
220
- "DIVERSE_MUSIC_ART": 5
221
  }
222
  }
 
1
  {
2
  "recent_generations": [
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  {
4
  "title": "Scanarea cu Raze X a Plăcintelor la Vama Leușeni",
5
  "category": "TIKTOK_REELS_SHORT",
6
  "persona": "vames",
7
  "genre": "Customs Comedy Skit",
8
+ "timestamp": 1787978018.128892
9
+ },
10
+ {
11
+ "title": "Audi A6 din Lituania cu kilometraj 'real' 180.000",
12
+ "category": "PARODY_STANDUP",
13
+ "persona": "taximetrist",
14
+ "genre": "Car Culture Roast",
15
+ "timestamp": 1787978018.133158
16
  },
17
  {
18
  "title": "Mirosul de Pâine Coaptă în Cuptorul de Lut",
19
  "category": "INSPIRING_CULTURE",
20
  "persona": "babusca",
21
  "genre": "Memoir & Cultural Essay",
22
+ "timestamp": 1787978018.13728
23
  },
24
  {
25
+ "title": "Războiul Trotinetelor vs Troleibuze",
26
+ "category": "DIVERSE_MUSIC_ART",
27
+ "persona": "standup",
28
+ "genre": "Satirical Diss Track",
29
+ "timestamp": 1787978018.141835
30
  },
31
  {
32
  "title": "Primul Congres Mondial al Șoferilor de Rutiere",
33
  "category": "SATIRICAL_NEWS",
34
  "persona": "protv_anchor",
35
  "genre": "Absurdist News",
36
+ "timestamp": 1787978018.1461701
37
+ },
38
+ {
39
+ "title": "Misterul Plăcintelor Dispărute la Piața Centrală",
40
+ "category": "SATIRICAL_NEWS",
41
+ "persona": "protv_anchor",
42
+ "genre": "Investigative Comedy Report",
43
+ "timestamp": 1787978018.151928
44
  },
45
  {
46
  "title": "Mâinile Bătătorite care au Clădit O Țară",
47
  "category": "INSPIRING_CULTURE",
48
  "persona": "lyricist_poet",
49
  "genre": "Tribute & Historical Respect",
50
+ "timestamp": 1787978417.4525492
51
  },
52
  {
53
+ "title": "Mătușa Ileana dă sfaturi de relații pe TikTok Live",
54
+ "category": "TIKTOK_REELS_SHORT",
55
+ "persona": "babusca",
56
+ "genre": "Modern vs Traditional Skit",
57
+ "timestamp": 1787978417.488802
58
+ },
59
+ {
60
+ "title": "Mirosul de Pâine Coaptă în Cuptorul de Lut",
61
+ "category": "INSPIRING_CULTURE",
62
  "persona": "babusca",
63
+ "genre": "Memoir & Cultural Essay",
64
+ "timestamp": 1787978417.4932039
65
  },
66
  {
67
  "title": "Misterul Plăcintelor Dispărute la Piața Centrală",
68
  "category": "SATIRICAL_NEWS",
69
  "persona": "protv_anchor",
70
  "genre": "Investigative Comedy Report",
71
+ "timestamp": 1787978417.497844
72
  },
73
  {
74
  "title": "Scanarea cu Raze X a Plăcintelor la Vama Leușeni",
75
  "category": "TIKTOK_REELS_SHORT",
76
  "persona": "vames",
77
  "genre": "Customs Comedy Skit",
78
+ "timestamp": 1787978417.502639
79
  },
80
  {
81
+ "title": "Războiul Trotinetelor vs Troleibuze",
82
+ "category": "DIVERSE_MUSIC_ART",
83
+ "persona": "standup",
84
+ "genre": "Satirical Diss Track",
85
+ "timestamp": 1787978417.5071738
 
 
 
 
 
 
 
86
  },
87
  {
88
  "title": "De ce Ne Întoarcem Mereu Acasă",
89
  "category": "INSPIRING_CULTURE",
90
  "persona": "lyricist_poet",
91
  "genre": "Diaspora Reflection",
92
+ "timestamp": 1787978417.511688
93
  },
94
  {
95
  "title": "Primul Congres Mondial al Șoferilor de Rutiere",
96
  "category": "SATIRICAL_NEWS",
97
  "persona": "protv_anchor",
98
  "genre": "Absurdist News",
99
+ "timestamp": 1787978417.516754
100
  },
101
  {
102
+ "title": "Birocrația la Pașapoarte și Cozile de la 5 Dimineața",
103
  "category": "PARODY_STANDUP",
104
+ "persona": "vames",
105
+ "genre": "Bureaucracy Satire",
106
+ "timestamp": 1787978826.427448
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
  },
108
  {
109
  "title": "Nea Vasile Taximetristul vs Sergiu de la Vamă: Teoriile Geopolitice Supreme",
110
  "category": "PERSONA_DUEL",
111
  "persona": "taximetrist",
112
  "genre": "Street Debate Duel",
113
+ "timestamp": 1787978826.470415
114
  },
115
  {
116
+ "title": "Ploaie pe Ștefan cel Mare",
117
+ "category": "DIVERSE_MUSIC_ART",
118
+ "persona": "lyricist_poet",
119
+ "genre": "Doină Lo-Fi Melancolică",
120
+ "timestamp": 1787978826.4749482
121
  },
122
  {
123
+ "title": "Dating la Valea Morilor vs Tinder Botanica",
124
+ "category": "PARODY_STANDUP",
125
+ "persona": "standup",
126
+ "genre": "Urban Comedy Skit",
127
+ "timestamp": 1787978826.47958
128
  },
129
  {
130
  "title": "Programatorul pe 3000€ care mănâncă harbuz pe ziar",
131
  "category": "TIKTOK_REELS_SHORT",
132
  "persona": "it_chisinau",
133
  "genre": "Tech Satire Skit",
134
+ "timestamp": 1787978826.483779
135
+ },
136
+ {
137
+ "title": "Scrisoare din Verona (Dor de Casă)",
138
+ "category": "DIVERSE_MUSIC_ART",
139
+ "persona": "lyricist_poet",
140
+ "genre": "Baladă Acustică despre Dor & Diasporă",
141
+ "timestamp": 1787978826.488663
142
+ },
143
+ {
144
+ "title": "Mătușa Ileana dă sfaturi de relații pe TikTok Live",
145
+ "category": "TIKTOK_REELS_SHORT",
146
+ "persona": "babusca",
147
+ "genre": "Modern vs Traditional Skit",
148
+ "timestamp": 1787978826.4928389
149
+ },
150
+ {
151
+ "title": "Primul Congres Mondial al Șoferilor de Rutiere",
152
+ "category": "SATIRICAL_NEWS",
153
+ "persona": "protv_anchor",
154
+ "genre": "Absurdist News",
155
+ "timestamp": 1787978826.4979
156
+ },
157
+ {
158
+ "title": "Războiul Trotinetelor vs Troleibuze",
159
+ "category": "DIVERSE_MUSIC_ART",
160
+ "persona": "standup",
161
+ "genre": "Satirical Diss Track",
162
+ "timestamp": 1787978965.382914
163
+ },
164
+ {
165
+ "title": "Scanarea cu Raze X a Plăcintelor la Vama Leușeni",
166
+ "category": "TIKTOK_REELS_SHORT",
167
+ "persona": "vames",
168
+ "genre": "Customs Comedy Skit",
169
+ "timestamp": 1787978965.4173388
170
  },
171
  {
172
  "title": "Nea Vasile Taximetristul vs Sergiu de la Vamă: Teoriile Geopolitice Supreme",
173
  "category": "PERSONA_DUEL",
174
  "persona": "taximetrist",
175
  "genre": "Street Debate Duel",
176
+ "timestamp": 1787978965.421803
177
  },
178
  {
179
  "title": "Misterul Plăcintelor Dispărute la Piața Centrală",
180
  "category": "SATIRICAL_NEWS",
181
  "persona": "protv_anchor",
182
  "genre": "Investigative Comedy Report",
183
+ "timestamp": 1787978965.426205
184
  },
185
  {
186
+ "title": "De ce Ne Întoarcem Mereu Acasă",
187
+ "category": "INSPIRING_CULTURE",
188
+ "persona": "lyricist_poet",
189
+ "genre": "Diaspora Reflection",
190
+ "timestamp": 1787978965.430509
191
+ },
192
+ {
193
+ "title": "Viaduct Midnight 1986",
194
+ "category": "DIVERSE_MUSIC_ART",
195
+ "persona": "it_chisinau",
196
+ "genre": "Chișinău Synthwave & Retrowave",
197
+ "timestamp": 1787978965.434687
198
+ },
199
+ {
200
+ "title": "Primul Congres Mondial al Șoferilor de Rutiere",
201
+ "category": "SATIRICAL_NEWS",
202
+ "persona": "protv_anchor",
203
+ "genre": "Absurdist News",
204
+ "timestamp": 1787978965.439157
205
  },
206
  {
207
  "title": "Primul Congres Mondial al Șoferilor de Rutiere",
208
  "category": "SATIRICAL_NEWS",
209
  "persona": "protv_anchor",
210
  "genre": "Absurdist News",
211
+ "timestamp": 1787978965.444821
212
  }
213
  ],
214
  "category_counts": {
215
+ "INSPIRING_CULTURE": 19,
216
+ "TIKTOK_REELS_SHORT": 21,
217
+ "PARODY_STANDUP": 16,
218
+ "PERSONA_DUEL": 16,
219
+ "SATIRICAL_NEWS": 29,
220
+ "DIVERSE_MUSIC_ART": 12
221
  }
222
  }
moldovan-qwen/engine/lyricist_engine.py CHANGED
@@ -159,14 +159,27 @@ class LyricistEngine:
159
  dialect_level: int = 3,
160
  duration_seconds: int = 60,
161
  vocal_style: str = "Aggressive Moldovan street rap, fast triplets flow, autotune",
162
- custom_instructions: str = ""
 
163
  ) -> Dict[str, Any]:
164
  """
165
  Dynamically synthesize structured, authentic, rhymed lyrics tailored to the user's intent.
166
  Injects the complete compiled master dictionary of Moldovan terms into the system prompt.
 
167
  """
168
  clean_topic = topic.strip() or "Noaptea la Chișinău"
169
 
 
 
 
 
 
 
 
 
 
 
 
170
  # Determine target syllable count per bar based on BPM
171
  if bpm >= 135:
172
  syllables_per_line = "8-10 silabe per vers (ritm alert, flow rapid, trap/rap)"
@@ -198,6 +211,7 @@ REGULI CRUCIALE DE COMPOZIȚIE:
198
  6. Intenție Emoțională / Vibe: {intent}.
199
  7. Nivel Dialectal ({dialect_level}/4): {dialect_rule}.
200
  {f'8. Producer Brief / Instrucțiuni Speciale: {custom_instructions}' if custom_instructions else ''}
 
201
 
202
  Format de ieșire:
203
  # Titlul Piesei
 
159
  dialect_level: int = 3,
160
  duration_seconds: int = 60,
161
  vocal_style: str = "Aggressive Moldovan street rap, fast triplets flow, autotune",
162
+ custom_instructions: str = "",
163
+ persona_id: str = ""
164
  ) -> Dict[str, Any]:
165
  """
166
  Dynamically synthesize structured, authentic, rhymed lyrics tailored to the user's intent.
167
  Injects the complete compiled master dictionary of Moldovan terms into the system prompt.
168
+ Supports dedicated Moldovan personas (e.g. ion_king_kong).
169
  """
170
  clean_topic = topic.strip() or "Noaptea la Chișinău"
171
 
172
+ persona_context = ""
173
+ if persona_id:
174
+ try:
175
+ from personas.persona_engine import PersonaEngine
176
+ engine = PersonaEngine()
177
+ p = engine.get_persona(persona_id)
178
+ if p:
179
+ persona_context = f"\n\nATENȚIE: Scrii versurile din perspectiva și vocea personajului '{p.get('name')}' ({persona_id}).\nDescriere personaj: {p.get('system_prompt', '')}\nExpresii cheie obligatorii de inclus: {', '.join(p.get('catchphrases', []))}\n"
180
+ except Exception:
181
+ pass
182
+
183
  # Determine target syllable count per bar based on BPM
184
  if bpm >= 135:
185
  syllables_per_line = "8-10 silabe per vers (ritm alert, flow rapid, trap/rap)"
 
211
  6. Intenție Emoțională / Vibe: {intent}.
212
  7. Nivel Dialectal ({dialect_level}/4): {dialect_rule}.
213
  {f'8. Producer Brief / Instrucțiuni Speciale: {custom_instructions}' if custom_instructions else ''}
214
+ {persona_context}
215
 
216
  Format de ieșire:
217
  # Titlul Piesei
moldovan-qwen/engine/media_creator.py CHANGED
@@ -188,14 +188,15 @@ class MediaCreator:
188
  mood: str = "Energetic & Sarcastic",
189
  custom_lyrics: Optional[str] = None,
190
  audio_engine: str = "acestep",
191
- cover_style: str = "Cyberpunk Vinyl"
 
192
  ) -> Dict[str, Any]:
193
  """
194
  Generate a complete Moldovan music production package:
195
  - Full parameter customization: 30s-240s duration, 20+ genres, custom BPM & key, vocal style
196
  - Authentic Moldovan lyrics with slang and cultural references (or custom lyrics)
197
  - Suno/Udio/MiniMax prompt blueprint
198
- - Real studio audio generation (MiniMax Music 3 / ACE-Step v1.5 XL / MusicGen / 808 Synth)
199
  - Real FLUX.1 album / vinyl cover art
200
  """
201
  topic_clean = topic.strip() if topic.strip() else "Viața la Chișinău și vama de noapte"
@@ -222,15 +223,15 @@ class MediaCreator:
222
  if custom_lyrics and custom_lyrics.strip():
223
  lyrics_raw = custom_lyrics.strip()
224
  else:
225
- lyrics_raw = self._synthesize_song_lyrics(topic_clean, genre, dialect_level, duration_seconds=dur)
226
 
227
  suno_prompt = (
228
  f"[{genre}, {target_bpm} BPM, {target_key}, {instrumentation}. "
229
  f"Vocal: {target_vocal}. Mood: {mood}. Moldovan Romanian urban dialect, energetic, viral hook, studio mastering]"
230
  )
231
 
232
- # 1. Real AI Music Generation (MiniMax Music 3 / ACE-Step v1.5 XL / MusicGen / 808 Synth)
233
- print(f"[MediaCreator] Generating music audio for: {title} ({genre}, {dur}s @ {target_bpm} BPM)...")
234
  music_res = self.dispatcher.generate_music(
235
  prompt=f"{genre}, {instrumentation}",
236
  lyrics=lyrics_raw,
@@ -239,7 +240,8 @@ class MediaCreator:
239
  bpm=target_bpm,
240
  key=target_key,
241
  vocal_style=target_vocal,
242
- engine=audio_engine
 
243
  )
244
 
245
  audio_url = music_res.get("download_url") or music_res.get("audio_url", "")
@@ -269,6 +271,7 @@ class MediaCreator:
269
  "bpm": target_bpm,
270
  "key": target_key,
271
  "vocal_style": target_vocal,
 
272
  "instrumentation": cfg["instrumentation"],
273
  "suno_style_prompt": suno_prompt,
274
  "full_lyrics": lyrics_raw,
@@ -341,8 +344,8 @@ class MediaCreator:
341
 
342
  return pkg
343
 
344
- def _synthesize_song_lyrics(self, topic: str, genre: str, dialect_level: int, duration_seconds: int = 60) -> str:
345
- """Synthesize original Moldovan verses, chorus, bridge, and outro tailored to the exact topic and genre."""
346
  try:
347
  from engine.lyricist_engine import LyricistEngine
348
  lyricist = LyricistEngine()
@@ -350,7 +353,8 @@ class MediaCreator:
350
  topic=topic,
351
  genre=genre,
352
  dialect_level=dialect_level,
353
- duration_seconds=duration_seconds
 
354
  )
355
  if comp and comp.get("full_lyrics"):
356
  return comp["full_lyrics"]
 
188
  mood: str = "Energetic & Sarcastic",
189
  custom_lyrics: Optional[str] = None,
190
  audio_engine: str = "acestep",
191
+ cover_style: str = "Cyberpunk Vinyl",
192
+ persona_id: str = ""
193
  ) -> Dict[str, Any]:
194
  """
195
  Generate a complete Moldovan music production package:
196
  - Full parameter customization: 30s-240s duration, 20+ genres, custom BPM & key, vocal style
197
  - Authentic Moldovan lyrics with slang and cultural references (or custom lyrics)
198
  - Suno/Udio/MiniMax prompt blueprint
199
+ - Real studio audio generation (MiniMax Music 3 / ACE-Step v1.5 XL / MusicGen / Voice Cloner / 808 Synth)
200
  - Real FLUX.1 album / vinyl cover art
201
  """
202
  topic_clean = topic.strip() if topic.strip() else "Viața la Chișinău și vama de noapte"
 
223
  if custom_lyrics and custom_lyrics.strip():
224
  lyrics_raw = custom_lyrics.strip()
225
  else:
226
+ lyrics_raw = self._synthesize_song_lyrics(topic_clean, genre, dialect_level, duration_seconds=dur, persona_id=persona_id)
227
 
228
  suno_prompt = (
229
  f"[{genre}, {target_bpm} BPM, {target_key}, {instrumentation}. "
230
  f"Vocal: {target_vocal}. Mood: {mood}. Moldovan Romanian urban dialect, energetic, viral hook, studio mastering]"
231
  )
232
 
233
+ # 1. Real AI Music Generation (MiniMax Music 3 / ACE-Step v1.5 XL / MusicGen / Voice Cloner / 808 Synth)
234
+ print(f"[MediaCreator] Generating music audio for: {title} ({genre}, {dur}s @ {target_bpm} BPM, persona={persona_id})...")
235
  music_res = self.dispatcher.generate_music(
236
  prompt=f"{genre}, {instrumentation}",
237
  lyrics=lyrics_raw,
 
240
  bpm=target_bpm,
241
  key=target_key,
242
  vocal_style=target_vocal,
243
+ engine=audio_engine,
244
+ persona_id=persona_id
245
  )
246
 
247
  audio_url = music_res.get("download_url") or music_res.get("audio_url", "")
 
271
  "bpm": target_bpm,
272
  "key": target_key,
273
  "vocal_style": target_vocal,
274
+ "persona_id": persona_id,
275
  "instrumentation": cfg["instrumentation"],
276
  "suno_style_prompt": suno_prompt,
277
  "full_lyrics": lyrics_raw,
 
344
 
345
  return pkg
346
 
347
+ def _synthesize_song_lyrics(self, topic: str, genre: str, dialect_level: int, duration_seconds: int = 60, persona_id: str = "") -> str:
348
+ """Synthesize original Moldovan verses, chorus, bridge, and outro tailored to the exact topic, genre, and persona."""
349
  try:
350
  from engine.lyricist_engine import LyricistEngine
351
  lyricist = LyricistEngine()
 
353
  topic=topic,
354
  genre=genre,
355
  dialect_level=dialect_level,
356
+ duration_seconds=duration_seconds,
357
+ persona_id=persona_id
358
  )
359
  if comp and comp.get("full_lyrics"):
360
  return comp["full_lyrics"]
moldovan-qwen/engine/model_dispatcher.py CHANGED
@@ -524,11 +524,13 @@ class ModelDispatcher:
524
  bpm: Optional[int] = None,
525
  key: Optional[str] = None,
526
  vocal_style: Optional[str] = None,
527
- engine: str = "acestep"
 
528
  ) -> Dict[str, Any]:
529
  """
530
  Generates full AI music track via ACE-Step v1.5 XL (complete vocals & instruments, 30s-240s),
531
- or HF MusicGen, with fallback to multi-track 808 beat synthesis.
 
532
  """
533
  s = self._load_settings()
534
  di_key = s.get("deepinfra_key") or _env_di()
@@ -542,6 +544,30 @@ class ModelDispatcher:
542
  filename = f"studio_track_{int(time.time())}_{abs(hash(safe_prompt)) % 10000}.mp3"
543
  filepath = os.path.join(AUDIO_DIR, filename)
544
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
545
  # 0. Primary full-song vocal route: ZeroGPU Space REST API / OmniRoute gateway
546
  minimax_url = (
547
  os.environ.get("MINIMAX_MUSIC_URL")
@@ -668,7 +694,30 @@ class ModelDispatcher:
668
  except Exception as e:
669
  print(f"[ModelDispatcher] HF MusicGen failed: {e}")
670
 
671
- # 3. Fallback: Local Multi-Track 808 Synthesizer
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
672
  from engine.audio_synthesizer import MoldovanAudioSynthesizer
673
  synth = MoldovanAudioSynthesizer(output_dir=AUDIO_DIR)
674
  bars_count = max(8, int((dur / 60) * 32))
 
524
  bpm: Optional[int] = None,
525
  key: Optional[str] = None,
526
  vocal_style: Optional[str] = None,
527
+ engine: str = "acestep",
528
+ persona_id: str = ""
529
  ) -> Dict[str, Any]:
530
  """
531
  Generates full AI music track via ACE-Step v1.5 XL (complete vocals & instruments, 30s-240s),
532
+ Moldovan Voice Cloner (cloned persona vocals on 808 beats), or HF MusicGen,
533
+ with fallback to multi-track 808 beat synthesis.
534
  """
535
  s = self._load_settings()
536
  di_key = s.get("deepinfra_key") or _env_di()
 
544
  filename = f"studio_track_{int(time.time())}_{abs(hash(safe_prompt)) % 10000}.mp3"
545
  filepath = os.path.join(AUDIO_DIR, filename)
546
 
547
+ # 0. Explicit Voice Cloner Engine
548
+ if engine in ["voice_cloner", "persona", "ion_king_kong"]:
549
+ try:
550
+ from engine.voice_cloner import MoldovanVoiceCloner
551
+ cloner = MoldovanVoiceCloner(output_dir=AUDIO_DIR)
552
+ pid = persona_id if (persona_id and persona_id in cloner.get_available_personas()) else "ion_king_kong"
553
+ bars_count = max(8, int((dur / 60) * 32))
554
+ res = cloner.create_persona_song(
555
+ persona_id=pid,
556
+ topic=prompt,
557
+ genre=genre,
558
+ bpm=bpm or 140,
559
+ bars=bars_count,
560
+ lyrics=lyrics,
561
+ dialect_level=3
562
+ )
563
+ res["bpm"] = bpm or 140
564
+ res["key"] = key or "C Minor"
565
+ res["duration_seconds"] = dur
566
+ res["audio_url"] = res.get("download_url")
567
+ return res
568
+ except Exception as e:
569
+ print(f"[ModelDispatcher] Explicit VoiceCloner engine failed: {e}")
570
+
571
  # 0. Primary full-song vocal route: ZeroGPU Space REST API / OmniRoute gateway
572
  minimax_url = (
573
  os.environ.get("MINIMAX_MUSIC_URL")
 
694
  except Exception as e:
695
  print(f"[ModelDispatcher] HF MusicGen failed: {e}")
696
 
697
+ # 3. Fallback: Local Voice Cloner (if persona selected) or Multi-Track 808 Synthesizer
698
+ if persona_id:
699
+ try:
700
+ from engine.voice_cloner import MoldovanVoiceCloner
701
+ cloner = MoldovanVoiceCloner(output_dir=AUDIO_DIR)
702
+ if persona_id in cloner.get_available_personas() or persona_id == "ion_king_kong":
703
+ bars_count = max(8, int((dur / 60) * 32))
704
+ res = cloner.create_persona_song(
705
+ persona_id=persona_id,
706
+ topic=prompt,
707
+ genre=genre,
708
+ bpm=bpm or 140,
709
+ bars=bars_count,
710
+ lyrics=lyrics,
711
+ dialect_level=3
712
+ )
713
+ res["bpm"] = bpm or 140
714
+ res["key"] = key or "C Minor"
715
+ res["duration_seconds"] = dur
716
+ res["audio_url"] = res.get("download_url")
717
+ return res
718
+ except Exception as e:
719
+ print(f"[ModelDispatcher] Fallback VoiceCloner failed: {e}")
720
+
721
  from engine.audio_synthesizer import MoldovanAudioSynthesizer
722
  synth = MoldovanAudioSynthesizer(output_dir=AUDIO_DIR)
723
  bars_count = max(8, int((dur / 60) * 32))
moldovan-qwen/engine/voice_cloner.py CHANGED
@@ -19,9 +19,20 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
19
  PERSONAS_DIR = os.path.join(BASE_DIR, "personas")
20
 
21
  class MoldovanVoiceCloner:
22
- def __init__(self, api_key: Optional[str] = None):
23
  self.api_key = api_key or os.environ.get("FISHAUDIO_API_KEY", "")
 
24
  self.personas_dir = PERSONAS_DIR
 
 
 
 
 
 
 
 
 
 
25
 
26
  def get_persona_audio_samples(self, persona_id: str = "ion_king_kong") -> List[str]:
27
  """Returns list of reference audio files for the given persona."""
@@ -48,9 +59,7 @@ class MoldovanVoiceCloner:
48
  Otherwise falls back to native expressive acoustic synth.
49
  """
50
  if not output_path:
51
- output_dir = os.path.join(BASE_DIR, "output", "voices")
52
- os.makedirs(output_dir, exist_ok=True)
53
- output_path = os.path.join(output_dir, f"{persona_id}_speech_{int(time.time())}.wav")
54
 
55
  # Check for reference samples
56
  samples = self.get_persona_audio_samples(persona_id)
@@ -71,20 +80,23 @@ class MoldovanVoiceCloner:
71
 
72
  def create_persona_song(
73
  self,
74
- lyrics: str,
75
  genre: str = "Chișinău 808 Trap",
76
  persona_id: str = "ion_king_kong",
77
  bpm: int = 135,
78
  bars: int = 8,
79
- output_path: Optional[str] = None
 
 
 
80
  ) -> Dict[str, Any]:
81
  """
82
  Generates a complete song with beat + cloned vocal tracks using MoldovanAudioSynthesizer.
83
  """
84
  from engine.audio_synthesizer import MoldovanAudioSynthesizer
85
- synth = MoldovanAudioSynthesizer()
86
 
87
- title = f"{persona_id.replace('_', ' ').title()} - Track"
88
  track_info = synth.generate_track_audio(
89
  title=title,
90
  genre=genre,
@@ -100,14 +112,22 @@ class MoldovanVoiceCloner:
100
  os.makedirs(os.path.dirname(os.path.abspath(output_path)), exist_ok=True)
101
  shutil.copyfile(generated_file, output_path)
102
  track_info["file_path"] = output_path
 
 
103
 
104
  return {
105
  "status": "success",
106
  "persona_id": persona_id,
107
  "genre": genre,
108
  "bpm": bpm,
109
- "duration": track_info.get("duration", 0),
 
110
  "master_song_file": track_info["file_path"],
 
 
 
 
 
111
  "track_info": track_info
112
  }
113
 
 
19
  PERSONAS_DIR = os.path.join(BASE_DIR, "personas")
20
 
21
  class MoldovanVoiceCloner:
22
+ def __init__(self, api_key: Optional[str] = None, output_dir: Optional[str] = None):
23
  self.api_key = api_key or os.environ.get("FISHAUDIO_API_KEY", "")
24
+ self.output_dir = output_dir or os.path.join(BASE_DIR, "output", "voices")
25
  self.personas_dir = PERSONAS_DIR
26
+ os.makedirs(self.output_dir, exist_ok=True)
27
+
28
+ def get_available_personas(self) -> List[str]:
29
+ """Returns list of registered persona identifiers."""
30
+ personas = ["ion_king_kong", "babusca", "taximetrist", "it_ist", "diaspora", "tanti_zina"]
31
+ if os.path.exists(self.personas_dir):
32
+ for d in os.listdir(self.personas_dir):
33
+ if os.path.isdir(os.path.join(self.personas_dir, d)) and d not in personas:
34
+ personas.append(d)
35
+ return personas
36
 
37
  def get_persona_audio_samples(self, persona_id: str = "ion_king_kong") -> List[str]:
38
  """Returns list of reference audio files for the given persona."""
 
59
  Otherwise falls back to native expressive acoustic synth.
60
  """
61
  if not output_path:
62
+ output_path = os.path.join(self.output_dir, f"{persona_id}_speech_{int(time.time())}.wav")
 
 
63
 
64
  # Check for reference samples
65
  samples = self.get_persona_audio_samples(persona_id)
 
80
 
81
  def create_persona_song(
82
  self,
83
+ lyrics: str = "",
84
  genre: str = "Chișinău 808 Trap",
85
  persona_id: str = "ion_king_kong",
86
  bpm: int = 135,
87
  bars: int = 8,
88
+ output_path: Optional[str] = None,
89
+ topic: str = "",
90
+ dialect_level: int = 2,
91
+ **kwargs
92
  ) -> Dict[str, Any]:
93
  """
94
  Generates a complete song with beat + cloned vocal tracks using MoldovanAudioSynthesizer.
95
  """
96
  from engine.audio_synthesizer import MoldovanAudioSynthesizer
97
+ synth = MoldovanAudioSynthesizer(output_dir=self.output_dir)
98
 
99
+ title = topic or f"{persona_id.replace('_', ' ').title()} - Track"
100
  track_info = synth.generate_track_audio(
101
  title=title,
102
  genre=genre,
 
112
  os.makedirs(os.path.dirname(os.path.abspath(output_path)), exist_ok=True)
113
  shutil.copyfile(generated_file, output_path)
114
  track_info["file_path"] = output_path
115
+ track_info["filename"] = os.path.basename(output_path)
116
+ track_info["download_url"] = f"/api/media/audio/stream/{track_info['filename']}"
117
 
118
  return {
119
  "status": "success",
120
  "persona_id": persona_id,
121
  "genre": genre,
122
  "bpm": bpm,
123
+ "duration": track_info.get("duration_seconds", track_info.get("duration", 0)),
124
+ "duration_seconds": track_info.get("duration_seconds", 0),
125
  "master_song_file": track_info["file_path"],
126
+ "filename": track_info.get("filename", ""),
127
+ "download_url": track_info.get("download_url", ""),
128
+ "audio_url": track_info.get("download_url", ""),
129
+ "engine": f"Moldovan Voice Cloner ({persona_id})",
130
+ "provider": "Local Moldovan Voice Cloner Engine",
131
  "track_info": track_info
132
  }
133
 
moldovan-qwen/static/index.html CHANGED
@@ -177,12 +177,13 @@
177
 
178
  <!-- Presets Chips -->
179
  <div class="flex flex-wrap gap-1.5 mb-3">
 
180
  <span class="chip" onclick="applyMusicPreset('Noapte de iarnă pe Botanica', 'Chișinău 808 Trap', 60, 140, 'C Minor')">🌃 Botanica Trap</span>
181
  <span class="chip" onclick="applyMusicPreset('Vama Leușeni la 3 Noaptea', 'Balkan Etno-Rock', 60, 128, 'D Dorian')">🎸 Vama Leușeni</span>
182
  <span class="chip" onclick="applyMusicPreset('Plimbare pe Bulevardul Ștefan cel Mare', 'Modern Moldovan Pop', 90, 122, 'A Minor')">🏙️ Ștefan cel Mare</span>
183
  <span class="chip" onclick="applyMusicPreset('Nuntă la Butuceni cu 500 de oameni', 'Traditional Folk / Lăutărească', 90, 132, 'G Harmonic Minor')">🎻 Nuntă Butuceni</span>
184
  <span class="chip" onclick="applyMusicPreset('Chișinău Cyberpunk 2088', 'Synthwave Post-Sovietic', 120, 115, 'F Minor')">⚡ Cyberpunk 2088</span>
185
- <span class="chip" onclick="applyMusicPreset('Dor de casă din Diaspora', 'Akustik Balcanic / Folkloric Ballad', 120, 105, 'D Minor')">✈️ Diaspora</span>
186
  </div>
187
 
188
  <!-- Topic & Title Concept -->
@@ -234,6 +235,23 @@
234
  <input id="songCustomIntent" class="input text-xs mt-1 text-amber-300" placeholder="✏️ Or enter custom vibe: e.g. Dark cinematic street energy before club opening">
235
  </div>
236
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
237
  <!-- Vocal Delivery & Flow with Custom Override -->
238
  <div class="mt-2">
239
  <label class="text-xs font-bold text-slate-300">Vocal Delivery &amp; Flow</label>
@@ -276,7 +294,8 @@
276
  <div>
277
  <label>Audio Engine</label>
278
  <select id="songEngine" class="input font-bold text-emerald-400">
279
- <option value="minimax_music3" selected>MiniMax Music 3 (ZeroGPU Full Vocal Song)</option>
 
280
  <option value="acestep">ACE-Step v1.5 XL (Studio Vocals + FLUX Cover)</option>
281
  <option value="musicgen">HF MusicGen (Instrumental Diffusion)</option>
282
  <option value="synth808">Multi-Track 808 Synthesizer (Instant $0)</option>
@@ -975,7 +994,7 @@
975
  if (elem) elem.textContent = labels[sec] || `${sec}s`;
976
  }
977
 
978
- function applyMusicPreset(topic, genre, dur, bpm, key) {
979
  $('songTopic').value = topic;
980
  $('songGenre').value = genre;
981
  $('songDuration').value = dur;
@@ -983,7 +1002,10 @@
983
  $('songBpm').value = bpm;
984
  $('bpmVal').textContent = bpm;
985
  $('songKey').value = key;
986
- toast(`Applied preset: ${genre}`);
 
 
 
987
  composeAILyrics();
988
  }
989
 
@@ -1013,6 +1035,8 @@
1013
  const customVocal = $('songCustomVocalStyle')?.value.trim();
1014
  const vocalStyle = customVocal || $('songVocalStyle')?.value || 'Male Baritone Rap & Flow';
1015
 
 
 
1016
  const customKey = $('songCustomKey')?.value.trim();
1017
  const key = customKey || $('songKey')?.value || 'C Minor';
1018
 
@@ -1025,6 +1049,7 @@
1025
  genre,
1026
  intent,
1027
  vocalStyle,
 
1028
  key,
1029
  coverStyle,
1030
  producerBrief
@@ -1056,7 +1081,8 @@
1056
  dialect_level: +$('songDialect').value,
1057
  duration: +$('songDuration').value,
1058
  vocal_style: params.vocalStyle,
1059
- custom_instructions: params.producerBrief
 
1060
  })
1061
  });
1062
 
@@ -1296,7 +1322,8 @@
1296
  mood: params.intent,
1297
  custom_lyrics: lyricsToSend,
1298
  audio_engine: $('songEngine').value,
1299
- cover_style: params.coverStyle
 
1300
  })
1301
  });
1302
  renderSongPackage(d);
 
177
 
178
  <!-- Presets Chips -->
179
  <div class="flex flex-wrap gap-1.5 mb-3">
180
+ <span class="chip border-amber-500/50 bg-amber-950/40 text-amber-300 font-bold" onclick="applyMusicPreset('Chișinău King Kong Trap — De la Botanica la Rîșcani', 'Chișinău 808 Trap', 60, 140, 'C Minor', 'ion_king_kong', 'Aggressive Moldovan street rap, fast triplets flow, autotune', 'Hype & Street Trap')">🦍 King Kong Trap</span>
181
  <span class="chip" onclick="applyMusicPreset('Noapte de iarnă pe Botanica', 'Chișinău 808 Trap', 60, 140, 'C Minor')">🌃 Botanica Trap</span>
182
  <span class="chip" onclick="applyMusicPreset('Vama Leușeni la 3 Noaptea', 'Balkan Etno-Rock', 60, 128, 'D Dorian')">🎸 Vama Leușeni</span>
183
  <span class="chip" onclick="applyMusicPreset('Plimbare pe Bulevardul Ștefan cel Mare', 'Modern Moldovan Pop', 90, 122, 'A Minor')">🏙️ Ștefan cel Mare</span>
184
  <span class="chip" onclick="applyMusicPreset('Nuntă la Butuceni cu 500 de oameni', 'Traditional Folk / Lăutărească', 90, 132, 'G Harmonic Minor')">🎻 Nuntă Butuceni</span>
185
  <span class="chip" onclick="applyMusicPreset('Chișinău Cyberpunk 2088', 'Synthwave Post-Sovietic', 120, 115, 'F Minor')">⚡ Cyberpunk 2088</span>
186
+ <span class="chip" onclick="applyMusicPreset('Dor de casă din Diaspora', 'Akustik Balcanic / Folkloric Ballad', 120, 105, 'D Minor', 'diaspora')">✈️ Diaspora</span>
187
  </div>
188
 
189
  <!-- Topic & Title Concept -->
 
235
  <input id="songCustomIntent" class="input text-xs mt-1 text-amber-300" placeholder="✏️ Or enter custom vibe: e.g. Dark cinematic street energy before club opening">
236
  </div>
237
 
238
+ <!-- Artist Persona & Cloned Voice -->
239
+ <div class="mt-2">
240
+ <label class="text-xs font-bold text-amber-300 flex items-center justify-between">
241
+ <span>🎤 Artist Persona &amp; Cloned Voice</span>
242
+ <span class="text-[10px] text-slate-400 font-normal">Multi-track vocal synthesis</span>
243
+ </label>
244
+ <select id="songPersona" class="input font-medium text-amber-200">
245
+ <option value="">Standard Studio Vocalist (AI Multi-Engine)</option>
246
+ <option value="ion_king_kong" selected>🦍 Ion King Kong (Bariton Urban Brutal &amp; Chișinău Flow)</option>
247
+ <option value="babusca">👵🏻 Mătușa Ileana (Babușca de la Sat)</option>
248
+ <option value="taximetrist">🚖 Nea Vasile (Taximetristul din Chișinău)</option>
249
+ <option value="it_guy">💻 Radu (IT-ist la Start-up Chișinău)</option>
250
+ <option value="diaspora">✈️ Andrei (Moldovean în Diaspora / Padova)</option>
251
+ <option value="piata_centrala">👜 Tanti Zina (Negustor la Piața Centrală)</option>
252
+ </select>
253
+ </div>
254
+
255
  <!-- Vocal Delivery & Flow with Custom Override -->
256
  <div class="mt-2">
257
  <label class="text-xs font-bold text-slate-300">Vocal Delivery &amp; Flow</label>
 
294
  <div>
295
  <label>Audio Engine</label>
296
  <select id="songEngine" class="input font-bold text-emerald-400">
297
+ <option value="voice_cloner" selected>🦍 Moldovan Voice Cloner (Ion King Kong Engine)</option>
298
+ <option value="minimax_music3">MiniMax Music 3 (ZeroGPU Full Vocal Song)</option>
299
  <option value="acestep">ACE-Step v1.5 XL (Studio Vocals + FLUX Cover)</option>
300
  <option value="musicgen">HF MusicGen (Instrumental Diffusion)</option>
301
  <option value="synth808">Multi-Track 808 Synthesizer (Instant $0)</option>
 
994
  if (elem) elem.textContent = labels[sec] || `${sec}s`;
995
  }
996
 
997
+ function applyMusicPreset(topic, genre, dur, bpm, key, persona = '', vocalStyle = '', intent = '') {
998
  $('songTopic').value = topic;
999
  $('songGenre').value = genre;
1000
  $('songDuration').value = dur;
 
1002
  $('songBpm').value = bpm;
1003
  $('bpmVal').textContent = bpm;
1004
  $('songKey').value = key;
1005
+ if ($('songPersona')) $('songPersona').value = persona;
1006
+ if (vocalStyle && $('songVocalStyle')) $('songVocalStyle').value = vocalStyle;
1007
+ if (intent && $('songIntent')) $('songIntent').value = intent;
1008
+ toast(`Applied preset: ${genre}${persona ? ' (' + persona + ')' : ''}`);
1009
  composeAILyrics();
1010
  }
1011
 
 
1035
  const customVocal = $('songCustomVocalStyle')?.value.trim();
1036
  const vocalStyle = customVocal || $('songVocalStyle')?.value || 'Male Baritone Rap & Flow';
1037
 
1038
+ const personaId = $('songPersona')?.value || '';
1039
+
1040
  const customKey = $('songCustomKey')?.value.trim();
1041
  const key = customKey || $('songKey')?.value || 'C Minor';
1042
 
 
1049
  genre,
1050
  intent,
1051
  vocalStyle,
1052
+ personaId,
1053
  key,
1054
  coverStyle,
1055
  producerBrief
 
1081
  dialect_level: +$('songDialect').value,
1082
  duration: +$('songDuration').value,
1083
  vocal_style: params.vocalStyle,
1084
+ custom_instructions: params.producerBrief,
1085
+ persona_id: params.personaId
1086
  })
1087
  });
1088
 
 
1322
  mood: params.intent,
1323
  custom_lyrics: lyricsToSend,
1324
  audio_engine: $('songEngine').value,
1325
+ cover_style: params.coverStyle,
1326
+ persona_id: params.personaId
1327
  })
1328
  });
1329
  renderSongPackage(d);
moldovan-qwen/studio_server.py CHANGED
@@ -482,6 +482,7 @@ async def compose_custom_lyrics(body: Dict = Body(...)):
482
  duration_seconds = int(body.get("duration", body.get("duration_seconds", 60)))
483
  vocal_style = body.get("vocal_style", "Aggressive Moldovan street rap, fast triplets flow, autotune")
484
  custom_instructions = body.get("custom_instructions", "")
 
485
 
486
  from engine.lyricist_engine import LyricistEngine
487
  lyricist = LyricistEngine()
@@ -495,7 +496,8 @@ async def compose_custom_lyrics(body: Dict = Body(...)):
495
  dialect_level=dialect_level,
496
  duration_seconds=duration_seconds,
497
  vocal_style=vocal_style,
498
- custom_instructions=custom_instructions
 
499
  )
500
  return res
501
  except Exception as e:
@@ -522,6 +524,7 @@ async def create_custom_song(body: Dict = Body(...)):
522
  custom_lyrics = body.get("lyrics", body.get("custom_lyrics"))
523
  audio_engine = body.get("engine", body.get("audio_engine", "acestep"))
524
  cover_style = body.get("cover_style", "Cyberpunk Vinyl")
 
525
 
526
  from engine.media_creator import MediaCreator
527
  mc = MediaCreator()
@@ -538,7 +541,8 @@ async def create_custom_song(body: Dict = Body(...)):
538
  mood=mood,
539
  custom_lyrics=custom_lyrics,
540
  audio_engine=audio_engine,
541
- cover_style=cover_style
 
542
  )
543
  except RuntimeError as e:
544
  # A selected real model must never silently degrade to procedural audio.
 
482
  duration_seconds = int(body.get("duration", body.get("duration_seconds", 60)))
483
  vocal_style = body.get("vocal_style", "Aggressive Moldovan street rap, fast triplets flow, autotune")
484
  custom_instructions = body.get("custom_instructions", "")
485
+ persona_id = body.get("persona_id", body.get("persona", ""))
486
 
487
  from engine.lyricist_engine import LyricistEngine
488
  lyricist = LyricistEngine()
 
496
  dialect_level=dialect_level,
497
  duration_seconds=duration_seconds,
498
  vocal_style=vocal_style,
499
+ custom_instructions=custom_instructions,
500
+ persona_id=persona_id
501
  )
502
  return res
503
  except Exception as e:
 
524
  custom_lyrics = body.get("lyrics", body.get("custom_lyrics"))
525
  audio_engine = body.get("engine", body.get("audio_engine", "acestep"))
526
  cover_style = body.get("cover_style", "Cyberpunk Vinyl")
527
+ persona_id = body.get("persona_id", body.get("persona", ""))
528
 
529
  from engine.media_creator import MediaCreator
530
  mc = MediaCreator()
 
541
  mood=mood,
542
  custom_lyrics=custom_lyrics,
543
  audio_engine=audio_engine,
544
+ cover_style=cover_style,
545
+ persona_id=persona_id
546
  )
547
  except RuntimeError as e:
548
  # A selected real model must never silently degrade to procedural audio.
tests/unit/test_api_endpoints.py CHANGED
@@ -102,3 +102,39 @@ def test_space_models_catalog_includes_new_models(space_client):
102
  assert resolve_model("deepseek-v4-pro", choices) == "DeepSeek-V4-Pro-Qwen3.5-9B-MTP-Q4_K_M.gguf"
103
  assert resolve_model("petruhonk/qwen3.8-9b-distill-uncensored-heretic", choices) == "Qwen3.8-9B-Distill-Heretic-Uncensored-Q8_0.gguf"
104
  assert resolve_model("jackrong/deepseek-v4-pro-qwen3.5-9b-mtp", choices) == "DeepSeek-V4-Pro-Qwen3.5-9B-MTP-Q4_K_M.gguf"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  assert resolve_model("deepseek-v4-pro", choices) == "DeepSeek-V4-Pro-Qwen3.5-9B-MTP-Q4_K_M.gguf"
103
  assert resolve_model("petruhonk/qwen3.8-9b-distill-uncensored-heretic", choices) == "Qwen3.8-9B-Distill-Heretic-Uncensored-Q8_0.gguf"
104
  assert resolve_model("jackrong/deepseek-v4-pro-qwen3.5-9b-mtp", choices) == "DeepSeek-V4-Pro-Qwen3.5-9B-MTP-Q4_K_M.gguf"
105
+
106
+
107
+ def test_studio_lyrics_compose_with_persona(studio_client):
108
+ """Verify /api/media/lyrics/compose accepts persona_id and generates themed song sections."""
109
+ payload = {
110
+ "topic": "Botanica vs Rîșcani",
111
+ "genre": "Chișinău 808 Trap",
112
+ "duration": 60,
113
+ "persona_id": "ion_king_kong",
114
+ "dialect_level": 3
115
+ }
116
+ resp = studio_client.post("/api/media/lyrics/compose", json=payload)
117
+ assert resp.status_code == 200
118
+ data = resp.json()
119
+ assert "sections" in data
120
+ assert len(data["sections"]) > 0
121
+
122
+
123
+ def test_studio_song_create_with_persona(studio_client):
124
+ """Verify /api/media/song/create passes persona_id and returns audio package."""
125
+ payload = {
126
+ "topic": "King Kong Chișinău",
127
+ "genre": "Chișinău 808 Trap",
128
+ "duration": 30,
129
+ "audio_engine": "voice_cloner",
130
+ "persona_id": "ion_king_kong"
131
+ }
132
+ resp = studio_client.post("/api/media/song/create", json=payload)
133
+ assert resp.status_code == 200
134
+ data = resp.json()
135
+ assert "id" in data
136
+ assert data["persona_id"] == "ion_king_kong"
137
+ assert "audio_url" in data
138
+ assert "full_lyrics" in data
139
+
140
+