Spaces:
Running on Zero
Running on Zero
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 +122 -122
- moldovan-qwen/engine/lyricist_engine.py +15 -1
- moldovan-qwen/engine/media_creator.py +13 -9
- moldovan-qwen/engine/model_dispatcher.py +52 -3
- moldovan-qwen/engine/voice_cloner.py +29 -9
- moldovan-qwen/static/index.html +33 -6
- moldovan-qwen/studio_server.py +6 -2
- tests/unit/test_api_endpoints.py +36 -0
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":
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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":
|
| 65 |
},
|
| 66 |
{
|
| 67 |
-
"title": "
|
| 68 |
-
"category": "
|
| 69 |
-
"persona": "
|
| 70 |
-
"genre": "
|
| 71 |
-
"timestamp":
|
| 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":
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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":
|
| 86 |
},
|
| 87 |
{
|
| 88 |
-
"title": "Mătușa Ileana
|
| 89 |
-
"category": "
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
"persona": "babusca",
|
| 91 |
-
"genre": "
|
| 92 |
-
"timestamp":
|
| 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":
|
| 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":
|
| 107 |
},
|
| 108 |
{
|
| 109 |
-
"title": "
|
| 110 |
-
"category": "
|
| 111 |
-
"persona": "
|
| 112 |
-
"genre": "
|
| 113 |
-
"timestamp":
|
| 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":
|
| 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":
|
| 135 |
},
|
| 136 |
{
|
| 137 |
-
"title": "
|
| 138 |
"category": "PARODY_STANDUP",
|
| 139 |
-
"persona": "
|
| 140 |
-
"genre": "
|
| 141 |
-
"timestamp":
|
| 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":
|
| 163 |
},
|
| 164 |
{
|
| 165 |
-
"title": "
|
| 166 |
-
"category": "
|
| 167 |
-
"persona": "
|
| 168 |
-
"genre": "
|
| 169 |
-
"timestamp":
|
| 170 |
},
|
| 171 |
{
|
| 172 |
-
"title": "
|
| 173 |
-
"category": "
|
| 174 |
-
"persona": "
|
| 175 |
-
"genre": "
|
| 176 |
-
"timestamp":
|
| 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":
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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":
|
| 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":
|
| 198 |
},
|
| 199 |
{
|
| 200 |
-
"title": "
|
| 201 |
-
"category": "
|
| 202 |
-
"persona": "
|
| 203 |
-
"genre": "
|
| 204 |
-
"timestamp":
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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":
|
| 212 |
}
|
| 213 |
],
|
| 214 |
"category_counts": {
|
| 215 |
-
"INSPIRING_CULTURE":
|
| 216 |
-
"TIKTOK_REELS_SHORT":
|
| 217 |
-
"PARODY_STANDUP":
|
| 218 |
-
"PERSONA_DUEL":
|
| 219 |
-
"SATIRICAL_NEWS":
|
| 220 |
-
"DIVERSE_MUSIC_ART":
|
| 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
|
| 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 |
-
|
|
|
|
| 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 |
-
|
| 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 & 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="
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 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 & 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 & 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 & 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 |
+
|