koby commited on
Commit
bf455aa
ยท
1 Parent(s): 3959a20

add Community Songs tab: latest gens with tags + audio player, add tags to meta

Browse files
Files changed (1) hide show
  1. app.py +246 -39
app.py CHANGED
@@ -1,8 +1,11 @@
 
1
  import json
2
  import os
3
  import random
4
  import secrets
5
  import subprocess
 
 
6
  from datetime import datetime, timezone
7
  import spaces
8
  import gradio as gr
@@ -14,6 +17,11 @@ os.makedirs(OUT, exist_ok=True)
14
 
15
  REPO = "m-a-p/YuE2-3B"
16
  COMMUNITY_REPO = "directedbykobyperez/YuE2-community"
 
 
 
 
 
17
  pipe = None
18
 
19
 
@@ -72,7 +80,6 @@ def derive_title(style, title_input, song_id):
72
  return t[:120]
73
  s = (style or "").strip().replace("\n", " ")
74
  if s:
75
- # first ~8 words as title, title-cased
76
  words = s.split()[:8]
77
  title = " ".join(words)[:80]
78
  return title.title() if title else f"YuE2 Song {song_id[:6]}"
@@ -91,7 +98,6 @@ def upload_to_community(wav_path, meta_dict, song_id):
91
  wav_in_repo = f"songs/{song_id}/{song_id}.wav"
92
  meta_in_repo = f"songs/{song_id}/meta.json"
93
 
94
- # write meta to a temp file next to OUT
95
  meta_local = os.path.join(OUT, f"{song_id}_meta.json")
96
  with open(meta_local, "w", encoding="utf-8") as f:
97
  json.dump(meta_dict, f, ensure_ascii=False, indent=2)
@@ -111,6 +117,8 @@ def upload_to_community(wav_path, meta_dict, song_id):
111
  commit_message=f"add meta {song_id}",
112
  )
113
  url = f"https://huggingface.co/datasets/{COMMUNITY_REPO}/tree/main/songs/{song_id}"
 
 
114
  return True, f"uploaded to community โœ… {url}"
115
  except Exception as e:
116
  print(f"[community-upload] failed: {e}")
@@ -118,7 +126,7 @@ def upload_to_community(wav_path, meta_dict, song_id):
118
 
119
 
120
  @spaces.GPU(duration=120)
121
- def generate(style, lyrics, cot, seed, cfg_scale, title_input, bpm_input, lang_input):
122
  pipeline = get_pipeline()
123
 
124
  if seed is None or seed < 1:
@@ -153,11 +161,13 @@ def generate(style, lyrics, cot, seed, cfg_scale, title_input, bpm_input, lang_i
153
  language = (lang_input or "en").strip() or "en"
154
  title = derive_title(style, title_input, song_id)
155
  created_at = datetime.now(timezone.utc).isoformat()
 
156
 
157
  meta = {
158
  "id": song_id,
159
  "title": title,
160
  "description": (style or "").strip(),
 
161
  "lyrics": lyrics or "",
162
  "caption": (style or "").strip(),
163
  "bpm": bpm,
@@ -179,6 +189,149 @@ def generate(style, lyrics, cot, seed, cfg_scale, title_input, bpm_input, lang_i
179
  return flac_path, mp3_path, info
180
 
181
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
182
  # CSS ืœืขื™ืฆื•ื‘ ื™ื•ืงืจืชื™ ื‘ืกื’ื ื•ืŸ Dark Theme
183
  custom_css = """
184
  body {
@@ -215,6 +368,33 @@ body {
215
  transform: translateY(-2px);
216
  box-shadow: 0 6px 20px rgba(255, 75, 75, 0.5) !important;
217
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
218
  """
219
 
220
  with gr.Blocks(css=custom_css, title="YuE2-3B Music Generator") as demo:
@@ -228,44 +408,71 @@ with gr.Blocks(css=custom_css, title="YuE2-3B Music Generator") as demo:
228
  """
229
  )
230
 
231
- with gr.Row():
232
- with gr.Column(scale=5):
233
- title_input = gr.Textbox(
234
- label="๐Ÿท๏ธ Title (optional โ€” auto from style if empty)",
235
- value="",
236
- placeholder="e.g. Reggaeton Latin Spanish Old",
237
- lines=1,
238
- )
239
- style = gr.Textbox(
240
- label="๐ŸŽจ Style Prompt",
241
- value="Cyber metal, English, aggressive male vocals, distorted guitars, double-kick drums",
242
- lines=3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
243
  )
244
- lyrics = gr.Textbox(
245
- label="๐Ÿ“œ Lyrics ([verse] / [chorus] structure)",
246
- value="[verse]\nnothing here but static skies\nwe ride the current till it dies",
247
- lines=8
 
 
248
  )
249
  with gr.Row():
250
- cot = gr.Radio(["full", "melody", "off"], value="full", label="CoT Mode")
251
- seed = gr.Number(label="Seed (0 for Random)", value=0, precision=0)
252
- cfg = gr.Number(label="CFG Scale", value=1.0, precision=2)
253
- with gr.Row():
254
- bpm_input = gr.Number(label="BPM", value=120, precision=0)
255
- lang_input = gr.Textbox(label="Language", value="en", lines=1)
256
-
257
- btn = gr.Button("๐Ÿš€ Generate Song", elem_classes=["generate-btn"])
258
-
259
- with gr.Column(scale=5):
260
- audio = gr.Audio(label="๐ŸŽง Generated Song Player", type="filepath")
261
- mp3_file = gr.File(label="๐Ÿ“ฅ Download MP3 (320kbps)")
262
- info = gr.Textbox(label="โ„น๏ธ Generation Info", interactive=False)
263
-
264
- btn.click(
265
- fn=generate,
266
- inputs=[style, lyrics, cot, seed, cfg, title_input, bpm_input, lang_input],
267
- outputs=[audio, mp3_file, info],
268
- queue=True
269
- )
270
 
271
  demo.queue().launch()
 
1
+ import html
2
  import json
3
  import os
4
  import random
5
  import secrets
6
  import subprocess
7
+ import time
8
+ from concurrent.futures import ThreadPoolExecutor
9
  from datetime import datetime, timezone
10
  import spaces
11
  import gradio as gr
 
17
 
18
  REPO = "m-a-p/YuE2-3B"
19
  COMMUNITY_REPO = "directedbykobyperez/YuE2-community"
20
+
21
+ # Community feed cache (avoid hammering the Hub on every click)
22
+ _FEED_CACHE = {"ts": 0.0, "songs": []}
23
+ FEED_TTL = 30 # seconds
24
+
25
  pipe = None
26
 
27
 
 
80
  return t[:120]
81
  s = (style or "").strip().replace("\n", " ")
82
  if s:
 
83
  words = s.split()[:8]
84
  title = " ".join(words)[:80]
85
  return title.title() if title else f"YuE2 Song {song_id[:6]}"
 
98
  wav_in_repo = f"songs/{song_id}/{song_id}.wav"
99
  meta_in_repo = f"songs/{song_id}/meta.json"
100
 
 
101
  meta_local = os.path.join(OUT, f"{song_id}_meta.json")
102
  with open(meta_local, "w", encoding="utf-8") as f:
103
  json.dump(meta_dict, f, ensure_ascii=False, indent=2)
 
117
  commit_message=f"add meta {song_id}",
118
  )
119
  url = f"https://huggingface.co/datasets/{COMMUNITY_REPO}/tree/main/songs/{song_id}"
120
+ # invalidate feed cache so the new song appears fast
121
+ _FEED_CACHE["ts"] = 0.0
122
  return True, f"uploaded to community โœ… {url}"
123
  except Exception as e:
124
  print(f"[community-upload] failed: {e}")
 
126
 
127
 
128
  @spaces.GPU(duration=120)
129
+ def generate(style, lyrics, cot, seed, cfg_scale, title_input, bpm_input, lang_input, tags_input):
130
  pipeline = get_pipeline()
131
 
132
  if seed is None or seed < 1:
 
161
  language = (lang_input or "en").strip() or "en"
162
  title = derive_title(style, title_input, song_id)
163
  created_at = datetime.now(timezone.utc).isoformat()
164
+ tags = (tags_input or "").strip()
165
 
166
  meta = {
167
  "id": song_id,
168
  "title": title,
169
  "description": (style or "").strip(),
170
+ "tags": tags,
171
  "lyrics": lyrics or "",
172
  "caption": (style or "").strip(),
173
  "bpm": bpm,
 
189
  return flac_path, mp3_path, info
190
 
191
 
192
+ # ---------------- Community feed ----------------
193
+
194
+ def _fetch_one_meta(path, token):
195
+ """Download + parse one meta.json. Returns dict or None."""
196
+ try:
197
+ from huggingface_hub import hf_hub_download
198
+ local = hf_hub_download(
199
+ repo_id=COMMUNITY_REPO, repo_type="dataset",
200
+ filename=path, token=token,
201
+ )
202
+ with open(local, "r", encoding="utf-8") as f:
203
+ m = json.load(f)
204
+ if isinstance(m, dict) and m.get("id"):
205
+ return m
206
+ except Exception as e:
207
+ print(f"[feed] skip {path}: {e}")
208
+ return None
209
+
210
+
211
+ def _load_feed_songs():
212
+ """List all songs/*/meta.json, download, sort newest first. Cached."""
213
+ now = time.time()
214
+ if now - _FEED_CACHE["ts"] < FEED_TTL and _FEED_CACHE["songs"]:
215
+ return _FEED_CACHE["songs"], True
216
+
217
+ token = os.environ.get("HF_TOKEN") or os.environ.get("HUGGINGFACE_TOKEN")
218
+ try:
219
+ from huggingface_hub import HfApi
220
+ api = HfApi(token=token) if token else HfApi()
221
+ files = api.list_repo_files(repo_id=COMMUNITY_REPO, repo_type="dataset")
222
+ meta_paths = [f for f in files if f.startswith("songs/") and f.endswith("/meta.json")]
223
+ except Exception as e:
224
+ print(f"[feed] list failed: {e}")
225
+ # serve stale cache if we have it
226
+ if _FEED_CACHE["songs"]:
227
+ return _FEED_CACHE["songs"], True
228
+ return [], False
229
+
230
+ # cap to keep the tab fast (newest unknown until we read created_at,
231
+ # so take a slice; 300 metas โ‰ˆ a few MB max)
232
+ meta_paths = sorted(meta_paths)[-300:]
233
+
234
+ songs = []
235
+ with ThreadPoolExecutor(max_workers=8) as ex:
236
+ for m in ex.map(lambda p: _fetch_one_meta(p, token), meta_paths):
237
+ if m:
238
+ songs.append(m)
239
+
240
+ def _key(m):
241
+ try:
242
+ return m.get("created_at", "")
243
+ except Exception:
244
+ return ""
245
+ songs.sort(key=_key, reverse=True)
246
+
247
+ _FEED_CACHE["ts"] = time.time()
248
+ _FEED_CACHE["songs"] = songs
249
+ return songs, False
250
+
251
+
252
+ def _tags_html(tags_raw):
253
+ tags_raw = (tags_raw or "").strip()
254
+ if not tags_raw:
255
+ return '<span class="no-tags">no tags</span>'
256
+ parts = [t.strip() for t in tags_raw.replace(";", ",").split(",") if t.strip()][:10]
257
+ if not parts:
258
+ return '<span class="no-tags">no tags</span>'
259
+ return "".join(f'<span class="tag-pill">{html.escape(t)}</span>' for t in parts)
260
+
261
+
262
+ def render_feed_html(songs):
263
+ if not songs:
264
+ return """
265
+ <div class="empty-feed">
266
+ <p style="font-size:1.2rem;">๐ŸŽถ No songs yet โ€” be the first!</p>
267
+ <p style="color:#64748b;">Generate a song in the Generate tab and it will appear here automatically.</p>
268
+ </div>
269
+ """
270
+ cards = []
271
+ for m in songs:
272
+ sid = html.escape(str(m.get("id", "?")))
273
+ title = html.escape(str(m.get("title", sid)))
274
+ caption = html.escape(str(m.get("caption", m.get("description", ""))))
275
+ lyrics = str(m.get("lyrics", ""))
276
+ bpm = html.escape(str(m.get("bpm", "?")))
277
+ lang = html.escape(str(m.get("language", "?")))
278
+ seed = html.escape(str(m.get("seed", "?")))
279
+ created = html.escape(str(m.get("created_at", ""))[:19].replace("T", " "))
280
+ try:
281
+ dur = float(m.get("duration", 0) or 0)
282
+ dur_s = f"{dur:.1f}s"
283
+ except Exception:
284
+ dur_s = "?"
285
+ audio_url = f"https://huggingface.co/datasets/{COMMUNITY_REPO}/resolve/main/songs/{m.get('id')}/{m.get('id')}.wav"
286
+ audio_url_e = html.escape(audio_url, quote=True)
287
+ lyrics_e = html.escape(lyrics[:2000])
288
+ cards.append(f"""
289
+ <div class="song-card">
290
+ <div class="song-head">
291
+ <div>
292
+ <div class="song-title">๐ŸŽต {title}</div>
293
+ <div class="song-sub">ID <code>{sid}</code> ยท {created} UTC ยท BPM {bpm} ยท {lang} ยท โฑ {dur_s} ยท seed {seed}</div>
294
+ </div>
295
+ </div>
296
+ <audio controls preload="none" src="{audio_url_e}" style="width:100%;margin:8px 0;"></audio>
297
+ <div class="song-tags">{_tags_html(m.get("tags", ""))}</div>
298
+ <div class="song-prompt"><b>Prompt:</b> {caption or "โ€”"}</div>
299
+ <details class="song-lyrics"><summary>๐Ÿ“œ Lyrics</summary><pre>{lyrics_e or "โ€”"}</pre></details>
300
+ <div class="song-links"><a href="{audio_url_e}" target="_blank">โฌ‡ Download WAV</a> ยท <a href="https://huggingface.co/datasets/{COMMUNITY_REPO}/tree/main/songs/{sid}" target="_blank">View in dataset</a></div>
301
+ </div>
302
+ """)
303
+ return "\n".join(cards)
304
+
305
+
306
+ def refresh_feed(search, limit):
307
+ """Gradio handler: returns (html, status). Never raises."""
308
+ try:
309
+ limit = int(limit) if limit else 20
310
+ except Exception:
311
+ limit = 20
312
+ limit = max(1, min(int(limit), 60))
313
+ try:
314
+ songs, cached = _load_feed_songs()
315
+ total = len(songs)
316
+ q = (search or "").strip().lower()
317
+ if q:
318
+ def _match(m):
319
+ blob = " ".join([
320
+ str(m.get("title", "")), str(m.get("caption", "")),
321
+ str(m.get("description", "")), str(m.get("lyrics", "")),
322
+ str(m.get("tags", "")), str(m.get("id", "")),
323
+ ]).lower()
324
+ return q in blob
325
+ songs = [m for m in songs if _match(m)]
326
+ shown = songs[:limit]
327
+ html_out = render_feed_html(shown)
328
+ status = f"Showing {len(shown)}/{total} songs" + (" โ€ข cached" if cached else " โ€ข live") + (f" โ€ข filter: '{search}'" if q else "")
329
+ return html_out, status
330
+ except Exception as e:
331
+ print(f"[feed] refresh failed: {e}")
332
+ return render_feed_html([]), f"Failed to load feed: {e}"
333
+
334
+
335
  # CSS ืœืขื™ืฆื•ื‘ ื™ื•ืงืจืชื™ ื‘ืกื’ื ื•ืŸ Dark Theme
336
  custom_css = """
337
  body {
 
368
  transform: translateY(-2px);
369
  box-shadow: 0 6px 20px rgba(255, 75, 75, 0.5) !important;
370
  }
371
+ .song-card {
372
+ background: #111827;
373
+ border: 1px solid #1f2937;
374
+ border-radius: 14px;
375
+ padding: 16px;
376
+ margin-bottom: 14px;
377
+ }
378
+ .song-title { font-size: 1.15rem; font-weight: 700; color: #f1f5f9; }
379
+ .song-sub { font-size: 0.78rem; color: #64748b; margin-top: 2px; }
380
+ .song-sub code { color: #a855f7; }
381
+ .song-tags { margin: 6px 0; display: flex; flex-wrap: wrap; gap: 6px; }
382
+ .tag-pill {
383
+ background: #1e1b4b; color: #c4b5fd; border: 1px solid #4c1d95;
384
+ font-size: 0.75rem; padding: 2px 10px; border-radius: 999px;
385
+ }
386
+ .no-tags { color: #475569; font-size: 0.78rem; }
387
+ .song-prompt { font-size: 0.88rem; color: #cbd5e1; margin: 6px 0; }
388
+ .song-lyrics { margin-top: 6px; }
389
+ .song-lyrics summary { cursor: pointer; color: #94a3b8; font-size: 0.85rem; }
390
+ .song-lyrics pre {
391
+ white-space: pre-wrap; font-size: 0.82rem; color: #94a3b8;
392
+ background: #0b0f19; padding: 10px; border-radius: 8px; max-height: 220px; overflow: auto;
393
+ }
394
+ .song-links { margin-top: 8px; font-size: 0.82rem; }
395
+ .song-links a { color: #a855f7; text-decoration: none; margin-right: 8px; }
396
+ .empty-feed { text-align: center; padding: 40px 10px; color: #94a3b8; }
397
+ audio::-webkit-media-controls-panel { background: #1f2937; }
398
  """
399
 
400
  with gr.Blocks(css=custom_css, title="YuE2-3B Music Generator") as demo:
 
408
  """
409
  )
410
 
411
+ with gr.Tabs():
412
+ with gr.Tab("๐ŸŽต Generate"):
413
+ with gr.Row():
414
+ with gr.Column(scale=5):
415
+ title_input = gr.Textbox(
416
+ label="๐Ÿท๏ธ Title (optional โ€” auto from style if empty)",
417
+ value="",
418
+ placeholder="e.g. Reggaeton Latin Spanish Old",
419
+ lines=1,
420
+ )
421
+ style = gr.Textbox(
422
+ label="๐ŸŽจ Style Prompt",
423
+ value="Cyber metal, English, aggressive male vocals, distorted guitars, double-kick drums",
424
+ lines=3
425
+ )
426
+ tags_input = gr.Textbox(
427
+ label="๐Ÿท๏ธ Tags (comma separated, shown in community feed)",
428
+ value="",
429
+ placeholder="e.g. pop, melodic, cinematic",
430
+ lines=1,
431
+ )
432
+ lyrics = gr.Textbox(
433
+ label="๐Ÿ“œ Lyrics ([verse] / [chorus] structure)",
434
+ value="[verse]\nnothing here but static skies\nwe ride the current till it dies",
435
+ lines=8
436
+ )
437
+ with gr.Row():
438
+ cot = gr.Radio(["full", "melody", "off"], value="full", label="CoT Mode")
439
+ seed = gr.Number(label="Seed (0 for Random)", value=0, precision=0)
440
+ cfg = gr.Number(label="CFG Scale", value=1.0, precision=2)
441
+ with gr.Row():
442
+ bpm_input = gr.Number(label="BPM", value=120, precision=0)
443
+ lang_input = gr.Textbox(label="Language", value="en", lines=1)
444
+
445
+ btn = gr.Button("๐Ÿš€ Generate Song", elem_classes=["generate-btn"])
446
+
447
+ with gr.Column(scale=5):
448
+ audio = gr.Audio(label="๐ŸŽง Generated Song Player", type="filepath")
449
+ mp3_file = gr.File(label="๐Ÿ“ฅ Download MP3 (320kbps)")
450
+ info = gr.Textbox(label="โ„น๏ธ Generation Info", interactive=False)
451
+
452
+ btn.click(
453
+ fn=generate,
454
+ inputs=[style, lyrics, cot, seed, cfg, title_input, bpm_input, lang_input, tags_input],
455
+ outputs=[audio, mp3_file, info],
456
+ queue=True
457
  )
458
+
459
+ with gr.Tab("๐ŸŒ Community Songs"):
460
+ gr.Markdown(
461
+ "Latest songs generated by users โ€” auto-shared to the "
462
+ "[YuE2-community dataset](https://huggingface.co/datasets/directedbykobyperez/YuE2-community). "
463
+ "Press **๐Ÿ”„ Refresh** to load the newest."
464
  )
465
  with gr.Row():
466
+ search_box = gr.Textbox(label="๐Ÿ” Search (title / prompt / lyrics / tags / id)", value="", lines=1, scale=4)
467
+ limit_slider = gr.Slider(5, 60, value=20, step=5, label="How many")
468
+ refresh_btn = gr.Button("๐Ÿ”„ Refresh", scale=1)
469
+ feed_status = gr.Textbox(label="Status", interactive=False, value="Press Refresh to load songs.")
470
+ feed_html = gr.HTML("<div class='empty-feed'>Press ๐Ÿ”„ Refresh to load the latest community songs.</div>")
471
+
472
+ refresh_btn.click(
473
+ fn=refresh_feed,
474
+ inputs=[search_box, limit_slider],
475
+ outputs=[feed_html, feed_status],
476
+ )
 
 
 
 
 
 
 
 
 
477
 
478
  demo.queue().launch()