ghostai1 commited on
Commit
faf0c20
·
verified ·
1 Parent(s): 68cac4e

Upload publicapi.py

Browse files
Files changed (1) hide show
  1. 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.0
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.0"
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
- # split csv fields
342
- for key in ["drum_beat", "synthesizer", "rhythmic_steps", "bass_style", "guitar_style", "variations"]:
343
- if key in d:
 
 
 
 
 
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 build_prompt(self, style: str, bpm: int, chunk_num: int = 1,
360
- drum_beat="none", synthesizer="none", rhythmic_steps="none",
361
- bass_style="none", guitar_style="none") -> str:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- def pick(field_name: str, incoming: str) -> str:
371
- if incoming and incoming != "none":
372
- return incoming
373
- vals = s.get(field_name, [])
374
- return random.choice(vals) if vals else "none"
375
-
376
- d = pick("drum_beat", drum_beat)
377
- syn = pick("synthesizer", synthesizer)
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
- var = ""
383
- if var_list:
384
- # Prefer different variations across chunks
385
  if chunk_num == 1:
386
- var = random.choice(var_list[: max(1, len(var_list)//2)])
 
 
 
 
 
 
 
 
387
  else:
388
- var = random.choice(var_list)
389
- tpl = s.get("prompt_template",
390
- "Instrumental track at {bpm} BPM {variation}.")
391
- prompt = tpl.format(
392
- bpm=final_bpm,
393
- drum=d,
394
- synth=syn if syn != "none" else "",
395
- rhythm=r if r != "none" else "",
396
- bass=b if b != "none" else "",
397
- guitar=g if g != "none" else "",
398
- variation=var
 
 
 
 
 
 
 
399
  )
400
- return re.sub(r"\s{2,}", " ", prompt).strip()
 
 
 
 
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, 60s READY)
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
- for sec, cfg in STYLES.styles.items():
 
781
  api_name = cfg.get("api_name")
782
  if api_name:
783
- route = api_name
784
- def make_route(sname):
785
- @fastapp.get(route)
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
- return CSS_FILE.read_text(encoding="utf-8")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 · 60s ready · API & status</p>
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", "—") # spacer to keep 4 columns
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