Spaces:
Running
Running
Add generated score SVG fallback
Browse files- apps/theme_lab/app.py +95 -2
apps/theme_lab/app.py
CHANGED
|
@@ -3,6 +3,7 @@
|
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
import argparse
|
|
|
|
| 6 |
import os
|
| 7 |
import pickle
|
| 8 |
import random
|
|
@@ -46,7 +47,7 @@ MARKOV_CACHE_DIR = ROOT / "models" / "theme_lab_markov_cache"
|
|
| 46 |
DEFAULT_MARKOV_CACHE = MARKOV_CACHE_DIR / "default.pkl"
|
| 47 |
MARKOV_CACHE_FORMAT_VERSION = 1
|
| 48 |
MARKOV_PRECOMPUTED_WARM_SAMPLES = 4
|
| 49 |
-
SCORE_PREVIEW_RENDER_VERSION = "strip-ties-beams-
|
| 50 |
GENERATED_ROOT.mkdir(parents=True, exist_ok=True)
|
| 51 |
CATALOG_SCORE_ROOT.mkdir(parents=True, exist_ok=True)
|
| 52 |
|
|
@@ -371,6 +372,95 @@ def sequence_note_events(sequence: tuple[Symbol, ...], key_name: str) -> list[di
|
|
| 371 |
return events
|
| 372 |
|
| 373 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 374 |
def has_verovio_renderer() -> bool:
|
| 375 |
if shutil.which("verovio") is not None:
|
| 376 |
return True
|
|
@@ -721,14 +811,17 @@ def generate(request: GenerateRequest) -> dict[str, object]:
|
|
| 721 |
for index, sequence in enumerate(generated, start=1):
|
| 722 |
stem = f"generated_{index:02d}"
|
| 723 |
musicxml_path = output_dir / f"{stem}.musicxml"
|
|
|
|
| 724 |
svg_path = render_musicxml_to_svg(musicxml_path)
|
|
|
|
|
|
|
| 725 |
samples.append(
|
| 726 |
{
|
| 727 |
"index": index,
|
| 728 |
"title": f"{request.engine} sample {index:02d}",
|
| 729 |
"relative_pcs": [symbol.rpc for symbol in sequence],
|
| 730 |
"durations": [symbol.duration for symbol in sequence],
|
| 731 |
-
"notes":
|
| 732 |
"midi_url": f"/generated/{run_id}/{stem}.mid",
|
| 733 |
"abc_url": f"/generated/{run_id}/{stem}.abc",
|
| 734 |
"musicxml_url": f"/generated/{run_id}/{stem}.musicxml",
|
|
|
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
import argparse
|
| 6 |
+
from html import escape
|
| 7 |
import os
|
| 8 |
import pickle
|
| 9 |
import random
|
|
|
|
| 47 |
DEFAULT_MARKOV_CACHE = MARKOV_CACHE_DIR / "default.pkl"
|
| 48 |
MARKOV_CACHE_FORMAT_VERSION = 1
|
| 49 |
MARKOV_PRECOMPUTED_WARM_SAMPLES = 4
|
| 50 |
+
SCORE_PREVIEW_RENDER_VERSION = "strip-ties-beams-simple-svg-v2"
|
| 51 |
GENERATED_ROOT.mkdir(parents=True, exist_ok=True)
|
| 52 |
CATALOG_SCORE_ROOT.mkdir(parents=True, exist_ok=True)
|
| 53 |
|
|
|
|
| 372 |
return events
|
| 373 |
|
| 374 |
|
| 375 |
+
def write_simple_score_svg(notes: list[dict[str, object]], svg_path: Path, title: str) -> Path | None:
|
| 376 |
+
if not notes:
|
| 377 |
+
return None
|
| 378 |
+
|
| 379 |
+
total_beats = max(float(note["start_beat"]) + float(note["duration_beats"]) for note in notes)
|
| 380 |
+
width = max(980, int(160 + total_beats * 58))
|
| 381 |
+
height = 260
|
| 382 |
+
left = 72
|
| 383 |
+
right = 42
|
| 384 |
+
staff_top = 92
|
| 385 |
+
line_gap = 12
|
| 386 |
+
staff_bottom = staff_top + line_gap * 4
|
| 387 |
+
pitch_step = 3.5
|
| 388 |
+
x_scale = (width - left - right) / max(total_beats, 1.0)
|
| 389 |
+
|
| 390 |
+
def note_x(note: dict[str, object]) -> float:
|
| 391 |
+
return left + float(note["start_beat"]) * x_scale
|
| 392 |
+
|
| 393 |
+
def note_y(pitch: int) -> float:
|
| 394 |
+
return staff_bottom - (pitch - 64) * pitch_step
|
| 395 |
+
|
| 396 |
+
def duration_to_fill(duration: float) -> str:
|
| 397 |
+
return "#fff" if duration >= 2.0 else "#000"
|
| 398 |
+
|
| 399 |
+
def duration_to_stem(duration: float) -> bool:
|
| 400 |
+
return duration < 4.0
|
| 401 |
+
|
| 402 |
+
elements = [
|
| 403 |
+
f'<svg xmlns="http://www.w3.org/2000/svg" width="{width}" height="{height}" viewBox="0 0 {width} {height}">',
|
| 404 |
+
'<rect width="100%" height="100%" fill="#fff"/>',
|
| 405 |
+
f'<text x="{width / 2:.1f}" y="42" text-anchor="middle" font-family="Georgia, serif" font-size="26">{escape(title)}</text>',
|
| 406 |
+
f'<text x="22" y="{staff_top + 30}" font-family="Georgia, serif" font-size="48">𝄞</text>',
|
| 407 |
+
f'<text x="{left - 20}" y="{staff_top + 33}" font-family="Georgia, serif" font-size="27" font-weight="700">4</text>',
|
| 408 |
+
f'<text x="{left - 20}" y="{staff_top + 58}" font-family="Georgia, serif" font-size="27" font-weight="700">4</text>',
|
| 409 |
+
]
|
| 410 |
+
for line in range(5):
|
| 411 |
+
y = staff_top + line * line_gap
|
| 412 |
+
elements.append(f'<line x1="18" x2="{width - 24}" y1="{y}" y2="{y}" stroke="#000" stroke-width="1"/>')
|
| 413 |
+
|
| 414 |
+
measure_beat = 4.0
|
| 415 |
+
bar = measure_beat
|
| 416 |
+
while bar < total_beats:
|
| 417 |
+
x = left + bar * x_scale
|
| 418 |
+
elements.append(f'<line x1="{x:.1f}" x2="{x:.1f}" y1="{staff_top}" y2="{staff_bottom}" stroke="#000" stroke-width="1.3"/>')
|
| 419 |
+
bar += measure_beat
|
| 420 |
+
|
| 421 |
+
for note in notes:
|
| 422 |
+
pitch = int(note["pitch"])
|
| 423 |
+
duration = float(note["duration_beats"])
|
| 424 |
+
x = note_x(note)
|
| 425 |
+
y = note_y(pitch)
|
| 426 |
+
while y < staff_top - line_gap:
|
| 427 |
+
elements.append(
|
| 428 |
+
f'<line x1="{x - 12:.1f}" x2="{x + 12:.1f}" y1="{y:.1f}" y2="{y:.1f}" stroke="#000" stroke-width="1"/>'
|
| 429 |
+
)
|
| 430 |
+
y += line_gap
|
| 431 |
+
y = note_y(pitch)
|
| 432 |
+
while y > staff_bottom + line_gap:
|
| 433 |
+
elements.append(
|
| 434 |
+
f'<line x1="{x - 12:.1f}" x2="{x + 12:.1f}" y1="{y:.1f}" y2="{y:.1f}" stroke="#000" stroke-width="1"/>'
|
| 435 |
+
)
|
| 436 |
+
y -= line_gap
|
| 437 |
+
y = note_y(pitch)
|
| 438 |
+
accidental = {1: "#", 3: "#", 6: "#", 8: "#", 10: "#"}.get(pitch % 12)
|
| 439 |
+
if accidental:
|
| 440 |
+
elements.append(
|
| 441 |
+
f'<text x="{x - 22:.1f}" y="{y + 5:.1f}" font-family="Georgia, serif" font-size="18">{accidental}</text>'
|
| 442 |
+
)
|
| 443 |
+
elements.append(
|
| 444 |
+
f'<ellipse cx="{x:.1f}" cy="{y:.1f}" rx="8" ry="5.5" fill="{duration_to_fill(duration)}" stroke="#000" stroke-width="1.6" transform="rotate(-18 {x:.1f} {y:.1f})"/>'
|
| 445 |
+
)
|
| 446 |
+
if duration_to_stem(duration):
|
| 447 |
+
stem_x = x + 7
|
| 448 |
+
stem_y = y - 42
|
| 449 |
+
elements.append(f'<line x1="{stem_x:.1f}" x2="{stem_x:.1f}" y1="{y:.1f}" y2="{stem_y:.1f}" stroke="#000" stroke-width="1.6"/>')
|
| 450 |
+
if duration <= 0.5:
|
| 451 |
+
elements.append(
|
| 452 |
+
f'<path d="M {stem_x:.1f} {stem_y:.1f} q 18 8 6 24" fill="none" stroke="#000" stroke-width="1.5"/>'
|
| 453 |
+
)
|
| 454 |
+
|
| 455 |
+
final_x = left + total_beats * x_scale + 14
|
| 456 |
+
elements.append(f'<line x1="{final_x:.1f}" x2="{final_x:.1f}" y1="{staff_top}" y2="{staff_bottom}" stroke="#000" stroke-width="1.3"/>')
|
| 457 |
+
elements.append(f'<line x1="{final_x + 5:.1f}" x2="{final_x + 5:.1f}" y1="{staff_top}" y2="{staff_bottom}" stroke="#000" stroke-width="3"/>')
|
| 458 |
+
elements.append("</svg>")
|
| 459 |
+
|
| 460 |
+
svg_path.write_text("\n".join(elements), encoding="utf-8")
|
| 461 |
+
return svg_path if svg_path.exists() else None
|
| 462 |
+
|
| 463 |
+
|
| 464 |
def has_verovio_renderer() -> bool:
|
| 465 |
if shutil.which("verovio") is not None:
|
| 466 |
return True
|
|
|
|
| 811 |
for index, sequence in enumerate(generated, start=1):
|
| 812 |
stem = f"generated_{index:02d}"
|
| 813 |
musicxml_path = output_dir / f"{stem}.musicxml"
|
| 814 |
+
notes = sequence_note_events(sequence, request.key)
|
| 815 |
svg_path = render_musicxml_to_svg(musicxml_path)
|
| 816 |
+
if svg_path is None:
|
| 817 |
+
svg_path = write_simple_score_svg(notes, musicxml_path.with_suffix(".svg"), f"{request.engine} sample {index:02d}")
|
| 818 |
samples.append(
|
| 819 |
{
|
| 820 |
"index": index,
|
| 821 |
"title": f"{request.engine} sample {index:02d}",
|
| 822 |
"relative_pcs": [symbol.rpc for symbol in sequence],
|
| 823 |
"durations": [symbol.duration for symbol in sequence],
|
| 824 |
+
"notes": notes,
|
| 825 |
"midi_url": f"/generated/{run_id}/{stem}.mid",
|
| 826 |
"abc_url": f"/generated/{run_id}/{stem}.abc",
|
| 827 |
"musicxml_url": f"/generated/{run_id}/{stem}.musicxml",
|