Spaces:
Paused
Paused
AndrianBalanescu commited on
Commit ·
ab60391
1
Parent(s): d542710
chore: sync deploy state
Browse files- .gitignore +1 -89
- INDEX.md +1 -1
- moldovan-qwen/data/generated_audio/studio_track_1788546676_7746.json +4 -1
- moldovan-qwen/engine/lyricist_engine.py +111 -3
- moldovan-qwen/personas/moldovan_master_lyricist_lexicon.md +1 -2
- moldovan-qwen/scripts/daily_album_cron.py +144 -56
- moldovan-qwen/scripts/run_studio.sh +20 -0
- moldovan-qwen/static/index.html +546 -89
- moldovan-qwen/studio_server.py +151 -47
.gitignore
CHANGED
|
@@ -1,89 +1 @@
|
|
| 1 |
-
|
| 2 |
-
.env
|
| 3 |
-
.env.local
|
| 4 |
-
HF_TOKEN
|
| 5 |
-
HF_API_KEY
|
| 6 |
-
|
| 7 |
-
# Python
|
| 8 |
-
__pycache__/
|
| 9 |
-
*.py[cod]
|
| 10 |
-
*$py.class
|
| 11 |
-
*.pyo
|
| 12 |
-
*.so
|
| 13 |
-
build/
|
| 14 |
-
dist/
|
| 15 |
-
*.egg-info/
|
| 16 |
-
.eggs/
|
| 17 |
-
.venv/
|
| 18 |
-
venv/
|
| 19 |
-
ENV/
|
| 20 |
-
|
| 21 |
-
# IDE / editors
|
| 22 |
-
.idea/
|
| 23 |
-
.vscode/
|
| 24 |
-
*.swp
|
| 25 |
-
*.swo
|
| 26 |
-
*~
|
| 27 |
-
.DS_Store
|
| 28 |
-
|
| 29 |
-
# Project-specific
|
| 30 |
-
.memrize/
|
| 31 |
-
memrize.db*
|
| 32 |
-
.fastembed_cache/
|
| 33 |
-
*.lock
|
| 34 |
-
uv.lock
|
| 35 |
-
poetry.lock
|
| 36 |
-
.pyc
|
| 37 |
-
*.pyc
|
| 38 |
-
*.pyd
|
| 39 |
-
|
| 40 |
-
# Logs & temp
|
| 41 |
-
*.log
|
| 42 |
-
*.tmp
|
| 43 |
-
temp/
|
| 44 |
-
tmp/
|
| 45 |
-
node_modules/
|
| 46 |
-
|
| 47 |
-
# HuggingFace Spaces
|
| 48 |
-
space_files/
|
| 49 |
-
space_code/
|
| 50 |
-
spaces/
|
| 51 |
-
|
| 52 |
-
# Build artifacts
|
| 53 |
-
*.egg-info/
|
| 54 |
-
*.egg
|
| 55 |
-
*.whl
|
| 56 |
-
|
| 57 |
-
# Local model cache (optional — uncomment if you want to commit models)
|
| 58 |
-
# .cache/
|
| 59 |
-
# models/
|
| 60 |
-
# data/
|
| 61 |
-
|
| 62 |
-
# Docker
|
| 63 |
-
docker-compose.override.yml
|
| 64 |
-
.docker/
|
| 65 |
-
|
| 66 |
-
# Coverage
|
| 67 |
-
.coverage
|
| 68 |
-
htmlcov/
|
| 69 |
-
.pytest_cache/moldovan-qwen/.env
|
| 70 |
-
|
| 71 |
-
# Generated media binary files
|
| 72 |
-
*.wav
|
| 73 |
-
*.mp3
|
| 74 |
-
*.mp4
|
| 75 |
-
*.png
|
| 76 |
-
*.jpg
|
| 77 |
-
*.jpeg
|
| 78 |
-
data/generated_audio/
|
| 79 |
-
data/generated_images/
|
| 80 |
-
data/generated_videos/
|
| 81 |
-
moldovan-qwen/data/generated_albums/
|
| 82 |
-
moldovan-qwen/data/generated_audio/
|
| 83 |
-
moldovan-qwen/data/generated_music/song_*.json
|
| 84 |
-
moldovan-qwen/data/generated_digests/*.json
|
| 85 |
-
moldovan-qwen/data/generated_digests/*.md
|
| 86 |
-
moldovan-qwen/data/zerogpu_daily_budget.json
|
| 87 |
-
moldovan-qwen/data/*.log
|
| 88 |
-
moldovan-qwen/data/archive/
|
| 89 |
-
studio-pb/
|
|
|
|
| 1 |
+
moldovan-qwen/data/studio_settings.json
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
INDEX.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
<!-- auto; hash=
|
| 2 |
# hf/ (4 src, 7 meta)
|
| 3 |
meta: CHANGELOG.md
|
| 4 |
meta: Dockerfile
|
|
|
|
| 1 |
+
<!-- auto; hash=91f4ae744ac1d054 -->
|
| 2 |
# hf/ (4 src, 7 meta)
|
| 3 |
meta: CHANGELOG.md
|
| 4 |
meta: Dockerfile
|
moldovan-qwen/data/generated_audio/studio_track_1788546676_7746.json
CHANGED
|
@@ -31,5 +31,8 @@
|
|
| 31 |
}
|
| 32 |
],
|
| 33 |
"created_at": 1788546676,
|
| 34 |
-
"updated_at": 1788552989.574851
|
|
|
|
|
|
|
|
|
|
| 35 |
}
|
|
|
|
| 31 |
}
|
| 32 |
],
|
| 33 |
"created_at": 1788546676,
|
| 34 |
+
"updated_at": 1788552989.574851,
|
| 35 |
+
"tags": [
|
| 36 |
+
"hit"
|
| 37 |
+
]
|
| 38 |
}
|
moldovan-qwen/engine/lyricist_engine.py
CHANGED
|
@@ -197,6 +197,98 @@ class LyricistEngine:
|
|
| 197 |
"traditional": self.rural_idioms
|
| 198 |
}
|
| 199 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 200 |
def compose_lyrics(
|
| 201 |
self,
|
| 202 |
topic: str,
|
|
@@ -269,6 +361,16 @@ class LyricistEngine:
|
|
| 269 |
"NICIODATĂ nu menționa aceste reguli sau sistemul în versuri (no prompt leak)."
|
| 270 |
)
|
| 271 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 272 |
system_prompt = f"""
|
| 273 |
Ești cel mai mare compozitor, textier și director muzical din Republica Moldova.
|
| 274 |
Misiunea ta: scrie o piesă 100% ORIGINALĂ, GENIALĂ și AUTENTIC MOLDOVENEASCĂ. "Less is more": versuri SCURTE, PERCUTANTE, memorabile — nu umple versurile cu dicționarul; alege regionalismele cu impact, nu pe toate.
|
|
@@ -277,15 +379,21 @@ Misiunea ta: scrie o piesă 100% ORIGINALĂ, GENIALĂ și AUTENTIC MOLDOVENEASC
|
|
| 277 |
|
| 278 |
REGULI CRUCIALE DE COMPOZIȚIE:
|
| 279 |
0. Răspunsul conține EXCLUSIV piesa: titlu + secțiuni + versuri. Fără titluri de reguli, fără explicații, fără meta-comentarii, fără "REGULI", "Format", "Producer Brief" — nimic în afara structurii piesei. Orice linie care reia regulile sau promptul este un EȘEC.
|
| 280 |
-
1. Rime scurte și percutante: fiecare vers 6-11 silabe, lovitură directă, fără umplutură. Folosește regionalisme autentice din baza de date ({sample_rural}), argou din Chișinău ({sample_slang}) și fonetică moldovenească (
|
| 281 |
1b-FAMILY. {rule_1b}
|
| 282 |
2. NU repeta fraza utilizatorului ca vers literal! Analizează conceptul ('{clean_topic}') și creează metafore, personaje, rime și poveste dedicată.
|
| 283 |
-
3.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 284 |
4. Gen Muzical: {genre} ({bpm} BPM).
|
| 285 |
5. Stil Vocal & Flow: {vocal_style}.
|
| 286 |
6. Intenție Emoțională / Vibe: {intent}.
|
| 287 |
7. Nivel Dialectal ({dialect_level}/4): {dialect_rule}.
|
| 288 |
-
|
|
|
|
| 289 |
{persona_context}
|
| 290 |
"""
|
| 291 |
|
|
|
|
| 197 |
"traditional": self.rural_idioms
|
| 198 |
}
|
| 199 |
|
| 200 |
+
def _sample_dynamic_lexical_palette(
|
| 201 |
+
self,
|
| 202 |
+
genre: str,
|
| 203 |
+
topic: str,
|
| 204 |
+
dialect_level: int = 3,
|
| 205 |
+
family_mode: bool = False
|
| 206 |
+
) -> Dict[str, str]:
|
| 207 |
+
"""
|
| 208 |
+
Dynamically sample a diverse, non-repeating palette of words and expressions
|
| 209 |
+
from the master dictionary tailored specifically to the song genre and topic.
|
| 210 |
+
Guarantees that songs avoid monotonous word reuse.
|
| 211 |
+
"""
|
| 212 |
+
import random
|
| 213 |
+
|
| 214 |
+
genre_lower = (genre or "").lower()
|
| 215 |
+
topic_lower = (topic or "").lower()
|
| 216 |
+
|
| 217 |
+
urban_slang = []
|
| 218 |
+
chisinau_places = []
|
| 219 |
+
cozy_nature = []
|
| 220 |
+
traditional_folklore = []
|
| 221 |
+
family_friendly = []
|
| 222 |
+
action_verbs = []
|
| 223 |
+
|
| 224 |
+
for e in self.master_dict_entries:
|
| 225 |
+
w = (e.get("word") or e.get("term") or "").strip()
|
| 226 |
+
cat = (e.get("category") or "").lower()
|
| 227 |
+
if not w or len(w) < 3:
|
| 228 |
+
continue
|
| 229 |
+
|
| 230 |
+
if any(k in cat for k in ["argou", "slang"]):
|
| 231 |
+
urban_slang.append(w)
|
| 232 |
+
elif any(k in cat for k in ["neighborhoods", "regions", "landmarks"]):
|
| 233 |
+
chisinau_places.append(w)
|
| 234 |
+
elif any(k in cat for k in ["natură", "faună", "gastronomie"]):
|
| 235 |
+
cozy_nature.append(w)
|
| 236 |
+
family_friendly.append(w)
|
| 237 |
+
elif any(k in cat for k in ["regionalisme", "arhaisme", "tradiție"]):
|
| 238 |
+
traditional_folklore.append(w)
|
| 239 |
+
elif "acțiuni" in cat or "verbe" in cat:
|
| 240 |
+
action_verbs.append(w)
|
| 241 |
+
|
| 242 |
+
# Ensure non-empty fallback lists
|
| 243 |
+
if not urban_slang:
|
| 244 |
+
urban_slang = ["koroce", "dăvai", "pațani", "blană", "wăi", "harosh", "jăpcan", "mahala", "dric", "bulevard"]
|
| 245 |
+
if not chisinau_places:
|
| 246 |
+
chisinau_places = ["Botanica", "Ciocana", "Râșcani", "Centru", "Buiucani", "Telecentru", "Valea Morilor", "Bulevardul Dacia", "Calea Ieșilor"]
|
| 247 |
+
if not cozy_nature:
|
| 248 |
+
cozy_nature = ["izvor", "răsărit", "ploaie", "frunze", "fântână", "grădină", "amintiri", "miere", "livadă", "apus"]
|
| 249 |
+
if not traditional_folklore:
|
| 250 |
+
traditional_folklore = ["prisacă", "cucoș", "harbuz", "cobză", "fluier", "doină", "horă", "opincă", "hultan"]
|
| 251 |
+
|
| 252 |
+
# Genre-specific palette configuration
|
| 253 |
+
if family_mode or "albina" in genre_lower:
|
| 254 |
+
family_pool = list(set(family_friendly + ["albină", "miere", "stup", "flori", "nectar", "soare", "zâmbet", "curcubeu", "joacă", "bucurie", "mărușor"]))
|
| 255 |
+
selected_words = random.sample(family_pool, min(8, len(family_pool)))
|
| 256 |
+
phonetics = ["oleacă", "amu", "ghini", "tăt", "drăgălaș"]
|
| 257 |
+
ad_libs = ["(bzz-bzz!)", "(zum-zum!)", "(hop-așa!)", "(ce dulce!)", "(hei-hop!)", "(veselie!)"]
|
| 258 |
+
genre_notes = "MOD FAMILIE / COPII: Folosește exclusiv cuvinte luminoase, jucăușe, miere, flori, soare, zumzet de albine. ZERO argou sau vorbe dure."
|
| 259 |
+
elif any(k in genre_lower for k in ["deep house", "house", "edm", "club", "nocturn"]):
|
| 260 |
+
# Nocturnal deep house: urban, lights, bass, rhythm, night, streets. NO rustic farm words!
|
| 261 |
+
nocturnal_pool = list(set(chisinau_places + [
|
| 262 |
+
"bulevard", "noapte", "bas", "lumini", "neon", "puls", "ritm", "fior", "ecou",
|
| 263 |
+
"viteză", "priviri", "asfalt", "umbre", "vibrație", "scânteie", "ceață", "miezul nopții"
|
| 264 |
+
]))
|
| 265 |
+
selected_words = random.sample(nocturnal_pool, min(8, len(nocturnal_pool)))
|
| 266 |
+
phonetics = ["șî", "pi", "tăt", "amu"]
|
| 267 |
+
ad_libs = ["(dă-i play)", "(noaptea)", "(simte basul)", "(yeah)", "(drop)", "(așa)", "(doar noi)", "(vibe)"]
|
| 268 |
+
genre_notes = "DIRECTIVĂ DEEP HOUSE: Cadru urban nocturn, lumini de neon, condus nocturn, priviri în club, puls de bas hipnotic la 124 BPM. Stil minimalist, aerisit, repetitiv senzual. STRICT INTERZIS vocabularul de fermă/gospodărie (fără harbuz, cucoș, băligar, oloi)."
|
| 269 |
+
elif any(k in genre_lower for k in ["chill", "lo-fi", "ambient", "acoustic", "doina"]):
|
| 270 |
+
chill_pool = list(set(cozy_nature + chisinau_places[:4] + [
|
| 271 |
+
"ceai", "cafea", "fereastră", "nori", "liniște", "toamnă", "amintire", "dor", "vânt", "pas", "șoaptă", "frunze galbene", "balcon"
|
| 272 |
+
]))
|
| 273 |
+
selected_words = random.sample(chill_pool, min(8, len(chill_pool)))
|
| 274 |
+
phonetics = ["oleacă", "amu", "tăt", "di ce", "ghini"]
|
| 275 |
+
ad_libs = ["(ușor)", "(ascultă)", "(ploaia)", "(așa)", "(liniște)", "(mhm)", "(adânc)"]
|
| 276 |
+
genre_notes = "DIRECTIVĂ CHILL / LO-FI: Ritm lent, căldură, nostalgie intimă, conversațional, pauze melodioase, amintiri, ploaie sau apus. Repetiții calde și line."
|
| 277 |
+
else:
|
| 278 |
+
# Trap / Balkan 808 / Rap / High Energy
|
| 279 |
+
trap_pool = list(set(urban_slang + chisinau_places + action_verbs + traditional_folklore[:10]))
|
| 280 |
+
selected_words = random.sample(trap_pool, min(8, len(trap_pool)))
|
| 281 |
+
phonetics = ["pi", "di", "șî", "ghini", "tăt", "amu", "wăi"]
|
| 282 |
+
ad_libs = ["(dă-i!)", "(așa!)", "(Chișinău!)", "(hop!)", "(808!)", "(trage!)", "(foc!)", "(yeah!)", "(blană!)"]
|
| 283 |
+
genre_notes = "DIRECTIVĂ MOLDOVAN TRAP & BALKAN 808: Flow alert, cadență sacadată, triolete, energie de stradă autentică, punchlines, bas greu distorsionat, ad-libs explozive."
|
| 284 |
+
|
| 285 |
+
return {
|
| 286 |
+
"selected_words_str": ", ".join(selected_words),
|
| 287 |
+
"phonetics_str": ", ".join(phonetics),
|
| 288 |
+
"ad_libs_str": ", ".join(ad_libs),
|
| 289 |
+
"genre_notes": genre_notes
|
| 290 |
+
}
|
| 291 |
+
|
| 292 |
def compose_lyrics(
|
| 293 |
self,
|
| 294 |
topic: str,
|
|
|
|
| 361 |
"NICIODATĂ nu menționa aceste reguli sau sistemul în versuri (no prompt leak)."
|
| 362 |
)
|
| 363 |
|
| 364 |
+
palette = self._sample_dynamic_lexical_palette(
|
| 365 |
+
genre=genre,
|
| 366 |
+
topic=clean_topic,
|
| 367 |
+
dialect_level=dialect_level,
|
| 368 |
+
family_mode=family_mode
|
| 369 |
+
)
|
| 370 |
+
|
| 371 |
+
sample_rural = palette["selected_words_str"]
|
| 372 |
+
sample_slang = palette["selected_words_str"]
|
| 373 |
+
|
| 374 |
system_prompt = f"""
|
| 375 |
Ești cel mai mare compozitor, textier și director muzical din Republica Moldova.
|
| 376 |
Misiunea ta: scrie o piesă 100% ORIGINALĂ, GENIALĂ și AUTENTIC MOLDOVENEASCĂ. "Less is more": versuri SCURTE, PERCUTANTE, memorabile — nu umple versurile cu dicționarul; alege regionalismele cu impact, nu pe toate.
|
|
|
|
| 379 |
|
| 380 |
REGULI CRUCIALE DE COMPOZIȚIE:
|
| 381 |
0. Răspunsul conține EXCLUSIV piesa: titlu + secțiuni + versuri. Fără titluri de reguli, fără explicații, fără meta-comentarii, fără "REGULI", "Format", "Producer Brief" — nimic în afara structurii piesei. Orice linie care reia regulile sau promptul este un EȘEC.
|
| 382 |
+
1. Rime scurte și percutante: fiecare vers 6-11 silabe, lovitură directă, fără umplutură. Folosește regionalisme autentice din baza de date ({sample_rural}), argou din Chișinău ({sample_slang}) și fonetică moldovenească ({palette['phonetics_str']}).
|
| 383 |
1b-FAMILY. {rule_1b}
|
| 384 |
2. NU repeta fraza utilizatorului ca vers literal! Analizează conceptul ('{clean_topic}') și creează metafore, personaje, rime și poveste dedicată.
|
| 385 |
+
3. LEGEA MUZICII (SONGWRITING REAL VS POEZIE ȘCOLARĂ):
|
| 386 |
+
- O piesă muzicală NU este o poezie monotonă de recitat la școală! Trăiește prin RITM, GROOVE, CADENȚĂ ȘI REPETIȚIE CATCHY! ({syllables_per_line}).
|
| 387 |
+
- REFRENUL (CHORUS / HOOK) ESTE UN EARWORM: Repetă fraza principală (hook-ul) de 2-4 ori cu variații de energie și ad-libs! Refrenul trebuie să fie memorabil, hipnotic și ușor de fredonat. Nu schimba fiecare vers cu idei noi în refren — repetiția creează hitul!
|
| 388 |
+
- PRE-CHORUS: Creează tensiune ritmică în creștere înainte de refren / drop (2-4 versuri scurte).
|
| 389 |
+
- AD-LIBS & ACCENTE RITMICE ÎN PARANTEZE: Presară ad-libs vocale și accente ritmice specifice stilului: {palette['ad_libs_str']}. Ele dau viață vocii AI și creează groove!
|
| 390 |
+
- Rimează natural (AABB sau ABAB) și asigură cadență ritmată pe beat ({syllables_per_line}).
|
| 391 |
4. Gen Muzical: {genre} ({bpm} BPM).
|
| 392 |
5. Stil Vocal & Flow: {vocal_style}.
|
| 393 |
6. Intenție Emoțională / Vibe: {intent}.
|
| 394 |
7. Nivel Dialectal ({dialect_level}/4): {dialect_rule}.
|
| 395 |
+
8. {palette['genre_notes']}
|
| 396 |
+
{f'9. Producer Brief / Instrucțiuni Speciale: {custom_instructions}' if custom_instructions else ''}
|
| 397 |
{persona_context}
|
| 398 |
"""
|
| 399 |
|
moldovan-qwen/personas/moldovan_master_lyricist_lexicon.md
CHANGED
|
@@ -77,7 +77,7 @@ Ghid lexical, fonetic și cultural complet generat din baza de date master (mold
|
|
| 77 |
- Irina Rimes
|
| 78 |
- SunStroke Project
|
| 79 |
|
| 80 |
-
## 3. VOCABULAR COMPLET DIN MASTER DB (
|
| 81 |
- **a abişi** (L4 • Argou Urban Chișinău) = a promite (A promite (argou stradal).)
|
| 82 |
- **a ahui** (L4 • Argou Urban Chișinău) = a încălca regulile de conduită (A încălca regulile de conduită, a o lua razna.)
|
| 83 |
- **a dăvi** (L3 • Expresii & Idiomuri) = a presa / a forța / a bate la cap (A insista exagerat asupra cuiva sau a grăbi o decizie.)
|
|
@@ -757,7 +757,6 @@ Ghid lexical, fonetic și cultural complet generat din baza de date master (mold
|
|
| 757 |
- **îndruliga** (L3 • Acțiuni & Verbe Regionale) = (vb) a coase sau a țese când gros, când subțire
|
| 758 |
- **îngena** (L3 • Fonetică & Schimbări Dialectale) = (vb) a încetini
|
| 759 |
- **îngingia** (L3 • Gastronomie & Tradiție) = (vb) despre porumb, a începe a face boabe
|
| 760 |
-
- **îngurlui** (L3 • Acțiuni & Verbe Regionale) = (vb) a se încurca cu cineva în sensul de a avea o relație cu cineva
|
| 761 |
- **însâmbrare** (L3 • Fonetică & Schimbări Dialectale) = asociere, întovărășire
|
| 762 |
- **întercleți** (L3 • Acțiuni & Verbe Regionale) = (vb) a încremeni
|
| 763 |
- **întiri** (L3 • Acțiuni & Verbe Regionale) = (vb) a alunga, a îndepărta
|
|
|
|
| 77 |
- Irina Rimes
|
| 78 |
- SunStroke Project
|
| 79 |
|
| 80 |
+
## 3. VOCABULAR COMPLET DIN MASTER DB (730 TERMENI)
|
| 81 |
- **a abişi** (L4 • Argou Urban Chișinău) = a promite (A promite (argou stradal).)
|
| 82 |
- **a ahui** (L4 • Argou Urban Chișinău) = a încălca regulile de conduită (A încălca regulile de conduită, a o lua razna.)
|
| 83 |
- **a dăvi** (L3 • Expresii & Idiomuri) = a presa / a forța / a bate la cap (A insista exagerat asupra cuiva sau a grăbi o decizie.)
|
|
|
|
| 757 |
- **îndruliga** (L3 • Acțiuni & Verbe Regionale) = (vb) a coase sau a țese când gros, când subțire
|
| 758 |
- **îngena** (L3 • Fonetică & Schimbări Dialectale) = (vb) a încetini
|
| 759 |
- **îngingia** (L3 • Gastronomie & Tradiție) = (vb) despre porumb, a începe a face boabe
|
|
|
|
| 760 |
- **însâmbrare** (L3 • Fonetică & Schimbări Dialectale) = asociere, întovărășire
|
| 761 |
- **întercleți** (L3 • Acțiuni & Verbe Regionale) = (vb) a încremeni
|
| 762 |
- **întiri** (L3 • Acțiuni & Verbe Regionale) = (vb) a alunga, a îndepărta
|
moldovan-qwen/scripts/daily_album_cron.py
CHANGED
|
@@ -78,63 +78,137 @@ def _random_genre_key() -> str:
|
|
| 78 |
|
| 79 |
# Moldovan Qwen track concepts
|
| 80 |
MOLDOVAN_QWEN_TRACKS = [
|
| 81 |
-
{"title": "
|
| 82 |
-
{"title": "Ciuleandra din
|
| 83 |
-
{"title": "
|
| 84 |
-
{"title": "Colțuni
|
| 85 |
-
{"title": "
|
| 86 |
-
{"title": "
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
]
|
| 88 |
|
|
|
|
| 89 |
CHILL_TRACKS = [
|
| 90 |
{"title": "Cafea de la Fântână", "tempo": "75 BPM", "mood": "Lo-Fi Chill", "topic": "O cafea caldă la o fântână de sat în ploaia de toamnă cu nori grei"},
|
| 91 |
{"title": "Răsărit pe Prut", "tempo": "80 BPM", "mood": "Acoustic Ambient", "topic": "Mergând pe malul Prutului la răsărit cu nori roz și păsări care se trezesc"},
|
| 92 |
{"title": "Ploaie pe Terasă", "tempo": "70 BPM", "mood": "Chillhop", "topic": "Ascultând ploaia pe terasă cu o carte bună și o cană de ceai de mușețel"},
|
| 93 |
-
{"title": "Gânduri la
|
| 94 |
-
{"title": "Lumini de
|
| 95 |
-
{"title": "Drumul spre
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
]
|
| 97 |
|
|
|
|
| 98 |
DEEP_HOUSE_TRACKS = [
|
| 99 |
-
{"title": "Râșcani Sunset Groove", "tempo": "
|
| 100 |
-
{"title": "Calea Ieșilor Drive", "tempo": "125 BPM", "mood": "Late Night Vocal", "topic": "Conducând pe Calea Ieșilor la miezul nopții cu un deep house
|
| 101 |
-
{"title": "Piața Centrală
|
| 102 |
-
{"title": "
|
| 103 |
-
{"title": "
|
| 104 |
-
{"title": "
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
]
|
| 106 |
|
|
|
|
| 107 |
RANDOM_TRACKS = [
|
| 108 |
{"title": "Doina Drift D&B", "tempo": "174 BPM", "mood": "Liquid Drum & Bass", "topic": "O doină tradițională transformată într-un liquid drum & bass frenetic"},
|
| 109 |
{"title": "Cyberpunk Chișinău", "tempo": "140 BPM", "mood": "Industrial Synth", "topic": "Chișinău din 2085 cu roboți care vând colțuni pe piața centrală"},
|
| 110 |
{"title": "Reggae la Botanica", "tempo": "100 BPM", "mood": "Island Reggae", "topic": "Un DJ reggae care mixează pe acoperișul blocului din Botanica la furtună"},
|
| 111 |
{"title": "Metal Folclor Epic", "tempo": "160 BPM", "mood": "Heavy Metal", "topic": "Gitară electrică heavy care cântă un cântec de horă tradițional moldovenesc"},
|
| 112 |
{"title": "Funk 621", "tempo": "110 BPM", "mood": "Disco Funk", "topic": "O Dacia 621 cu motor de funcție disco la un concurs de viteze retro"},
|
| 113 |
-
{"title": "Haituk Pop Punk", "tempo": "155 BPM", "mood": "Punk Rock", "topic": "Un haiduk modern care conduce un skuter pe autostradă cu o chitară electrică pe spate"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 114 |
]
|
| 115 |
|
| 116 |
-
def _build_album_tracks(genre_key: str) -> List[Dict[str, Any]]:
|
| 117 |
-
"""Select tracks for the given genre key
|
|
|
|
| 118 |
if genre_key == "ALBINA_BONDAR":
|
| 119 |
# Catalogul are 24 concepte; un album Albina ia un subset rotit de 6
|
| 120 |
# (fara repetare) ca sa incapa in fereastra de cota ZeroGPU zilnica.
|
| 121 |
-
return build_albina_tracks(count=
|
| 122 |
elif genre_key == "MOLDOVAN_QWEN":
|
| 123 |
pool = [dict(t) for t in MOLDOVAN_QWEN_TRACKS]
|
| 124 |
-
random.shuffle(pool)
|
| 125 |
-
return pool
|
| 126 |
elif genre_key == "CHILL":
|
| 127 |
pool = [dict(t) for t in CHILL_TRACKS]
|
| 128 |
-
random.shuffle(pool)
|
| 129 |
-
return pool
|
| 130 |
elif genre_key == "DEEP_HOUSE":
|
| 131 |
pool = [dict(t) for t in DEEP_HOUSE_TRACKS]
|
| 132 |
-
random.shuffle(pool)
|
| 133 |
-
return pool
|
| 134 |
else:
|
| 135 |
pool = [dict(t) for t in RANDOM_TRACKS]
|
| 136 |
-
|
| 137 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
|
| 139 |
class DailyAlbumGenerator:
|
| 140 |
def __init__(self, target_minutes: float = 35.0, dry_run: bool = False, genre_override: Optional[str] = None):
|
|
@@ -341,6 +415,16 @@ class DailyAlbumGenerator:
|
|
| 341 |
with open(manifest_file, "w", encoding="utf-8") as f:
|
| 342 |
json.dump(_save, f, indent=2, ensure_ascii=False)
|
| 343 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 344 |
print("\n" + "=" * 65)
|
| 345 |
print(f"✨ ALBUM COMPLET GENERAT: {album_title}")
|
| 346 |
print(f"📁 Director: {album_dir}")
|
|
@@ -416,35 +500,39 @@ class DailyAlbumGenerator:
|
|
| 416 |
keep burning quota on the remaining ones."""
|
| 417 |
track_title = track_info["title"]
|
| 418 |
|
| 419 |
-
# 1. Generate rich Moldovan lyrics
|
| 420 |
-
if
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
else
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 448 |
|
| 449 |
lyrics_file = album_dir / f"{idx:02d}_{track_title.replace(' ', '_')}_lyrics.txt"
|
| 450 |
with open(lyrics_file, "w", encoding="utf-8") as f:
|
|
|
|
| 78 |
|
| 79 |
# Moldovan Qwen track concepts
|
| 80 |
MOLDOVAN_QWEN_TRACKS = [
|
| 81 |
+
{"title": "Balkan 808 la Ciocana", "tempo": "140 BPM", "mood": "Heavy Balkan 808", "topic": "Subbox-uri vibrând în parcarea de la Ciocana, bas 808 distorsionat și nai tradițional moldovenesc"},
|
| 82 |
+
{"title": "Ciuleandra din Centru", "tempo": "145 BPM", "mood": "Folclor Trap", "topic": "Ciuleandra dansată pe ritm de trap pe scările de la Teatrul Național"},
|
| 83 |
+
{"title": "Troleibuzul de Noapte", "tempo": "130 BPM", "mood": "Street Rap", "topic": "Ultimul troleibuz prin Chișinău gol, lumini galbene pe asfalt ud și flow sacadat"},
|
| 84 |
+
{"title": "Colțuni și Bas Greu", "tempo": "138 BPM", "mood": "Balkan 808", "topic": "Bucătărie de bloc cu colțuni aburind și beat de 808 care mișcă paharele în bufet"},
|
| 85 |
+
{"title": "Dacia 1310 pe Bulevard", "tempo": "135 BPM", "mood": "Phonk Moldovenesc", "topic": "Dacie clasică coborând bulevardul Dacia cu geamul lăsat și rap moldovenesc în difuzoare"},
|
| 86 |
+
{"title": "Băieții de la Poșta Veche", "tempo": "142 BPM", "mood": "Chișinău Drill", "topic": "Povești de cartier la Poșta Veche, rime percutante, loialitate și respect"},
|
| 87 |
+
{"title": "Cobza Electrică la Râșcani", "tempo": "140 BPM", "mood": "Folclor Trap", "topic": "Cobză acustică combinată cu hi-hat-uri rapide de trap la cinematograful Moscova"},
|
| 88 |
+
{"title": "Piața Centrală Cypher", "tempo": "132 BPM", "mood": "Boom Bap Moldovenesc", "topic": "Cypher spontan de rap printre tarabele din Piața Centrală din Chișinău"},
|
| 89 |
+
{"title": "Valea Morilor Sunset Drop", "tempo": "136 BPM", "mood": "Doina Trap", "topic": "Doină de dor transformată într-un drop masiv de trap pe malul lacului Valea Morilor"},
|
| 90 |
+
{"title": "Buiucani Vibe", "tempo": "134 BPM", "mood": "Street Rap", "topic": "Plimbare pe străzile din Buiucani, bătăi de tobe murdare și poveste urbană"},
|
| 91 |
+
{"title": "Mahala 808", "tempo": "144 BPM", "mood": "Heavy Balkan 808", "topic": "Viața de mahala chișinăuiană cu bas subwoofere și fluier ciobănesc"},
|
| 92 |
+
{"title": "Haiduc Modern pe Trotinetă", "tempo": "130 BPM", "mood": "Satirical Rap", "topic": "Haiduc din timpurile noastre pe trotinetă electrică ocolind gropile de pe Calea Ieșilor"},
|
| 93 |
+
{"title": "Telecentru Turnul de Foc", "tempo": "140 BPM", "mood": "Chișinău Drill", "topic": "Priveliștea orașului de pe dealul Telecentru la apus cu beat tăios"},
|
| 94 |
+
{"title": "Frăția de la Scara 4", "tempo": "132 BPM", "mood": "Street Rap", "topic": "Prietenia adevărată între băieții de la bloc care nu uită de unde au plecat"},
|
| 95 |
+
{"title": "Doina de la Butuceni 808", "tempo": "145 BPM", "mood": "Doina Trap", "topic": "Stâncile de la Orheiul Vechi și ecoul fluierului peste un kick de 808"},
|
| 96 |
+
{"title": "Plăcinte Fierbinți la 3 AM", "tempo": "138 BPM", "mood": "Balkan Bounce", "topic": "Oprire la toneta de plăcinte la miezul nopții după un concert plin de energie"},
|
| 97 |
+
{"title": "Asfalt și Cobză", "tempo": "135 BPM", "mood": "Folclor Trap", "topic": "Armonia dintre folclorul autentic de nuntă și sound-ul modern de trap"},
|
| 98 |
+
{"title": "Botanica Bass Syndicate", "tempo": "142 BPM", "mood": "Heavy Balkan 808", "topic": "Subwooferele zguduie blocurile sovietice de la porțile orașului"},
|
| 99 |
+
{"title": "Gara de Nord Nocturn", "tempo": "128 BPM", "mood": "Boom Bap Moldovenesc", "topic": "Mersul trenurilor, valize vechi și dor de casă pe un beat clasic de hip-hop"},
|
| 100 |
+
{"title": "Ritm de Horă Rebelă", "tempo": "148 BPM", "mood": "Folclor Trap", "topic": "O horă rapidă moldovenească accelerată cu sintetizatoare moderne și claps"},
|
| 101 |
+
{"title": "Podul de la Circ", "tempo": "136 BPM", "mood": "Street Rap", "topic": "Reflecții urbane sub podul de la Circul din Chișinău la apus de soare"},
|
| 102 |
+
{"title": "Graiul Străzii", "tempo": "134 BPM", "mood": "Chișinău Drill", "topic": "Dicționarul străzii moldovenești vorbit clar, cu demnitate și flow alert"},
|
| 103 |
+
{"title": "Noaptea la Durlești", "tempo": "138 BPM", "mood": "Balkan 808", "topic": "Dealurile Durleștiului văzute noaptea cu faruri aprinse și muzică la maxim"},
|
| 104 |
+
{"title": "Bulevardul Dacia Anthem", "tempo": "140 BPM", "mood": "Heavy Balkan 808", "topic": "Imnul porților orașului Chișinău cu bas tremurat și refren contagios"}
|
| 105 |
]
|
| 106 |
|
| 107 |
+
# Chill track concepts (24 diverse tracks)
|
| 108 |
CHILL_TRACKS = [
|
| 109 |
{"title": "Cafea de la Fântână", "tempo": "75 BPM", "mood": "Lo-Fi Chill", "topic": "O cafea caldă la o fântână de sat în ploaia de toamnă cu nori grei"},
|
| 110 |
{"title": "Răsărit pe Prut", "tempo": "80 BPM", "mood": "Acoustic Ambient", "topic": "Mergând pe malul Prutului la răsărit cu nori roz și păsări care se trezesc"},
|
| 111 |
{"title": "Ploaie pe Terasă", "tempo": "70 BPM", "mood": "Chillhop", "topic": "Ascultând ploaia pe terasă cu o carte bună și o cană de ceai de mușețel"},
|
| 112 |
+
{"title": "Gânduri la Prisacă", "tempo": "78 BPM", "mood": "Nostalgic Chill", "topic": "Amintiri de la bunici la prisacă cu o grădină plină de pomi și miere proaspătă"},
|
| 113 |
+
{"title": "Lumini de Seară la Valea Morilor", "tempo": "74 BPM", "mood": "Lo-Fi Ambient", "topic": "Bănci de lemn pe malul apei la Valea Morilor cu reflexia felinarelor"},
|
| 114 |
+
{"title": "Drumul spre Casă", "tempo": "82 BPM", "mood": "Acoustic Lo-Fi", "topic": "Călătorie liniștită spre casă cu trenul local, peisaje de dealuri verzi"},
|
| 115 |
+
{"title": "Ceai de Mentă la Balcon", "tempo": "72 BPM", "mood": "Chillhop", "topic": "Aburul din cană într-o dimineață răcoroasă de octombrie în Chișinău"},
|
| 116 |
+
{"title": "Frunze Galbene în Parc", "tempo": "76 BPM", "mood": "Lo-Fi Beats", "topic": "Pași lenți prin covorul de frunze din Parcul Ștefan cel Mare"},
|
| 117 |
+
{"title": "Fereastra de la Mansardă", "tempo": "68 BPM", "mood": "Ambient Acoustic", "topic": "Privind norii cum trec leneș peste acoperișurile vechi ale centrului istoric"},
|
| 118 |
+
{"title": "Liniștea din Codri", "tempo": "80 BPM", "mood": "Nature Ambient", "topic": "Vântul foșnind prin stejarii bătrâni din Codrii Moldovei"},
|
| 119 |
+
{"title": "Pâine Caldă din Cuptor", "tempo": "75 BPM", "mood": "Nostalgic Chill", "topic": "Mirosul de aluat copt în cuptorul de lut al bunicii la țară"},
|
| 120 |
+
{"title": "Casetofonul Vechi", "tempo": "74 BPM", "mood": "Jazzy Lo-Fi", "topic": "Casetă audio cu bandă magnetică ușor uzată redând acorduri calde de pian"},
|
| 121 |
+
{"title": "Seară de Toamnă la Butuceni", "tempo": "70 BPM", "mood": "Acoustic Ambient", "topic": "Amurg liniștit peste meandrele Răutului și fânețele adormite"},
|
| 122 |
+
{"title": "Troleibuzul Gol", "tempo": "76 BPM", "mood": "Lo-Fi Chillhop", "topic": "Scaun la geam, stropi de ploaie curgând pe sticlă și muzică în căști"},
|
| 123 |
+
{"title": "Apus peste Nistru", "tempo": "82 BPM", "mood": "Chillout Melodic", "topic": "Culorile aurii ale apusului reflectate în apele liniștite ale Nistrului"},
|
| 124 |
+
{"title": "Dulceață de Nuci Verzi", "tempo": "72 BPM", "mood": "Warm Chill", "topic": "Amintiri dulci din copilărie, borcane la soare și liniște de duminică"},
|
| 125 |
+
{"title": "Pian la Fereastră Deschizată", "tempo": "68 BPM", "mood": "Lo-Fi Piano", "topic": "Note blânde de pian plutind peste curtea liniștită de bloc"},
|
| 126 |
+
{"title": "Cărți Vechi și Cafea", "tempo": "75 BPM", "mood": "Coffee Shop Lo-Fi", "topic": "Miros de hârtie îngălbenită într-un anticariat cochet din Chișinău"},
|
| 127 |
+
{"title": "Scrisoare Nescrisă", "tempo": "78 BPM", "mood": "Nostalgic Acoustic", "topic": "Gânduri pentru cei dragi plecați departe, lăsate pe hârtie la lumina lămpii"},
|
| 128 |
+
{"title": "Zori de Zi la Sat", "tempo": "74 BPM", "mood": "Acoustic Ambient", "topic": "Ceața subțire se ridică peste livezi, primul cântat de cocoș în depărtare"},
|
| 129 |
+
{"title": "Melancolie de Septembrie", "tempo": "70 BPM", "mood": "Lo-Fi Beats", "topic": "Trecerea blândă a verii în toamnă, aer răcoros și nostalgie dulce"},
|
| 130 |
+
{"title": "Popas la Răscruce", "tempo": "80 BPM", "mood": "Acoustic Chill", "topic": "O troiță veche de lemn la margine de sat, umbră de nuc bătrân"},
|
| 131 |
+
{"title": "Ploaie Măruntă pe Asfalt", "tempo": "73 BPM", "mood": "Chillhop", "topic": "Sunetul hipnotic al picăturilor de ploaie pe strada Pușkin adormită"},
|
| 132 |
+
{"title": "Vis de Seară", "tempo": "76 BPM", "mood": "Dreamy Ambient", "topic": "O stare de pace totală la sfârșitul unei zile lungi și pline de sens"}
|
| 133 |
]
|
| 134 |
|
| 135 |
+
# Deep House track concepts (24 diverse tracks)
|
| 136 |
DEEP_HOUSE_TRACKS = [
|
| 137 |
+
{"title": "Râșcani Sunset Groove", "tempo": "124 BPM", "mood": "Melodic Deep House", "topic": "Apus auriu peste bulevardul Moscova cu pad-uri calde și bas rotund de club"},
|
| 138 |
+
{"title": "Calea Ieșilor Drive", "tempo": "125 BPM", "mood": "Late Night Vocal", "topic": "Conducând pe Calea Ieșilor la miezul nopții cu un deep house hipnotic și ecouri vocale"},
|
| 139 |
+
{"title": "Piața Centrală Underground", "tempo": "122 BPM", "mood": "Club Deep", "topic": "Petrecere underground într-un subsol istoric din Chișinău cu beat minimalist"},
|
| 140 |
+
{"title": "Noapte la Ciocana", "tempo": "125 BPM", "mood": "Deep Drive", "topic": "Groove hipnotic de noapte la Ciocana, kick rotund și lumini de neon reflectate pe asfalt"},
|
| 141 |
+
{"title": "Botanica Afterhours", "tempo": "123 BPM", "mood": "Melodic Deep House", "topic": "Zori de zi la porțile orașului, sintetizatoare calde și stare de visare pe ringul de dans"},
|
| 142 |
+
{"title": "Centru Neon Lights", "tempo": "124 BPM", "mood": "Vocal Deep House", "topic": "Pași pe strada 31 August la miezul nopții, ritm melodic și voce feminină senzuală"},
|
| 143 |
+
{"title": "Valea Morilor Moonlight", "tempo": "122 BPM", "mood": "Sunset Groove", "topic": "Reflexia lunii pe lacul Valea Morilor cu un beat elegant și percuții organice"},
|
| 144 |
+
{"title": "Bulevardul Dacia Midnight", "tempo": "125 BPM", "mood": "Deep Drive", "topic": "Viteză constantă pe bulevard cu luminile verzi ale semafoarelor și bas pulsant"},
|
| 145 |
+
{"title": "Telecentru Skyline Groove", "tempo": "124 BPM", "mood": "Melodic Club", "topic": "Priveliște panoramică peste luminile orașului noaptea, acorduri de pian și hi-hats aerisite"},
|
| 146 |
+
{"title": "Rooftop la Chișinău", "tempo": "123 BPM", "mood": "Sunset Groove", "topic": "Cocktail pe acoperiș la apus, prieteni buni și ritm relaxant de 123 BPM"},
|
| 147 |
+
{"title": "Buiucani Deep Sessions", "tempo": "125 BPM", "mood": "Underground Deep", "topic": "Bas adânc și atmosferă intimă de club exclusivist din cartierul Buiucani"},
|
| 148 |
+
{"title": "Strada Pietonală Groove", "tempo": "122 BPM", "mood": "Vocal Deep House", "topic": "Terase luminate, discuții șoptite și o linie de bas care face toată lumea să se miște"},
|
| 149 |
+
{"title": "Aeroport Midnight Flight", "tempo": "125 BPM", "mood": "Late Night Drive", "topic": "Drumul drept spre aeroportul din Chișinău la 2 dimineața cu synth-uri progresive"},
|
| 150 |
+
{"title": "Sculeni Warehouse Beat", "tempo": "124 BPM", "mood": "Industrial Deep", "topic": "Hale industriale reamenajate pentru un rave underground elegant și profund"},
|
| 151 |
+
{"title": "Arca de Triumf Echoes", "tempo": "123 BPM", "mood": "Melodic Deep House", "topic": "Piața Marii Adunări Naționale învăluită în ceață nocturnă și reverberații calde"},
|
| 152 |
+
{"title": "Vibe de Club Chișinău", "tempo": "125 BPM", "mood": "Club Deep", "topic": "Ringul de dans plin, fum dens, stroboscop lent și conexiune prin muzică"},
|
| 153 |
+
{"title": "Malina Mică Shadows", "tempo": "122 BPM", "mood": "Minimal Deep", "topic": "Străduțe liniștite cu case vechi și un beat minimalist care pulsează constant"},
|
| 154 |
+
{"title": "Dendrariu Dusk", "tempo": "124 BPM", "mood": "Organic Deep House", "topic": "Plimbare la înserat prin parcul Dendrariu cu sunete naturale mixate cu muzică electronică"},
|
| 155 |
+
{"title": "Cricova Vault Vibes", "tempo": "123 BPM", "mood": "Hypnotic Deep", "topic": "Galeriile subterane răcoroase cu ecou hipnotic și bas subacvatic"},
|
| 156 |
+
{"title": "Dimineața la Lac", "tempo": "122 BPM", "mood": "Morning Deep House", "topic": "Răsăritul prinde ultimii dansatori pe ponton, muzica devine mai aerisită și luminoasă"},
|
| 157 |
+
{"title": "Viaduct Speed of Sound", "tempo": "125 BPM", "mood": "Late Night Drive", "topic": "Traversarea viaductului dintre Botanica și Centru în miez de noapte pe bas plin"},
|
| 158 |
+
{"title": "Ecouri de Pian Nocturn", "tempo": "123 BPM", "mood": "Melodic Club", "topic": "Armonii sofisticate de pian house cu claps curate și vocale învăluitoare"},
|
| 159 |
+
{"title": "Chișinău Never Sleeps", "tempo": "125 BPM", "mood": "Peak Time Deep", "topic": "Energia neîntreruptă a orașului când toți ceilalți dorm, ritm care nu se oprește"},
|
| 160 |
+
{"title": "Apus la Nistru Groove", "tempo": "124 BPM", "mood": "Sunset Melodic", "topic": "Plajă de râu la sfârșit de vară, acorduri majore și dans descălțat în nisip"}
|
| 161 |
]
|
| 162 |
|
| 163 |
+
# Random / Wildcard track concepts (24 diverse tracks)
|
| 164 |
RANDOM_TRACKS = [
|
| 165 |
{"title": "Doina Drift D&B", "tempo": "174 BPM", "mood": "Liquid Drum & Bass", "topic": "O doină tradițională transformată într-un liquid drum & bass frenetic"},
|
| 166 |
{"title": "Cyberpunk Chișinău", "tempo": "140 BPM", "mood": "Industrial Synth", "topic": "Chișinău din 2085 cu roboți care vând colțuni pe piața centrală"},
|
| 167 |
{"title": "Reggae la Botanica", "tempo": "100 BPM", "mood": "Island Reggae", "topic": "Un DJ reggae care mixează pe acoperișul blocului din Botanica la furtună"},
|
| 168 |
{"title": "Metal Folclor Epic", "tempo": "160 BPM", "mood": "Heavy Metal", "topic": "Gitară electrică heavy care cântă un cântec de horă tradițional moldovenesc"},
|
| 169 |
{"title": "Funk 621", "tempo": "110 BPM", "mood": "Disco Funk", "topic": "O Dacia 621 cu motor de funcție disco la un concurs de viteze retro"},
|
| 170 |
+
{"title": "Haituk Pop Punk", "tempo": "155 BPM", "mood": "Punk Rock", "topic": "Un haiduk modern care conduce un skuter pe autostradă cu o chitară electrică pe spate"},
|
| 171 |
+
{"title": "Electro Swing la Târg", "tempo": "125 BPM", "mood": "Electro Swing", "topic": "Alămuri vesele și ritm saltăreț de clarinet la târgul de toamnă din Hîncești"},
|
| 172 |
+
{"title": "Chișinău Dubstep Riot", "tempo": "145 BPM", "mood": "Heavy Dubstep", "topic": "Wobble bass masiv și drops agresive într-un festival de muzică electronică"},
|
| 173 |
+
{"title": "Balkan Brass Madness", "tempo": "150 BPM", "mood": "Balkan Gypsy Brass", "topic": "Trompete și tubă cântând cu viteză uluitoare la o sărbătoare nebună"},
|
| 174 |
+
{"title": "Afrobeat de Moldova", "tempo": "118 BPM", "mood": "Afrobeat Fusion", "topic": "Poliritmii africane împletite cu vioară moldovenească și percuții vii"},
|
| 175 |
+
{"title": "Trance la Orheiul Vechi", "tempo": "138 BPM", "mood": "Uplifting Trance", "topic": "Pian euforic și sintetizatoare zburătoare peste canionul spectaculos al Răutului"},
|
| 176 |
+
{"title": "Căruța Punk Rock", "tempo": "165 BPM", "mood": "Garage Punk", "topic": "Energie pură de rock rebel din garaj cu versuri directe și atitudine fără compromis"},
|
| 177 |
+
{"title": "Ska de Bălți", "tempo": "140 BPM", "mood": "Upbeat Ska", "topic": "Ritm de chitară pe offbeat și tromboane vesele într-un parc din Bălți"},
|
| 178 |
+
{"title": "Vaporwave la Telecentru", "tempo": "85 BPM", "mood": "Chill Vaporwave", "topic": "Sunete nostalgice de mall din anii '90 cu mostre încetinite din televiziunea națională"},
|
| 179 |
+
{"title": "Tango pe Podul Înalt", "tempo": "120 BPM", "mood": "Electro Tango", "topic": "Acordeon pasional combinat cu beat modern de trip-hop într-o seară răcoroasă"},
|
| 180 |
+
{"title": "Hardstyle la Butuceni", "tempo": "150 BPM", "mood": "Euphoric Hardstyle", "topic": "Kick distorsionat masiv și melodie înălțătoare de nai moldovenesc"},
|
| 181 |
+
{"title": "Gipsy Rumba la Soroca", "tempo": "115 BPM", "mood": "Acoustic Rumba", "topic": "Chitări spaniole și ritm de palmadas la cetatea Soroca pe malul apei"},
|
| 182 |
+
{"title": "Bossa Nova de Chișinău", "tempo": "126 BPM", "mood": "Bossa Nova Chill", "topic": "Acorduri calde de jazz brazilian amestecate cu plimbarea pe străzile verzi ale capitalei"},
|
| 183 |
+
{"title": "Country Moldovenesc", "tempo": "130 BPM", "mood": "Folk Country", "topic": "Banjo sprinten și vioară veselă într-o poveste despre călătorii prin satele Moldovei"},
|
| 184 |
+
{"title": "Darkwave la Subsol", "tempo": "122 BPM", "mood": "Post-Punk Darkwave", "topic": "Linii reci de sintetizator și bas melancolic într-o atmosferă gotică chișinăuiană"},
|
| 185 |
+
{"title": "Speed Garage la Gară", "tempo": "135 BPM", "mood": "UK Garage & 2-Step", "topic": "Ritmuri sincopate de 2-step cu vocale chopped și bas warp"},
|
| 186 |
+
{"title": "Lăutari Psychedelic Rock", "tempo": "132 BPM", "mood": "Psychedelic Rock", "topic": "Solo prelungit de chitară cu efect de phaser peste ritm tradițional de horă"},
|
| 187 |
+
{"title": "Trap Metal la Ciocana", "tempo": "140 BPM", "mood": "Trap Metal Screamo", "topic": "Chitări cu distors extrem și 808 distorsionat pentru descătușare maximă de energie"},
|
| 188 |
+
{"title": "Carnaval la Cahul", "tempo": "124 BPM", "mood": "Samba Fusion", "topic": "Percuții explozive de samba combinate cu strigături vesele moldovenești"}
|
| 189 |
]
|
| 190 |
|
| 191 |
+
def _build_album_tracks(genre_key: str, count: int = 6) -> List[Dict[str, Any]]:
|
| 192 |
+
"""Select a random, non-repeating subset of tracks for the given genre key.
|
| 193 |
+
Each genre pool contains 24 diverse, culturally rich concepts so daily albums never repeat."""
|
| 194 |
if genre_key == "ALBINA_BONDAR":
|
| 195 |
# Catalogul are 24 concepte; un album Albina ia un subset rotit de 6
|
| 196 |
# (fara repetare) ca sa incapa in fereastra de cota ZeroGPU zilnica.
|
| 197 |
+
return build_albina_tracks(count=count)
|
| 198 |
elif genre_key == "MOLDOVAN_QWEN":
|
| 199 |
pool = [dict(t) for t in MOLDOVAN_QWEN_TRACKS]
|
|
|
|
|
|
|
| 200 |
elif genre_key == "CHILL":
|
| 201 |
pool = [dict(t) for t in CHILL_TRACKS]
|
|
|
|
|
|
|
| 202 |
elif genre_key == "DEEP_HOUSE":
|
| 203 |
pool = [dict(t) for t in DEEP_HOUSE_TRACKS]
|
|
|
|
|
|
|
| 204 |
else:
|
| 205 |
pool = [dict(t) for t in RANDOM_TRACKS]
|
| 206 |
+
|
| 207 |
+
if count >= len(pool):
|
| 208 |
+
shuffled = list(pool)
|
| 209 |
+
random.shuffle(shuffled)
|
| 210 |
+
return shuffled
|
| 211 |
+
return random.sample(pool, count)
|
| 212 |
|
| 213 |
class DailyAlbumGenerator:
|
| 214 |
def __init__(self, target_minutes: float = 35.0, dry_run: bool = False, genre_override: Optional[str] = None):
|
|
|
|
| 415 |
with open(manifest_file, "w", encoding="utf-8") as f:
|
| 416 |
json.dump(_save, f, indent=2, ensure_ascii=False)
|
| 417 |
|
| 418 |
+
# Sync with PocketBase if available
|
| 419 |
+
try:
|
| 420 |
+
pb_import = BASE_DIR.parent / "studio-pb" / "tools" / "import_data.py"
|
| 421 |
+
if pb_import.exists():
|
| 422 |
+
import subprocess
|
| 423 |
+
subprocess.run([sys.executable, str(pb_import)], check=False, capture_output=True, timeout=45)
|
| 424 |
+
print(" [✓] Sincronizat automat în PocketBase (:8096)")
|
| 425 |
+
except Exception:
|
| 426 |
+
pass
|
| 427 |
+
|
| 428 |
print("\n" + "=" * 65)
|
| 429 |
print(f"✨ ALBUM COMPLET GENERAT: {album_title}")
|
| 430 |
print(f"📁 Director: {album_dir}")
|
|
|
|
| 500 |
keep burning quota on the remaining ones."""
|
| 501 |
track_title = track_info["title"]
|
| 502 |
|
| 503 |
+
# 1. Generate rich Moldovan lyrics using dynamic Lexical Palette & Songwriting
|
| 504 |
+
persona_id = "albina_bondar" if genre_key == "ALBINA_BONDAR" else ""
|
| 505 |
+
if persona_id:
|
| 506 |
+
print(f" 🐝 Persona activă: Albina Bondar (happy family lyrics)")
|
| 507 |
+
|
| 508 |
+
bpm_val = int(track_info["tempo"].split()[0]) if track_info.get("tempo") else 130
|
| 509 |
+
mood_val = track_info.get("mood", "Modern")
|
| 510 |
+
|
| 511 |
+
try:
|
| 512 |
+
lyrics_data = self.lyricist.compose_lyrics(
|
| 513 |
+
topic=track_info["topic"],
|
| 514 |
+
genre=genre_key,
|
| 515 |
+
dialect_level=2,
|
| 516 |
+
bpm=bpm_val,
|
| 517 |
+
intent=mood_val,
|
| 518 |
+
duration_seconds=180,
|
| 519 |
+
persona_id=persona_id,
|
| 520 |
+
)
|
| 521 |
+
raw_lyrics = lyrics_data.get("full_lyrics", "")
|
| 522 |
+
phonetic_lyrics = lyrics_data.get("phonetic_lyrics", raw_lyrics)
|
| 523 |
+
except Exception as e:
|
| 524 |
+
# Dynamic dictionary-rhymed fallback — NEVER static canned placeholder
|
| 525 |
+
print(f" [!][fallback lyricist] {e}. Generare versuri dinamice din dicționarul master.")
|
| 526 |
+
fallback_data = self.lyricist._generate_intelligent_fallback(
|
| 527 |
+
topic=track_info["topic"],
|
| 528 |
+
intent=mood_val,
|
| 529 |
+
genre=genre_key,
|
| 530 |
+
bpm=bpm_val,
|
| 531 |
+
dialect_level=2,
|
| 532 |
+
duration=180
|
| 533 |
+
)
|
| 534 |
+
raw_lyrics = fallback_data.get("full_lyrics", "")
|
| 535 |
+
phonetic_lyrics = fallback_data.get("phonetic_lyrics", raw_lyrics)
|
| 536 |
|
| 537 |
lyrics_file = album_dir / f"{idx:02d}_{track_title.replace(' ', '_')}_lyrics.txt"
|
| 538 |
with open(lyrics_file, "w", encoding="utf-8") as f:
|
moldovan-qwen/scripts/run_studio.sh
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
set -e
|
| 3 |
+
|
| 4 |
+
# Load environment if available
|
| 5 |
+
if [ -f "$HOME/.flow_cron_env" ]; then
|
| 6 |
+
. "$HOME/.flow_cron_env"
|
| 7 |
+
fi
|
| 8 |
+
|
| 9 |
+
export PATH="$HOME/.local/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:$PATH"
|
| 10 |
+
export PORT="${PORT:-8090}"
|
| 11 |
+
export PYTHONPATH="/Users/flowmaster/Projects/hf/moldovan-qwen:$PYTHONPATH"
|
| 12 |
+
|
| 13 |
+
# Choose python binary
|
| 14 |
+
PYTHON_BIN="/opt/homebrew/bin/python3.14"
|
| 15 |
+
if [ ! -x "$PYTHON_BIN" ]; then
|
| 16 |
+
PYTHON_BIN="$(which python3)"
|
| 17 |
+
fi
|
| 18 |
+
|
| 19 |
+
cd /Users/flowmaster/Projects/hf/moldovan-qwen
|
| 20 |
+
exec "$PYTHON_BIN" studio_server.py
|
moldovan-qwen/static/index.html
CHANGED
|
@@ -723,9 +723,10 @@
|
|
| 723 |
<span class="daw-badge badge-cyan text-[9px] uppercase">🎵 Master Workstation Player</span>
|
| 724 |
<span id="vstage-audioStatus" class="text-[10px] text-slate-400 font-mono">Double-click any song to play</span>
|
| 725 |
</div>
|
| 726 |
-
<div class="flex items-center gap-1.5">
|
| 727 |
<button id="vstage-favBtn" class="btn alt text-xs py-0.5 px-2 text-amber-300" onclick="toggleStageFavorite()">⭐ Favorite</button>
|
| 728 |
-
<button class="btn alt text-xs py-0.5 px-2 text-cyan-300" onclick="
|
|
|
|
| 729 |
<button class="btn alt text-xs py-0.5 px-2 text-emerald-300" onclick="bundleStageProject()" title="Save into dedicated folder with audio, json, lyrics">📦 Bundle Folder</button>
|
| 730 |
</div>
|
| 731 |
</div>
|
|
@@ -751,6 +752,8 @@
|
|
| 751 |
<div id="vstage-trackMeta" class="text-xs text-cyan-400 font-mono truncate">
|
| 752 |
140 BPM • C Minor • MiniMax Music 3
|
| 753 |
</div>
|
|
|
|
|
|
|
| 754 |
|
| 755 |
<!-- Animated Equalizer Bars when playing -->
|
| 756 |
<div class="flex items-end gap-1 h-3 py-0.5" id="vstage-eqContainer">
|
|
@@ -793,6 +796,65 @@
|
|
| 793 |
</div>
|
| 794 |
</div>
|
| 795 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 796 |
</div>
|
| 797 |
|
| 798 |
<!-- RIGHT: LIVE LYRICS & NOTES WORKSTATION PANEL (4 Cols on Wide) -->
|
|
@@ -1875,6 +1937,11 @@
|
|
| 1875 |
<label class="text-[11px] font-bold text-slate-400">Suno / MusicGen Style Prompt</label>
|
| 1876 |
<input id="editSongSunoPrompt" class="input text-xs font-mono" placeholder="Ex: 808 sub bass, trap beat with Moldovan accordion riffs">
|
| 1877 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1878 |
</div>
|
| 1879 |
|
| 1880 |
<div class="flex flex-wrap items-center justify-between gap-2 pt-2 border-t border-slate-800">
|
|
@@ -3319,14 +3386,29 @@ Davai pa!`;
|
|
| 3319 |
}
|
| 3320 |
|
| 3321 |
function togglePlayPause() {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3322 |
const player = $('globalAudioPlayer');
|
| 3323 |
if (!player) return;
|
| 3324 |
-
if (!player.src
|
| 3325 |
-
|
| 3326 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3327 |
}
|
| 3328 |
if (player.paused) {
|
| 3329 |
-
player.play();
|
| 3330 |
} else {
|
| 3331 |
player.pause();
|
| 3332 |
}
|
|
@@ -3341,6 +3423,22 @@ Davai pa!`;
|
|
| 3341 |
}
|
| 3342 |
|
| 3343 |
function playNextTrack() {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3344 |
if (musicQueue.length === 0) return;
|
| 3345 |
if (isShuffleEnabled) {
|
| 3346 |
const nextIdx = Math.floor(Math.random() * musicQueue.length);
|
|
@@ -3352,6 +3450,22 @@ Davai pa!`;
|
|
| 3352 |
}
|
| 3353 |
|
| 3354 |
function playPrevTrack() {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3355 |
if (musicQueue.length === 0) return;
|
| 3356 |
const prevIdx = (currentTrackIndex - 1 + musicQueue.length) % musicQueue.length;
|
| 3357 |
playTrackByIndex(prevIdx);
|
|
@@ -3359,25 +3473,29 @@ Davai pa!`;
|
|
| 3359 |
|
| 3360 |
function toggleAutoplay() {
|
| 3361 |
isAutoplayEnabled = !isAutoplayEnabled;
|
| 3362 |
-
|
| 3363 |
-
|
| 3364 |
-
btn
|
| 3365 |
-
|
| 3366 |
-
|
|
|
|
|
|
|
| 3367 |
toast(isAutoplayEnabled ? 'Autoplay activat' : 'Autoplay dezactivat');
|
| 3368 |
}
|
| 3369 |
|
| 3370 |
function toggleShuffle() {
|
| 3371 |
isShuffleEnabled = !isShuffleEnabled;
|
| 3372 |
-
|
| 3373 |
-
|
| 3374 |
-
|
| 3375 |
-
|
|
|
|
|
|
|
| 3376 |
toast(isShuffleEnabled ? 'Shuffle activat 🔀' : 'Shuffle dezactivat');
|
| 3377 |
}
|
| 3378 |
|
| 3379 |
function seekPlayer(event) {
|
| 3380 |
-
const container = $('playerProgressBarContainer');
|
| 3381 |
const player = $('globalAudioPlayer');
|
| 3382 |
if (!container || !player || !player.duration) return;
|
| 3383 |
const rect = container.getBoundingClientRect();
|
|
@@ -3429,22 +3547,25 @@ Davai pa!`;
|
|
| 3429 |
|
| 3430 |
// Modal Song Editor CRUD
|
| 3431 |
async function openEditSongModal(songId) {
|
| 3432 |
-
const
|
| 3433 |
-
|
| 3434 |
-
|
| 3435 |
-
|
| 3436 |
-
|
| 3437 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3438 |
toast('Nu s-a putut deschide piesa: ' + e.message, true);
|
| 3439 |
}
|
| 3440 |
-
return;
|
| 3441 |
}
|
| 3442 |
-
fillEditSongForm(song);
|
| 3443 |
}
|
| 3444 |
|
| 3445 |
function fillEditSongForm(song) {
|
| 3446 |
-
$('editSongId').value = song.id || '';
|
| 3447 |
-
$('editSongTitle').value = song.title || '';
|
| 3448 |
$('editSongBpm').value = song.bpm || 140;
|
| 3449 |
$('editSongGenre').value = song.genre || '';
|
| 3450 |
$('editSongKey').value = song.key || '';
|
|
@@ -3452,6 +3573,9 @@ Davai pa!`;
|
|
| 3452 |
$('editSongVocalStyle').value = song.vocal_style || '';
|
| 3453 |
$('editSongLyrics').value = song.full_lyrics || song.lyrics || '';
|
| 3454 |
$('editSongSunoPrompt').value = song.suno_style_prompt || '';
|
|
|
|
|
|
|
|
|
|
| 3455 |
$('songEditModal').classList.remove('hidden');
|
| 3456 |
}
|
| 3457 |
|
|
@@ -3460,6 +3584,7 @@ Davai pa!`;
|
|
| 3460 |
const songId = $('editSongId').value;
|
| 3461 |
if (!songId) return;
|
| 3462 |
|
|
|
|
| 3463 |
const payload = {
|
| 3464 |
title: $('editSongTitle').value.trim(),
|
| 3465 |
bpm: parseInt($('editSongBpm').value) || 140,
|
|
@@ -3469,7 +3594,8 @@ Davai pa!`;
|
|
| 3469 |
vocal_style: $('editSongVocalStyle').value.trim(),
|
| 3470 |
lyrics: $('editSongLyrics').value.trim(),
|
| 3471 |
full_lyrics: $('editSongLyrics').value.trim(),
|
| 3472 |
-
suno_style_prompt: $('editSongSunoPrompt').value.trim()
|
|
|
|
| 3473 |
};
|
| 3474 |
|
| 3475 |
const btn = $('saveSongBtn');
|
|
@@ -3482,7 +3608,12 @@ Davai pa!`;
|
|
| 3482 |
});
|
| 3483 |
toast('Modificările au fost salvate cu succes!');
|
| 3484 |
$('songEditModal').classList.add('hidden');
|
| 3485 |
-
loadMusicVault();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3486 |
} catch (err) {
|
| 3487 |
toast('Eroare salvare piesă: ' + err.message, true);
|
| 3488 |
} finally {
|
|
@@ -4110,7 +4241,9 @@ Davai pa!`;
|
|
| 4110 |
|
| 4111 |
// ─── Karaoke Video UI ───────────────────────────
|
| 4112 |
function _findMusicTrack(trackId) {
|
| 4113 |
-
return (cachedMusic || []).find(t => t.id === trackId)
|
|
|
|
|
|
|
| 4114 |
}
|
| 4115 |
|
| 4116 |
function openKaraokeModal(videoUrl, title) {
|
|
@@ -4140,7 +4273,8 @@ Davai pa!`;
|
|
| 4140 |
try {
|
| 4141 |
const res = await api(`/api/media/song/${encodeURIComponent(trackId)}/karaoke-video`, { method: 'POST', body: JSON.stringify({}) });
|
| 4142 |
toast(`Karaoke video gata: ${res.file || 'randat'}`);
|
| 4143 |
-
await loadMusicVault();
|
|
|
|
| 4144 |
} catch (e) {
|
| 4145 |
toast(`Eroare randare karaoke: ${e.message}`, true);
|
| 4146 |
if (btn) { btn.disabled = false; btn.innerHTML = original; }
|
|
@@ -5065,9 +5199,70 @@ Davai pa!`;
|
|
| 5065 |
}
|
| 5066 |
|
| 5067 |
const sizeLabel = it.size_kb ? `${it.size_kb} KB` : (it.size ? `${Math.round(it.size / 1024)} KB` : 'Ready');
|
| 5068 |
-
const genreLabel = it.genre || (it.type === 'music' ? 'Chișinău 808 Trap' : it.type === 'video' ? 'MP4 Video' : 'Audio');
|
| 5069 |
-
const bpmKeyLabel = it.bpm ? `${it.bpm} BPM ${it.key ? '• ' + it.key : ''}` : (it.key ? it.key : '—');
|
| 5070 |
-
const fileDownloadUrl = it.audio_url || it.url || `/api/media/file/${encodeURIComponent(fileRef)}`;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5071 |
|
| 5072 |
return `
|
| 5073 |
<tr class="hover:bg-slate-800/50 cursor-pointer transition-colors ${isSelected ? 'bg-cyan-950/30 font-medium' : ''} ${isPlaying ? 'bg-cyan-900/40 border-l-2 border-cyan-400' : ''}"
|
|
@@ -5075,16 +5270,14 @@ Davai pa!`;
|
|
| 5075 |
onclick="selectVaultItemById('${esc(trackId)}', false)"
|
| 5076 |
ondblclick="playVaultItemById('${esc(trackId)}', event)">
|
| 5077 |
<td class="py-2 px-2 text-center text-slate-400 font-mono text-[11px]">
|
| 5078 |
-
|
| 5079 |
-
${isPlaying ? '<span class="text-cyan-400 animate-pulse font-bold">▶</span>' : idx + 1}
|
| 5080 |
-
</button>
|
| 5081 |
</td>
|
| 5082 |
<td class="py-2 px-2 min-w-0">
|
| 5083 |
<div class="flex items-center gap-2">
|
| 5084 |
${coverImgHtml}
|
| 5085 |
<div class="min-w-0">
|
| 5086 |
<div class="font-bold text-slate-100 text-xs truncate max-w-xs sm:max-w-sm md:max-w-md" title="${esc(title)}">${esc(title)}</div>
|
| 5087 |
-
<div class="text-[10px] text-slate-400 font-mono truncate max-w-xs">${esc(fileRef)}</div>
|
| 5088 |
</div>
|
| 5089 |
</div>
|
| 5090 |
</td>
|
|
@@ -5093,22 +5286,14 @@ Davai pa!`;
|
|
| 5093 |
<td class="py-2 px-2 text-cyan-400 font-mono text-[10px] whitespace-nowrap">${esc(bpmKeyLabel)}</td>
|
| 5094 |
<td class="py-2 px-2 text-slate-400 font-mono text-[10px] whitespace-nowrap">${sizeLabel}</td>
|
| 5095 |
<td class="py-2 px-2 text-center">
|
| 5096 |
-
<button class="text-xs p-0.5 hover:scale-125 transition-transform ${it.favorite ? 'text-amber-400' : 'text-slate-600 hover:text-amber-300'}"
|
| 5097 |
onclick="toggleVaultFavorite('${esc(trackId)}', event)" title="${it.favorite ? 'Favorited ⭐' : 'Add to Favorites'}">
|
| 5098 |
-
⭐
|
| 5099 |
</button>
|
| 5100 |
</td>
|
| 5101 |
<td class="py-2 px-2 text-right whitespace-nowrap">
|
| 5102 |
<div class="flex items-center justify-end gap-1" onclick="event.stopPropagation()">
|
| 5103 |
-
|
| 5104 |
-
${isPlaying ? '⏸' : '▶ Play'}
|
| 5105 |
-
</button>
|
| 5106 |
-
${(it.type === 'music' && (it.karaoke_video || it.karaoke_video_file)) ? `<button class="btn alt text-[10px] py-0.5 px-1.5 text-emerald-300 hover:text-emerald-200" title="Redare Karaoke Video" onclick="openKaraokeModalById('${esc(trackId)}')">🎤</button>` : ''}
|
| 5107 |
-
${(it.type === 'music' && !(it.karaoke_video || it.karaoke_video_file) && (it.audio_url || it.audio_file)) ? `<button id="karaokeBtn-${esc(trackId)}" class="btn alt text-[10px] py-0.5 px-1.5 text-amber-300 hover:text-amber-200" title="Randare Karaoke Video (word-synced)" onclick="renderKaraokeForTrack('${esc(trackId)}')">🎤+</button>` : ''}
|
| 5108 |
-
${(it.lyrics || it.full_lyrics) ? `<button class="btn alt text-[10px] py-0.5 px-1.5" onclick="selectVaultItemById('${esc(trackId)}', false); setSidePanelTab('lyrics')" title="View Lyrics">📜</button>` : ''}
|
| 5109 |
-
<button class="btn alt text-[10px] py-0.5 px-1.5" onclick="openEditSongModal('${esc(trackId)}', event)" title="Edit Metadata">✎</button>
|
| 5110 |
-
<a href="${fileDownloadUrl}" download="${esc(fileRef)}" class="btn alt text-[10px] py-0.5 px-1.5 text-cyan-300 hover:text-white" title="Download">⬇️</a>
|
| 5111 |
-
<button class="text-slate-500 hover:text-rose-400 text-xs px-1" onclick="deleteVaultMedia('${esc(fileRef)}', '${esc(it.type)}', event)" title="Delete">🗑️</button>
|
| 5112 |
</div>
|
| 5113 |
</td>
|
| 5114 |
</tr>
|
|
@@ -5138,13 +5323,70 @@ Davai pa!`;
|
|
| 5138 |
const src = coverUrl.startsWith('http') || coverUrl.startsWith('/') ? coverUrl : `/api/media/file/${encodeURIComponent(coverUrl)}`;
|
| 5139 |
coverThumb = `<img src="${src}" class="w-full h-28 object-cover rounded border border-slate-700/80">`;
|
| 5140 |
} else {
|
| 5141 |
-
const icon = it.type === 'music' ? '🎼' : it.type === 'video' ? '🎬' : it.type === 'album' ? '💿' : '🎵';
|
| 5142 |
coverThumb = `<div class="w-full h-28 rounded bg-slate-800 border border-slate-700/80 flex items-center justify-center text-3xl">${icon}</div>`;
|
| 5143 |
}
|
| 5144 |
|
| 5145 |
const sizeLabel = it.size_kb ? `${it.size_kb} KB` : (it.size ? `${Math.round(it.size / 1024)} KB` : '');
|
| 5146 |
-
const genreLabel = it.genre || (it.type === 'music' ? 'Chișinău 808 Trap' : it.type === 'video' ? 'Video' : 'Audio');
|
| 5147 |
-
const fileDownloadUrl = it.audio_url || it.url || `/api/media/file/${encodeURIComponent(fileRef)}`;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5148 |
|
| 5149 |
return `
|
| 5150 |
<div class="daw-rack p-2.5 rounded-lg border transition-all cursor-pointer flex flex-col justify-between ${isPlaying ? 'border-cyan-500/80 bg-slate-900/90 shadow-[0_0_12px_rgba(6,182,212,0.3)]' : isSelected ? 'border-cyan-600/60 bg-slate-900/50' : 'border-slate-800 hover:border-slate-700'}"
|
|
@@ -5154,34 +5396,25 @@ Davai pa!`;
|
|
| 5154 |
<div class="space-y-1.5">
|
| 5155 |
<div class="relative group overflow-hidden rounded">
|
| 5156 |
${coverThumb}
|
| 5157 |
-
|
| 5158 |
-
onclick="playVaultItemById('${esc(trackId)}', event)">
|
| 5159 |
-
${isPlaying ? '⏸' : '▶'}
|
| 5160 |
-
</button>
|
| 5161 |
</div>
|
| 5162 |
<div class="flex items-start justify-between gap-1">
|
| 5163 |
<div class="font-bold text-slate-100 text-xs truncate flex-1" title="${esc(title)}">${esc(title)}</div>
|
| 5164 |
-
<button class="text-xs shrink-0 ${it.favorite ? 'text-amber-400' : 'text-slate-600 hover:text-amber-300'}"
|
| 5165 |
-
onclick="toggleVaultFavorite('${esc(trackId)}', event)">
|
| 5166 |
-
⭐
|
| 5167 |
</button>
|
| 5168 |
</div>
|
| 5169 |
<div class="text-[10px] text-slate-400 font-mono truncate">${esc(genreLabel)}</div>
|
|
|
|
| 5170 |
<div class="flex items-center justify-between text-[9px] text-cyan-400 font-mono">
|
| 5171 |
-
<span>${it.bpm ? it.bpm + ' BPM' : ''}</span>
|
| 5172 |
<span>${sizeLabel}</span>
|
| 5173 |
</div>
|
| 5174 |
</div>
|
| 5175 |
|
| 5176 |
<div class="flex items-center justify-between pt-2 border-t border-slate-800/80 mt-2 text-[10px]" onclick="event.stopPropagation()">
|
| 5177 |
-
|
| 5178 |
-
${isPlaying ? '⏸ Pause' : '▶ Play'}
|
| 5179 |
-
</button>
|
| 5180 |
-
<div class="flex items-center gap-1">
|
| 5181 |
-
<button class="btn alt text-[10px] py-0.5 px-1.5" onclick="openEditSongModal('${esc(trackId)}', event)" title="Edit">✎</button>
|
| 5182 |
-
<a href="${fileDownloadUrl}" download="${esc(fileRef)}" class="text-cyan-400 hover:text-cyan-200 text-xs px-1" title="Download">⬇️</a>
|
| 5183 |
-
<button class="text-slate-500 hover:text-rose-400 text-xs px-1" onclick="deleteVaultMedia('${esc(fileRef)}', '${esc(it.type)}', event)" title="Delete">🗑️</button>
|
| 5184 |
-
</div>
|
| 5185 |
</div>
|
| 5186 |
</div>
|
| 5187 |
`;
|
|
@@ -5197,11 +5430,18 @@ Davai pa!`;
|
|
| 5197 |
selectedVaultItem = it;
|
| 5198 |
const trackId = it.id || it.file || it.filename;
|
| 5199 |
const fileRef = it.file || it.filename || '';
|
|
|
|
| 5200 |
|
| 5201 |
if (it.type === 'video') {
|
| 5202 |
// Switch Stage to Video Player
|
| 5203 |
if ($('vstage-videoContainer')) $('vstage-videoContainer').classList.remove('hidden');
|
| 5204 |
if ($('vstage-audioContainer')) $('vstage-audioContainer').classList.add('hidden');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5205 |
|
| 5206 |
const vPlayer = $('vstage-videoPlayer');
|
| 5207 |
const videoUrl = it.url || `/api/media/video/stream/${encodeURIComponent(fileRef)}`;
|
|
@@ -5210,9 +5450,6 @@ Davai pa!`;
|
|
| 5210 |
vPlayer.src = videoUrl;
|
| 5211 |
}
|
| 5212 |
if (autoPlay) {
|
| 5213 |
-
// Pause global audio
|
| 5214 |
-
const aPlayer = $('globalAudioPlayer');
|
| 5215 |
-
if (aPlayer) aPlayer.pause();
|
| 5216 |
currentPlayingVaultId = trackId;
|
| 5217 |
vPlayer.play().catch(e => console.warn(e));
|
| 5218 |
}
|
|
@@ -5221,10 +5458,98 @@ Davai pa!`;
|
|
| 5221 |
if ($('vstage-videoMeta')) $('vstage-videoMeta').textContent = `${it.size_kb ? it.size_kb + ' KB • ' : ''}${fileRef}`;
|
| 5222 |
if ($('vstage-videoDownload')) $('vstage-videoDownload').href = videoUrl;
|
| 5223 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5224 |
} else {
|
| 5225 |
-
// Switch Stage to Audio Player Workstation
|
| 5226 |
if ($('vstage-audioContainer')) $('vstage-audioContainer').classList.remove('hidden');
|
| 5227 |
if ($('vstage-videoContainer')) $('vstage-videoContainer').classList.add('hidden');
|
|
|
|
|
|
|
| 5228 |
|
| 5229 |
// Pause video player
|
| 5230 |
const vPlayer = $('vstage-videoPlayer');
|
|
@@ -5232,7 +5557,6 @@ Davai pa!`;
|
|
| 5232 |
|
| 5233 |
const title = it.title || it.name || fileRef;
|
| 5234 |
const genre = it.genre || (it.type === 'music' ? 'Chișinău 808 Trap' : 'Audio Track');
|
| 5235 |
-
const coverUrl = it.cover_image_url || it.cover_image || it.cover_url || '';
|
| 5236 |
const audioSrc = it.audio_url || it.url || (it.audio_file ? `/api/media/audio/stream/${encodeURIComponent(it.audio_file)}` : (fileRef ? `/api/media/audio/stream/${encodeURIComponent(fileRef)}` : ''));
|
| 5237 |
|
| 5238 |
if ($('vstage-trackTitle')) $('vstage-trackTitle').textContent = title;
|
|
@@ -5251,12 +5575,18 @@ Davai pa!`;
|
|
| 5251 |
$('vstage-coverImg').src = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120' viewBox='0 0 120 120'%3E%3Crect width='120' height='120' fill='%230f172a'/%3E%3Ctext x='50%25' y='50%25' dominant-baseline='middle' text-anchor='middle' font-size='36' fill='%2338bdf8'%3E🎵%3C/text%3E%3C/svg%3E";
|
| 5252 |
}
|
| 5253 |
}
|
| 5254 |
-
if ($('vstage-downloadBtn'))
|
|
|
|
|
|
|
|
|
|
| 5255 |
if ($('vstage-favBtn')) {
|
| 5256 |
$('vstage-favBtn').textContent = it.favorite ? '⭐ Favorited' : '☆ Add to Favs';
|
| 5257 |
-
$('vstage-favBtn').className = it.favorite ? 'btn accent text-xs py-0.5 px-2 text-amber-300 font-bold' : 'btn alt text-xs py-0.5 px-2 text-amber-300';
|
| 5258 |
}
|
| 5259 |
|
|
|
|
|
|
|
|
|
|
| 5260 |
// Load Lyrics into side panel
|
| 5261 |
loadStageLyrics(it);
|
| 5262 |
// Load Comments into side panel
|
|
@@ -5306,11 +5636,24 @@ Davai pa!`;
|
|
| 5306 |
function playVaultItemById(trackId, evt) {
|
| 5307 |
if (evt) evt.stopPropagation();
|
| 5308 |
const it = (currentVaultItems || []).find(x => (x.id || x.file || x.filename) === trackId);
|
| 5309 |
-
if (it)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5310 |
}
|
| 5311 |
|
| 5312 |
function playVaultItem(it, evt) {
|
| 5313 |
if (evt) evt.stopPropagation();
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5314 |
selectVaultItem(it, true);
|
| 5315 |
}
|
| 5316 |
|
|
@@ -5515,17 +5858,22 @@ Davai pa!`;
|
|
| 5515 |
async function toggleStageFavorite() {
|
| 5516 |
if (!selectedVaultItem) return;
|
| 5517 |
const trackId = selectedVaultItem.id || selectedVaultItem.file || selectedVaultItem.filename;
|
|
|
|
| 5518 |
try {
|
| 5519 |
const res = await api(`/api/media/song/${encodeURIComponent(trackId)}/favorite`, {
|
| 5520 |
method: 'POST',
|
| 5521 |
-
body: JSON.stringify({})
|
| 5522 |
});
|
| 5523 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5524 |
if ($('vstage-favBtn')) {
|
| 5525 |
-
$('vstage-favBtn').textContent =
|
| 5526 |
-
$('vstage-favBtn').className =
|
| 5527 |
}
|
| 5528 |
-
toast(
|
| 5529 |
renderVault();
|
| 5530 |
} catch (e) {
|
| 5531 |
toast('Eroare favorite: ' + e.message, true);
|
|
@@ -5536,25 +5884,124 @@ Davai pa!`;
|
|
| 5536 |
if (evt) evt.stopPropagation();
|
| 5537 |
try {
|
| 5538 |
const it = (currentVaultItems || []).find(x => (x.id || x.file || x.filename) === trackId);
|
|
|
|
| 5539 |
const res = await api(`/api/media/song/${encodeURIComponent(trackId)}/favorite`, {
|
| 5540 |
method: 'POST',
|
| 5541 |
-
body: JSON.stringify({})
|
| 5542 |
});
|
| 5543 |
-
|
|
|
|
| 5544 |
if (selectedVaultItem && (selectedVaultItem.id || selectedVaultItem.file) === trackId) {
|
| 5545 |
-
selectedVaultItem.favorite =
|
| 5546 |
if ($('vstage-favBtn')) {
|
| 5547 |
-
$('vstage-favBtn').textContent =
|
| 5548 |
-
$('vstage-favBtn').className =
|
| 5549 |
}
|
| 5550 |
}
|
| 5551 |
-
toast(
|
| 5552 |
renderVault();
|
| 5553 |
} catch (e) {
|
| 5554 |
toast('Eroare favorite: ' + e.message, true);
|
| 5555 |
}
|
| 5556 |
}
|
| 5557 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5558 |
async function bundleStageProject() {
|
| 5559 |
if (!selectedVaultItem) return;
|
| 5560 |
const trackId = selectedVaultItem.id || selectedVaultItem.file || selectedVaultItem.filename;
|
|
@@ -5585,10 +6032,14 @@ Davai pa!`;
|
|
| 5585 |
|
| 5586 |
async function deleteVaultMedia(fileRef, type, evt) {
|
| 5587 |
if (evt) evt.stopPropagation();
|
| 5588 |
-
if (!confirm(`Sigur vrei să ștergi
|
| 5589 |
try {
|
| 5590 |
-
|
| 5591 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5592 |
loadVault(currentVaultFilter, true);
|
| 5593 |
} catch (e) {
|
| 5594 |
toast('Eroare ștergere: ' + e.message, true);
|
|
@@ -6045,6 +6496,12 @@ Davai pa!`;
|
|
| 6045 |
window.submitStageComment = submitStageComment;
|
| 6046 |
window.toggleStageFavorite = toggleStageFavorite;
|
| 6047 |
window.toggleVaultFavorite = toggleVaultFavorite;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6048 |
window.bundleStageProject = bundleStageProject;
|
| 6049 |
window.openStageSongEdit = openStageSongEdit;
|
| 6050 |
window.pruneEmptyImages = pruneEmptyImages;
|
|
|
|
| 723 |
<span class="daw-badge badge-cyan text-[9px] uppercase">🎵 Master Workstation Player</span>
|
| 724 |
<span id="vstage-audioStatus" class="text-[10px] text-slate-400 font-mono">Double-click any song to play</span>
|
| 725 |
</div>
|
| 726 |
+
<div class="flex items-center gap-1.5 flex-wrap">
|
| 727 |
<button id="vstage-favBtn" class="btn alt text-xs py-0.5 px-2 text-amber-300" onclick="toggleStageFavorite()">⭐ Favorite</button>
|
| 728 |
+
<button id="vstage-tagBtn" class="btn alt text-xs py-0.5 px-2 text-cyan-300" onclick="promptAddStageTag()">🏷️ +Tag</button>
|
| 729 |
+
<button class="btn alt text-xs py-0.5 px-2 text-slate-300" onclick="openStageSongEdit()">✎ Edit Song</button>
|
| 730 |
<button class="btn alt text-xs py-0.5 px-2 text-emerald-300" onclick="bundleStageProject()" title="Save into dedicated folder with audio, json, lyrics">📦 Bundle Folder</button>
|
| 731 |
</div>
|
| 732 |
</div>
|
|
|
|
| 752 |
<div id="vstage-trackMeta" class="text-xs text-cyan-400 font-mono truncate">
|
| 753 |
140 BPM • C Minor • MiniMax Music 3
|
| 754 |
</div>
|
| 755 |
+
<!-- Dynamic Tag Chips -->
|
| 756 |
+
<div id="vstage-tagsContainer" class="flex flex-wrap items-center gap-1 text-[10px] font-mono min-h-[18px]"></div>
|
| 757 |
|
| 758 |
<!-- Animated Equalizer Bars when playing -->
|
| 759 |
<div class="flex items-end gap-1 h-3 py-0.5" id="vstage-eqContainer">
|
|
|
|
| 796 |
</div>
|
| 797 |
</div>
|
| 798 |
|
| 799 |
+
<!-- IMAGE & WALLPAPER STAGE (Shown when image/artwork is selected) -->
|
| 800 |
+
<div id="vstage-imageContainer" class="hidden space-y-3">
|
| 801 |
+
<div class="flex items-center justify-between text-xs pb-1.5 border-b border-slate-800">
|
| 802 |
+
<div class="flex items-center gap-2">
|
| 803 |
+
<span class="daw-badge badge-indigo text-[9px] uppercase">🖼️ HD Artwork & Wallpaper</span>
|
| 804 |
+
<span id="vstage-imageStatus" class="text-[10px] text-slate-400 font-mono">FLUX.1 Generated Artwork</span>
|
| 805 |
+
</div>
|
| 806 |
+
<div class="flex items-center gap-1.5">
|
| 807 |
+
<a id="vstage-imageDownload" href="#" download class="btn accent text-xs py-0.5 px-2.5 text-cyan-300 font-bold">⬇️ Download HD</a>
|
| 808 |
+
<a id="vstage-imageOpenTab" href="#" target="_blank" class="btn alt text-xs py-0.5 px-2 text-slate-300">🔍 Full Resolution</a>
|
| 809 |
+
<button class="btn alt text-xs py-0.5 px-2" onclick="switchToAudioStage()">🎵 Switch to Audio</button>
|
| 810 |
+
</div>
|
| 811 |
+
</div>
|
| 812 |
+
<div class="w-full h-[360px] bg-slate-950/90 rounded-lg border border-slate-800 flex items-center justify-center overflow-hidden p-2 relative group">
|
| 813 |
+
<img id="vstage-imageDisplay" src="" class="max-w-full max-h-full object-contain rounded shadow-lg transition-transform duration-200 group-hover:scale-[1.02]">
|
| 814 |
+
</div>
|
| 815 |
+
<div class="flex items-center justify-between text-[11px] text-slate-400 pt-1">
|
| 816 |
+
<div class="space-y-0.5 min-w-0">
|
| 817 |
+
<div id="vstage-imageTitle" class="font-bold text-slate-200 text-xs truncate">Artwork Title</div>
|
| 818 |
+
<div id="vstage-imageMeta" class="font-mono text-[10px] text-slate-500">1024x1024 • PNG • FLUX.1 dev</div>
|
| 819 |
+
</div>
|
| 820 |
+
<div id="vstage-imageTags" class="flex items-center gap-1"></div>
|
| 821 |
+
</div>
|
| 822 |
+
</div>
|
| 823 |
+
|
| 824 |
+
<!-- ALBUM SHOWCASE STAGE (Shown when an album is selected) -->
|
| 825 |
+
<div id="vstage-albumContainer" class="hidden space-y-3">
|
| 826 |
+
<div class="flex items-center justify-between text-xs pb-1.5 border-b border-slate-800">
|
| 827 |
+
<div class="flex items-center gap-2">
|
| 828 |
+
<span class="daw-badge badge-violet text-[9px] uppercase">💿 Daily Album Showcase</span>
|
| 829 |
+
<span id="vstage-albumStatusBadge" class="daw-badge badge-emerald text-[9px] uppercase">REAL AUDIO</span>
|
| 830 |
+
</div>
|
| 831 |
+
<div class="flex items-center gap-1.5">
|
| 832 |
+
<a id="vstage-albumZipBtn" href="#" download class="btn accent text-xs py-0.5 px-2.5 text-cyan-300 font-bold">📦 Download ZIP</a>
|
| 833 |
+
<button id="vstage-albumPlayBtn" class="btn py-0.5 px-3 text-xs font-bold" onclick="playCurrentAlbum()">▶ Play Album</button>
|
| 834 |
+
<button class="btn alt text-xs py-0.5 px-2" onclick="switchToAudioStage()">🎵 Switch to Audio</button>
|
| 835 |
+
</div>
|
| 836 |
+
</div>
|
| 837 |
+
<div class="flex flex-col sm:flex-row items-center gap-3.5 p-2.5 bg-slate-900/60 rounded-lg border border-slate-800/80">
|
| 838 |
+
<div class="w-24 h-24 shrink-0 rounded-lg bg-slate-800 border border-slate-700/80 flex items-center justify-center text-4xl shadow-lg">
|
| 839 |
+
💿
|
| 840 |
+
</div>
|
| 841 |
+
<div class="min-w-0 flex-1 w-full space-y-1">
|
| 842 |
+
<h3 id="vstage-albumTitle" class="text-base font-bold text-slate-100 truncate">Album Title</h3>
|
| 843 |
+
<div id="vstage-albumGenre" class="text-xs text-amber-400 font-mono truncate">Genre</div>
|
| 844 |
+
<div id="vstage-albumMeta" class="text-[11px] text-slate-400 font-mono">6 tracks • MiniMax Music 3</div>
|
| 845 |
+
</div>
|
| 846 |
+
</div>
|
| 847 |
+
<div class="space-y-1 pt-1 border-t border-slate-800/80">
|
| 848 |
+
<div class="flex items-center justify-between text-[10px] font-mono text-slate-400 uppercase tracking-wider font-bold">
|
| 849 |
+
<span>Album Tracklist</span>
|
| 850 |
+
<span id="vstage-albumTrackCount">6 Tracks</span>
|
| 851 |
+
</div>
|
| 852 |
+
<div id="vstage-albumTracksList" class="space-y-1 max-h-[180px] overflow-y-auto pr-1">
|
| 853 |
+
<!-- Tracks dynamically rendered -->
|
| 854 |
+
</div>
|
| 855 |
+
</div>
|
| 856 |
+
</div>
|
| 857 |
+
|
| 858 |
</div>
|
| 859 |
|
| 860 |
<!-- RIGHT: LIVE LYRICS & NOTES WORKSTATION PANEL (4 Cols on Wide) -->
|
|
|
|
| 1937 |
<label class="text-[11px] font-bold text-slate-400">Suno / MusicGen Style Prompt</label>
|
| 1938 |
<input id="editSongSunoPrompt" class="input text-xs font-mono" placeholder="Ex: 808 sub bass, trap beat with Moldovan accordion riffs">
|
| 1939 |
</div>
|
| 1940 |
+
|
| 1941 |
+
<div class="col-span-full">
|
| 1942 |
+
<label class="text-[11px] font-bold text-cyan-400">Tags / Categorii (separate prin virgulă)</label>
|
| 1943 |
+
<input id="editSongTags" class="input text-xs font-mono" placeholder="Ex: trap, 808, chisinau, albina, hit, favorit">
|
| 1944 |
+
</div>
|
| 1945 |
</div>
|
| 1946 |
|
| 1947 |
<div class="flex flex-wrap items-center justify-between gap-2 pt-2 border-t border-slate-800">
|
|
|
|
| 3386 |
}
|
| 3387 |
|
| 3388 |
function togglePlayPause() {
|
| 3389 |
+
// If video container is active on stage and has source:
|
| 3390 |
+
const vContainer = $('vstage-videoContainer');
|
| 3391 |
+
const vPlayer = $('vstage-videoPlayer');
|
| 3392 |
+
if (vContainer && !vContainer.classList.contains('hidden') && vPlayer && vPlayer.src) {
|
| 3393 |
+
if (vPlayer.paused) vPlayer.play();
|
| 3394 |
+
else vPlayer.pause();
|
| 3395 |
+
return;
|
| 3396 |
+
}
|
| 3397 |
+
|
| 3398 |
const player = $('globalAudioPlayer');
|
| 3399 |
if (!player) return;
|
| 3400 |
+
if (!player.src) {
|
| 3401 |
+
if (selectedVaultItem && (selectedVaultItem.type === 'music' || selectedVaultItem.type === 'audio')) {
|
| 3402 |
+
playVaultItem(selectedVaultItem);
|
| 3403 |
+
return;
|
| 3404 |
+
}
|
| 3405 |
+
if (musicQueue.length > 0) {
|
| 3406 |
+
playTrackByIndex(0);
|
| 3407 |
+
return;
|
| 3408 |
+
}
|
| 3409 |
}
|
| 3410 |
if (player.paused) {
|
| 3411 |
+
player.play().catch(e => console.warn(e));
|
| 3412 |
} else {
|
| 3413 |
player.pause();
|
| 3414 |
}
|
|
|
|
| 3423 |
}
|
| 3424 |
|
| 3425 |
function playNextTrack() {
|
| 3426 |
+
// If playing from Media Vault, advance through playable vault items
|
| 3427 |
+
if (currentPlayingVaultId && currentVaultItems && currentVaultItems.length > 0) {
|
| 3428 |
+
const playableItems = currentVaultItems.filter(x => x.type === 'music' || x.type === 'audio');
|
| 3429 |
+
if (playableItems.length > 0) {
|
| 3430 |
+
let currentIdx = playableItems.findIndex(x => (x.id || x.file || x.filename) === currentPlayingVaultId);
|
| 3431 |
+
let nextIdx;
|
| 3432 |
+
if (isShuffleEnabled) {
|
| 3433 |
+
nextIdx = Math.floor(Math.random() * playableItems.length);
|
| 3434 |
+
} else {
|
| 3435 |
+
nextIdx = currentIdx === -1 ? 0 : (currentIdx + 1) % playableItems.length;
|
| 3436 |
+
}
|
| 3437 |
+
playVaultItem(playableItems[nextIdx]);
|
| 3438 |
+
return;
|
| 3439 |
+
}
|
| 3440 |
+
}
|
| 3441 |
+
|
| 3442 |
if (musicQueue.length === 0) return;
|
| 3443 |
if (isShuffleEnabled) {
|
| 3444 |
const nextIdx = Math.floor(Math.random() * musicQueue.length);
|
|
|
|
| 3450 |
}
|
| 3451 |
|
| 3452 |
function playPrevTrack() {
|
| 3453 |
+
// If playing from Media Vault, go to previous playable vault item
|
| 3454 |
+
if (currentPlayingVaultId && currentVaultItems && currentVaultItems.length > 0) {
|
| 3455 |
+
const playableItems = currentVaultItems.filter(x => x.type === 'music' || x.type === 'audio');
|
| 3456 |
+
if (playableItems.length > 0) {
|
| 3457 |
+
let currentIdx = playableItems.findIndex(x => (x.id || x.file || x.filename) === currentPlayingVaultId);
|
| 3458 |
+
let prevIdx;
|
| 3459 |
+
if (isShuffleEnabled) {
|
| 3460 |
+
prevIdx = Math.floor(Math.random() * playableItems.length);
|
| 3461 |
+
} else {
|
| 3462 |
+
prevIdx = currentIdx <= 0 ? playableItems.length - 1 : currentIdx - 1;
|
| 3463 |
+
}
|
| 3464 |
+
playVaultItem(playableItems[prevIdx]);
|
| 3465 |
+
return;
|
| 3466 |
+
}
|
| 3467 |
+
}
|
| 3468 |
+
|
| 3469 |
if (musicQueue.length === 0) return;
|
| 3470 |
const prevIdx = (currentTrackIndex - 1 + musicQueue.length) % musicQueue.length;
|
| 3471 |
playTrackByIndex(prevIdx);
|
|
|
|
| 3473 |
|
| 3474 |
function toggleAutoplay() {
|
| 3475 |
isAutoplayEnabled = !isAutoplayEnabled;
|
| 3476 |
+
['playerAutoplayBtn', 'vstage-autoplayBtn'].forEach(id => {
|
| 3477 |
+
const btn = $(id);
|
| 3478 |
+
if (btn) {
|
| 3479 |
+
btn.textContent = isAutoplayEnabled ? '🔁 Auto: ON' : '🔁 Auto: OFF';
|
| 3480 |
+
btn.className = isAutoplayEnabled ? 'btn alt text-xs py-1 px-2 text-emerald-400 border-emerald-500/40 font-bold' : 'btn alt text-xs py-1 px-2 text-slate-500';
|
| 3481 |
+
}
|
| 3482 |
+
});
|
| 3483 |
toast(isAutoplayEnabled ? 'Autoplay activat' : 'Autoplay dezactivat');
|
| 3484 |
}
|
| 3485 |
|
| 3486 |
function toggleShuffle() {
|
| 3487 |
isShuffleEnabled = !isShuffleEnabled;
|
| 3488 |
+
['playerShuffleBtn', 'vstage-shuffleBtn'].forEach(id => {
|
| 3489 |
+
const btn = $(id);
|
| 3490 |
+
if (btn) {
|
| 3491 |
+
btn.className = isShuffleEnabled ? 'btn success text-xs py-1 px-2 font-bold shadow-[0_0_8px_rgba(16,185,129,0.4)]' : 'btn alt text-xs py-1 px-2 text-slate-400 hover:text-white';
|
| 3492 |
+
}
|
| 3493 |
+
});
|
| 3494 |
toast(isShuffleEnabled ? 'Shuffle activat 🔀' : 'Shuffle dezactivat');
|
| 3495 |
}
|
| 3496 |
|
| 3497 |
function seekPlayer(event) {
|
| 3498 |
+
const container = event.currentTarget || $('playerProgressBarContainer');
|
| 3499 |
const player = $('globalAudioPlayer');
|
| 3500 |
if (!container || !player || !player.duration) return;
|
| 3501 |
const rect = container.getBoundingClientRect();
|
|
|
|
| 3547 |
|
| 3548 |
// Modal Song Editor CRUD
|
| 3549 |
async function openEditSongModal(songId) {
|
| 3550 |
+
const localSong = (cachedMusic || []).find(s => s.id === songId)
|
| 3551 |
+
|| (currentVaultItems || []).find(s => (s.id || s.file || s.filename) === songId)
|
| 3552 |
+
|| (selectedVaultItem && (selectedVaultItem.id === songId || selectedVaultItem.file === songId) ? selectedVaultItem : null);
|
| 3553 |
+
if (localSong) {
|
| 3554 |
+
fillEditSongForm(localSong);
|
| 3555 |
+
}
|
| 3556 |
+
try {
|
| 3557 |
+
const fetched = await api(`/api/media/song/${encodeURIComponent(songId)}`);
|
| 3558 |
+
fillEditSongForm(fetched);
|
| 3559 |
+
} catch (e) {
|
| 3560 |
+
if (!localSong) {
|
| 3561 |
toast('Nu s-a putut deschide piesa: ' + e.message, true);
|
| 3562 |
}
|
|
|
|
| 3563 |
}
|
|
|
|
| 3564 |
}
|
| 3565 |
|
| 3566 |
function fillEditSongForm(song) {
|
| 3567 |
+
$('editSongId').value = song.id || song.file || '';
|
| 3568 |
+
$('editSongTitle').value = song.title || song.name || '';
|
| 3569 |
$('editSongBpm').value = song.bpm || 140;
|
| 3570 |
$('editSongGenre').value = song.genre || '';
|
| 3571 |
$('editSongKey').value = song.key || '';
|
|
|
|
| 3573 |
$('editSongVocalStyle').value = song.vocal_style || '';
|
| 3574 |
$('editSongLyrics').value = song.full_lyrics || song.lyrics || '';
|
| 3575 |
$('editSongSunoPrompt').value = song.suno_style_prompt || '';
|
| 3576 |
+
if ($('editSongTags')) {
|
| 3577 |
+
$('editSongTags').value = Array.isArray(song.tags) ? song.tags.join(', ') : (song.tags || '');
|
| 3578 |
+
}
|
| 3579 |
$('songEditModal').classList.remove('hidden');
|
| 3580 |
}
|
| 3581 |
|
|
|
|
| 3584 |
const songId = $('editSongId').value;
|
| 3585 |
if (!songId) return;
|
| 3586 |
|
| 3587 |
+
const tagsVal = $('editSongTags') ? $('editSongTags').value.split(',').map(s => s.trim().replace(/^#/, '')).filter(Boolean) : [];
|
| 3588 |
const payload = {
|
| 3589 |
title: $('editSongTitle').value.trim(),
|
| 3590 |
bpm: parseInt($('editSongBpm').value) || 140,
|
|
|
|
| 3594 |
vocal_style: $('editSongVocalStyle').value.trim(),
|
| 3595 |
lyrics: $('editSongLyrics').value.trim(),
|
| 3596 |
full_lyrics: $('editSongLyrics').value.trim(),
|
| 3597 |
+
suno_style_prompt: $('editSongSunoPrompt').value.trim(),
|
| 3598 |
+
tags: tagsVal
|
| 3599 |
};
|
| 3600 |
|
| 3601 |
const btn = $('saveSongBtn');
|
|
|
|
| 3608 |
});
|
| 3609 |
toast('Modificările au fost salvate cu succes!');
|
| 3610 |
$('songEditModal').classList.add('hidden');
|
| 3611 |
+
if (typeof loadMusicVault === 'function') loadMusicVault();
|
| 3612 |
+
if (typeof loadVault === 'function') loadVault(currentVaultFilter, false);
|
| 3613 |
+
if (selectedVaultItem && (selectedVaultItem.id === songId || selectedVaultItem.file === songId)) {
|
| 3614 |
+
Object.assign(selectedVaultItem, payload);
|
| 3615 |
+
selectVaultItem(selectedVaultItem, false);
|
| 3616 |
+
}
|
| 3617 |
} catch (err) {
|
| 3618 |
toast('Eroare salvare piesă: ' + err.message, true);
|
| 3619 |
} finally {
|
|
|
|
| 4241 |
|
| 4242 |
// ─── Karaoke Video UI ───────────────────────────
|
| 4243 |
function _findMusicTrack(trackId) {
|
| 4244 |
+
return (cachedMusic || []).find(t => t.id === trackId)
|
| 4245 |
+
|| (currentVaultItems || []).find(t => (t.id || t.file || t.filename) === trackId)
|
| 4246 |
+
|| (selectedVaultItem && (selectedVaultItem.id === trackId || selectedVaultItem.file === trackId) ? selectedVaultItem : null);
|
| 4247 |
}
|
| 4248 |
|
| 4249 |
function openKaraokeModal(videoUrl, title) {
|
|
|
|
| 4273 |
try {
|
| 4274 |
const res = await api(`/api/media/song/${encodeURIComponent(trackId)}/karaoke-video`, { method: 'POST', body: JSON.stringify({}) });
|
| 4275 |
toast(`Karaoke video gata: ${res.file || 'randat'}`);
|
| 4276 |
+
if (typeof loadMusicVault === 'function') await loadMusicVault();
|
| 4277 |
+
if (typeof loadVault === 'function') loadVault(currentVaultFilter, false);
|
| 4278 |
} catch (e) {
|
| 4279 |
toast(`Eroare randare karaoke: ${e.message}`, true);
|
| 4280 |
if (btn) { btn.disabled = false; btn.innerHTML = original; }
|
|
|
|
| 5199 |
}
|
| 5200 |
|
| 5201 |
const sizeLabel = it.size_kb ? `${it.size_kb} KB` : (it.size ? `${Math.round(it.size / 1024)} KB` : 'Ready');
|
| 5202 |
+
const genreLabel = it.genre || (it.type === 'music' ? 'Chișinău 808 Trap' : it.type === 'video' ? 'MP4 Video' : it.type === 'album' ? 'Daily Album' : it.type === 'image' ? 'FLUX Artwork' : 'Audio');
|
| 5203 |
+
const bpmKeyLabel = it.bpm ? `${it.bpm} BPM ${it.key ? '• ' + it.key : ''}` : (it.key ? it.key : (it.type === 'album' ? `${it.track_count || 0} tracks` : '—'));
|
| 5204 |
+
const fileDownloadUrl = it.zip_url || it.audio_url || it.url || `/api/media/file/${encodeURIComponent(fileRef)}`;
|
| 5205 |
+
|
| 5206 |
+
const tagsHtml = (it.tags || []).slice(0, 2).map(t => `<span class="inline-block bg-slate-800 text-cyan-400 text-[8px] font-mono px-1 py-0.5 rounded mr-1">#${esc(t)}</span>`).join('');
|
| 5207 |
+
|
| 5208 |
+
let playColBtn = '';
|
| 5209 |
+
let actionButtons = '';
|
| 5210 |
+
|
| 5211 |
+
if (it.type === 'image') {
|
| 5212 |
+
playColBtn = `
|
| 5213 |
+
<button class="w-6 h-6 rounded flex items-center justify-center hover:bg-slate-700 text-indigo-300 hover:text-white text-xs" onclick="selectVaultItemById('${esc(trackId)}', false)" title="View Image">
|
| 5214 |
+
🖼️
|
| 5215 |
+
</button>
|
| 5216 |
+
`;
|
| 5217 |
+
actionButtons = `
|
| 5218 |
+
<button class="btn alt text-[10px] py-0.5 px-2 font-bold text-indigo-300 hover:text-white" onclick="selectVaultItemById('${esc(trackId)}', false)" title="View Image">👁️ View</button>
|
| 5219 |
+
<a href="${coverUrl ? (coverUrl.startsWith('http') || coverUrl.startsWith('/') ? coverUrl : `/api/media/file/${encodeURIComponent(coverUrl)}`) : fileDownloadUrl}" download="${esc(fileRef)}" class="btn alt text-[10px] py-0.5 px-1.5 text-cyan-300 hover:text-white" title="Download Image">⬇️</a>
|
| 5220 |
+
<button class="text-slate-500 hover:text-rose-400 text-xs px-1" onclick="deleteVaultMedia('${esc(fileRef)}', '${esc(it.type)}', event)" title="Delete">🗑️</button>
|
| 5221 |
+
`;
|
| 5222 |
+
} else if (it.type === 'album') {
|
| 5223 |
+
playColBtn = `
|
| 5224 |
+
<button class="w-6 h-6 rounded flex items-center justify-center hover:bg-slate-700 text-violet-300 hover:text-white text-xs" onclick="selectVaultItemById('${esc(trackId)}', true)" title="Open Album">
|
| 5225 |
+
💿
|
| 5226 |
+
</button>
|
| 5227 |
+
`;
|
| 5228 |
+
actionButtons = `
|
| 5229 |
+
<button class="btn accent text-[10px] py-0.5 px-2 font-bold text-violet-300 hover:text-white" onclick="selectVaultItemById('${esc(trackId)}', true)" title="Open Album & Play">💿 Browse</button>
|
| 5230 |
+
<a href="${it.zip_url || `/api/media/album/zip/${encodeURIComponent(trackId)}`}" download class="btn alt text-[10px] py-0.5 px-1.5 text-amber-300 font-bold" title="Download Full Album ZIP">📦 ZIP</a>
|
| 5231 |
+
<button class="text-slate-500 hover:text-rose-400 text-xs px-1" onclick="deleteVaultMedia('${esc(fileRef)}', '${esc(it.type)}', event)" title="Delete">🗑️</button>
|
| 5232 |
+
`;
|
| 5233 |
+
} else if (it.type === 'video') {
|
| 5234 |
+
playColBtn = `
|
| 5235 |
+
<button class="w-6 h-6 rounded flex items-center justify-center hover:bg-slate-700 text-rose-400 hover:text-white" onclick="playVaultItemById('${esc(trackId)}', event)" title="Play Video">
|
| 5236 |
+
${isPlaying ? '<span class="text-rose-400 animate-pulse font-bold">⏸</span>' : '🎬'}
|
| 5237 |
+
</button>
|
| 5238 |
+
`;
|
| 5239 |
+
actionButtons = `
|
| 5240 |
+
<button class="btn ${isPlaying ? 'success' : 'accent'} text-[10px] py-0.5 px-2 font-bold text-rose-300" onclick="playVaultItemById('${esc(trackId)}', event)">
|
| 5241 |
+
${isPlaying ? '⏸' : '🎬 Play'}
|
| 5242 |
+
</button>
|
| 5243 |
+
<a href="${fileDownloadUrl}" download="${esc(fileRef)}" class="btn alt text-[10px] py-0.5 px-1.5 text-cyan-300 hover:text-white" title="Download Video">⬇️</a>
|
| 5244 |
+
<button class="text-slate-500 hover:text-rose-400 text-xs px-1" onclick="deleteVaultMedia('${esc(fileRef)}', '${esc(it.type)}', event)" title="Delete">🗑️</button>
|
| 5245 |
+
`;
|
| 5246 |
+
} else {
|
| 5247 |
+
// Song / Audio
|
| 5248 |
+
playColBtn = `
|
| 5249 |
+
<button class="w-6 h-6 rounded flex items-center justify-center hover:bg-slate-700 text-slate-300 hover:text-white" onclick="playVaultItemById('${esc(trackId)}', event)" title="Play Track">
|
| 5250 |
+
${isPlaying ? '<span class="text-cyan-400 animate-pulse font-bold">▶</span>' : idx + 1}
|
| 5251 |
+
</button>
|
| 5252 |
+
`;
|
| 5253 |
+
actionButtons = `
|
| 5254 |
+
<button class="btn ${isPlaying ? 'success' : 'accent'} text-[10px] py-0.5 px-2 font-bold" onclick="playVaultItemById('${esc(trackId)}', event)">
|
| 5255 |
+
${isPlaying ? '⏸' : '▶ Play'}
|
| 5256 |
+
</button>
|
| 5257 |
+
${(it.type === 'music' && (it.karaoke_video || it.karaoke_video_file)) ? `<button class="btn alt text-[10px] py-0.5 px-1.5 text-emerald-300 hover:text-emerald-200" title="Redare Karaoke Video" onclick="openKaraokeModalById('${esc(trackId)}')">🎤</button>` : ''}
|
| 5258 |
+
${(it.type === 'music' && !(it.karaoke_video || it.karaoke_video_file) && (it.audio_url || it.audio_file)) ? `<button id="karaokeBtn-${esc(trackId)}" class="btn alt text-[10px] py-0.5 px-1.5 text-amber-300 hover:text-amber-200" title="Randare Karaoke Video (word-synced)" onclick="renderKaraokeForTrack('${esc(trackId)}')">🎤+</button>` : ''}
|
| 5259 |
+
${(it.lyrics || it.full_lyrics) ? `<button class="btn alt text-[10px] py-0.5 px-1.5" onclick="selectVaultItemById('${esc(trackId)}', false); setSidePanelTab('lyrics')" title="View Lyrics">📜</button>` : ''}
|
| 5260 |
+
<button class="btn alt text-[10px] py-0.5 px-1.5 text-cyan-400" onclick="promptAddTagById('${esc(trackId)}', event)" title="Add / Edit Tags">🏷️</button>
|
| 5261 |
+
<button class="btn alt text-[10px] py-0.5 px-1.5" onclick="openEditSongModal('${esc(trackId)}', event)" title="Edit Metadata">✎</button>
|
| 5262 |
+
<a href="${fileDownloadUrl}" download="${esc(fileRef)}" class="btn alt text-[10px] py-0.5 px-1.5 text-cyan-300 hover:text-white" title="Download Audio">⬇️</a>
|
| 5263 |
+
<button class="text-slate-500 hover:text-rose-400 text-xs px-1" onclick="deleteVaultMedia('${esc(fileRef)}', '${esc(it.type)}', event)" title="Delete">🗑️</button>
|
| 5264 |
+
`;
|
| 5265 |
+
}
|
| 5266 |
|
| 5267 |
return `
|
| 5268 |
<tr class="hover:bg-slate-800/50 cursor-pointer transition-colors ${isSelected ? 'bg-cyan-950/30 font-medium' : ''} ${isPlaying ? 'bg-cyan-900/40 border-l-2 border-cyan-400' : ''}"
|
|
|
|
| 5270 |
onclick="selectVaultItemById('${esc(trackId)}', false)"
|
| 5271 |
ondblclick="playVaultItemById('${esc(trackId)}', event)">
|
| 5272 |
<td class="py-2 px-2 text-center text-slate-400 font-mono text-[11px]">
|
| 5273 |
+
${playColBtn}
|
|
|
|
|
|
|
| 5274 |
</td>
|
| 5275 |
<td class="py-2 px-2 min-w-0">
|
| 5276 |
<div class="flex items-center gap-2">
|
| 5277 |
${coverImgHtml}
|
| 5278 |
<div class="min-w-0">
|
| 5279 |
<div class="font-bold text-slate-100 text-xs truncate max-w-xs sm:max-w-sm md:max-w-md" title="${esc(title)}">${esc(title)}</div>
|
| 5280 |
+
<div class="text-[10px] text-slate-400 font-mono truncate max-w-xs">${tagsHtml}${esc(fileRef)}</div>
|
| 5281 |
</div>
|
| 5282 |
</div>
|
| 5283 |
</td>
|
|
|
|
| 5286 |
<td class="py-2 px-2 text-cyan-400 font-mono text-[10px] whitespace-nowrap">${esc(bpmKeyLabel)}</td>
|
| 5287 |
<td class="py-2 px-2 text-slate-400 font-mono text-[10px] whitespace-nowrap">${sizeLabel}</td>
|
| 5288 |
<td class="py-2 px-2 text-center">
|
| 5289 |
+
<button class="text-xs p-0.5 hover:scale-125 transition-transform ${it.favorite ? 'text-amber-400 font-bold' : 'text-slate-600 hover:text-amber-300'}"
|
| 5290 |
onclick="toggleVaultFavorite('${esc(trackId)}', event)" title="${it.favorite ? 'Favorited ⭐' : 'Add to Favorites'}">
|
| 5291 |
+
${it.favorite ? '⭐' : '☆'}
|
| 5292 |
</button>
|
| 5293 |
</td>
|
| 5294 |
<td class="py-2 px-2 text-right whitespace-nowrap">
|
| 5295 |
<div class="flex items-center justify-end gap-1" onclick="event.stopPropagation()">
|
| 5296 |
+
${actionButtons}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5297 |
</div>
|
| 5298 |
</td>
|
| 5299 |
</tr>
|
|
|
|
| 5323 |
const src = coverUrl.startsWith('http') || coverUrl.startsWith('/') ? coverUrl : `/api/media/file/${encodeURIComponent(coverUrl)}`;
|
| 5324 |
coverThumb = `<img src="${src}" class="w-full h-28 object-cover rounded border border-slate-700/80">`;
|
| 5325 |
} else {
|
| 5326 |
+
const icon = it.type === 'music' ? '🎼' : it.type === 'video' ? '🎬' : it.type === 'album' ? '💿' : it.type === 'image' ? '🖼️' : '🎵';
|
| 5327 |
coverThumb = `<div class="w-full h-28 rounded bg-slate-800 border border-slate-700/80 flex items-center justify-center text-3xl">${icon}</div>`;
|
| 5328 |
}
|
| 5329 |
|
| 5330 |
const sizeLabel = it.size_kb ? `${it.size_kb} KB` : (it.size ? `${Math.round(it.size / 1024)} KB` : '');
|
| 5331 |
+
const genreLabel = it.genre || (it.type === 'music' ? 'Chișinău 808 Trap' : it.type === 'video' ? 'Video' : it.type === 'album' ? 'Album' : it.type === 'image' ? 'Artwork' : 'Audio');
|
| 5332 |
+
const fileDownloadUrl = it.zip_url || it.audio_url || it.url || `/api/media/file/${encodeURIComponent(fileRef)}`;
|
| 5333 |
+
const tagsHtml = (it.tags || []).slice(0, 3).map(t => `<span class="bg-slate-800 text-cyan-400 text-[8px] font-mono px-1 py-0.5 rounded">#${esc(t)}</span>`).join(' ');
|
| 5334 |
+
|
| 5335 |
+
let hoverOverlay = '';
|
| 5336 |
+
let cardBottomActions = '';
|
| 5337 |
+
|
| 5338 |
+
if (it.type === 'image') {
|
| 5339 |
+
hoverOverlay = `
|
| 5340 |
+
<button class="absolute inset-0 bg-black/40 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity text-2xl text-white font-bold"
|
| 5341 |
+
onclick="selectVaultItemById('${esc(trackId)}', false)">
|
| 5342 |
+
🔍
|
| 5343 |
+
</button>
|
| 5344 |
+
`;
|
| 5345 |
+
cardBottomActions = `
|
| 5346 |
+
<button class="btn alt text-[10px] py-0.5 px-2 font-bold text-indigo-300" onclick="selectVaultItemById('${esc(trackId)}', false)">
|
| 5347 |
+
👁️ View Image
|
| 5348 |
+
</button>
|
| 5349 |
+
<div class="flex items-center gap-1">
|
| 5350 |
+
<a href="${coverUrl ? (coverUrl.startsWith('http') || coverUrl.startsWith('/') ? coverUrl : `/api/media/file/${encodeURIComponent(coverUrl)}`) : fileDownloadUrl}" download="${esc(fileRef)}" class="text-cyan-400 hover:text-cyan-200 text-xs px-1" title="Download Image">⬇️</a>
|
| 5351 |
+
<button class="text-slate-500 hover:text-rose-400 text-xs px-1" onclick="deleteVaultMedia('${esc(fileRef)}', '${esc(it.type)}', event)" title="Delete">🗑️</button>
|
| 5352 |
+
</div>
|
| 5353 |
+
`;
|
| 5354 |
+
} else if (it.type === 'album') {
|
| 5355 |
+
hoverOverlay = `
|
| 5356 |
+
<button class="absolute inset-0 bg-black/40 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity text-2xl text-white font-bold"
|
| 5357 |
+
onclick="selectVaultItemById('${esc(trackId)}', true)">
|
| 5358 |
+
💿
|
| 5359 |
+
</button>
|
| 5360 |
+
`;
|
| 5361 |
+
cardBottomActions = `
|
| 5362 |
+
<button class="btn accent text-[10px] py-0.5 px-2 font-bold text-violet-300" onclick="selectVaultItemById('${esc(trackId)}', true)">
|
| 5363 |
+
💿 Open Album
|
| 5364 |
+
</button>
|
| 5365 |
+
<div class="flex items-center gap-1">
|
| 5366 |
+
<a href="${it.zip_url || `/api/media/album/zip/${encodeURIComponent(trackId)}`}" download class="text-amber-400 hover:text-amber-200 text-xs px-1 font-bold" title="Download Album ZIP">📦 ZIP</a>
|
| 5367 |
+
<button class="text-slate-500 hover:text-rose-400 text-xs px-1" onclick="deleteVaultMedia('${esc(fileRef)}', '${esc(it.type)}', event)" title="Delete">🗑️</button>
|
| 5368 |
+
</div>
|
| 5369 |
+
`;
|
| 5370 |
+
} else {
|
| 5371 |
+
// Song, Audio, Video
|
| 5372 |
+
hoverOverlay = `
|
| 5373 |
+
<button class="absolute inset-0 bg-black/40 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity text-2xl text-white font-bold"
|
| 5374 |
+
onclick="playVaultItemById('${esc(trackId)}', event)">
|
| 5375 |
+
${isPlaying ? '⏸' : '▶'}
|
| 5376 |
+
</button>
|
| 5377 |
+
`;
|
| 5378 |
+
cardBottomActions = `
|
| 5379 |
+
<button class="btn ${isPlaying ? 'success' : 'accent'} text-[10px] py-0.5 px-2 font-bold" onclick="playVaultItemById('${esc(trackId)}', event)">
|
| 5380 |
+
${isPlaying ? '⏸ Pause' : '▶ Play'}
|
| 5381 |
+
</button>
|
| 5382 |
+
<div class="flex items-center gap-1">
|
| 5383 |
+
<button class="btn alt text-[10px] py-0.5 px-1 text-cyan-400" onclick="promptAddTagById('${esc(trackId)}', event)" title="Tag">🏷️</button>
|
| 5384 |
+
<button class="btn alt text-[10px] py-0.5 px-1.5" onclick="openEditSongModal('${esc(trackId)}', event)" title="Edit">✎</button>
|
| 5385 |
+
<a href="${fileDownloadUrl}" download="${esc(fileRef)}" class="text-cyan-400 hover:text-cyan-200 text-xs px-1" title="Download">⬇️</a>
|
| 5386 |
+
<button class="text-slate-500 hover:text-rose-400 text-xs px-1" onclick="deleteVaultMedia('${esc(fileRef)}', '${esc(it.type)}', event)" title="Delete">🗑️</button>
|
| 5387 |
+
</div>
|
| 5388 |
+
`;
|
| 5389 |
+
}
|
| 5390 |
|
| 5391 |
return `
|
| 5392 |
<div class="daw-rack p-2.5 rounded-lg border transition-all cursor-pointer flex flex-col justify-between ${isPlaying ? 'border-cyan-500/80 bg-slate-900/90 shadow-[0_0_12px_rgba(6,182,212,0.3)]' : isSelected ? 'border-cyan-600/60 bg-slate-900/50' : 'border-slate-800 hover:border-slate-700'}"
|
|
|
|
| 5396 |
<div class="space-y-1.5">
|
| 5397 |
<div class="relative group overflow-hidden rounded">
|
| 5398 |
${coverThumb}
|
| 5399 |
+
${hoverOverlay}
|
|
|
|
|
|
|
|
|
|
| 5400 |
</div>
|
| 5401 |
<div class="flex items-start justify-between gap-1">
|
| 5402 |
<div class="font-bold text-slate-100 text-xs truncate flex-1" title="${esc(title)}">${esc(title)}</div>
|
| 5403 |
+
<button class="text-xs shrink-0 ${it.favorite ? 'text-amber-400 font-bold' : 'text-slate-600 hover:text-amber-300'}"
|
| 5404 |
+
onclick="toggleVaultFavorite('${esc(trackId)}', event)" title="${it.favorite ? 'Favorited ⭐' : 'Add to Favorites'}">
|
| 5405 |
+
${it.favorite ? '⭐' : '☆'}
|
| 5406 |
</button>
|
| 5407 |
</div>
|
| 5408 |
<div class="text-[10px] text-slate-400 font-mono truncate">${esc(genreLabel)}</div>
|
| 5409 |
+
${tagsHtml ? `<div class="flex flex-wrap gap-1">${tagsHtml}</div>` : ''}
|
| 5410 |
<div class="flex items-center justify-between text-[9px] text-cyan-400 font-mono">
|
| 5411 |
+
<span>${it.bpm ? it.bpm + ' BPM' : (it.track_count ? it.track_count + ' tracks' : '')}</span>
|
| 5412 |
<span>${sizeLabel}</span>
|
| 5413 |
</div>
|
| 5414 |
</div>
|
| 5415 |
|
| 5416 |
<div class="flex items-center justify-between pt-2 border-t border-slate-800/80 mt-2 text-[10px]" onclick="event.stopPropagation()">
|
| 5417 |
+
${cardBottomActions}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5418 |
</div>
|
| 5419 |
</div>
|
| 5420 |
`;
|
|
|
|
| 5430 |
selectedVaultItem = it;
|
| 5431 |
const trackId = it.id || it.file || it.filename;
|
| 5432 |
const fileRef = it.file || it.filename || '';
|
| 5433 |
+
const coverUrl = it.cover_image_url || it.cover_image || it.cover_url || '';
|
| 5434 |
|
| 5435 |
if (it.type === 'video') {
|
| 5436 |
// Switch Stage to Video Player
|
| 5437 |
if ($('vstage-videoContainer')) $('vstage-videoContainer').classList.remove('hidden');
|
| 5438 |
if ($('vstage-audioContainer')) $('vstage-audioContainer').classList.add('hidden');
|
| 5439 |
+
if ($('vstage-imageContainer')) $('vstage-imageContainer').classList.add('hidden');
|
| 5440 |
+
if ($('vstage-albumContainer')) $('vstage-albumContainer').classList.add('hidden');
|
| 5441 |
+
|
| 5442 |
+
// Pause global audio
|
| 5443 |
+
const aPlayer = $('globalAudioPlayer');
|
| 5444 |
+
if (aPlayer && !aPlayer.paused) aPlayer.pause();
|
| 5445 |
|
| 5446 |
const vPlayer = $('vstage-videoPlayer');
|
| 5447 |
const videoUrl = it.url || `/api/media/video/stream/${encodeURIComponent(fileRef)}`;
|
|
|
|
| 5450 |
vPlayer.src = videoUrl;
|
| 5451 |
}
|
| 5452 |
if (autoPlay) {
|
|
|
|
|
|
|
|
|
|
| 5453 |
currentPlayingVaultId = trackId;
|
| 5454 |
vPlayer.play().catch(e => console.warn(e));
|
| 5455 |
}
|
|
|
|
| 5458 |
if ($('vstage-videoMeta')) $('vstage-videoMeta').textContent = `${it.size_kb ? it.size_kb + ' KB • ' : ''}${fileRef}`;
|
| 5459 |
if ($('vstage-videoDownload')) $('vstage-videoDownload').href = videoUrl;
|
| 5460 |
|
| 5461 |
+
} else if (it.type === 'image') {
|
| 5462 |
+
// Switch Stage to Image Showcase
|
| 5463 |
+
if ($('vstage-imageContainer')) $('vstage-imageContainer').classList.remove('hidden');
|
| 5464 |
+
if ($('vstage-audioContainer')) $('vstage-audioContainer').classList.add('hidden');
|
| 5465 |
+
if ($('vstage-videoContainer')) $('vstage-videoContainer').classList.add('hidden');
|
| 5466 |
+
if ($('vstage-albumContainer')) $('vstage-albumContainer').classList.add('hidden');
|
| 5467 |
+
|
| 5468 |
+
// Pause audio and video
|
| 5469 |
+
const aPlayer = $('globalAudioPlayer');
|
| 5470 |
+
if (aPlayer && !aPlayer.paused) aPlayer.pause();
|
| 5471 |
+
const vPlayer = $('vstage-videoPlayer');
|
| 5472 |
+
if (vPlayer && !vPlayer.paused) vPlayer.pause();
|
| 5473 |
+
|
| 5474 |
+
const imgSrc = it.url || it.image_url || `/api/media/file/${encodeURIComponent(fileRef)}`;
|
| 5475 |
+
if ($('vstage-imageDisplay')) $('vstage-imageDisplay').src = imgSrc;
|
| 5476 |
+
if ($('vstage-imageTitle')) $('vstage-imageTitle').textContent = it.title || it.name || fileRef;
|
| 5477 |
+
if ($('vstage-imageMeta')) $('vstage-imageMeta').textContent = `${it.size_kb ? it.size_kb + ' KB • ' : ''}PNG / FLUX.1 Artwork`;
|
| 5478 |
+
if ($('vstage-imageDownload')) {
|
| 5479 |
+
$('vstage-imageDownload').href = imgSrc;
|
| 5480 |
+
$('vstage-imageDownload').download = fileRef;
|
| 5481 |
+
}
|
| 5482 |
+
if ($('vstage-imageOpenTab')) $('vstage-imageOpenTab').href = imgSrc;
|
| 5483 |
+
|
| 5484 |
+
currentPlayingVaultId = null;
|
| 5485 |
+
|
| 5486 |
+
} else if (it.type === 'album') {
|
| 5487 |
+
// Switch Stage to Album Showcase
|
| 5488 |
+
if ($('vstage-albumContainer')) $('vstage-albumContainer').classList.remove('hidden');
|
| 5489 |
+
if ($('vstage-audioContainer')) $('vstage-audioContainer').classList.add('hidden');
|
| 5490 |
+
if ($('vstage-videoContainer')) $('vstage-videoContainer').classList.add('hidden');
|
| 5491 |
+
if ($('vstage-imageContainer')) $('vstage-imageContainer').classList.add('hidden');
|
| 5492 |
+
|
| 5493 |
+
// Pause video
|
| 5494 |
+
const vPlayer = $('vstage-videoPlayer');
|
| 5495 |
+
if (vPlayer && !vPlayer.paused) vPlayer.pause();
|
| 5496 |
+
|
| 5497 |
+
if ($('vstage-albumTitle')) $('vstage-albumTitle').textContent = it.title || it.name || fileRef;
|
| 5498 |
+
if ($('vstage-albumGenre')) $('vstage-albumGenre').textContent = it.genre || 'Moldovan AI Album';
|
| 5499 |
+
if ($('vstage-albumMeta')) {
|
| 5500 |
+
const trCount = it.track_count || (it.tracks || []).length;
|
| 5501 |
+
$('vstage-albumMeta').textContent = `${trCount} tracks • ${it.real_audio_count || 0} real audio • ${it.size_kb ? it.size_kb + ' KB' : ''}`;
|
| 5502 |
+
}
|
| 5503 |
+
if ($('vstage-albumStatusBadge')) {
|
| 5504 |
+
const st = it.honest_status || (it.real_audio_count > 0 ? 'real_audio' : 'lyrics_only');
|
| 5505 |
+
$('vstage-albumStatusBadge').textContent = st.replace('_', ' ').toUpperCase();
|
| 5506 |
+
$('vstage-albumStatusBadge').className = st === 'real_audio' ? 'daw-badge badge-emerald text-[9px] uppercase' : (st === 'lyrics_only' ? 'daw-badge badge-slate text-[9px] uppercase' : 'daw-badge badge-amber text-[9px] uppercase');
|
| 5507 |
+
}
|
| 5508 |
+
if ($('vstage-albumZipBtn')) {
|
| 5509 |
+
const zip = it.zip_url || `/api/media/album/zip/${encodeURIComponent(trackId)}`;
|
| 5510 |
+
$('vstage-albumZipBtn').href = zip;
|
| 5511 |
+
}
|
| 5512 |
+
if ($('vstage-albumTrackCount')) {
|
| 5513 |
+
$('vstage-albumTrackCount').textContent = `${(it.tracks || []).length} Tracks`;
|
| 5514 |
+
}
|
| 5515 |
+
|
| 5516 |
+
// Render tracks
|
| 5517 |
+
const tList = $('vstage-albumTracksList');
|
| 5518 |
+
if (tList) {
|
| 5519 |
+
const trs = it.tracks || [];
|
| 5520 |
+
if (!trs.length) {
|
| 5521 |
+
tList.innerHTML = '<div class="text-slate-500 text-xs p-2 font-mono">No tracks found in album manifest.</div>';
|
| 5522 |
+
} else {
|
| 5523 |
+
tList.innerHTML = trs.map((t, tidx) => {
|
| 5524 |
+
const canPlay = Boolean(t.url);
|
| 5525 |
+
const jsonArg = JSON.stringify(t).replace(/"/g, '"');
|
| 5526 |
+
return `
|
| 5527 |
+
<div class="flex items-center justify-between p-1.5 rounded bg-slate-950/80 border border-slate-800/80 hover:bg-slate-800/60 text-xs font-mono">
|
| 5528 |
+
<div class="flex items-center gap-2 min-w-0 flex-1">
|
| 5529 |
+
<span class="text-slate-500 w-5 text-center">${tidx + 1}</span>
|
| 5530 |
+
<span class="text-slate-200 truncate font-sans font-medium" title="${esc(t.title)}">${esc(t.title)}</span>
|
| 5531 |
+
<span class="daw-badge ${t.audio_status === 'lyrics_only' ? 'badge-slate' : 'badge-emerald'} text-[8px] shrink-0">${esc(t.audio_status || 'audio')}</span>
|
| 5532 |
+
</div>
|
| 5533 |
+
<div class="flex items-center gap-2 shrink-0">
|
| 5534 |
+
<span class="text-[10px] text-slate-500">${t.size_kb ? t.size_kb + ' KB' : ''}</span>
|
| 5535 |
+
${canPlay ? `<button class="btn accent text-[9px] py-0.5 px-2 font-bold" onclick="playAlbumTrack('${esc(trackId)}', ${jsonArg})">▶ Play</button>` : '<span class="text-[9px] text-slate-600">lyrics only</span>'}
|
| 5536 |
+
</div>
|
| 5537 |
+
</div>
|
| 5538 |
+
`;
|
| 5539 |
+
}).join('');
|
| 5540 |
+
}
|
| 5541 |
+
}
|
| 5542 |
+
|
| 5543 |
+
if (autoPlay) {
|
| 5544 |
+
playCurrentAlbum();
|
| 5545 |
+
}
|
| 5546 |
+
|
| 5547 |
} else {
|
| 5548 |
+
// Switch Stage to Audio Player Workstation (music / audio)
|
| 5549 |
if ($('vstage-audioContainer')) $('vstage-audioContainer').classList.remove('hidden');
|
| 5550 |
if ($('vstage-videoContainer')) $('vstage-videoContainer').classList.add('hidden');
|
| 5551 |
+
if ($('vstage-imageContainer')) $('vstage-imageContainer').classList.add('hidden');
|
| 5552 |
+
if ($('vstage-albumContainer')) $('vstage-albumContainer').classList.add('hidden');
|
| 5553 |
|
| 5554 |
// Pause video player
|
| 5555 |
const vPlayer = $('vstage-videoPlayer');
|
|
|
|
| 5557 |
|
| 5558 |
const title = it.title || it.name || fileRef;
|
| 5559 |
const genre = it.genre || (it.type === 'music' ? 'Chișinău 808 Trap' : 'Audio Track');
|
|
|
|
| 5560 |
const audioSrc = it.audio_url || it.url || (it.audio_file ? `/api/media/audio/stream/${encodeURIComponent(it.audio_file)}` : (fileRef ? `/api/media/audio/stream/${encodeURIComponent(fileRef)}` : ''));
|
| 5561 |
|
| 5562 |
if ($('vstage-trackTitle')) $('vstage-trackTitle').textContent = title;
|
|
|
|
| 5575 |
$('vstage-coverImg').src = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120' viewBox='0 0 120 120'%3E%3Crect width='120' height='120' fill='%230f172a'/%3E%3Ctext x='50%25' y='50%25' dominant-baseline='middle' text-anchor='middle' font-size='36' fill='%2338bdf8'%3E🎵%3C/text%3E%3C/svg%3E";
|
| 5576 |
}
|
| 5577 |
}
|
| 5578 |
+
if ($('vstage-downloadBtn')) {
|
| 5579 |
+
$('vstage-downloadBtn').href = audioSrc;
|
| 5580 |
+
$('vstage-downloadBtn').download = fileRef;
|
| 5581 |
+
}
|
| 5582 |
if ($('vstage-favBtn')) {
|
| 5583 |
$('vstage-favBtn').textContent = it.favorite ? '⭐ Favorited' : '☆ Add to Favs';
|
| 5584 |
+
$('vstage-favBtn').className = it.favorite ? 'btn accent text-xs py-0.5 px-2 text-amber-300 font-bold shadow-[0_0_8px_rgba(245,158,11,0.3)]' : 'btn alt text-xs py-0.5 px-2 text-amber-300';
|
| 5585 |
}
|
| 5586 |
|
| 5587 |
+
// Render Tags
|
| 5588 |
+
renderStageTags(it.tags || []);
|
| 5589 |
+
|
| 5590 |
// Load Lyrics into side panel
|
| 5591 |
loadStageLyrics(it);
|
| 5592 |
// Load Comments into side panel
|
|
|
|
| 5636 |
function playVaultItemById(trackId, evt) {
|
| 5637 |
if (evt) evt.stopPropagation();
|
| 5638 |
const it = (currentVaultItems || []).find(x => (x.id || x.file || x.filename) === trackId);
|
| 5639 |
+
if (!it) return;
|
| 5640 |
+
if (it.type === 'image') {
|
| 5641 |
+
selectVaultItem(it, false);
|
| 5642 |
+
return;
|
| 5643 |
+
}
|
| 5644 |
+
if (currentPlayingVaultId === trackId) {
|
| 5645 |
+
togglePlayPause();
|
| 5646 |
+
} else {
|
| 5647 |
+
playVaultItem(it, evt);
|
| 5648 |
+
}
|
| 5649 |
}
|
| 5650 |
|
| 5651 |
function playVaultItem(it, evt) {
|
| 5652 |
if (evt) evt.stopPropagation();
|
| 5653 |
+
if (it.type === 'image') {
|
| 5654 |
+
selectVaultItem(it, false);
|
| 5655 |
+
return;
|
| 5656 |
+
}
|
| 5657 |
selectVaultItem(it, true);
|
| 5658 |
}
|
| 5659 |
|
|
|
|
| 5858 |
async function toggleStageFavorite() {
|
| 5859 |
if (!selectedVaultItem) return;
|
| 5860 |
const trackId = selectedVaultItem.id || selectedVaultItem.file || selectedVaultItem.filename;
|
| 5861 |
+
const nextFav = !selectedVaultItem.favorite;
|
| 5862 |
try {
|
| 5863 |
const res = await api(`/api/media/song/${encodeURIComponent(trackId)}/favorite`, {
|
| 5864 |
method: 'POST',
|
| 5865 |
+
body: JSON.stringify({ favorite: nextFav })
|
| 5866 |
});
|
| 5867 |
+
const isFav = res.favorite !== undefined ? res.favorite : nextFav;
|
| 5868 |
+
selectedVaultItem.favorite = isFav;
|
| 5869 |
+
const it = (currentVaultItems || []).find(x => (x.id || x.file || x.filename) === trackId);
|
| 5870 |
+
if (it) it.favorite = isFav;
|
| 5871 |
+
|
| 5872 |
if ($('vstage-favBtn')) {
|
| 5873 |
+
$('vstage-favBtn').textContent = isFav ? '⭐ Favorited' : '☆ Add to Favs';
|
| 5874 |
+
$('vstage-favBtn').className = isFav ? 'btn accent text-xs py-0.5 px-2 text-amber-300 font-bold shadow-[0_0_8px_rgba(245,158,11,0.3)]' : 'btn alt text-xs py-0.5 px-2 text-amber-300';
|
| 5875 |
}
|
| 5876 |
+
toast(isFav ? 'Adăugat la favorite ⭐' : 'Eliminat din favorite');
|
| 5877 |
renderVault();
|
| 5878 |
} catch (e) {
|
| 5879 |
toast('Eroare favorite: ' + e.message, true);
|
|
|
|
| 5884 |
if (evt) evt.stopPropagation();
|
| 5885 |
try {
|
| 5886 |
const it = (currentVaultItems || []).find(x => (x.id || x.file || x.filename) === trackId);
|
| 5887 |
+
const nextFav = it ? !it.favorite : true;
|
| 5888 |
const res = await api(`/api/media/song/${encodeURIComponent(trackId)}/favorite`, {
|
| 5889 |
method: 'POST',
|
| 5890 |
+
body: JSON.stringify({ favorite: nextFav })
|
| 5891 |
});
|
| 5892 |
+
const isFav = res.favorite !== undefined ? res.favorite : nextFav;
|
| 5893 |
+
if (it) it.favorite = isFav;
|
| 5894 |
if (selectedVaultItem && (selectedVaultItem.id || selectedVaultItem.file) === trackId) {
|
| 5895 |
+
selectedVaultItem.favorite = isFav;
|
| 5896 |
if ($('vstage-favBtn')) {
|
| 5897 |
+
$('vstage-favBtn').textContent = isFav ? '⭐ Favorited' : '☆ Add to Favs';
|
| 5898 |
+
$('vstage-favBtn').className = isFav ? 'btn accent text-xs py-0.5 px-2 text-amber-300 font-bold shadow-[0_0_8px_rgba(245,158,11,0.3)]' : 'btn alt text-xs py-0.5 px-2 text-amber-300';
|
| 5899 |
}
|
| 5900 |
}
|
| 5901 |
+
toast(isFav ? 'Adăugat la favorite ⭐' : 'Eliminat din favorite');
|
| 5902 |
renderVault();
|
| 5903 |
} catch (e) {
|
| 5904 |
toast('Eroare favorite: ' + e.message, true);
|
| 5905 |
}
|
| 5906 |
}
|
| 5907 |
|
| 5908 |
+
async function promptAddStageTag() {
|
| 5909 |
+
if (!selectedVaultItem) return;
|
| 5910 |
+
const trackId = selectedVaultItem.id || selectedVaultItem.file || selectedVaultItem.filename;
|
| 5911 |
+
promptAddTagById(trackId);
|
| 5912 |
+
}
|
| 5913 |
+
|
| 5914 |
+
async function promptAddTagById(trackId, evt) {
|
| 5915 |
+
if (evt) evt.stopPropagation();
|
| 5916 |
+
const tag = prompt('Introdu un tag (ex: trap, 808, chill, favorit):');
|
| 5917 |
+
if (!tag || !tag.trim()) return;
|
| 5918 |
+
try {
|
| 5919 |
+
const res = await api(`/api/media/song/${encodeURIComponent(trackId)}/tags`, {
|
| 5920 |
+
method: 'POST',
|
| 5921 |
+
body: JSON.stringify({ add_tag: tag.trim() })
|
| 5922 |
+
});
|
| 5923 |
+
const it = (currentVaultItems || []).find(x => (x.id || x.file || x.filename) === trackId);
|
| 5924 |
+
if (it) it.tags = res.tags || [];
|
| 5925 |
+
if (selectedVaultItem && (selectedVaultItem.id === trackId || selectedVaultItem.file === trackId)) {
|
| 5926 |
+
selectedVaultItem.tags = res.tags || [];
|
| 5927 |
+
renderStageTags(selectedVaultItem.tags);
|
| 5928 |
+
}
|
| 5929 |
+
renderVault();
|
| 5930 |
+
toast(`Tag adăugat: #${tag.trim()}`);
|
| 5931 |
+
} catch (e) {
|
| 5932 |
+
toast('Eroare adăugare tag: ' + e.message, true);
|
| 5933 |
+
}
|
| 5934 |
+
}
|
| 5935 |
+
|
| 5936 |
+
async function removeStageTag(tag) {
|
| 5937 |
+
if (!selectedVaultItem) return;
|
| 5938 |
+
const trackId = selectedVaultItem.id || selectedVaultItem.file || selectedVaultItem.filename;
|
| 5939 |
+
try {
|
| 5940 |
+
const res = await api(`/api/media/song/${encodeURIComponent(trackId)}/tags`, {
|
| 5941 |
+
method: 'POST',
|
| 5942 |
+
body: JSON.stringify({ remove_tag: tag })
|
| 5943 |
+
});
|
| 5944 |
+
selectedVaultItem.tags = res.tags || [];
|
| 5945 |
+
const it = (currentVaultItems || []).find(x => (x.id || x.file || x.filename) === trackId);
|
| 5946 |
+
if (it) it.tags = res.tags || [];
|
| 5947 |
+
renderStageTags(selectedVaultItem.tags);
|
| 5948 |
+
renderVault();
|
| 5949 |
+
toast(`Tag eliminat: #${tag}`);
|
| 5950 |
+
} catch (e) {
|
| 5951 |
+
toast('Eroare eliminare tag: ' + e.message, true);
|
| 5952 |
+
}
|
| 5953 |
+
}
|
| 5954 |
+
|
| 5955 |
+
function renderStageTags(tags) {
|
| 5956 |
+
const container = $('vstage-tagsContainer');
|
| 5957 |
+
if (!container) return;
|
| 5958 |
+
if (!tags || !tags.length) {
|
| 5959 |
+
container.innerHTML = '<span class="text-slate-600 text-[9px] italic">No tags. Click +Tag to categorize.</span>';
|
| 5960 |
+
return;
|
| 5961 |
+
}
|
| 5962 |
+
container.innerHTML = tags.map(t => `
|
| 5963 |
+
<span class="inline-flex items-center gap-1 bg-slate-800/90 text-cyan-300 border border-slate-700/80 px-1.5 py-0.5 rounded text-[9px]">
|
| 5964 |
+
#${esc(t)}
|
| 5965 |
+
<button type="button" class="text-slate-500 hover:text-rose-400 font-bold text-[10px] ml-0.5" onclick="removeStageTag('${esc(t)}')" title="Remove tag">×</button>
|
| 5966 |
+
</span>
|
| 5967 |
+
`).join('');
|
| 5968 |
+
}
|
| 5969 |
+
|
| 5970 |
+
function playCurrentAlbum() {
|
| 5971 |
+
if (!selectedVaultItem || selectedVaultItem.type !== 'album') return;
|
| 5972 |
+
const playable = (selectedVaultItem.tracks || []).find(t => t.url);
|
| 5973 |
+
if (playable) {
|
| 5974 |
+
playAlbumTrack(selectedVaultItem.id, playable);
|
| 5975 |
+
} else {
|
| 5976 |
+
toast('Acest album are doar versuri (nu are încă fișiere audio randate)', true);
|
| 5977 |
+
}
|
| 5978 |
+
}
|
| 5979 |
+
|
| 5980 |
+
function playAlbumTrack(albumId, track) {
|
| 5981 |
+
if (!track || !track.url) {
|
| 5982 |
+
toast('Piesa nu are audio randat', true);
|
| 5983 |
+
return;
|
| 5984 |
+
}
|
| 5985 |
+
const player = $('globalAudioPlayer');
|
| 5986 |
+
if (!player) return;
|
| 5987 |
+
player.src = track.url;
|
| 5988 |
+
player.volume = parseFloat($('playerVolume')?.value || 0.85);
|
| 5989 |
+
player.play().catch(e => console.warn('Playback error:', e));
|
| 5990 |
+
|
| 5991 |
+
currentPlayingVaultId = track.title;
|
| 5992 |
+
if ($('vstage-audioContainer')) $('vstage-audioContainer').classList.remove('hidden');
|
| 5993 |
+
if ($('vstage-trackTitle')) $('vstage-trackTitle').textContent = `${track.title} (${selectedVaultItem?.title || albumId})`;
|
| 5994 |
+
if ($('vstage-trackMeta')) $('vstage-trackMeta').textContent = `${track.audio_status || 'Real Audio'} • ${track.size_kb || ''} KB • MiniMax Music 3`;
|
| 5995 |
+
if ($('vstage-downloadBtn')) $('vstage-downloadBtn').href = track.url;
|
| 5996 |
+
|
| 5997 |
+
if ($('playerTrackTitle')) $('playerTrackTitle').textContent = track.title;
|
| 5998 |
+
if ($('playerTrackMeta')) $('playerTrackMeta').textContent = selectedVaultItem?.title || albumId;
|
| 5999 |
+
if ($('playerDownloadBtn')) $('playerDownloadBtn').href = track.url;
|
| 6000 |
+
|
| 6001 |
+
updateLibraryPlayingIndicators();
|
| 6002 |
+
toast(`Se redă piesa: ${track.title} 🎵`);
|
| 6003 |
+
}
|
| 6004 |
+
|
| 6005 |
async function bundleStageProject() {
|
| 6006 |
if (!selectedVaultItem) return;
|
| 6007 |
const trackId = selectedVaultItem.id || selectedVaultItem.file || selectedVaultItem.filename;
|
|
|
|
| 6032 |
|
| 6033 |
async function deleteVaultMedia(fileRef, type, evt) {
|
| 6034 |
if (evt) evt.stopPropagation();
|
| 6035 |
+
if (!confirm(`Sigur vrei să ștergi acest element (${fileRef})?`)) return;
|
| 6036 |
try {
|
| 6037 |
+
if (type === 'music' || type === 'song') {
|
| 6038 |
+
await api(`/api/media/song/${encodeURIComponent(fileRef)}`, { method: 'DELETE' });
|
| 6039 |
+
} else {
|
| 6040 |
+
await api(`/api/media/file/${encodeURIComponent(fileRef)}`, { method: 'DELETE' });
|
| 6041 |
+
}
|
| 6042 |
+
toast(`Element șters cu succes: ${fileRef}`);
|
| 6043 |
loadVault(currentVaultFilter, true);
|
| 6044 |
} catch (e) {
|
| 6045 |
toast('Eroare ștergere: ' + e.message, true);
|
|
|
|
| 6496 |
window.submitStageComment = submitStageComment;
|
| 6497 |
window.toggleStageFavorite = toggleStageFavorite;
|
| 6498 |
window.toggleVaultFavorite = toggleVaultFavorite;
|
| 6499 |
+
window.promptAddStageTag = promptAddStageTag;
|
| 6500 |
+
window.promptAddTagById = promptAddTagById;
|
| 6501 |
+
window.removeStageTag = removeStageTag;
|
| 6502 |
+
window.renderStageTags = renderStageTags;
|
| 6503 |
+
window.playCurrentAlbum = playCurrentAlbum;
|
| 6504 |
+
window.playAlbumTrack = playAlbumTrack;
|
| 6505 |
window.bundleStageProject = bundleStageProject;
|
| 6506 |
window.openStageSongEdit = openStageSongEdit;
|
| 6507 |
window.pruneEmptyImages = pruneEmptyImages;
|
moldovan-qwen/studio_server.py
CHANGED
|
@@ -1872,8 +1872,10 @@ async def list_media_assets(asset_type: str = Query("ALL"), type: str = Query("A
|
|
| 1872 |
"title": meta.get("title", fname.replace(".wav", "").replace(".mp3", "").replace("_", " ").title()),
|
| 1873 |
"path": fpath,
|
| 1874 |
"url": f"/api/media/audio/stream/{fname}",
|
|
|
|
| 1875 |
"lyrics": meta.get("lyrics", ""),
|
| 1876 |
"genre": meta.get("genre", ""),
|
|
|
|
| 1877 |
"favorite": bool(meta.get("favorite", False)),
|
| 1878 |
"comments": meta.get("comments", []),
|
| 1879 |
"size": sz,
|
|
@@ -1907,6 +1909,16 @@ async def list_media_assets(asset_type: str = Query("ALL"), type: str = Query("A
|
|
| 1907 |
cover_url = d.get("cover_image_url") or d.get("cover_image")
|
| 1908 |
song_id = d.get("id") or os.path.basename(fpath).replace(".json", "")
|
| 1909 |
lyrics_str = d.get("full_lyrics") or d.get("lyrics", "")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1910 |
assets.append({
|
| 1911 |
"id": song_id,
|
| 1912 |
"type": "music",
|
|
@@ -1918,6 +1930,7 @@ async def list_media_assets(asset_type: str = Query("ALL"), type: str = Query("A
|
|
| 1918 |
"bpm": d.get("bpm", 140),
|
| 1919 |
"key": d.get("key", "C Minor"),
|
| 1920 |
"vocal_style": d.get("vocal_style", ""),
|
|
|
|
| 1921 |
"lyrics": lyrics_str,
|
| 1922 |
"full_lyrics": lyrics_str,
|
| 1923 |
"favorite": bool(d.get("favorite", False)),
|
|
@@ -1927,6 +1940,8 @@ async def list_media_assets(asset_type: str = Query("ALL"), type: str = Query("A
|
|
| 1927 |
"url": audio_url,
|
| 1928 |
"cover_image": cover_url,
|
| 1929 |
"cover_image_url": cover_url,
|
|
|
|
|
|
|
| 1930 |
"size": sz,
|
| 1931 |
"size_kb": round(sz / 1024, 1),
|
| 1932 |
"updated_at": d.get("updated_at") or os.path.getmtime(fpath)
|
|
@@ -1978,8 +1993,10 @@ async def list_media_assets(asset_type: str = Query("ALL"), type: str = Query("A
|
|
| 1978 |
|
| 1979 |
# 6b. Daily generated albums (honest audio_status from manifests)
|
| 1980 |
for album in (await list_vault_albums()).get("items", []):
|
|
|
|
| 1981 |
assets.append({
|
| 1982 |
"type": "album",
|
|
|
|
| 1983 |
"filename": album["id"],
|
| 1984 |
"file": album["id"],
|
| 1985 |
"name": album["title"],
|
|
@@ -1989,6 +2006,10 @@ async def list_media_assets(asset_type: str = Query("ALL"), type: str = Query("A
|
|
| 1989 |
"track_count": album.get("track_count", 0),
|
| 1990 |
"real_audio_count": album.get("real_audio_count", 0),
|
| 1991 |
"honest_status": album.get("honest_status", "unknown"),
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1992 |
"size": album.get("size", 0),
|
| 1993 |
"size_kb": round(album.get("size", 0) / 1024, 1),
|
| 1994 |
"updated_at": album.get("updated_at", 0)
|
|
@@ -2003,6 +2024,9 @@ async def list_media_assets(asset_type: str = Query("ALL"), type: str = Query("A
|
|
| 2003 |
|
| 2004 |
if target_filter == "favorites":
|
| 2005 |
assets = [a for a in assets if a.get("favorite") is True]
|
|
|
|
|
|
|
|
|
|
| 2006 |
elif target_filter not in ["all", "*", ""]:
|
| 2007 |
if target_filter in ["audio", "sound"]:
|
| 2008 |
assets = [a for a in assets if a.get("type") in ["audio", "music"]]
|
|
@@ -2021,7 +2045,12 @@ async def list_media_assets(asset_type: str = Query("ALL"), type: str = Query("A
|
|
| 2021 |
|
| 2022 |
if search:
|
| 2023 |
s = search.lower()
|
| 2024 |
-
assets = [a for a in assets if s in a.get("title", "").lower()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2025 |
|
| 2026 |
sorted_assets = sorted(assets, key=lambda x: x.get("updated_at", 0), reverse=True)
|
| 2027 |
return {"assets": sorted_assets, "items": sorted_assets, "count": len(sorted_assets)}
|
|
@@ -2081,17 +2110,25 @@ def _find_track_audio(track_id: str) -> Optional[str]:
|
|
| 2081 |
|
| 2082 |
def _find_track_sidecar(track_id: str) -> Optional[str]:
|
| 2083 |
"""Locate the JSON sidecar for a track id (audio dir first, then music dir)."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2084 |
for d in (AUDIO_DIR, MUSIC_DIR):
|
| 2085 |
-
|
| 2086 |
-
|
| 2087 |
-
|
|
|
|
| 2088 |
return None
|
| 2089 |
|
| 2090 |
|
| 2091 |
def _read_or_init_sidecar(track_id: str) -> Dict:
|
| 2092 |
"""Read the track sidecar; auto-create with defaults when only audio exists."""
|
| 2093 |
-
|
| 2094 |
-
|
|
|
|
| 2095 |
if sc_path and os.path.exists(sc_path):
|
| 2096 |
try:
|
| 2097 |
with open(sc_path, "r", encoding="utf-8") as f:
|
|
@@ -2099,18 +2136,19 @@ def _read_or_init_sidecar(track_id: str) -> Dict:
|
|
| 2099 |
except Exception:
|
| 2100 |
pass
|
| 2101 |
meta: Dict[str, Any] = {
|
| 2102 |
-
"id": track_id,
|
| 2103 |
-
"title": track_id.replace("_", " ").title(),
|
| 2104 |
"favorite": False,
|
| 2105 |
"comments": [],
|
| 2106 |
"lyrics": "",
|
|
|
|
| 2107 |
}
|
| 2108 |
if audio_path:
|
| 2109 |
meta["audio_file"] = os.path.basename(audio_path)
|
| 2110 |
meta["audio_url"] = f"/api/media/audio/stream/{os.path.basename(audio_path)}"
|
| 2111 |
meta["created_at"] = time.time()
|
| 2112 |
# Persist auto-initialized sidecar next to the audio file
|
| 2113 |
-
sc_path = os.path.join(AUDIO_DIR, f"{
|
| 2114 |
try:
|
| 2115 |
with open(sc_path, "w", encoding="utf-8") as f:
|
| 2116 |
json.dump(meta, f, ensure_ascii=False, indent=2)
|
|
@@ -2121,11 +2159,12 @@ def _read_or_init_sidecar(track_id: str) -> Dict:
|
|
| 2121 |
|
| 2122 |
def _write_sidecar_field(track_id: str, **fields) -> Dict:
|
| 2123 |
"""Update one or more fields in the track sidecar and return it."""
|
|
|
|
| 2124 |
meta = _read_or_init_sidecar(track_id)
|
| 2125 |
meta.update(fields)
|
| 2126 |
if "id" not in meta:
|
| 2127 |
-
meta["id"] = track_id
|
| 2128 |
-
sc_path = _find_track_sidecar(track_id) or os.path.join(AUDIO_DIR, f"{
|
| 2129 |
with open(sc_path, "w", encoding="utf-8") as f:
|
| 2130 |
json.dump(meta, f, ensure_ascii=False, indent=2)
|
| 2131 |
return meta
|
|
@@ -2134,10 +2173,60 @@ def _write_sidecar_field(track_id: str, **fields) -> Dict:
|
|
| 2134 |
# ─── Media Vault: favorites / comments / lyrics / bundle / prune ────────────
|
| 2135 |
|
| 2136 |
@app.post("/api/media/song/{track_id}/favorite")
|
| 2137 |
-
async def toggle_track_favorite(track_id: str, body: Dict = Body(
|
| 2138 |
"""Toggle favorite flag for a song or sidecar-backed audio track."""
|
| 2139 |
-
|
| 2140 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2141 |
|
| 2142 |
|
| 2143 |
@app.post("/api/media/song/{track_id}/comment")
|
|
@@ -2625,46 +2714,61 @@ async def list_vault_music():
|
|
| 2625 |
async def get_song_details(song_id: str):
|
| 2626 |
"""Retrieve full details and manifest for a specific song."""
|
| 2627 |
fpath = find_song_file(song_id)
|
| 2628 |
-
if
|
| 2629 |
-
|
| 2630 |
-
|
| 2631 |
-
data =
|
| 2632 |
-
|
| 2633 |
-
|
| 2634 |
-
|
| 2635 |
-
|
| 2636 |
-
|
| 2637 |
-
|
| 2638 |
-
|
|
|
|
|
|
|
|
|
|
| 2639 |
|
| 2640 |
@app.put("/api/media/song/{song_id}")
|
| 2641 |
@app.patch("/api/media/song/{song_id}")
|
| 2642 |
async def update_song_metadata(song_id: str, body: Dict = Body(...)):
|
| 2643 |
-
"""Update title, genre, bpm, key, lyrics, vocal_style, mood for a song."""
|
| 2644 |
fpath = find_song_file(song_id)
|
| 2645 |
-
if
|
| 2646 |
-
|
| 2647 |
-
|
| 2648 |
-
data = json.load(f)
|
| 2649 |
|
| 2650 |
-
|
| 2651 |
-
|
| 2652 |
-
|
| 2653 |
-
|
| 2654 |
-
|
| 2655 |
-
|
| 2656 |
-
|
| 2657 |
-
|
| 2658 |
-
|
| 2659 |
-
|
| 2660 |
-
|
| 2661 |
-
|
| 2662 |
-
|
| 2663 |
-
|
| 2664 |
|
| 2665 |
-
|
| 2666 |
-
|
| 2667 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2668 |
|
| 2669 |
@app.delete("/api/media/song/{song_id}")
|
| 2670 |
async def delete_song(song_id: str, delete_media_files: bool = Query(True)):
|
|
|
|
| 1872 |
"title": meta.get("title", fname.replace(".wav", "").replace(".mp3", "").replace("_", " ").title()),
|
| 1873 |
"path": fpath,
|
| 1874 |
"url": f"/api/media/audio/stream/{fname}",
|
| 1875 |
+
"audio_url": f"/api/media/audio/stream/{fname}",
|
| 1876 |
"lyrics": meta.get("lyrics", ""),
|
| 1877 |
"genre": meta.get("genre", ""),
|
| 1878 |
+
"tags": meta.get("tags", []),
|
| 1879 |
"favorite": bool(meta.get("favorite", False)),
|
| 1880 |
"comments": meta.get("comments", []),
|
| 1881 |
"size": sz,
|
|
|
|
| 1909 |
cover_url = d.get("cover_image_url") or d.get("cover_image")
|
| 1910 |
song_id = d.get("id") or os.path.basename(fpath).replace(".json", "")
|
| 1911 |
lyrics_str = d.get("full_lyrics") or d.get("lyrics", "")
|
| 1912 |
+
karaoke_video = d.get("karaoke_video")
|
| 1913 |
+
karaoke_video_file = d.get("karaoke_video_file")
|
| 1914 |
+
if not karaoke_video and not karaoke_video_file:
|
| 1915 |
+
orphan = os.path.join(VIDEOS_OUT_DIR, f"{song_id}_karaoke.mp4")
|
| 1916 |
+
if os.path.isfile(orphan):
|
| 1917 |
+
karaoke_video_file = f"{song_id}_karaoke.mp4"
|
| 1918 |
+
karaoke_video = karaoke_video or (f"/api/media/video/stream/{karaoke_video_file}" if karaoke_video_file else None)
|
| 1919 |
+
tags = d.get("tags") or []
|
| 1920 |
+
if not tags and d.get("genre"):
|
| 1921 |
+
tags = [d["genre"].lower()]
|
| 1922 |
assets.append({
|
| 1923 |
"id": song_id,
|
| 1924 |
"type": "music",
|
|
|
|
| 1930 |
"bpm": d.get("bpm", 140),
|
| 1931 |
"key": d.get("key", "C Minor"),
|
| 1932 |
"vocal_style": d.get("vocal_style", ""),
|
| 1933 |
+
"tags": tags,
|
| 1934 |
"lyrics": lyrics_str,
|
| 1935 |
"full_lyrics": lyrics_str,
|
| 1936 |
"favorite": bool(d.get("favorite", False)),
|
|
|
|
| 1940 |
"url": audio_url,
|
| 1941 |
"cover_image": cover_url,
|
| 1942 |
"cover_image_url": cover_url,
|
| 1943 |
+
"karaoke_video": karaoke_video,
|
| 1944 |
+
"karaoke_video_file": karaoke_video_file,
|
| 1945 |
"size": sz,
|
| 1946 |
"size_kb": round(sz / 1024, 1),
|
| 1947 |
"updated_at": d.get("updated_at") or os.path.getmtime(fpath)
|
|
|
|
| 1993 |
|
| 1994 |
# 6b. Daily generated albums (honest audio_status from manifests)
|
| 1995 |
for album in (await list_vault_albums()).get("items", []):
|
| 1996 |
+
first_playable = next((t["url"] for t in album.get("tracks", []) if t.get("url")), None)
|
| 1997 |
assets.append({
|
| 1998 |
"type": "album",
|
| 1999 |
+
"id": album["id"],
|
| 2000 |
"filename": album["id"],
|
| 2001 |
"file": album["id"],
|
| 2002 |
"name": album["title"],
|
|
|
|
| 2006 |
"track_count": album.get("track_count", 0),
|
| 2007 |
"real_audio_count": album.get("real_audio_count", 0),
|
| 2008 |
"honest_status": album.get("honest_status", "unknown"),
|
| 2009 |
+
"tracks": album.get("tracks", []),
|
| 2010 |
+
"zip_url": f"/api/media/album/zip/{album['id']}",
|
| 2011 |
+
"audio_url": first_playable,
|
| 2012 |
+
"url": first_playable,
|
| 2013 |
"size": album.get("size", 0),
|
| 2014 |
"size_kb": round(album.get("size", 0) / 1024, 1),
|
| 2015 |
"updated_at": album.get("updated_at", 0)
|
|
|
|
| 2024 |
|
| 2025 |
if target_filter == "favorites":
|
| 2026 |
assets = [a for a in assets if a.get("favorite") is True]
|
| 2027 |
+
elif target_filter.startswith("tag:"):
|
| 2028 |
+
req_tag = target_filter.split("tag:", 1)[1].strip().lower()
|
| 2029 |
+
assets = [a for a in assets if any(req_tag == str(t).lower() for t in a.get("tags", []))]
|
| 2030 |
elif target_filter not in ["all", "*", ""]:
|
| 2031 |
if target_filter in ["audio", "sound"]:
|
| 2032 |
assets = [a for a in assets if a.get("type") in ["audio", "music"]]
|
|
|
|
| 2045 |
|
| 2046 |
if search:
|
| 2047 |
s = search.lower()
|
| 2048 |
+
assets = [a for a in assets if s in a.get("title", "").lower()
|
| 2049 |
+
or s in a.get("name", "").lower()
|
| 2050 |
+
or s in a.get("filename", "").lower()
|
| 2051 |
+
or s in a.get("genre", "").lower()
|
| 2052 |
+
or any(s in str(t).lower() for t in a.get("tags", []))
|
| 2053 |
+
or s in a.get("lyrics", "").lower()]
|
| 2054 |
|
| 2055 |
sorted_assets = sorted(assets, key=lambda x: x.get("updated_at", 0), reverse=True)
|
| 2056 |
return {"assets": sorted_assets, "items": sorted_assets, "count": len(sorted_assets)}
|
|
|
|
| 2110 |
|
| 2111 |
def _find_track_sidecar(track_id: str) -> Optional[str]:
|
| 2112 |
"""Locate the JSON sidecar for a track id (audio dir first, then music dir)."""
|
| 2113 |
+
if not track_id:
|
| 2114 |
+
return None
|
| 2115 |
+
clean_id = os.path.basename(track_id).rsplit(".", 1)[0]
|
| 2116 |
+
song_f = find_song_file(track_id) or find_song_file(clean_id)
|
| 2117 |
+
if song_f and os.path.exists(song_f):
|
| 2118 |
+
return song_f
|
| 2119 |
for d in (AUDIO_DIR, MUSIC_DIR):
|
| 2120 |
+
for name in (track_id, clean_id):
|
| 2121 |
+
cand = os.path.join(d, f"{name}.json")
|
| 2122 |
+
if os.path.exists(cand):
|
| 2123 |
+
return cand
|
| 2124 |
return None
|
| 2125 |
|
| 2126 |
|
| 2127 |
def _read_or_init_sidecar(track_id: str) -> Dict:
|
| 2128 |
"""Read the track sidecar; auto-create with defaults when only audio exists."""
|
| 2129 |
+
clean_id = os.path.basename(track_id).rsplit(".", 1)[0] if track_id else track_id
|
| 2130 |
+
sc_path = _find_track_sidecar(track_id) or _find_track_sidecar(clean_id)
|
| 2131 |
+
audio_path = _find_track_audio(track_id) or _find_track_audio(clean_id)
|
| 2132 |
if sc_path and os.path.exists(sc_path):
|
| 2133 |
try:
|
| 2134 |
with open(sc_path, "r", encoding="utf-8") as f:
|
|
|
|
| 2136 |
except Exception:
|
| 2137 |
pass
|
| 2138 |
meta: Dict[str, Any] = {
|
| 2139 |
+
"id": clean_id or track_id,
|
| 2140 |
+
"title": (clean_id or track_id).replace("_", " ").title(),
|
| 2141 |
"favorite": False,
|
| 2142 |
"comments": [],
|
| 2143 |
"lyrics": "",
|
| 2144 |
+
"tags": [],
|
| 2145 |
}
|
| 2146 |
if audio_path:
|
| 2147 |
meta["audio_file"] = os.path.basename(audio_path)
|
| 2148 |
meta["audio_url"] = f"/api/media/audio/stream/{os.path.basename(audio_path)}"
|
| 2149 |
meta["created_at"] = time.time()
|
| 2150 |
# Persist auto-initialized sidecar next to the audio file
|
| 2151 |
+
sc_path = os.path.join(AUDIO_DIR, f"{clean_id}.json")
|
| 2152 |
try:
|
| 2153 |
with open(sc_path, "w", encoding="utf-8") as f:
|
| 2154 |
json.dump(meta, f, ensure_ascii=False, indent=2)
|
|
|
|
| 2159 |
|
| 2160 |
def _write_sidecar_field(track_id: str, **fields) -> Dict:
|
| 2161 |
"""Update one or more fields in the track sidecar and return it."""
|
| 2162 |
+
clean_id = os.path.basename(track_id).rsplit(".", 1)[0] if track_id else track_id
|
| 2163 |
meta = _read_or_init_sidecar(track_id)
|
| 2164 |
meta.update(fields)
|
| 2165 |
if "id" not in meta:
|
| 2166 |
+
meta["id"] = clean_id or track_id
|
| 2167 |
+
sc_path = _find_track_sidecar(track_id) or os.path.join(AUDIO_DIR, f"{clean_id}.json")
|
| 2168 |
with open(sc_path, "w", encoding="utf-8") as f:
|
| 2169 |
json.dump(meta, f, ensure_ascii=False, indent=2)
|
| 2170 |
return meta
|
|
|
|
| 2173 |
# ─── Media Vault: favorites / comments / lyrics / bundle / prune ────────────
|
| 2174 |
|
| 2175 |
@app.post("/api/media/song/{track_id}/favorite")
|
| 2176 |
+
async def toggle_track_favorite(track_id: str, body: Dict = Body(default={})):
|
| 2177 |
"""Toggle favorite flag for a song or sidecar-backed audio track."""
|
| 2178 |
+
clean_id = os.path.basename(track_id).rsplit(".", 1)[0] if track_id else track_id
|
| 2179 |
+
meta = _read_or_init_sidecar(track_id)
|
| 2180 |
+
if body and "favorite" in body and body["favorite"] is not None:
|
| 2181 |
+
fav = bool(body["favorite"])
|
| 2182 |
+
else:
|
| 2183 |
+
fav = not bool(meta.get("favorite", False))
|
| 2184 |
+
meta = _write_sidecar_field(track_id, favorite=fav)
|
| 2185 |
+
# Also sync into song manifest if present
|
| 2186 |
+
fpath = find_song_file(track_id) or find_song_file(clean_id)
|
| 2187 |
+
if fpath and os.path.exists(fpath):
|
| 2188 |
+
try:
|
| 2189 |
+
with open(fpath, "r", encoding="utf-8") as f:
|
| 2190 |
+
d = json.load(f)
|
| 2191 |
+
d["favorite"] = fav
|
| 2192 |
+
with open(fpath, "w", encoding="utf-8") as f:
|
| 2193 |
+
json.dump(d, f, ensure_ascii=False, indent=2)
|
| 2194 |
+
except Exception:
|
| 2195 |
+
pass
|
| 2196 |
+
return {"status": "success", "id": track_id, "favorite": bool(meta.get("favorite", False))}
|
| 2197 |
+
|
| 2198 |
+
@app.post("/api/media/song/{track_id}/tags")
|
| 2199 |
+
async def update_track_tags(track_id: str, body: Dict = Body(...)):
|
| 2200 |
+
"""Update, add, or remove tags for a song or audio track."""
|
| 2201 |
+
clean_id = os.path.basename(track_id).rsplit(".", 1)[0] if track_id else track_id
|
| 2202 |
+
meta = _read_or_init_sidecar(track_id)
|
| 2203 |
+
tags = body.get("tags")
|
| 2204 |
+
add_tag = body.get("add_tag")
|
| 2205 |
+
remove_tag = body.get("remove_tag")
|
| 2206 |
+
|
| 2207 |
+
current_tags = list(meta.get("tags") or [])
|
| 2208 |
+
if tags is not None and isinstance(tags, list):
|
| 2209 |
+
current_tags = [str(t).strip() for t in tags if str(t).strip()]
|
| 2210 |
+
if add_tag and str(add_tag).strip():
|
| 2211 |
+
at = str(add_tag).strip().lower()
|
| 2212 |
+
if at not in [t.lower() for t in current_tags]:
|
| 2213 |
+
current_tags.append(at)
|
| 2214 |
+
if remove_tag and str(remove_tag).strip():
|
| 2215 |
+
rt = str(remove_tag).strip().lower()
|
| 2216 |
+
current_tags = [t for t in current_tags if t.lower() != rt]
|
| 2217 |
+
|
| 2218 |
+
meta = _write_sidecar_field(track_id, tags=current_tags)
|
| 2219 |
+
fpath = find_song_file(track_id) or find_song_file(clean_id)
|
| 2220 |
+
if fpath and os.path.exists(fpath):
|
| 2221 |
+
try:
|
| 2222 |
+
with open(fpath, "r", encoding="utf-8") as f:
|
| 2223 |
+
d = json.load(f)
|
| 2224 |
+
d["tags"] = current_tags
|
| 2225 |
+
with open(fpath, "w", encoding="utf-8") as f:
|
| 2226 |
+
json.dump(d, f, ensure_ascii=False, indent=2)
|
| 2227 |
+
except Exception:
|
| 2228 |
+
pass
|
| 2229 |
+
return {"status": "success", "id": track_id, "tags": current_tags}
|
| 2230 |
|
| 2231 |
|
| 2232 |
@app.post("/api/media/song/{track_id}/comment")
|
|
|
|
| 2714 |
async def get_song_details(song_id: str):
|
| 2715 |
"""Retrieve full details and manifest for a specific song."""
|
| 2716 |
fpath = find_song_file(song_id)
|
| 2717 |
+
if fpath and os.path.exists(fpath):
|
| 2718 |
+
with open(fpath, "r", encoding="utf-8") as f:
|
| 2719 |
+
data = json.load(f)
|
| 2720 |
+
data["id"] = data.get("id") or os.path.basename(fpath).replace(".json", "")
|
| 2721 |
+
audio_file = data.get("audio_file") or (os.path.basename(data.get("audio_path", "")) if data.get("audio_path") else "")
|
| 2722 |
+
if audio_file:
|
| 2723 |
+
data["audio_file"] = audio_file
|
| 2724 |
+
if not data.get("audio_url"):
|
| 2725 |
+
data["audio_url"] = f"/api/media/audio/stream/{audio_file}"
|
| 2726 |
+
return data
|
| 2727 |
+
sc = _find_track_sidecar(song_id)
|
| 2728 |
+
if sc or _find_track_audio(song_id):
|
| 2729 |
+
return _read_or_init_sidecar(song_id)
|
| 2730 |
+
raise HTTPException(status_code=404, detail=f"Song '{song_id}' not found")
|
| 2731 |
|
| 2732 |
@app.put("/api/media/song/{song_id}")
|
| 2733 |
@app.patch("/api/media/song/{song_id}")
|
| 2734 |
async def update_song_metadata(song_id: str, body: Dict = Body(...)):
|
| 2735 |
+
"""Update title, genre, bpm, key, lyrics, vocal_style, mood for a song or sidecar."""
|
| 2736 |
fpath = find_song_file(song_id)
|
| 2737 |
+
if fpath and os.path.exists(fpath):
|
| 2738 |
+
with open(fpath, "r", encoding="utf-8") as f:
|
| 2739 |
+
data = json.load(f)
|
|
|
|
| 2740 |
|
| 2741 |
+
for field in ["title", "genre", "bpm", "key", "vocal_style", "mood", "lyrics", "full_lyrics", "topic", "suno_style_prompt", "cover_style", "dialect_level", "tags"]:
|
| 2742 |
+
if field in body:
|
| 2743 |
+
data[field] = body[field]
|
| 2744 |
+
if "bpm" in body and body["bpm"] is not None:
|
| 2745 |
+
try:
|
| 2746 |
+
data["bpm"] = int(body["bpm"])
|
| 2747 |
+
except (ValueError, TypeError):
|
| 2748 |
+
pass
|
| 2749 |
+
if "dialect_level" in body and body["dialect_level"] is not None:
|
| 2750 |
+
try:
|
| 2751 |
+
data["dialect_level"] = int(body["dialect_level"])
|
| 2752 |
+
except (ValueError, TypeError):
|
| 2753 |
+
pass
|
| 2754 |
+
data["updated_at"] = time.time()
|
| 2755 |
|
| 2756 |
+
with open(fpath, "w", encoding="utf-8") as f:
|
| 2757 |
+
json.dump(data, f, ensure_ascii=False, indent=2)
|
| 2758 |
+
return {"status": "success", "message": "Song metadata updated", "song": data}
|
| 2759 |
+
|
| 2760 |
+
# If song manifest doesn't exist, update audio sidecar
|
| 2761 |
+
sc = _find_track_sidecar(song_id)
|
| 2762 |
+
if sc or _find_track_audio(song_id):
|
| 2763 |
+
updates = {}
|
| 2764 |
+
for field in ["title", "genre", "bpm", "key", "vocal_style", "mood", "lyrics", "full_lyrics", "topic", "tags"]:
|
| 2765 |
+
if field in body:
|
| 2766 |
+
updates[field] = body[field]
|
| 2767 |
+
updates["updated_at"] = time.time()
|
| 2768 |
+
meta = _write_sidecar_field(song_id, **updates)
|
| 2769 |
+
return {"status": "success", "message": "Track sidecar updated", "song": meta}
|
| 2770 |
+
|
| 2771 |
+
raise HTTPException(status_code=404, detail=f"Song or track '{song_id}' not found")
|
| 2772 |
|
| 2773 |
@app.delete("/api/media/song/{song_id}")
|
| 2774 |
async def delete_song(song_id: str, delete_media_files: bool = Query(True)):
|