Nekochu commited on
Commit
72e4b69
·
1 Parent(s): 2dc2899

full app: CLI + Gradio + training placeholder + MCP

Browse files
Files changed (1) hide show
  1. app.py +387 -84
app.py CHANGED
@@ -1,27 +1,49 @@
1
- """ACE-Step 1.5 XL (CPU) - Gradio frontend for ace-server GGUF inference"""
2
 
3
  import os
 
4
  import time
 
 
5
  import tempfile
6
  import requests
7
- import gradio as gr
8
 
9
- ACE_SERVER = "http://127.0.0.1:8085"
10
- OUTPUT_DIR = "/app/outputs"
11
  os.makedirs(OUTPUT_DIR, exist_ok=True)
12
 
 
 
 
 
13
  def _server_ok():
14
  try:
15
  return requests.get(f"{ACE_SERVER}/health", timeout=5).status_code == 200
16
  except Exception:
17
  return False
18
 
19
- def _poll_job(job_id, timeout=600):
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  t0 = time.time()
21
  while time.time() - t0 < timeout:
22
  try:
23
  r = requests.get(f"{ACE_SERVER}/job", params={"id": job_id}, timeout=10)
24
- status = r.json().get("status", "unknown")
 
 
 
25
  if status in ("done", "error"):
26
  return status, time.time() - t0
27
  except Exception:
@@ -29,94 +51,375 @@ def _poll_job(job_id, timeout=600):
29
  time.sleep(2)
30
  return "timeout", time.time() - t0
31
 
32
- def generate_music(caption, lyrics, instrumental, bpm, duration, seed, steps, progress=gr.Progress(track_tqdm=True)):
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  t0 = time.time()
34
- if not _server_ok():
35
- return None, "ace-server not running"
36
 
 
37
  req = {"caption": caption or "upbeat electronic dance music"}
 
38
 
39
- if instrumental or not lyrics or lyrics.strip() == "":
40
- req["lyrics"] = "[Instrumental]"
41
- else:
42
- req["lyrics"] = lyrics
 
 
 
 
 
 
43
 
44
- try:
45
- if bpm and int(bpm) > 0: req["bpm"] = int(bpm)
46
- if duration and float(duration) > 0: req["duration"] = min(float(duration), 300)
47
- if seed is not None and int(seed) >= 0: req["seed"] = int(seed)
48
- if steps and int(steps) > 0: req["inference_steps"] = int(steps)
49
- except (ValueError, TypeError) as e:
50
- return None, f"Bad param: {e}"
51
-
52
- progress(0.05, desc="Submitting LM job...")
53
- try:
54
- r = requests.post(f"{ACE_SERVER}/lm", json=req, timeout=30)
55
- if r.status_code != 200:
56
- return None, f"LM failed: {r.status_code} {r.text}"
57
- lm_job_id = r.json().get("id")
58
- except Exception as e:
59
- return None, f"LM error: {e}"
60
-
61
- progress(0.1, desc=f"LM generating (job {lm_job_id})...")
62
  lm_status, lm_elapsed = _poll_job(lm_job_id, timeout=300)
63
  if lm_status != "done":
64
- return None, f"LM {lm_status} after {lm_elapsed:.0f}s"
65
 
66
- try:
67
- r = requests.get(f"{ACE_SERVER}/job", params={"id": lm_job_id, "result": 1}, timeout=30)
68
- lm_results = r.json()
69
- if not isinstance(lm_results, list) or len(lm_results) == 0:
70
- return None, f"LM no results: {lm_results}"
71
- synth_request = lm_results[0]
72
- except Exception as e:
73
- return None, f"LM result error: {e}"
74
-
75
- progress(0.4, desc="Submitting synth job...")
76
- synth_request["output_format"] = "wav16"
77
- try:
78
- r = requests.post(f"{ACE_SERVER}/synth", json=synth_request, timeout=30)
79
- if r.status_code != 200:
80
- return None, f"Synth failed: {r.status_code} {r.text}"
81
- synth_job_id = r.json().get("id")
82
- except Exception as e:
83
- return None, f"Synth error: {e}"
84
-
85
- progress(0.5, desc=f"Synthesizing (job {synth_job_id})...")
86
  synth_status, synth_elapsed = _poll_job(synth_job_id, timeout=600)
87
  if synth_status != "done":
88
- return None, f"Synth {synth_status} after {synth_elapsed:.0f}s"
89
 
90
- progress(0.9, desc="Fetching audio...")
91
- try:
92
- r = requests.get(f"{ACE_SERVER}/job", params={"id": synth_job_id, "result": 1}, timeout=60)
93
- if r.status_code != 200:
94
- return None, f"Audio fetch failed: {r.status_code}"
95
- tmp = tempfile.NamedTemporaryFile(suffix=".wav", dir=OUTPUT_DIR, delete=False)
96
- tmp.write(r.content)
97
- tmp.close()
98
- except Exception as e:
99
- return None, f"Audio error: {e}"
100
 
101
  elapsed = time.time() - t0
102
- return tmp.name, f"Done in {elapsed:.0f}s | {duration}s audio, {steps} steps"
103
-
104
- with gr.Blocks(title="ACE-Step 1.5 XL (CPU)") as demo:
105
- gr.Markdown("**[ACE-Step 1.5 XL (CPU)](https://github.com/ace-step/ACE-Step-1.5)** GGUF Q4_K_M via [acestep.cpp](https://github.com/ServeurpersoCom/acestep.cpp)")
106
- with gr.Row():
107
- with gr.Column(scale=2):
108
- caption = gr.Textbox(label="Music Description", lines=2, value="upbeat electronic dance music, energetic synth leads")
109
- lyrics = gr.Textbox(label="Lyrics ([Instrumental] for no vocals)", lines=2, value="[Instrumental]")
110
- with gr.Column(scale=1):
111
- audio_out = gr.Audio(label="Output", type="filepath")
112
- status = gr.Textbox(label="Status", interactive=False, lines=1)
113
- with gr.Row():
114
- instrumental = gr.Checkbox(label="Instrumental", value=True, scale=1)
115
- bpm = gr.Number(label="BPM", value=120, minimum=0, maximum=300, scale=1)
116
- duration = gr.Slider(label="Duration (s)", minimum=10, maximum=120, value=10, step=5, scale=1)
117
- steps = gr.Slider(label="Steps", minimum=1, maximum=32, value=8, step=1, scale=1)
118
- seed = gr.Number(label="Seed", value=-1, scale=1)
119
- gen_btn = gr.Button("Generate Music", variant="primary")
120
- gen_btn.click(fn=generate_music, inputs=[caption, lyrics, instrumental, bpm, duration, seed, steps], outputs=[audio_out, status])
121
-
122
- demo.launch(server_name="0.0.0.0", server_port=7860)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """ACE-Step 1.5 XL (CPU) - Gradio frontend + CLI for ace-server GGUF inference"""
2
 
3
  import os
4
+ import sys
5
  import time
6
+ import json
7
+ import argparse
8
  import tempfile
9
  import requests
 
10
 
11
+ ACE_SERVER = os.environ.get("ACE_SERVER", "http://127.0.0.1:8085")
12
+ OUTPUT_DIR = os.environ.get("ACE_OUTPUT_DIR", "/app/outputs")
13
  os.makedirs(OUTPUT_DIR, exist_ok=True)
14
 
15
+ # ---------------------------------------------------------------------------
16
+ # ace-server helpers
17
+ # ---------------------------------------------------------------------------
18
+
19
  def _server_ok():
20
  try:
21
  return requests.get(f"{ACE_SERVER}/health", timeout=5).status_code == 200
22
  except Exception:
23
  return False
24
 
25
+
26
+ def _get_props():
27
+ """Fetch server properties (models, adapters)."""
28
+ try:
29
+ r = requests.get(f"{ACE_SERVER}/props", timeout=10)
30
+ if r.status_code == 200:
31
+ return r.json()
32
+ except Exception:
33
+ pass
34
+ return {}
35
+
36
+
37
+ def _poll_job(job_id, timeout=600, progress_cb=None):
38
+ """Poll a job until done/error/timeout. Returns (status, elapsed)."""
39
  t0 = time.time()
40
  while time.time() - t0 < timeout:
41
  try:
42
  r = requests.get(f"{ACE_SERVER}/job", params={"id": job_id}, timeout=10)
43
+ data = r.json()
44
+ status = data.get("status", "unknown")
45
+ if progress_cb:
46
+ progress_cb(status, data)
47
  if status in ("done", "error"):
48
  return status, time.time() - t0
49
  except Exception:
 
51
  time.sleep(2)
52
  return "timeout", time.time() - t0
53
 
54
+
55
+ def _fetch_result(job_id, timeout=60):
56
+ """Fetch result bytes/json for a completed job."""
57
+ r = requests.get(
58
+ f"{ACE_SERVER}/job",
59
+ params={"id": job_id, "result": 1},
60
+ timeout=timeout,
61
+ )
62
+ return r
63
+
64
+
65
+ def _run_pipeline(caption, lyrics, bpm, duration, seed, steps, output_format,
66
+ adapter=None, progress_cb=None):
67
+ """Run full LM -> synth pipeline. Returns (audio_path, status_msg) or raises."""
68
  t0 = time.time()
 
 
69
 
70
+ # -- Build LM request --
71
  req = {"caption": caption or "upbeat electronic dance music"}
72
+ req["lyrics"] = lyrics if lyrics and lyrics.strip() else "[Instrumental]"
73
 
74
+ if bpm and int(bpm) > 0:
75
+ req["bpm"] = int(bpm)
76
+ if duration and float(duration) > 0:
77
+ req["duration"] = min(float(duration), 300)
78
+ if seed is not None and int(seed) >= 0:
79
+ req["seed"] = int(seed)
80
+ if steps and int(steps) > 0:
81
+ req["inference_steps"] = int(steps)
82
+ if adapter:
83
+ req["adapter"] = adapter
84
 
85
+ fmt = output_format if output_format in ("wav", "mp3") else "wav"
86
+ synth_fmt = "wav16" if fmt == "wav" else "mp3"
87
+ suffix = f".{fmt}"
88
+
89
+ # -- LM phase --
90
+ if progress_cb:
91
+ progress_cb("lm_submit", None)
92
+ r = requests.post(f"{ACE_SERVER}/lm", json=req, timeout=30)
93
+ if r.status_code != 200:
94
+ raise RuntimeError(f"LM submit failed: {r.status_code} {r.text}")
95
+ lm_job_id = r.json().get("id")
96
+
97
+ if progress_cb:
98
+ progress_cb("lm_poll", {"job_id": lm_job_id})
 
 
 
 
99
  lm_status, lm_elapsed = _poll_job(lm_job_id, timeout=300)
100
  if lm_status != "done":
101
+ raise RuntimeError(f"LM {lm_status} after {lm_elapsed:.0f}s")
102
 
103
+ # Fetch LM result
104
+ r = _fetch_result(lm_job_id)
105
+ lm_results = r.json()
106
+ if not isinstance(lm_results, list) or len(lm_results) == 0:
107
+ raise RuntimeError(f"LM returned no results: {lm_results}")
108
+ synth_request = lm_results[0]
109
+
110
+ # -- Synth phase --
111
+ synth_request["output_format"] = synth_fmt
112
+ if progress_cb:
113
+ progress_cb("synth_submit", None)
114
+ r = requests.post(f"{ACE_SERVER}/synth", json=synth_request, timeout=30)
115
+ if r.status_code != 200:
116
+ raise RuntimeError(f"Synth submit failed: {r.status_code} {r.text}")
117
+ synth_job_id = r.json().get("id")
118
+
119
+ if progress_cb:
120
+ progress_cb("synth_poll", {"job_id": synth_job_id})
 
 
121
  synth_status, synth_elapsed = _poll_job(synth_job_id, timeout=600)
122
  if synth_status != "done":
123
+ raise RuntimeError(f"Synth {synth_status} after {synth_elapsed:.0f}s")
124
 
125
+ # Fetch audio
126
+ if progress_cb:
127
+ progress_cb("fetch", None)
128
+ r = _fetch_result(synth_job_id, timeout=60)
129
+ if r.status_code != 200:
130
+ raise RuntimeError(f"Audio fetch failed: {r.status_code}")
131
+
132
+ tmp = tempfile.NamedTemporaryFile(suffix=suffix, dir=OUTPUT_DIR, delete=False)
133
+ tmp.write(r.content)
134
+ tmp.close()
135
 
136
  elapsed = time.time() - t0
137
+ msg = f"Done in {elapsed:.0f}s | {duration}s audio, {steps} steps, {fmt}"
138
+ return tmp.name, msg
139
+
140
+
141
+ # ---------------------------------------------------------------------------
142
+ # CLI mode
143
+ # ---------------------------------------------------------------------------
144
+
145
+ def cli_main():
146
+ parser = argparse.ArgumentParser(
147
+ description="ACE-Step 1.5 XL (CPU) - CLI inference via ace-server",
148
+ )
149
+ parser.add_argument("caption", nargs="?", default="upbeat electronic dance music",
150
+ help="Music description / caption")
151
+ parser.add_argument("--lyrics", "-l", default="[Instrumental]",
152
+ help="Lyrics text (use '[Instrumental]' for no vocals)")
153
+ parser.add_argument("--bpm", type=int, default=120, help="Beats per minute")
154
+ parser.add_argument("--duration", "-d", type=float, default=10,
155
+ help="Duration in seconds (max 300)")
156
+ parser.add_argument("--steps", "-s", type=int, default=8,
157
+ help="Inference steps (1-32)")
158
+ parser.add_argument("--seed", type=int, default=-1,
159
+ help="Random seed (-1 for random)")
160
+ parser.add_argument("--format", "-f", choices=["wav", "mp3"], default="wav",
161
+ help="Output audio format")
162
+ parser.add_argument("--adapter", "-a", default=None,
163
+ help="LoRA adapter name")
164
+ parser.add_argument("-o", "--output", default=None,
165
+ help="Output file path (default: auto in outputs dir)")
166
+ parser.add_argument("--server", default=None,
167
+ help="ace-server URL (default: http://127.0.0.1:8085)")
168
+
169
+ args = parser.parse_args()
170
+
171
+ if args.server:
172
+ global ACE_SERVER
173
+ ACE_SERVER = args.server
174
+
175
+ if not _server_ok():
176
+ print(f"ERROR: ace-server not reachable at {ACE_SERVER}", file=sys.stderr)
177
+ sys.exit(1)
178
+
179
+ seed = args.seed if args.seed >= 0 else None
180
+
181
+ def cli_progress(phase, data):
182
+ phases = {
183
+ "lm_submit": "Submitting LM job...",
184
+ "lm_poll": f"LM generating (job {data['job_id']})..." if data else "LM generating...",
185
+ "synth_submit": "Submitting synth job...",
186
+ "synth_poll": f"Synthesizing (job {data['job_id']})..." if data else "Synthesizing...",
187
+ "fetch": "Fetching audio...",
188
+ }
189
+ msg = phases.get(phase, phase)
190
+ print(f" [{phase}] {msg}")
191
+
192
+ print(f"ACE-Step CLI | caption: {args.caption}")
193
+ print(f" lyrics: {args.lyrics} | bpm: {args.bpm} | duration: {args.duration}s "
194
+ f"| steps: {args.steps} | seed: {args.seed} | format: {args.format}")
195
+
196
+ try:
197
+ audio_path, status = _run_pipeline(
198
+ caption=args.caption,
199
+ lyrics=args.lyrics,
200
+ bpm=args.bpm,
201
+ duration=args.duration,
202
+ seed=seed,
203
+ steps=args.steps,
204
+ output_format=args.format,
205
+ adapter=args.adapter,
206
+ progress_cb=cli_progress,
207
+ )
208
+ except RuntimeError as e:
209
+ print(f"ERROR: {e}", file=sys.stderr)
210
+ sys.exit(1)
211
+
212
+ # Move to requested output path if specified
213
+ if args.output:
214
+ import shutil
215
+ out_dir = os.path.dirname(os.path.abspath(args.output))
216
+ os.makedirs(out_dir, exist_ok=True)
217
+ shutil.move(audio_path, args.output)
218
+ audio_path = args.output
219
+
220
+ print(f" {status}")
221
+ print(f" Output: {audio_path}")
222
+
223
+
224
+ # ---------------------------------------------------------------------------
225
+ # Gradio UI mode
226
+ # ---------------------------------------------------------------------------
227
+
228
+ def gradio_main():
229
+ import gradio as gr
230
+
231
+ # -- Generate tab handler --
232
+ def generate_music(caption, lyrics, instrumental, bpm, duration, seed,
233
+ steps, output_format, progress=gr.Progress(track_tqdm=True)):
234
+ if not _server_ok():
235
+ return None, "ace-server not running. Check logs."
236
+
237
+ if instrumental or not lyrics or lyrics.strip() == "":
238
+ lyrics = "[Instrumental]"
239
+
240
+ actual_seed = None if seed is None or int(seed) < 0 else int(seed)
241
+
242
+ progress_map = {
243
+ "lm_submit": (0.05, "Submitting LM job..."),
244
+ "lm_poll": (0.10, "LM generating..."),
245
+ "synth_submit": (0.40, "Submitting synth job..."),
246
+ "synth_poll": (0.50, "Synthesizing audio..."),
247
+ "fetch": (0.90, "Fetching audio..."),
248
+ }
249
+
250
+ def gr_progress(phase, data):
251
+ pct, desc = progress_map.get(phase, (0.5, phase))
252
+ if data and "job_id" in data:
253
+ desc += f" (job {data['job_id']})"
254
+ progress(pct, desc=desc)
255
+
256
+ try:
257
+ audio_path, status = _run_pipeline(
258
+ caption=caption,
259
+ lyrics=lyrics,
260
+ bpm=bpm,
261
+ duration=duration,
262
+ seed=actual_seed,
263
+ steps=steps,
264
+ output_format=output_format,
265
+ progress_cb=gr_progress,
266
+ )
267
+ return audio_path, status
268
+ except RuntimeError as e:
269
+ return None, str(e)
270
+ except Exception as e:
271
+ return None, f"Unexpected error: {e}"
272
+
273
+ # -- Server info helper --
274
+ def get_server_status():
275
+ if not _server_ok():
276
+ return "ace-server: OFFLINE"
277
+ props = _get_props()
278
+ lines = ["ace-server: ONLINE"]
279
+ if props:
280
+ lines.append(json.dumps(props, indent=2))
281
+ return "\n".join(lines)
282
+
283
+ # -- Training placeholder --
284
+ def train_lora_placeholder(*args):
285
+ return ("Training requires PyTorch and the ACE-Step Python package.\n\n"
286
+ "To enable training, install dependencies:\n"
287
+ " pip install torch torchaudio safetensors transformers "
288
+ "diffusers peft accelerate einops\n\n"
289
+ "Then restart the app. Training is not available on the "
290
+ "CPU-only HF Space — use a local GPU machine or a GPU Space.")
291
+
292
+ # -- Build UI --
293
+ CSS = """
294
+ .compact-row { gap: 8px !important; }
295
+ .status-box textarea { font-family: monospace; font-size: 13px; }
296
+ """
297
+
298
+ with gr.Blocks(title="ACE-Step 1.5 XL (CPU)") as demo:
299
+
300
+ with gr.Tabs():
301
+ # ============================================================
302
+ # Tab 1: Generate Music
303
+ # ============================================================
304
+ with gr.Tab("Generate Music"):
305
+ gr.Markdown(
306
+ "**[ACE-Step 1.5 XL (CPU)](https://github.com/ace-step/ACE-Step-1.5)** "
307
+ "GGUF Q4_K_M via "
308
+ "[acestep.cpp](https://github.com/ServeurpersoCom/acestep.cpp)"
309
+ )
310
+
311
+ with gr.Row(elem_classes="compact-row"):
312
+ with gr.Column(scale=2):
313
+ caption = gr.Textbox(
314
+ label="Music Description",
315
+ lines=2,
316
+ value="upbeat electronic dance music, energetic synth leads",
317
+ )
318
+ lyrics = gr.Textbox(
319
+ label="Lyrics",
320
+ lines=3,
321
+ value="[Instrumental]",
322
+ placeholder="Enter lyrics or [Instrumental] for no vocals",
323
+ )
324
+ with gr.Column(scale=1):
325
+ audio_out = gr.Audio(label="Output", type="filepath")
326
+ status = gr.Textbox(
327
+ label="Status",
328
+ interactive=False,
329
+ lines=2,
330
+ elem_classes="status-box",
331
+ )
332
+
333
+ with gr.Row(elem_classes="compact-row"):
334
+ instrumental = gr.Checkbox(label="Instrumental", value=True, scale=1)
335
+ bpm = gr.Number(label="BPM", value=120, minimum=0, maximum=300, scale=1)
336
+ duration = gr.Slider(
337
+ label="Duration (s)", minimum=10, maximum=120,
338
+ value=10, step=5, scale=1,
339
+ )
340
+ steps = gr.Slider(
341
+ label="Steps", minimum=1, maximum=32,
342
+ value=8, step=1, scale=1,
343
+ )
344
+ seed = gr.Number(label="Seed (-1=random)", value=-1, scale=1)
345
+ output_format = gr.Radio(
346
+ label="Format", choices=["wav", "mp3"],
347
+ value="wav", scale=1,
348
+ )
349
+
350
+ with gr.Row(elem_classes="compact-row"):
351
+ gen_btn = gr.Button("Generate Music", variant="primary", scale=2)
352
+ status_btn = gr.Button("Server Status", scale=1)
353
+
354
+ gen_btn.click(
355
+ fn=generate_music,
356
+ inputs=[caption, lyrics, instrumental, bpm, duration,
357
+ seed, steps, output_format],
358
+ outputs=[audio_out, status],
359
+ api_name="generate",
360
+ )
361
+
362
+ status_btn.click(
363
+ fn=get_server_status,
364
+ inputs=[],
365
+ outputs=[status],
366
+ api_name="server_status",
367
+ )
368
+
369
+ # ============================================================
370
+ # Tab 2: Train LoRA
371
+ # ============================================================
372
+ with gr.Tab("Train LoRA"):
373
+ gr.Markdown(
374
+ "### LoRA Training\n"
375
+ "Fine-tune ACE-Step on your own audio data. "
376
+ "Requires PyTorch + GPU (not available on CPU Spaces)."
377
+ )
378
+
379
+ with gr.Row(elem_classes="compact-row"):
380
+ with gr.Column(scale=2):
381
+ train_audio = gr.File(
382
+ label="Training Audio Files",
383
+ file_count="multiple",
384
+ file_types=["audio"],
385
+ )
386
+ with gr.Column(scale=1):
387
+ lora_name = gr.Textbox(label="LoRA Name", value="my-lora")
388
+ epochs = gr.Number(label="Epochs", value=100, minimum=1, maximum=10000)
389
+ lr = gr.Number(label="Learning Rate", value=1e-4)
390
+ rank = gr.Number(label="Rank (r)", value=16, minimum=1, maximum=256)
391
+
392
+ train_btn = gr.Button("Train", variant="primary")
393
+ train_log = gr.Textbox(
394
+ label="Training Log",
395
+ interactive=False,
396
+ lines=10,
397
+ elem_classes="status-box",
398
+ )
399
+
400
+ train_btn.click(
401
+ fn=train_lora_placeholder,
402
+ inputs=[train_audio, lora_name, epochs, lr, rank],
403
+ outputs=[train_log],
404
+ api_name="train_lora",
405
+ )
406
+
407
+ demo.launch(
408
+ server_name="0.0.0.0",
409
+ server_port=7860,
410
+ mcp_server=True,
411
+ css=CSS,
412
+ )
413
+
414
+
415
+ # ---------------------------------------------------------------------------
416
+ # Entry point
417
+ # ---------------------------------------------------------------------------
418
+
419
+ if __name__ == "__main__":
420
+ # If any CLI arguments besides the script name, run CLI mode
421
+ # (Gradio sets no extra args; start.sh calls `python3 /app/app.py`)
422
+ if len(sys.argv) > 1:
423
+ cli_main()
424
+ else:
425
+ gradio_main()