Upload publicapi.py
Browse files- public/publicapi.py +111 -51
public/publicapi.py
CHANGED
|
@@ -2,7 +2,7 @@
|
|
| 2 |
#!/usr/bin/env python3
|
| 3 |
# -*- coding: utf-8 -*-
|
| 4 |
|
| 5 |
-
# GhostAI Music Generator — Release v1.3.
|
| 6 |
# Gradio UI + FastAPI server, externalized styles (CSS), prompts (INI), and examples (MD).
|
| 7 |
# Saves MP3s to ./mp3, single rotating log (max 5MB) in ./logs, colorized console.
|
| 8 |
|
|
@@ -40,7 +40,7 @@ import uvicorn
|
|
| 40 |
|
| 41 |
from colorama import init as colorama_init, Fore, Style
|
| 42 |
|
| 43 |
-
RELEASE = "v1.3.
|
| 44 |
|
| 45 |
# ======================================================================================
|
| 46 |
# PATCHES & RUNTIME
|
|
@@ -263,7 +263,7 @@ def balance_stereo(seg: AudioSegment, noise_threshold=-40, sample_rate=48000) ->
|
|
| 263 |
stereo = stereo * mask
|
| 264 |
left, right = stereo[:, 0], stereo[:, 1]
|
| 265 |
l_rms = np.sqrt(np.mean(left[left != 0] ** 2)) if np.any(left != 0) else 0
|
| 266 |
-
r_rms = np.sqrt(np.mean(right[right != 0] ** 2)) if np.any(right != 0) else 0
|
| 267 |
if l_rms > 0 and r_rms > 0:
|
| 268 |
avg = (l_rms + r_rms) / 2
|
| 269 |
stereo[:, 0] *= (avg / l_rms)
|
|
@@ -316,13 +316,18 @@ def apply_fade(seg: AudioSegment, fade_in=500, fade_out=800) -> AudioSegment:
|
|
| 316 |
return seg
|
| 317 |
|
| 318 |
# ======================================================================================
|
| 319 |
-
# PROMPTS (FROM INI)
|
| 320 |
# ======================================================================================
|
| 321 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 322 |
class StylesConfig:
|
| 323 |
def __init__(self, path: Path):
|
| 324 |
self.path = path
|
| 325 |
-
self.cfg = configparser.ConfigParser()
|
| 326 |
self.mtime = 0.0
|
| 327 |
self.styles: Dict[str, Dict[str, Any]] = {}
|
| 328 |
self._load()
|
|
@@ -330,17 +335,22 @@ class StylesConfig:
|
|
| 330 |
def _load(self):
|
| 331 |
if not self.path.exists():
|
| 332 |
logger.error(f"prompts.ini not found: {self.path}")
|
| 333 |
-
self.cfg = configparser.ConfigParser()
|
| 334 |
self.styles = {}
|
| 335 |
self.mtime = 0.0
|
| 336 |
return
|
| 337 |
self.cfg.read(self.path, encoding="utf-8")
|
| 338 |
self.styles = {}
|
| 339 |
for sec in self.cfg.sections():
|
| 340 |
-
d = {k: v for k, v in self.cfg.items(sec)}
|
| 341 |
-
#
|
| 342 |
-
|
| 343 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 344 |
d[key] = [s.strip() for s in d[key].split(",") if s.strip()]
|
| 345 |
self.styles[sec] = d
|
| 346 |
self.mtime = self.path.stat().st_mtime
|
|
@@ -356,48 +366,81 @@ class StylesConfig:
|
|
| 356 |
self.maybe_reload()
|
| 357 |
return list(self.styles.keys())
|
| 358 |
|
| 359 |
-
def
|
| 360 |
-
|
| 361 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 362 |
self.maybe_reload()
|
| 363 |
if style not in self.styles:
|
| 364 |
return ""
|
| 365 |
s = self.styles[style]
|
|
|
|
|
|
|
| 366 |
bpm_min = int(s.get("bpm_min", "100"))
|
| 367 |
bpm_max = int(s.get("bpm_max", "140"))
|
| 368 |
final_bpm = bpm if bpm != 120 else random.randint(bpm_min, bpm_max)
|
| 369 |
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
r = pick("rhythmic_steps", rhythmic_steps)
|
| 379 |
-
b = pick("bass_style", bass_style)
|
| 380 |
-
g = pick("guitar_style", guitar_style)
|
| 381 |
var_list = s.get("variations", [])
|
| 382 |
-
|
| 383 |
-
if var_list:
|
| 384 |
-
# Prefer different variations across chunks
|
| 385 |
if chunk_num == 1:
|
| 386 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 387 |
else:
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
bpm
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 399 |
)
|
| 400 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 401 |
|
| 402 |
STYLES = StylesConfig(PROMPTS_INI)
|
| 403 |
|
|
@@ -430,7 +473,7 @@ def load_model():
|
|
| 430 |
musicgen_model = load_model()
|
| 431 |
|
| 432 |
# ======================================================================================
|
| 433 |
-
# GENERATION (30s CHUNKS,
|
| 434 |
# ======================================================================================
|
| 435 |
|
| 436 |
def _export_torch_to_segment(audio_tensor: torch.Tensor, sample_rate: int, bit_depth_int: int) -> Optional[AudioSegment]:
|
|
@@ -542,8 +585,6 @@ def generate_music(
|
|
| 542 |
if not check_disk_space():
|
| 543 |
return None, "⚠️ Low disk space (<1GB).", vram_status_text
|
| 544 |
|
| 545 |
-
# Preset (optional)
|
| 546 |
-
# (kept simple; user can override via UI)
|
| 547 |
CHUNK_SEC = 30
|
| 548 |
total_duration = max(30, min(int(total_duration), 120))
|
| 549 |
num_chunks = math.ceil(total_duration / CHUNK_SEC)
|
|
@@ -777,12 +818,13 @@ def prompt(style: str, bpm: int = 120, chunk: int = 1,
|
|
| 777 |
return {"style": style, "prompt": txt}
|
| 778 |
|
| 779 |
# Back-compat endpoints declared in prompts.ini (e.g., /set_classical_star_wars_prompt)
|
| 780 |
-
|
|
|
|
| 781 |
api_name = cfg.get("api_name")
|
| 782 |
if api_name:
|
| 783 |
-
|
| 784 |
-
def make_route(sname):
|
| 785 |
-
@fastapp.get(
|
| 786 |
def _(bpm: int = 120, chunk: int = 1,
|
| 787 |
drum_beat: str = "none", synthesizer: str = "none", rhythmic_steps: str = "none",
|
| 788 |
bass_style: str = "none", guitar_style: str = "none"):
|
|
@@ -790,7 +832,7 @@ for sec, cfg in STYLES.styles.items():
|
|
| 790 |
if not txt:
|
| 791 |
raise HTTPException(status_code=404, detail="Style not found")
|
| 792 |
return {"style": sname, "prompt": txt}
|
| 793 |
-
make_route(sec)
|
| 794 |
|
| 795 |
@fastapp.get("/config")
|
| 796 |
def get_config():
|
|
@@ -859,7 +901,25 @@ logger.info(f"FastAPI server started on http://0.0.0.0:8555 [{RELEASE}]")
|
|
| 859 |
|
| 860 |
def read_css() -> str:
|
| 861 |
try:
|
| 862 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 863 |
except Exception as e:
|
| 864 |
logger.error(f"Failed to read CSS: {e}")
|
| 865 |
return ""
|
|
@@ -879,7 +939,7 @@ with gr.Blocks(css=read_css(), analytics_enabled=False, title=f"GhostAI Music Ge
|
|
| 879 |
<div class="ga-header" role="banner" aria-label="GhostAI Music Generator">
|
| 880 |
<div class="logo">👻</div>
|
| 881 |
<h1>GhostAI Music Generator</h1>
|
| 882 |
-
<p>Unified 30s chunking ·
|
| 883 |
</div>
|
| 884 |
""")
|
| 885 |
|
|
@@ -928,7 +988,7 @@ with gr.Blocks(css=read_css(), analytics_enabled=False, title=f"GhostAI Music Ge
|
|
| 928 |
("detroit_techno", "Detroit Techno 🎛️"),
|
| 929 |
("deep_house", "Deep House 🏠"),
|
| 930 |
("classical_star_wars", "Classical (Star Wars Suite) ✨"),
|
| 931 |
-
("foo_pad", "—")
|
| 932 |
])
|
| 933 |
|
| 934 |
# SETTINGS
|
|
|
|
| 2 |
#!/usr/bin/env python3
|
| 3 |
# -*- coding: utf-8 -*-
|
| 4 |
|
| 5 |
+
# GhostAI Music Generator — Release v1.3.1
|
| 6 |
# Gradio UI + FastAPI server, externalized styles (CSS), prompts (INI), and examples (MD).
|
| 7 |
# Saves MP3s to ./mp3, single rotating log (max 5MB) in ./logs, colorized console.
|
| 8 |
|
|
|
|
| 40 |
|
| 41 |
from colorama import init as colorama_init, Fore, Style
|
| 42 |
|
| 43 |
+
RELEASE = "v1.3.1"
|
| 44 |
|
| 45 |
# ======================================================================================
|
| 46 |
# PATCHES & RUNTIME
|
|
|
|
| 263 |
stereo = stereo * mask
|
| 264 |
left, right = stereo[:, 0], stereo[:, 1]
|
| 265 |
l_rms = np.sqrt(np.mean(left[left != 0] ** 2)) if np.any(left != 0) else 0
|
| 266 |
+
r_rms = np.sqrt(np.mean(np.mean(right[right != 0] ** 2))) if np.any(right != 0) else 0
|
| 267 |
if l_rms > 0 and r_rms > 0:
|
| 268 |
avg = (l_rms + r_rms) / 2
|
| 269 |
stereo[:, 0] *= (avg / l_rms)
|
|
|
|
| 316 |
return seg
|
| 317 |
|
| 318 |
# ======================================================================================
|
| 319 |
+
# PROMPTS (FROM INI) — SAFE FORMAT TO AVOID KeyError('mood') AND OTHER PLACEHOLDERS
|
| 320 |
# ======================================================================================
|
| 321 |
|
| 322 |
+
class SafeFormatDict(dict):
|
| 323 |
+
def __missing__(self, key):
|
| 324 |
+
# Gracefully handle missing placeholders in templates (e.g., {mood}, {genre})
|
| 325 |
+
return ""
|
| 326 |
+
|
| 327 |
class StylesConfig:
|
| 328 |
def __init__(self, path: Path):
|
| 329 |
self.path = path
|
| 330 |
+
self.cfg = configparser.ConfigParser(interpolation=None)
|
| 331 |
self.mtime = 0.0
|
| 332 |
self.styles: Dict[str, Dict[str, Any]] = {}
|
| 333 |
self._load()
|
|
|
|
| 335 |
def _load(self):
|
| 336 |
if not self.path.exists():
|
| 337 |
logger.error(f"prompts.ini not found: {self.path}")
|
| 338 |
+
self.cfg = configparser.ConfigParser(interpolation=None)
|
| 339 |
self.styles = {}
|
| 340 |
self.mtime = 0.0
|
| 341 |
return
|
| 342 |
self.cfg.read(self.path, encoding="utf-8")
|
| 343 |
self.styles = {}
|
| 344 |
for sec in self.cfg.sections():
|
| 345 |
+
d: Dict[str, Any] = {k: v for k, v in self.cfg.items(sec)}
|
| 346 |
+
# Split known list-like fields
|
| 347 |
+
listish = {
|
| 348 |
+
"drum_beat", "synthesizer", "rhythmic_steps", "bass_style", "guitar_style",
|
| 349 |
+
"variations", "mood", "genre", "key", "scale", "feel", "instrument",
|
| 350 |
+
"lead", "pad", "arp", "drums", "bass", "guitar", "strings", "brass", "woodwinds"
|
| 351 |
+
}
|
| 352 |
+
for key in listish:
|
| 353 |
+
if key in d and isinstance(d[key], str):
|
| 354 |
d[key] = [s.strip() for s in d[key].split(",") if s.strip()]
|
| 355 |
self.styles[sec] = d
|
| 356 |
self.mtime = self.path.stat().st_mtime
|
|
|
|
| 366 |
self.maybe_reload()
|
| 367 |
return list(self.styles.keys())
|
| 368 |
|
| 369 |
+
def _pick(self, s: Dict[str, Any], field: str, incoming: Optional[str]) -> str:
|
| 370 |
+
if incoming and incoming != "none":
|
| 371 |
+
return str(incoming)
|
| 372 |
+
val = s.get(field, [])
|
| 373 |
+
if isinstance(val, list):
|
| 374 |
+
return random.choice(val) if val else "none"
|
| 375 |
+
return str(val) if val else "none"
|
| 376 |
+
|
| 377 |
+
def build_prompt(
|
| 378 |
+
self,
|
| 379 |
+
style: str,
|
| 380 |
+
bpm: int,
|
| 381 |
+
chunk_num: int = 1,
|
| 382 |
+
drum_beat: str = "none",
|
| 383 |
+
synthesizer: str = "none",
|
| 384 |
+
rhythmic_steps: str = "none",
|
| 385 |
+
bass_style: str = "none",
|
| 386 |
+
guitar_style: str = "none"
|
| 387 |
+
) -> str:
|
| 388 |
self.maybe_reload()
|
| 389 |
if style not in self.styles:
|
| 390 |
return ""
|
| 391 |
s = self.styles[style]
|
| 392 |
+
|
| 393 |
+
# BPM handling
|
| 394 |
bpm_min = int(s.get("bpm_min", "100"))
|
| 395 |
bpm_max = int(s.get("bpm_max", "140"))
|
| 396 |
final_bpm = bpm if bpm != 120 else random.randint(bpm_min, bpm_max)
|
| 397 |
|
| 398 |
+
# Picks for band controls
|
| 399 |
+
d = self._pick(s, "drum_beat", drum_beat)
|
| 400 |
+
syn = self._pick(s, "synthesizer", synthesizer)
|
| 401 |
+
r = self._pick(s, "rhythmic_steps", rhythmic_steps)
|
| 402 |
+
b = self._pick(s, "bass_style", bass_style)
|
| 403 |
+
g = self._pick(s, "guitar_style", guitar_style)
|
| 404 |
+
|
| 405 |
+
# Variation logic per chunk
|
|
|
|
|
|
|
|
|
|
| 406 |
var_list = s.get("variations", [])
|
| 407 |
+
variation = ""
|
| 408 |
+
if isinstance(var_list, list) and var_list:
|
|
|
|
| 409 |
if chunk_num == 1:
|
| 410 |
+
variation = random.choice(var_list[: max(1, len(var_list)//2)])
|
| 411 |
+
else:
|
| 412 |
+
variation = random.choice(var_list)
|
| 413 |
+
|
| 414 |
+
# Start with all keys from the style; choose a random item for list-type values
|
| 415 |
+
fields: Dict[str, Any] = {}
|
| 416 |
+
for k, v in s.items():
|
| 417 |
+
if isinstance(v, list):
|
| 418 |
+
fields[k] = random.choice(v) if v else ""
|
| 419 |
else:
|
| 420 |
+
fields[k] = v
|
| 421 |
+
|
| 422 |
+
# Overlay computed/required fields
|
| 423 |
+
fields.update({
|
| 424 |
+
"bpm": final_bpm,
|
| 425 |
+
"chunk": chunk_num,
|
| 426 |
+
"drum": d if d != "none" else "",
|
| 427 |
+
"synth": syn if syn != "none" else "",
|
| 428 |
+
"rhythm": r if r != "none" else "",
|
| 429 |
+
"bass": b if b != "none" else "",
|
| 430 |
+
"guitar": g if g != "none" else "",
|
| 431 |
+
"variation": variation
|
| 432 |
+
})
|
| 433 |
+
|
| 434 |
+
# Default template if none in INI
|
| 435 |
+
tpl = s.get(
|
| 436 |
+
"prompt_template",
|
| 437 |
+
"Instrumental track at {bpm} BPM {variation}. {mood} {genre} {drum} {bass} {guitar} {synth} {rhythm}"
|
| 438 |
)
|
| 439 |
+
|
| 440 |
+
# Safe formatting (prevents KeyError for undefined placeholders like {mood})
|
| 441 |
+
prompt = tpl.format_map(SafeFormatDict(fields))
|
| 442 |
+
prompt = re.sub(r"\s{2,}", " ", prompt).strip()
|
| 443 |
+
return prompt
|
| 444 |
|
| 445 |
STYLES = StylesConfig(PROMPTS_INI)
|
| 446 |
|
|
|
|
| 473 |
musicgen_model = load_model()
|
| 474 |
|
| 475 |
# ======================================================================================
|
| 476 |
+
# GENERATION (30s CHUNKS, 60–120s READY)
|
| 477 |
# ======================================================================================
|
| 478 |
|
| 479 |
def _export_torch_to_segment(audio_tensor: torch.Tensor, sample_rate: int, bit_depth_int: int) -> Optional[AudioSegment]:
|
|
|
|
| 585 |
if not check_disk_space():
|
| 586 |
return None, "⚠️ Low disk space (<1GB).", vram_status_text
|
| 587 |
|
|
|
|
|
|
|
| 588 |
CHUNK_SEC = 30
|
| 589 |
total_duration = max(30, min(int(total_duration), 120))
|
| 590 |
num_chunks = math.ceil(total_duration / CHUNK_SEC)
|
|
|
|
| 818 |
return {"style": style, "prompt": txt}
|
| 819 |
|
| 820 |
# Back-compat endpoints declared in prompts.ini (e.g., /set_classical_star_wars_prompt)
|
| 821 |
+
# Fix closure capture by binding route path explicitly.
|
| 822 |
+
for sec, cfg in list(STYLES.styles.items()):
|
| 823 |
api_name = cfg.get("api_name")
|
| 824 |
if api_name:
|
| 825 |
+
route_path = api_name
|
| 826 |
+
def make_route(sname, route_path_):
|
| 827 |
+
@fastapp.get(route_path_)
|
| 828 |
def _(bpm: int = 120, chunk: int = 1,
|
| 829 |
drum_beat: str = "none", synthesizer: str = "none", rhythmic_steps: str = "none",
|
| 830 |
bass_style: str = "none", guitar_style: str = "none"):
|
|
|
|
| 832 |
if not txt:
|
| 833 |
raise HTTPException(status_code=404, detail="Style not found")
|
| 834 |
return {"style": sname, "prompt": txt}
|
| 835 |
+
make_route(sec, route_path)
|
| 836 |
|
| 837 |
@fastapp.get("/config")
|
| 838 |
def get_config():
|
|
|
|
| 901 |
|
| 902 |
def read_css() -> str:
|
| 903 |
try:
|
| 904 |
+
if CSS_FILE.exists():
|
| 905 |
+
return CSS_FILE.read_text(encoding="utf-8")
|
| 906 |
+
# High-contrast ADA-compliant fallback (white text, dark bg)
|
| 907 |
+
return """
|
| 908 |
+
:root { color-scheme: dark; }
|
| 909 |
+
body, .gradio-container {
|
| 910 |
+
background: #0E1014 !important;
|
| 911 |
+
color: #FFFFFF !important;
|
| 912 |
+
}
|
| 913 |
+
* { color: #FFFFFF !important; }
|
| 914 |
+
input, textarea, select {
|
| 915 |
+
background: #151922 !important;
|
| 916 |
+
color: #FFFFFF !important;
|
| 917 |
+
border: 1px solid #2A3142 !important;
|
| 918 |
+
border-radius: 10px !important;
|
| 919 |
+
}
|
| 920 |
+
.ga-header { display:flex; gap:12px; align-items:center; }
|
| 921 |
+
.ga-header .logo { font-size: 28px; }
|
| 922 |
+
"""
|
| 923 |
except Exception as e:
|
| 924 |
logger.error(f"Failed to read CSS: {e}")
|
| 925 |
return ""
|
|
|
|
| 939 |
<div class="ga-header" role="banner" aria-label="GhostAI Music Generator">
|
| 940 |
<div class="logo">👻</div>
|
| 941 |
<h1>GhostAI Music Generator</h1>
|
| 942 |
+
<p>Unified 30s chunking · 60–120s ready · API & status</p>
|
| 943 |
</div>
|
| 944 |
""")
|
| 945 |
|
|
|
|
| 988 |
("detroit_techno", "Detroit Techno 🎛️"),
|
| 989 |
("deep_house", "Deep House 🏠"),
|
| 990 |
("classical_star_wars", "Classical (Star Wars Suite) ✨"),
|
| 991 |
+
("foo_pad", "—")
|
| 992 |
])
|
| 993 |
|
| 994 |
# SETTINGS
|