Spaces:
Running on Zero
Running on Zero
Initial MuScriptor ZeroGPU studio
Browse files- .gitignore +8 -0
- README.md +17 -7
- app.py +692 -0
- assets/app.css +1235 -0
- assets/visualizer.html +94 -0
- assets/visualizer.js +898 -0
- requirements.txt +9 -0
.gitignore
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
__pycache__/
|
| 2 |
+
*.pyc
|
| 3 |
+
.venv/
|
| 4 |
+
.codex/
|
| 5 |
+
rollout.jsonl
|
| 6 |
+
*.rollout.jsonl
|
| 7 |
+
*.session.jsonl
|
| 8 |
+
verify*.png
|
README.md
CHANGED
|
@@ -1,13 +1,23 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
colorFrom: gray
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version: 6.
|
| 8 |
-
python_version: '3.12'
|
| 9 |
app_file: app.py
|
| 10 |
-
|
|
|
|
| 11 |
---
|
| 12 |
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: MuScriptor Studio
|
| 3 |
+
emoji: 🎼
|
| 4 |
colorFrom: gray
|
| 5 |
+
colorTo: indigo
|
| 6 |
sdk: gradio
|
| 7 |
+
sdk_version: 6.10.0
|
|
|
|
| 8 |
app_file: app.py
|
| 9 |
+
short_description: Multi-instrument audio transcription to editable MIDI
|
| 10 |
+
startup_duration_timeout: 1h
|
| 11 |
---
|
| 12 |
|
| 13 |
+
# MuScriptor Studio
|
| 14 |
+
|
| 15 |
+
A custom ZeroGPU interface for [MuScriptor](https://github.com/muscriptor/muscriptor), the multi-instrument audio-to-MIDI model from Kyutai and Mirelo.
|
| 16 |
+
|
| 17 |
+
The app runs the 307M-parameter medium checkpoint, streams transcription progress after each 5-second audio window, renders an interactive color-coded piano roll, and exports both the full arrangement and isolated instrument MIDI files.
|
| 18 |
+
|
| 19 |
+
The checkpoint is gated under CC BY-NC 4.0. This Space requires an `HF_TOKEN` secret whose owner has accepted access to [`MuScriptor/muscriptor-medium`](https://huggingface.co/MuScriptor/muscriptor-medium).
|
| 20 |
+
|
| 21 |
+
## API
|
| 22 |
+
|
| 23 |
+
The Gradio endpoint is `/transcribe`. It accepts an audio file, optional exact instrument group names, a sampling toggle, and temperature. It returns the combined MIDI, all isolated track files, the browser visualization payload, and a machine-readable manifest.
|
app.py
ADDED
|
@@ -0,0 +1,692 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
|
| 3 |
+
# ZeroGPU and library caches must be configured before any third-party import.
|
| 4 |
+
os.environ.setdefault("HF_HOME", "/tmp/.cache/huggingface")
|
| 5 |
+
os.environ.setdefault("HF_MODULES_CACHE", "/tmp/hf_modules")
|
| 6 |
+
os.environ.setdefault("MPLCONFIGDIR", "/tmp/matplotlib")
|
| 7 |
+
os.environ.setdefault("TOKENIZERS_PARALLELISM", "false")
|
| 8 |
+
|
| 9 |
+
import spaces # noqa: E402 # Must precede torch on ZeroGPU.
|
| 10 |
+
|
| 11 |
+
import base64
|
| 12 |
+
import hashlib
|
| 13 |
+
import json
|
| 14 |
+
import math
|
| 15 |
+
import re
|
| 16 |
+
import sys
|
| 17 |
+
import tempfile
|
| 18 |
+
import time
|
| 19 |
+
from collections import defaultdict
|
| 20 |
+
from pathlib import Path
|
| 21 |
+
from typing import Any, Iterator
|
| 22 |
+
|
| 23 |
+
import gradio as gr
|
| 24 |
+
import soundfile as sf
|
| 25 |
+
import torch
|
| 26 |
+
from huggingface_hub import hf_hub_download
|
| 27 |
+
from muscriptor.events import NoteEndEvent, NoteStartEvent, ProgressEvent
|
| 28 |
+
from muscriptor.tokenizer.mt3 import MT3Tokenizer, MT3_FULL_PLUS_GROUP_NAMES
|
| 29 |
+
from muscriptor.transcription_model import (
|
| 30 |
+
TranscriptionModel,
|
| 31 |
+
_build_model,
|
| 32 |
+
_remap_single_codebook_keys,
|
| 33 |
+
_resolve_config,
|
| 34 |
+
_resolve_source,
|
| 35 |
+
)
|
| 36 |
+
from safetensors.torch import load_file
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
APP_ROOT = Path(__file__).resolve().parent
|
| 40 |
+
ASSET_ROOT = APP_ROOT / "assets"
|
| 41 |
+
MODEL_ID = "MuScriptor/muscriptor-medium"
|
| 42 |
+
MODEL_VARIANT = "medium"
|
| 43 |
+
MAX_AUDIO_SECONDS = 60.0
|
| 44 |
+
|
| 45 |
+
# Initial deployment values. These are recalibrated from cold/warm live calls.
|
| 46 |
+
GPU_BASE_SECONDS = 45
|
| 47 |
+
GPU_SECONDS_PER_CHUNK = 18
|
| 48 |
+
GPU_DURATION_CAP = 240
|
| 49 |
+
|
| 50 |
+
COLORS = (
|
| 51 |
+
"#8b7cff",
|
| 52 |
+
"#35c8ff",
|
| 53 |
+
"#67e8a5",
|
| 54 |
+
"#f6c667",
|
| 55 |
+
"#fb7185",
|
| 56 |
+
"#c084fc",
|
| 57 |
+
"#2dd4bf",
|
| 58 |
+
"#60a5fa",
|
| 59 |
+
"#f472b6",
|
| 60 |
+
"#a3e635",
|
| 61 |
+
)
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
@spaces.GPU(duration=1)
|
| 65 |
+
def _zerogpu_probe() -> str:
|
| 66 |
+
"""Required lightweight ZeroGPU allocation probe."""
|
| 67 |
+
return "ready"
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def _load_model_zerogpu() -> TranscriptionModel:
|
| 71 |
+
"""Build MuScriptor with CUDA attributes while loading weights on CPU.
|
| 72 |
+
|
| 73 |
+
ZeroGPU intercepts ``.to('cuda')`` at module scope and packs the singleton
|
| 74 |
+
for restoration inside decorated calls. Safetensors cannot target the fake
|
| 75 |
+
CUDA device at startup, so its state dictionary must first be read on CPU.
|
| 76 |
+
"""
|
| 77 |
+
|
| 78 |
+
token = os.environ.get("HF_TOKEN")
|
| 79 |
+
if not token:
|
| 80 |
+
raise RuntimeError(
|
| 81 |
+
"HF_TOKEN is required. Add a read token whose owner has accepted "
|
| 82 |
+
f"the gated license at https://huggingface.co/{MODEL_ID}."
|
| 83 |
+
)
|
| 84 |
+
|
| 85 |
+
device = torch.device("cuda")
|
| 86 |
+
source = _resolve_source(MODEL_VARIANT)
|
| 87 |
+
weights_path = Path(
|
| 88 |
+
hf_hub_download(
|
| 89 |
+
repo_id=MODEL_ID,
|
| 90 |
+
filename="model.safetensors",
|
| 91 |
+
token=token,
|
| 92 |
+
)
|
| 93 |
+
)
|
| 94 |
+
config = _resolve_config(source, weights_path)
|
| 95 |
+
|
| 96 |
+
model = _build_model(device, config)
|
| 97 |
+
model.eval()
|
| 98 |
+
state_dict = load_file(str(weights_path), device="cpu")
|
| 99 |
+
state_dict = _remap_single_codebook_keys(state_dict)
|
| 100 |
+
model.load_state_dict(state_dict)
|
| 101 |
+
model.to("cuda")
|
| 102 |
+
|
| 103 |
+
tokenizer = MT3Tokenizer(
|
| 104 |
+
instrument_vocabulary="MT3_FULL_PLUS",
|
| 105 |
+
max_shift_steps=1001,
|
| 106 |
+
)
|
| 107 |
+
return TranscriptionModel(model=model, tokenizer=tokenizer, device=device)
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
print(f"[MuScriptor Studio] Loading {MODEL_VARIANT} model once at startup…", flush=True)
|
| 111 |
+
_model_load_started = time.perf_counter()
|
| 112 |
+
MODEL = _load_model_zerogpu()
|
| 113 |
+
print(
|
| 114 |
+
"[MuScriptor Studio] Model ready in "
|
| 115 |
+
f"{time.perf_counter() - _model_load_started:.2f}s.",
|
| 116 |
+
flush=True,
|
| 117 |
+
)
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
def _audio_path(value: Any) -> str | None:
|
| 121 |
+
if value is None:
|
| 122 |
+
return None
|
| 123 |
+
if isinstance(value, (str, Path)):
|
| 124 |
+
return str(value)
|
| 125 |
+
if isinstance(value, dict):
|
| 126 |
+
path = value.get("path") or value.get("name")
|
| 127 |
+
return str(path) if path else None
|
| 128 |
+
path = getattr(value, "path", None) or getattr(value, "name", None)
|
| 129 |
+
return str(path) if path else None
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
def _audio_duration(path: str | None) -> float | None:
|
| 133 |
+
if not path:
|
| 134 |
+
return None
|
| 135 |
+
try:
|
| 136 |
+
return float(sf.info(path).duration)
|
| 137 |
+
except Exception:
|
| 138 |
+
return None
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
def _estimate_gpu_duration(audio: Any, *args: Any, **kwargs: Any) -> int:
|
| 142 |
+
"""Estimate allocation from 5-second chunks; accepts Gradio extras."""
|
| 143 |
+
|
| 144 |
+
duration = _audio_duration(_audio_path(audio))
|
| 145 |
+
chunks = max(1, math.ceil((duration or 15.0) / 5.0))
|
| 146 |
+
return min(GPU_DURATION_CAP, GPU_BASE_SECONDS + chunks * GPU_SECONDS_PER_CHUNK)
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
def _display_name(name: str) -> str:
|
| 150 |
+
if name.startswith("program_"):
|
| 151 |
+
return f"MIDI program {name.removeprefix('program_')}"
|
| 152 |
+
special = {
|
| 153 |
+
"drums": "Drums",
|
| 154 |
+
"voice": "Voice",
|
| 155 |
+
"flutes": "Flutes",
|
| 156 |
+
"soprano_and_alto_sax": "Soprano & alto sax",
|
| 157 |
+
}
|
| 158 |
+
return special.get(name, name.replace("_", " ").title())
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
def _color_for(name: str) -> str:
|
| 162 |
+
digest = hashlib.sha1(name.encode("utf-8")).digest()
|
| 163 |
+
return COLORS[int.from_bytes(digest[:2], "big") % len(COLORS)]
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
def _slug(name: str) -> str:
|
| 167 |
+
return re.sub(r"[^a-z0-9]+", "-", name.lower()).strip("-") or "track"
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
def _data_uri(data: bytes) -> str:
|
| 171 |
+
encoded = base64.b64encode(data).decode("ascii")
|
| 172 |
+
return f"data:audio/midi;base64,{encoded}"
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
def _program_for(name: str) -> int:
|
| 176 |
+
if name == "drums":
|
| 177 |
+
return 0
|
| 178 |
+
try:
|
| 179 |
+
return int(MODEL._program_for_instrument(name))
|
| 180 |
+
except Exception:
|
| 181 |
+
if name.startswith("program_"):
|
| 182 |
+
try:
|
| 183 |
+
return int(name.removeprefix("program_"))
|
| 184 |
+
except ValueError:
|
| 185 |
+
pass
|
| 186 |
+
return 0
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
def _notes_from_events(
|
| 190 |
+
events: list[NoteStartEvent | NoteEndEvent | ProgressEvent],
|
| 191 |
+
) -> dict[str, list[dict[str, float | int]]]:
|
| 192 |
+
notes: dict[str, list[dict[str, float | int]]] = defaultdict(list)
|
| 193 |
+
for event in events:
|
| 194 |
+
if not isinstance(event, NoteEndEvent):
|
| 195 |
+
continue
|
| 196 |
+
start = event.start_event
|
| 197 |
+
notes[start.instrument].append(
|
| 198 |
+
{
|
| 199 |
+
"pitch": int(start.pitch),
|
| 200 |
+
"start": round(float(start.start_time), 4),
|
| 201 |
+
"end": round(max(float(event.end_time), float(start.start_time) + 0.01), 4),
|
| 202 |
+
"velocity": 100,
|
| 203 |
+
}
|
| 204 |
+
)
|
| 205 |
+
for instrument_notes in notes.values():
|
| 206 |
+
instrument_notes.sort(key=lambda note: (note["start"], note["pitch"]))
|
| 207 |
+
return dict(notes)
|
| 208 |
+
|
| 209 |
+
|
| 210 |
+
def _track_payloads(
|
| 211 |
+
notes: dict[str, list[dict[str, float | int]]],
|
| 212 |
+
requested: list[str] | None = None,
|
| 213 |
+
midi_by_instrument: dict[str, bytes] | None = None,
|
| 214 |
+
) -> list[dict[str, Any]]:
|
| 215 |
+
names = set(notes)
|
| 216 |
+
names.update(requested or [])
|
| 217 |
+
ordered = sorted(
|
| 218 |
+
names,
|
| 219 |
+
key=lambda name: (
|
| 220 |
+
notes.get(name, [{}])[0].get("start", float("inf")) if notes.get(name) else float("inf"),
|
| 221 |
+
name,
|
| 222 |
+
),
|
| 223 |
+
)
|
| 224 |
+
tracks: list[dict[str, Any]] = []
|
| 225 |
+
for index, name in enumerate(ordered):
|
| 226 |
+
track_notes = notes.get(name, [])
|
| 227 |
+
midi = (midi_by_instrument or {}).get(name)
|
| 228 |
+
tracks.append(
|
| 229 |
+
{
|
| 230 |
+
"id": index,
|
| 231 |
+
"key": name,
|
| 232 |
+
"name": _display_name(name),
|
| 233 |
+
"color": _color_for(name),
|
| 234 |
+
"note_count": len(track_notes),
|
| 235 |
+
"program": _program_for(name),
|
| 236 |
+
"is_drum": name == "drums",
|
| 237 |
+
"midi": _data_uri(midi) if midi else "",
|
| 238 |
+
"notes": track_notes,
|
| 239 |
+
}
|
| 240 |
+
)
|
| 241 |
+
return tracks
|
| 242 |
+
|
| 243 |
+
|
| 244 |
+
def _payload(
|
| 245 |
+
*,
|
| 246 |
+
state: str,
|
| 247 |
+
status: str,
|
| 248 |
+
progress: float,
|
| 249 |
+
audio_name: str,
|
| 250 |
+
elapsed: float,
|
| 251 |
+
duration: float,
|
| 252 |
+
tracks: list[dict[str, Any]],
|
| 253 |
+
full_midi: bytes | None = None,
|
| 254 |
+
) -> str:
|
| 255 |
+
return json.dumps(
|
| 256 |
+
{
|
| 257 |
+
"state": state,
|
| 258 |
+
"status": status,
|
| 259 |
+
"progress": round(max(0.0, min(1.0, progress)), 4),
|
| 260 |
+
"audio_name": audio_name,
|
| 261 |
+
"elapsed": round(elapsed, 2),
|
| 262 |
+
"duration": round(duration, 2),
|
| 263 |
+
"note_count": sum(track["note_count"] for track in tracks),
|
| 264 |
+
"full_midi": _data_uri(full_midi) if full_midi else "",
|
| 265 |
+
"tracks": tracks,
|
| 266 |
+
},
|
| 267 |
+
separators=(",", ":"),
|
| 268 |
+
)
|
| 269 |
+
|
| 270 |
+
|
| 271 |
+
def _event_is_for_instrument(
|
| 272 |
+
event: NoteStartEvent | NoteEndEvent | ProgressEvent,
|
| 273 |
+
instrument: str,
|
| 274 |
+
) -> bool:
|
| 275 |
+
if isinstance(event, NoteStartEvent):
|
| 276 |
+
return event.instrument == instrument
|
| 277 |
+
if isinstance(event, NoteEndEvent):
|
| 278 |
+
return event.start_event.instrument == instrument
|
| 279 |
+
return False
|
| 280 |
+
|
| 281 |
+
|
| 282 |
+
def _write_outputs(
|
| 283 |
+
events: list[NoteStartEvent | NoteEndEvent | ProgressEvent],
|
| 284 |
+
track_names: list[str],
|
| 285 |
+
) -> tuple[Path, dict[str, Path], bytes, dict[str, bytes]]:
|
| 286 |
+
request_dir = Path(tempfile.mkdtemp(prefix="muscriptor-output-"))
|
| 287 |
+
full_bytes = MODEL.events_to_midi_bytes(iter(events))
|
| 288 |
+
full_path = request_dir / "muscriptor-transcription.mid"
|
| 289 |
+
full_path.write_bytes(full_bytes)
|
| 290 |
+
|
| 291 |
+
track_paths: dict[str, Path] = {}
|
| 292 |
+
midi_by_instrument: dict[str, bytes] = {}
|
| 293 |
+
for name in track_names:
|
| 294 |
+
filtered = [event for event in events if _event_is_for_instrument(event, name)]
|
| 295 |
+
track_bytes = MODEL.events_to_midi_bytes(iter(filtered))
|
| 296 |
+
track_path = request_dir / f"{_slug(name)}.mid"
|
| 297 |
+
track_path.write_bytes(track_bytes)
|
| 298 |
+
track_paths[name] = track_path
|
| 299 |
+
midi_by_instrument[name] = track_bytes
|
| 300 |
+
return full_path, track_paths, full_bytes, midi_by_instrument
|
| 301 |
+
|
| 302 |
+
|
| 303 |
+
def _manifest(
|
| 304 |
+
*,
|
| 305 |
+
state: str,
|
| 306 |
+
audio_name: str,
|
| 307 |
+
elapsed: float,
|
| 308 |
+
duration: float,
|
| 309 |
+
tracks: list[dict[str, Any]],
|
| 310 |
+
full_path: Path | None = None,
|
| 311 |
+
track_paths: dict[str, Path] | None = None,
|
| 312 |
+
message: str | None = None,
|
| 313 |
+
) -> dict[str, Any]:
|
| 314 |
+
result: dict[str, Any] = {
|
| 315 |
+
"state": state,
|
| 316 |
+
"model": MODEL_VARIANT,
|
| 317 |
+
"model_id": MODEL_ID,
|
| 318 |
+
"audio_name": audio_name,
|
| 319 |
+
"audio_duration_seconds": round(duration, 3),
|
| 320 |
+
"inference_seconds": round(elapsed, 3),
|
| 321 |
+
"note_count": sum(track["note_count"] for track in tracks),
|
| 322 |
+
"track_count": len(tracks),
|
| 323 |
+
"tracks": [
|
| 324 |
+
{
|
| 325 |
+
"instrument": track["key"],
|
| 326 |
+
"display_name": track["name"],
|
| 327 |
+
"note_count": track["note_count"],
|
| 328 |
+
"program": track["program"],
|
| 329 |
+
"is_drum": track["is_drum"],
|
| 330 |
+
"file": str(track_paths[track["key"]])
|
| 331 |
+
if track_paths and track["key"] in track_paths
|
| 332 |
+
else None,
|
| 333 |
+
}
|
| 334 |
+
for track in tracks
|
| 335 |
+
],
|
| 336 |
+
"full_midi": str(full_path) if full_path else None,
|
| 337 |
+
}
|
| 338 |
+
if message:
|
| 339 |
+
result["message"] = message
|
| 340 |
+
return result
|
| 341 |
+
|
| 342 |
+
|
| 343 |
+
@spaces.GPU(duration=_estimate_gpu_duration)
|
| 344 |
+
def transcribe_audio(
|
| 345 |
+
audio: Any,
|
| 346 |
+
instruments: list[str] | None,
|
| 347 |
+
use_sampling: bool,
|
| 348 |
+
temperature: float,
|
| 349 |
+
) -> Iterator[tuple[str | None, list[str] | None, str, dict[str, Any]]]:
|
| 350 |
+
"""Stream chunk progress, then return combined and isolated MIDI files."""
|
| 351 |
+
|
| 352 |
+
path = _audio_path(audio)
|
| 353 |
+
if not path:
|
| 354 |
+
empty_tracks: list[dict[str, Any]] = []
|
| 355 |
+
message = "Drop an audio file before starting transcription."
|
| 356 |
+
yield (
|
| 357 |
+
None,
|
| 358 |
+
None,
|
| 359 |
+
_payload(
|
| 360 |
+
state="error",
|
| 361 |
+
status=message,
|
| 362 |
+
progress=0,
|
| 363 |
+
audio_name="",
|
| 364 |
+
elapsed=0,
|
| 365 |
+
duration=0,
|
| 366 |
+
tracks=empty_tracks,
|
| 367 |
+
),
|
| 368 |
+
_manifest(
|
| 369 |
+
state="error",
|
| 370 |
+
audio_name="",
|
| 371 |
+
elapsed=0,
|
| 372 |
+
duration=0,
|
| 373 |
+
tracks=empty_tracks,
|
| 374 |
+
message=message,
|
| 375 |
+
),
|
| 376 |
+
)
|
| 377 |
+
return
|
| 378 |
+
|
| 379 |
+
audio_name = Path(path).name
|
| 380 |
+
duration = _audio_duration(path) or 0.0
|
| 381 |
+
if duration > MAX_AUDIO_SECONDS:
|
| 382 |
+
tracks = _track_payloads({}, instruments)
|
| 383 |
+
message = f"This demo accepts audio up to {MAX_AUDIO_SECONDS:.0f} seconds."
|
| 384 |
+
yield (
|
| 385 |
+
None,
|
| 386 |
+
None,
|
| 387 |
+
_payload(
|
| 388 |
+
state="error",
|
| 389 |
+
status=message,
|
| 390 |
+
progress=0,
|
| 391 |
+
audio_name=audio_name,
|
| 392 |
+
elapsed=0,
|
| 393 |
+
duration=duration,
|
| 394 |
+
tracks=tracks,
|
| 395 |
+
),
|
| 396 |
+
_manifest(
|
| 397 |
+
state="error",
|
| 398 |
+
audio_name=audio_name,
|
| 399 |
+
elapsed=0,
|
| 400 |
+
duration=duration,
|
| 401 |
+
tracks=tracks,
|
| 402 |
+
message=message,
|
| 403 |
+
),
|
| 404 |
+
)
|
| 405 |
+
return
|
| 406 |
+
|
| 407 |
+
requested = list(dict.fromkeys(instruments or []))
|
| 408 |
+
started = time.perf_counter()
|
| 409 |
+
events: list[NoteStartEvent | NoteEndEvent | ProgressEvent] = []
|
| 410 |
+
last_completed = -1
|
| 411 |
+
|
| 412 |
+
try:
|
| 413 |
+
stream = MODEL.transcribe(
|
| 414 |
+
path,
|
| 415 |
+
instruments=requested or None,
|
| 416 |
+
use_sampling=bool(use_sampling),
|
| 417 |
+
temperature=float(temperature),
|
| 418 |
+
batch_size=1,
|
| 419 |
+
)
|
| 420 |
+
for event in stream:
|
| 421 |
+
events.append(event)
|
| 422 |
+
if not isinstance(event, ProgressEvent):
|
| 423 |
+
continue
|
| 424 |
+
if event.completed == last_completed:
|
| 425 |
+
continue
|
| 426 |
+
last_completed = event.completed
|
| 427 |
+
elapsed = time.perf_counter() - started
|
| 428 |
+
fraction = event.completed / max(1, event.total)
|
| 429 |
+
notes = _notes_from_events(events)
|
| 430 |
+
tracks = _track_payloads(notes, requested)
|
| 431 |
+
status = (
|
| 432 |
+
"GPU ready · preparing the first 5-second window"
|
| 433 |
+
if event.completed == 0
|
| 434 |
+
else f"Transcribed {event.completed} of {event.total} audio windows"
|
| 435 |
+
)
|
| 436 |
+
yield (
|
| 437 |
+
None,
|
| 438 |
+
None,
|
| 439 |
+
_payload(
|
| 440 |
+
state="transcribing",
|
| 441 |
+
status=status,
|
| 442 |
+
progress=fraction,
|
| 443 |
+
audio_name=audio_name,
|
| 444 |
+
elapsed=elapsed,
|
| 445 |
+
duration=duration,
|
| 446 |
+
tracks=tracks,
|
| 447 |
+
),
|
| 448 |
+
_manifest(
|
| 449 |
+
state="running",
|
| 450 |
+
audio_name=audio_name,
|
| 451 |
+
elapsed=elapsed,
|
| 452 |
+
duration=duration,
|
| 453 |
+
tracks=tracks,
|
| 454 |
+
),
|
| 455 |
+
)
|
| 456 |
+
|
| 457 |
+
elapsed = time.perf_counter() - started
|
| 458 |
+
notes = _notes_from_events(events)
|
| 459 |
+
preview_tracks = _track_payloads(notes, requested)
|
| 460 |
+
track_names = [track["key"] for track in preview_tracks]
|
| 461 |
+
full_path, track_paths, full_bytes, midi_by_instrument = _write_outputs(
|
| 462 |
+
events, track_names
|
| 463 |
+
)
|
| 464 |
+
tracks = _track_payloads(notes, requested, midi_by_instrument)
|
| 465 |
+
print(
|
| 466 |
+
"[MuScriptor Studio] "
|
| 467 |
+
f"{audio_name}: {duration:.2f}s audio, {len(tracks)} tracks, "
|
| 468 |
+
f"{sum(track['note_count'] for track in tracks)} notes, "
|
| 469 |
+
f"{elapsed:.2f}s inference.",
|
| 470 |
+
flush=True,
|
| 471 |
+
)
|
| 472 |
+
yield (
|
| 473 |
+
str(full_path),
|
| 474 |
+
[str(track_paths[track["key"]]) for track in tracks],
|
| 475 |
+
_payload(
|
| 476 |
+
state="complete",
|
| 477 |
+
status=f"Transcription complete in {elapsed:.1f} seconds",
|
| 478 |
+
progress=1,
|
| 479 |
+
audio_name=audio_name,
|
| 480 |
+
elapsed=elapsed,
|
| 481 |
+
duration=duration,
|
| 482 |
+
tracks=tracks,
|
| 483 |
+
full_midi=full_bytes,
|
| 484 |
+
),
|
| 485 |
+
_manifest(
|
| 486 |
+
state="complete",
|
| 487 |
+
audio_name=audio_name,
|
| 488 |
+
elapsed=elapsed,
|
| 489 |
+
duration=duration,
|
| 490 |
+
tracks=tracks,
|
| 491 |
+
full_path=full_path,
|
| 492 |
+
track_paths=track_paths,
|
| 493 |
+
),
|
| 494 |
+
)
|
| 495 |
+
except Exception as exc:
|
| 496 |
+
elapsed = time.perf_counter() - started
|
| 497 |
+
notes = _notes_from_events(events)
|
| 498 |
+
tracks = _track_payloads(notes, requested)
|
| 499 |
+
message = f"Transcription stopped: {type(exc).__name__}: {exc}"
|
| 500 |
+
print(f"[MuScriptor Studio] {message}", file=sys.stderr, flush=True)
|
| 501 |
+
yield (
|
| 502 |
+
None,
|
| 503 |
+
None,
|
| 504 |
+
_payload(
|
| 505 |
+
state="error",
|
| 506 |
+
status=message,
|
| 507 |
+
progress=0,
|
| 508 |
+
audio_name=audio_name,
|
| 509 |
+
elapsed=elapsed,
|
| 510 |
+
duration=duration,
|
| 511 |
+
tracks=tracks,
|
| 512 |
+
),
|
| 513 |
+
_manifest(
|
| 514 |
+
state="error",
|
| 515 |
+
audio_name=audio_name,
|
| 516 |
+
elapsed=elapsed,
|
| 517 |
+
duration=duration,
|
| 518 |
+
tracks=tracks,
|
| 519 |
+
message=message,
|
| 520 |
+
),
|
| 521 |
+
)
|
| 522 |
+
|
| 523 |
+
|
| 524 |
+
def _read_asset(name: str) -> str:
|
| 525 |
+
return (ASSET_ROOT / name).read_text(encoding="utf-8")
|
| 526 |
+
|
| 527 |
+
|
| 528 |
+
APP_CSS = _read_asset("app.css")
|
| 529 |
+
VISUALIZER_TEMPLATE = _read_asset("visualizer.html")
|
| 530 |
+
VISUALIZER_JS = _read_asset("visualizer.js")
|
| 531 |
+
|
| 532 |
+
INSTRUMENT_CHOICES = [
|
| 533 |
+
(_display_name(name), name)
|
| 534 |
+
for name in sorted(MT3_FULL_PLUS_GROUP_NAMES, key=MT3_FULL_PLUS_GROUP_NAMES.get)
|
| 535 |
+
]
|
| 536 |
+
|
| 537 |
+
INITIAL_VISUALIZER = _payload(
|
| 538 |
+
state="idle",
|
| 539 |
+
status="Ready for audio",
|
| 540 |
+
progress=0,
|
| 541 |
+
audio_name="",
|
| 542 |
+
elapsed=0,
|
| 543 |
+
duration=0,
|
| 544 |
+
tracks=[],
|
| 545 |
+
)
|
| 546 |
+
|
| 547 |
+
HEADER = """
|
| 548 |
+
<header class="masthead-inner">
|
| 549 |
+
<a class="brand" href="https://github.com/muscriptor/muscriptor" target="_blank" rel="noreferrer">
|
| 550 |
+
<span class="brand-mark" aria-hidden="true"><i></i><i></i><i></i><i></i></span>
|
| 551 |
+
<span>MuScriptor</span>
|
| 552 |
+
</a>
|
| 553 |
+
<div class="header-meta">
|
| 554 |
+
<span id="model-badge"><b></b> Medium · 307M</span>
|
| 555 |
+
<span class="zerogpu-badge">ZeroGPU</span>
|
| 556 |
+
</div>
|
| 557 |
+
</header>
|
| 558 |
+
"""
|
| 559 |
+
|
| 560 |
+
INTRO = """
|
| 561 |
+
<div class="intro-copy">
|
| 562 |
+
<span class="eyebrow">Multitrack transcription</span>
|
| 563 |
+
<h1>Turn a recording into<br><em>editable music.</em></h1>
|
| 564 |
+
<p>MuScriptor separates notes by instrument and shapes them into MIDI—one 5-second window at a time.</p>
|
| 565 |
+
</div>
|
| 566 |
+
"""
|
| 567 |
+
|
| 568 |
+
UPLOAD_HEADING = """
|
| 569 |
+
<div class="section-heading">
|
| 570 |
+
<span class="step-number">01</span>
|
| 571 |
+
<div><h2>Choose audio</h2><p>WAV, MP3, FLAC or OGG · up to 60 seconds</p></div>
|
| 572 |
+
</div>
|
| 573 |
+
"""
|
| 574 |
+
|
| 575 |
+
RESULT_HEADING = """
|
| 576 |
+
<div class="section-heading result-heading">
|
| 577 |
+
<span class="step-number">02</span>
|
| 578 |
+
<div><h2>Transcription</h2><p>Live notes, isolated tracks and MIDI export</p></div>
|
| 579 |
+
</div>
|
| 580 |
+
"""
|
| 581 |
+
|
| 582 |
+
FOOTNOTE = """
|
| 583 |
+
<div class="footnote">
|
| 584 |
+
<span>Your audio is processed ephemerally and is not retained.</span>
|
| 585 |
+
<span>Kyutai × Mirelo · CC BY-NC 4.0 weights</span>
|
| 586 |
+
</div>
|
| 587 |
+
"""
|
| 588 |
+
|
| 589 |
+
with gr.Blocks(title="MuScriptor — Audio to MIDI") as demo:
|
| 590 |
+
with gr.Column(elem_id="app-shell"):
|
| 591 |
+
gr.HTML(HEADER, elem_id="masthead")
|
| 592 |
+
gr.HTML(INTRO, elem_id="intro")
|
| 593 |
+
|
| 594 |
+
with gr.Row(elem_id="studio-grid", equal_height=False):
|
| 595 |
+
with gr.Column(scale=4, min_width=320, elem_id="upload-card"):
|
| 596 |
+
gr.HTML(UPLOAD_HEADING)
|
| 597 |
+
audio_input = gr.Audio(
|
| 598 |
+
label="Drop audio here",
|
| 599 |
+
sources=["upload"],
|
| 600 |
+
type="filepath",
|
| 601 |
+
elem_id="audio-input",
|
| 602 |
+
)
|
| 603 |
+
with gr.Accordion(
|
| 604 |
+
"Guide the transcription",
|
| 605 |
+
open=False,
|
| 606 |
+
elem_id="advanced-settings",
|
| 607 |
+
):
|
| 608 |
+
instrument_input = gr.CheckboxGroup(
|
| 609 |
+
choices=INSTRUMENT_CHOICES,
|
| 610 |
+
value=[],
|
| 611 |
+
label="Known instruments",
|
| 612 |
+
info="Optional. Leave blank for automatic discovery.",
|
| 613 |
+
)
|
| 614 |
+
use_sampling_input = gr.Checkbox(
|
| 615 |
+
label="Creative decoding",
|
| 616 |
+
value=False,
|
| 617 |
+
info="Uses stochastic sampling instead of deterministic decoding.",
|
| 618 |
+
)
|
| 619 |
+
temperature_input = gr.Slider(
|
| 620 |
+
minimum=0.2,
|
| 621 |
+
maximum=1.4,
|
| 622 |
+
value=1.0,
|
| 623 |
+
step=0.1,
|
| 624 |
+
label="Temperature",
|
| 625 |
+
)
|
| 626 |
+
transcribe_button = gr.Button(
|
| 627 |
+
"Transcribe audio",
|
| 628 |
+
variant="primary",
|
| 629 |
+
elem_id="transcribe-button",
|
| 630 |
+
)
|
| 631 |
+
gr.HTML(
|
| 632 |
+
'<p class="cold-note"><span></span> Cold starts can take a moment while ZeroGPU allocates the model.</p>'
|
| 633 |
+
)
|
| 634 |
+
|
| 635 |
+
with gr.Column(scale=7, min_width=420, elem_id="result-panel"):
|
| 636 |
+
gr.HTML(RESULT_HEADING)
|
| 637 |
+
visualizer_output = gr.HTML(
|
| 638 |
+
value=INITIAL_VISUALIZER,
|
| 639 |
+
html_template=VISUALIZER_TEMPLATE,
|
| 640 |
+
js_on_load=VISUALIZER_JS,
|
| 641 |
+
elem_id="visualizer-host",
|
| 642 |
+
)
|
| 643 |
+
full_midi_output = gr.File(
|
| 644 |
+
label="Full arrangement MIDI",
|
| 645 |
+
elem_id="download-output",
|
| 646 |
+
)
|
| 647 |
+
track_files_output = gr.File(
|
| 648 |
+
label="Instrument MIDI files",
|
| 649 |
+
file_count="multiple",
|
| 650 |
+
visible=False,
|
| 651 |
+
)
|
| 652 |
+
manifest_output = gr.JSON(label="Transcription manifest", visible=False)
|
| 653 |
+
|
| 654 |
+
gr.HTML(FOOTNOTE)
|
| 655 |
+
|
| 656 |
+
transcribe_button.click(
|
| 657 |
+
fn=transcribe_audio,
|
| 658 |
+
inputs=[
|
| 659 |
+
audio_input,
|
| 660 |
+
instrument_input,
|
| 661 |
+
use_sampling_input,
|
| 662 |
+
temperature_input,
|
| 663 |
+
],
|
| 664 |
+
outputs=[
|
| 665 |
+
full_midi_output,
|
| 666 |
+
track_files_output,
|
| 667 |
+
visualizer_output,
|
| 668 |
+
manifest_output,
|
| 669 |
+
],
|
| 670 |
+
api_name="transcribe",
|
| 671 |
+
concurrency_limit=1,
|
| 672 |
+
concurrency_id="muscriptor-medium",
|
| 673 |
+
show_progress="full",
|
| 674 |
+
)
|
| 675 |
+
|
| 676 |
+
demo.queue(default_concurrency_limit=1, max_size=8)
|
| 677 |
+
|
| 678 |
+
|
| 679 |
+
if __name__ == "__main__":
|
| 680 |
+
demo.launch(
|
| 681 |
+
theme=gr.themes.Base(
|
| 682 |
+
primary_hue="indigo",
|
| 683 |
+
neutral_hue="slate",
|
| 684 |
+
font=["Inter", "ui-sans-serif", "system-ui", "sans-serif"],
|
| 685 |
+
),
|
| 686 |
+
css=APP_CSS,
|
| 687 |
+
js="""() => {
|
| 688 |
+
document.documentElement.classList.add('dark');
|
| 689 |
+
try { localStorage.setItem('theme', 'dark'); } catch (_) {}
|
| 690 |
+
}""",
|
| 691 |
+
show_error=True,
|
| 692 |
+
)
|
assets/app.css
ADDED
|
@@ -0,0 +1,1235 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
:root {
|
| 2 |
+
color-scheme: dark;
|
| 3 |
+
--ms-bg: #090b0f;
|
| 4 |
+
--ms-bg-elevated: #0d1016;
|
| 5 |
+
--ms-panel: rgba(18, 21, 29, 0.82);
|
| 6 |
+
--ms-panel-solid: #12151d;
|
| 7 |
+
--ms-panel-hover: #171b25;
|
| 8 |
+
--ms-border: rgba(255, 255, 255, 0.085);
|
| 9 |
+
--ms-border-strong: rgba(255, 255, 255, 0.14);
|
| 10 |
+
--ms-text: #f4f5f7;
|
| 11 |
+
--ms-text-soft: #b5bac6;
|
| 12 |
+
--ms-muted: #747b8c;
|
| 13 |
+
--ms-faint: #4f5564;
|
| 14 |
+
--ms-accent: #8b7cff;
|
| 15 |
+
--ms-accent-2: #5f8cff;
|
| 16 |
+
--ms-accent-soft: rgba(139, 124, 255, 0.15);
|
| 17 |
+
--ms-success: #63d8a2;
|
| 18 |
+
--ms-warning: #f4bf75;
|
| 19 |
+
--ms-danger: #ff758e;
|
| 20 |
+
--ms-radius-xl: 22px;
|
| 21 |
+
--ms-radius-lg: 16px;
|
| 22 |
+
--ms-radius-md: 12px;
|
| 23 |
+
--ms-shadow: 0 28px 80px rgba(0, 0, 0, 0.34), 0 1px 0 rgba(255, 255, 255, 0.025) inset;
|
| 24 |
+
--ms-font: Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
| 25 |
+
--ms-mono: "SFMono-Regular", Consolas, "Liberation Mono", monospace;
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
html,
|
| 29 |
+
body {
|
| 30 |
+
min-height: 100%;
|
| 31 |
+
background: var(--ms-bg) !important;
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
body {
|
| 35 |
+
margin: 0;
|
| 36 |
+
color: var(--ms-text);
|
| 37 |
+
font-family: var(--ms-font);
|
| 38 |
+
-webkit-font-smoothing: antialiased;
|
| 39 |
+
text-rendering: optimizeLegibility;
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
body::before {
|
| 43 |
+
position: fixed;
|
| 44 |
+
z-index: -2;
|
| 45 |
+
inset: 0;
|
| 46 |
+
content: "";
|
| 47 |
+
pointer-events: none;
|
| 48 |
+
background:
|
| 49 |
+
radial-gradient(1000px 620px at 10% -10%, rgba(92, 75, 202, 0.18), transparent 62%),
|
| 50 |
+
radial-gradient(780px 500px at 96% 8%, rgba(53, 103, 175, 0.12), transparent 66%),
|
| 51 |
+
linear-gradient(180deg, #0a0c11 0%, #090b0f 50%, #080a0d 100%);
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
body::after {
|
| 55 |
+
position: fixed;
|
| 56 |
+
z-index: -1;
|
| 57 |
+
inset: 0;
|
| 58 |
+
content: "";
|
| 59 |
+
pointer-events: none;
|
| 60 |
+
opacity: 0.28;
|
| 61 |
+
background-image: linear-gradient(rgba(255, 255, 255, 0.012) 1px, transparent 1px),
|
| 62 |
+
linear-gradient(90deg, rgba(255, 255, 255, 0.012) 1px, transparent 1px);
|
| 63 |
+
background-size: 32px 32px;
|
| 64 |
+
mask-image: linear-gradient(to bottom, black, transparent 78%);
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
.gradio-container,
|
| 68 |
+
.gradio-container.dark {
|
| 69 |
+
min-height: 100vh !important;
|
| 70 |
+
background: transparent !important;
|
| 71 |
+
color: var(--ms-text) !important;
|
| 72 |
+
font-family: var(--ms-font) !important;
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
.gradio-container .prose,
|
| 76 |
+
.gradio-container .prose * {
|
| 77 |
+
color: inherit;
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
#app-shell {
|
| 81 |
+
width: min(1180px, calc(100% - 40px));
|
| 82 |
+
margin: 0 auto;
|
| 83 |
+
padding: 38px 0 64px;
|
| 84 |
+
gap: 20px;
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
#masthead {
|
| 88 |
+
position: relative;
|
| 89 |
+
overflow: hidden;
|
| 90 |
+
padding: 18px 4px 28px;
|
| 91 |
+
border: 0 !important;
|
| 92 |
+
background: transparent !important;
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
#masthead .masthead-inner {
|
| 96 |
+
display: flex;
|
| 97 |
+
align-items: center;
|
| 98 |
+
justify-content: space-between;
|
| 99 |
+
gap: 18px;
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
#masthead .brand {
|
| 103 |
+
display: inline-flex;
|
| 104 |
+
align-items: center;
|
| 105 |
+
gap: 11px;
|
| 106 |
+
color: var(--ms-text) !important;
|
| 107 |
+
font-size: 0.92rem;
|
| 108 |
+
font-weight: 680;
|
| 109 |
+
letter-spacing: -0.025em;
|
| 110 |
+
text-decoration: none;
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
.brand-mark {
|
| 114 |
+
display: flex;
|
| 115 |
+
align-items: end;
|
| 116 |
+
justify-content: center;
|
| 117 |
+
width: 30px;
|
| 118 |
+
height: 30px;
|
| 119 |
+
gap: 2px;
|
| 120 |
+
padding: 7px;
|
| 121 |
+
border: 1px solid rgba(139, 124, 255, 0.3);
|
| 122 |
+
border-radius: 9px;
|
| 123 |
+
background: linear-gradient(145deg, rgba(139, 124, 255, 0.2), rgba(95, 140, 255, 0.08));
|
| 124 |
+
box-shadow: 0 8px 28px rgba(75, 60, 180, 0.2), inset 0 1px rgba(255, 255, 255, 0.13);
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
.brand-mark i {
|
| 128 |
+
display: block;
|
| 129 |
+
width: 2px;
|
| 130 |
+
border-radius: 3px;
|
| 131 |
+
background: #d8d4ff;
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
.brand-mark i:nth-child(1) { height: 6px; opacity: 0.62; }
|
| 135 |
+
.brand-mark i:nth-child(2) { height: 12px; }
|
| 136 |
+
.brand-mark i:nth-child(3) { height: 9px; opacity: 0.8; }
|
| 137 |
+
.brand-mark i:nth-child(4) { height: 14px; }
|
| 138 |
+
|
| 139 |
+
.header-meta {
|
| 140 |
+
display: flex;
|
| 141 |
+
align-items: center;
|
| 142 |
+
gap: 8px;
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
#masthead #model-badge {
|
| 146 |
+
margin: 0 !important;
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
.zerogpu-badge {
|
| 150 |
+
display: inline-flex;
|
| 151 |
+
align-items: center;
|
| 152 |
+
min-height: 30px;
|
| 153 |
+
padding: 5px 10px;
|
| 154 |
+
border: 1px solid var(--ms-border);
|
| 155 |
+
border-radius: 999px;
|
| 156 |
+
background: rgba(255, 255, 255, 0.025);
|
| 157 |
+
color: var(--ms-muted);
|
| 158 |
+
font-family: var(--ms-mono);
|
| 159 |
+
font-size: 0.65rem;
|
| 160 |
+
font-weight: 620;
|
| 161 |
+
letter-spacing: 0.07em;
|
| 162 |
+
text-transform: uppercase;
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
#intro {
|
| 166 |
+
padding: 30px 4px 34px;
|
| 167 |
+
border: 0 !important;
|
| 168 |
+
background: transparent !important;
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
.intro-copy {
|
| 172 |
+
max-width: 780px;
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
.eyebrow {
|
| 176 |
+
display: inline-block;
|
| 177 |
+
margin-bottom: 16px;
|
| 178 |
+
color: #aaa1ff;
|
| 179 |
+
font-family: var(--ms-mono);
|
| 180 |
+
font-size: 0.68rem;
|
| 181 |
+
font-weight: 670;
|
| 182 |
+
letter-spacing: 0.13em;
|
| 183 |
+
text-transform: uppercase;
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
.intro-copy h1 {
|
| 187 |
+
margin: 0;
|
| 188 |
+
color: var(--ms-text);
|
| 189 |
+
font-size: clamp(2.55rem, 7.2vw, 5.8rem);
|
| 190 |
+
font-weight: 590;
|
| 191 |
+
letter-spacing: -0.072em;
|
| 192 |
+
line-height: 0.94;
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
.intro-copy h1 em {
|
| 196 |
+
color: transparent;
|
| 197 |
+
background: linear-gradient(112deg, #d9d5ff 4%, #8b7cff 48%, #5f9cff 94%);
|
| 198 |
+
-webkit-background-clip: text;
|
| 199 |
+
background-clip: text;
|
| 200 |
+
font-style: normal;
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
.intro-copy p {
|
| 204 |
+
max-width: 630px;
|
| 205 |
+
margin: 23px 0 0;
|
| 206 |
+
color: var(--ms-text-soft);
|
| 207 |
+
font-size: clamp(0.96rem, 1.8vw, 1.12rem);
|
| 208 |
+
line-height: 1.68;
|
| 209 |
+
letter-spacing: -0.015em;
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
#studio-grid {
|
| 213 |
+
align-items: flex-start;
|
| 214 |
+
gap: 16px;
|
| 215 |
+
}
|
| 216 |
+
|
| 217 |
+
.section-heading {
|
| 218 |
+
display: flex;
|
| 219 |
+
align-items: center;
|
| 220 |
+
gap: 12px;
|
| 221 |
+
padding: 2px 2px 12px;
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
.section-heading .step-number {
|
| 225 |
+
color: var(--ms-faint);
|
| 226 |
+
font-family: var(--ms-mono);
|
| 227 |
+
font-size: 0.62rem;
|
| 228 |
+
letter-spacing: 0.08em;
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
.section-heading h2 {
|
| 232 |
+
margin: 0 0 4px;
|
| 233 |
+
color: var(--ms-text);
|
| 234 |
+
font-size: 0.9rem;
|
| 235 |
+
font-weight: 630;
|
| 236 |
+
letter-spacing: -0.02em;
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
.section-heading p {
|
| 240 |
+
margin: 0;
|
| 241 |
+
color: var(--ms-muted);
|
| 242 |
+
font-size: 0.68rem;
|
| 243 |
+
line-height: 1.4;
|
| 244 |
+
}
|
| 245 |
+
|
| 246 |
+
.cold-note {
|
| 247 |
+
display: flex;
|
| 248 |
+
align-items: flex-start;
|
| 249 |
+
gap: 8px;
|
| 250 |
+
margin: 3px 2px 0 !important;
|
| 251 |
+
color: var(--ms-faint);
|
| 252 |
+
font-size: 0.65rem;
|
| 253 |
+
line-height: 1.5;
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
.cold-note > span {
|
| 257 |
+
width: 6px;
|
| 258 |
+
height: 6px;
|
| 259 |
+
flex: none;
|
| 260 |
+
margin-top: 3px;
|
| 261 |
+
border-radius: 50%;
|
| 262 |
+
background: var(--ms-warning);
|
| 263 |
+
box-shadow: 0 0 8px rgba(244, 191, 117, 0.38);
|
| 264 |
+
}
|
| 265 |
+
|
| 266 |
+
.footnote {
|
| 267 |
+
display: flex;
|
| 268 |
+
justify-content: space-between;
|
| 269 |
+
gap: 16px;
|
| 270 |
+
padding: 14px 5px 0;
|
| 271 |
+
color: var(--ms-faint);
|
| 272 |
+
font-family: var(--ms-mono);
|
| 273 |
+
font-size: 0.58rem;
|
| 274 |
+
line-height: 1.5;
|
| 275 |
+
}
|
| 276 |
+
|
| 277 |
+
#masthead .prose {
|
| 278 |
+
max-width: 760px;
|
| 279 |
+
}
|
| 280 |
+
|
| 281 |
+
#masthead h1 {
|
| 282 |
+
margin: 0 0 13px;
|
| 283 |
+
color: var(--ms-text);
|
| 284 |
+
font-size: clamp(2.35rem, 6vw, 4.9rem);
|
| 285 |
+
font-weight: 620;
|
| 286 |
+
letter-spacing: -0.064em;
|
| 287 |
+
line-height: 0.96;
|
| 288 |
+
}
|
| 289 |
+
|
| 290 |
+
#masthead h1 strong,
|
| 291 |
+
#masthead .accent {
|
| 292 |
+
color: transparent;
|
| 293 |
+
background: linear-gradient(112deg, #ffffff 8%, #beb7ff 52%, #77a6ff 94%);
|
| 294 |
+
-webkit-background-clip: text;
|
| 295 |
+
background-clip: text;
|
| 296 |
+
}
|
| 297 |
+
|
| 298 |
+
#masthead p {
|
| 299 |
+
max-width: 610px;
|
| 300 |
+
margin: 0;
|
| 301 |
+
color: var(--ms-text-soft);
|
| 302 |
+
font-size: clamp(0.98rem, 1.6vw, 1.12rem);
|
| 303 |
+
line-height: 1.68;
|
| 304 |
+
letter-spacing: -0.012em;
|
| 305 |
+
}
|
| 306 |
+
|
| 307 |
+
#masthead a {
|
| 308 |
+
color: #d7d3ff;
|
| 309 |
+
text-decoration-color: rgba(215, 211, 255, 0.35);
|
| 310 |
+
text-underline-offset: 3px;
|
| 311 |
+
}
|
| 312 |
+
|
| 313 |
+
#model-badge {
|
| 314 |
+
display: inline-flex !important;
|
| 315 |
+
align-items: center;
|
| 316 |
+
width: max-content !important;
|
| 317 |
+
min-width: 0 !important;
|
| 318 |
+
min-height: 30px;
|
| 319 |
+
margin-bottom: 22px;
|
| 320 |
+
padding: 5px 11px !important;
|
| 321 |
+
border: 1px solid rgba(139, 124, 255, 0.22) !important;
|
| 322 |
+
border-radius: 999px !important;
|
| 323 |
+
background: rgba(139, 124, 255, 0.09) !important;
|
| 324 |
+
box-shadow: 0 0 32px rgba(111, 92, 255, 0.07) inset;
|
| 325 |
+
color: #cec8ff !important;
|
| 326 |
+
font-family: var(--ms-mono) !important;
|
| 327 |
+
font-size: 0.7rem !important;
|
| 328 |
+
font-weight: 650 !important;
|
| 329 |
+
letter-spacing: 0.08em !important;
|
| 330 |
+
line-height: 1 !important;
|
| 331 |
+
text-transform: uppercase;
|
| 332 |
+
}
|
| 333 |
+
|
| 334 |
+
#model-badge::before {
|
| 335 |
+
width: 6px;
|
| 336 |
+
height: 6px;
|
| 337 |
+
margin-right: 7px;
|
| 338 |
+
border-radius: 50%;
|
| 339 |
+
background: var(--ms-success);
|
| 340 |
+
box-shadow: 0 0 0 4px rgba(99, 216, 162, 0.1), 0 0 12px rgba(99, 216, 162, 0.62);
|
| 341 |
+
content: "";
|
| 342 |
+
}
|
| 343 |
+
|
| 344 |
+
#upload-card,
|
| 345 |
+
#result-panel {
|
| 346 |
+
position: relative;
|
| 347 |
+
border: 1px solid var(--ms-border) !important;
|
| 348 |
+
border-radius: var(--ms-radius-xl) !important;
|
| 349 |
+
background: linear-gradient(145deg, rgba(20, 23, 32, 0.93), rgba(13, 16, 22, 0.9)) !important;
|
| 350 |
+
box-shadow: var(--ms-shadow) !important;
|
| 351 |
+
backdrop-filter: blur(22px);
|
| 352 |
+
}
|
| 353 |
+
|
| 354 |
+
#upload-card {
|
| 355 |
+
padding: clamp(16px, 2.5vw, 26px) !important;
|
| 356 |
+
}
|
| 357 |
+
|
| 358 |
+
#upload-card::before,
|
| 359 |
+
#result-panel::before {
|
| 360 |
+
position: absolute;
|
| 361 |
+
top: 0;
|
| 362 |
+
right: 12%;
|
| 363 |
+
left: 12%;
|
| 364 |
+
height: 1px;
|
| 365 |
+
content: "";
|
| 366 |
+
pointer-events: none;
|
| 367 |
+
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.16), transparent);
|
| 368 |
+
}
|
| 369 |
+
|
| 370 |
+
#audio-input {
|
| 371 |
+
overflow: hidden;
|
| 372 |
+
min-height: 214px;
|
| 373 |
+
border: 1px dashed rgba(255, 255, 255, 0.16) !important;
|
| 374 |
+
border-radius: var(--ms-radius-lg) !important;
|
| 375 |
+
background: rgba(7, 9, 13, 0.46) !important;
|
| 376 |
+
transition: border-color 180ms ease, background 180ms ease, transform 180ms ease;
|
| 377 |
+
}
|
| 378 |
+
|
| 379 |
+
#audio-input:hover,
|
| 380 |
+
#audio-input:focus-within {
|
| 381 |
+
border-color: rgba(139, 124, 255, 0.54) !important;
|
| 382 |
+
background: rgba(139, 124, 255, 0.045) !important;
|
| 383 |
+
}
|
| 384 |
+
|
| 385 |
+
#audio-input .wrap,
|
| 386 |
+
#audio-input > div,
|
| 387 |
+
#audio-input .upload-container {
|
| 388 |
+
border: 0 !important;
|
| 389 |
+
background: transparent !important;
|
| 390 |
+
}
|
| 391 |
+
|
| 392 |
+
#audio-input .upload-container {
|
| 393 |
+
min-height: 190px;
|
| 394 |
+
}
|
| 395 |
+
|
| 396 |
+
#audio-input button,
|
| 397 |
+
#audio-input .upload-button {
|
| 398 |
+
border-color: var(--ms-border-strong) !important;
|
| 399 |
+
border-radius: 10px !important;
|
| 400 |
+
background: rgba(255, 255, 255, 0.055) !important;
|
| 401 |
+
color: var(--ms-text) !important;
|
| 402 |
+
box-shadow: none !important;
|
| 403 |
+
}
|
| 404 |
+
|
| 405 |
+
#audio-input button:hover,
|
| 406 |
+
#audio-input .upload-button:hover {
|
| 407 |
+
border-color: rgba(139, 124, 255, 0.42) !important;
|
| 408 |
+
background: rgba(139, 124, 255, 0.12) !important;
|
| 409 |
+
}
|
| 410 |
+
|
| 411 |
+
#audio-input audio {
|
| 412 |
+
width: 100%;
|
| 413 |
+
filter: saturate(0.75) contrast(0.96);
|
| 414 |
+
}
|
| 415 |
+
|
| 416 |
+
#audio-input label,
|
| 417 |
+
#audio-input .label-wrap,
|
| 418 |
+
#advanced-settings label,
|
| 419 |
+
#advanced-settings .label-wrap {
|
| 420 |
+
color: var(--ms-text-soft) !important;
|
| 421 |
+
font-size: 0.78rem !important;
|
| 422 |
+
font-weight: 570 !important;
|
| 423 |
+
letter-spacing: -0.005em;
|
| 424 |
+
}
|
| 425 |
+
|
| 426 |
+
#transcribe-button {
|
| 427 |
+
position: relative;
|
| 428 |
+
overflow: hidden;
|
| 429 |
+
width: 100%;
|
| 430 |
+
min-height: 50px;
|
| 431 |
+
margin-top: 4px;
|
| 432 |
+
border: 1px solid rgba(255, 255, 255, 0.15) !important;
|
| 433 |
+
border-radius: 13px !important;
|
| 434 |
+
background: linear-gradient(128deg, #7a69eb 0%, #675ad2 48%, #4f75d5 100%) !important;
|
| 435 |
+
box-shadow: 0 10px 30px rgba(76, 66, 181, 0.25), 0 1px 0 rgba(255, 255, 255, 0.2) inset !important;
|
| 436 |
+
color: #fff !important;
|
| 437 |
+
font-size: 0.91rem !important;
|
| 438 |
+
font-weight: 650 !important;
|
| 439 |
+
letter-spacing: -0.012em;
|
| 440 |
+
transition: transform 150ms ease, box-shadow 150ms ease, filter 150ms ease !important;
|
| 441 |
+
}
|
| 442 |
+
|
| 443 |
+
#transcribe-button::after {
|
| 444 |
+
position: absolute;
|
| 445 |
+
inset: 0;
|
| 446 |
+
content: "";
|
| 447 |
+
pointer-events: none;
|
| 448 |
+
opacity: 0;
|
| 449 |
+
background: linear-gradient(105deg, transparent 20%, rgba(255, 255, 255, 0.2) 48%, transparent 74%);
|
| 450 |
+
transform: translateX(-100%);
|
| 451 |
+
}
|
| 452 |
+
|
| 453 |
+
#transcribe-button:hover:not(:disabled) {
|
| 454 |
+
filter: brightness(1.08);
|
| 455 |
+
transform: translateY(-1px);
|
| 456 |
+
box-shadow: 0 14px 38px rgba(76, 66, 181, 0.32), 0 1px 0 rgba(255, 255, 255, 0.22) inset !important;
|
| 457 |
+
}
|
| 458 |
+
|
| 459 |
+
#transcribe-button:hover:not(:disabled)::after {
|
| 460 |
+
opacity: 1;
|
| 461 |
+
transform: translateX(100%);
|
| 462 |
+
transition: transform 650ms ease;
|
| 463 |
+
}
|
| 464 |
+
|
| 465 |
+
#transcribe-button:active:not(:disabled) {
|
| 466 |
+
transform: translateY(0) scale(0.995);
|
| 467 |
+
}
|
| 468 |
+
|
| 469 |
+
#transcribe-button:disabled,
|
| 470 |
+
#transcribe-button[aria-busy="true"] {
|
| 471 |
+
cursor: wait;
|
| 472 |
+
filter: saturate(0.55) brightness(0.8);
|
| 473 |
+
}
|
| 474 |
+
|
| 475 |
+
#advanced-settings {
|
| 476 |
+
overflow: hidden;
|
| 477 |
+
border: 1px solid var(--ms-border) !important;
|
| 478 |
+
border-radius: var(--ms-radius-md) !important;
|
| 479 |
+
background: rgba(8, 10, 14, 0.42) !important;
|
| 480 |
+
}
|
| 481 |
+
|
| 482 |
+
#advanced-settings > button,
|
| 483 |
+
#advanced-settings > .label-wrap,
|
| 484 |
+
#advanced-settings summary {
|
| 485 |
+
min-height: 44px;
|
| 486 |
+
padding-inline: 14px !important;
|
| 487 |
+
background: transparent !important;
|
| 488 |
+
color: var(--ms-text-soft) !important;
|
| 489 |
+
font-size: 0.8rem !important;
|
| 490 |
+
font-weight: 570 !important;
|
| 491 |
+
}
|
| 492 |
+
|
| 493 |
+
#advanced-settings > div:not(.label-wrap) {
|
| 494 |
+
border-color: var(--ms-border) !important;
|
| 495 |
+
background: transparent !important;
|
| 496 |
+
}
|
| 497 |
+
|
| 498 |
+
#advanced-settings input,
|
| 499 |
+
#advanced-settings textarea,
|
| 500 |
+
#advanced-settings select {
|
| 501 |
+
border-color: var(--ms-border) !important;
|
| 502 |
+
border-radius: 9px !important;
|
| 503 |
+
background: rgba(255, 255, 255, 0.035) !important;
|
| 504 |
+
color: var(--ms-text) !important;
|
| 505 |
+
}
|
| 506 |
+
|
| 507 |
+
#advanced-settings input[type="range"] {
|
| 508 |
+
accent-color: var(--ms-accent);
|
| 509 |
+
}
|
| 510 |
+
|
| 511 |
+
#advanced-settings input[type="checkbox"] {
|
| 512 |
+
accent-color: var(--ms-accent);
|
| 513 |
+
}
|
| 514 |
+
|
| 515 |
+
#result-panel {
|
| 516 |
+
min-width: 0;
|
| 517 |
+
padding: clamp(12px, 2vw, 20px) !important;
|
| 518 |
+
}
|
| 519 |
+
|
| 520 |
+
#visualizer-host {
|
| 521 |
+
min-width: 0;
|
| 522 |
+
border: 0 !important;
|
| 523 |
+
background: transparent !important;
|
| 524 |
+
}
|
| 525 |
+
|
| 526 |
+
#visualizer-host > .wrap,
|
| 527 |
+
#visualizer-host > div,
|
| 528 |
+
#visualizer-host .html-container {
|
| 529 |
+
min-width: 0;
|
| 530 |
+
padding: 0 !important;
|
| 531 |
+
border: 0 !important;
|
| 532 |
+
background: transparent !important;
|
| 533 |
+
}
|
| 534 |
+
|
| 535 |
+
#download-output {
|
| 536 |
+
overflow: hidden;
|
| 537 |
+
border: 1px solid var(--ms-border) !important;
|
| 538 |
+
border-radius: var(--ms-radius-md) !important;
|
| 539 |
+
background: rgba(255, 255, 255, 0.025) !important;
|
| 540 |
+
}
|
| 541 |
+
|
| 542 |
+
#download-output a,
|
| 543 |
+
#download-output button {
|
| 544 |
+
color: #d4d0ff !important;
|
| 545 |
+
}
|
| 546 |
+
|
| 547 |
+
/* Custom transcription visualizer */
|
| 548 |
+
.msv-root {
|
| 549 |
+
--roll-height: 334px;
|
| 550 |
+
display: grid;
|
| 551 |
+
min-width: 0;
|
| 552 |
+
gap: 16px;
|
| 553 |
+
color: var(--ms-text);
|
| 554 |
+
font-family: var(--ms-font);
|
| 555 |
+
}
|
| 556 |
+
|
| 557 |
+
.msv-root *,
|
| 558 |
+
.msv-root *::before,
|
| 559 |
+
.msv-root *::after {
|
| 560 |
+
box-sizing: border-box;
|
| 561 |
+
}
|
| 562 |
+
|
| 563 |
+
.msv-topbar {
|
| 564 |
+
display: flex;
|
| 565 |
+
align-items: flex-start;
|
| 566 |
+
justify-content: space-between;
|
| 567 |
+
gap: 20px;
|
| 568 |
+
padding: 3px 3px 0;
|
| 569 |
+
}
|
| 570 |
+
|
| 571 |
+
.msv-heading {
|
| 572 |
+
min-width: 0;
|
| 573 |
+
}
|
| 574 |
+
|
| 575 |
+
.msv-eyebrow {
|
| 576 |
+
display: flex;
|
| 577 |
+
align-items: center;
|
| 578 |
+
gap: 8px;
|
| 579 |
+
margin: 0 0 7px;
|
| 580 |
+
color: var(--ms-muted);
|
| 581 |
+
font-family: var(--ms-mono);
|
| 582 |
+
font-size: 0.65rem;
|
| 583 |
+
font-weight: 650;
|
| 584 |
+
letter-spacing: 0.09em;
|
| 585 |
+
line-height: 1;
|
| 586 |
+
text-transform: uppercase;
|
| 587 |
+
}
|
| 588 |
+
|
| 589 |
+
.msv-status-dot {
|
| 590 |
+
width: 7px;
|
| 591 |
+
height: 7px;
|
| 592 |
+
flex: none;
|
| 593 |
+
border-radius: 50%;
|
| 594 |
+
background: var(--ms-faint);
|
| 595 |
+
box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.035);
|
| 596 |
+
}
|
| 597 |
+
|
| 598 |
+
.msv-root[data-state="queued"] .msv-status-dot,
|
| 599 |
+
.msv-root[data-state="loading"] .msv-status-dot,
|
| 600 |
+
.msv-root[data-state="cold_start"] .msv-status-dot,
|
| 601 |
+
.msv-root[data-state="transcribing"] .msv-status-dot,
|
| 602 |
+
.msv-root[data-state="processing"] .msv-status-dot {
|
| 603 |
+
background: var(--ms-warning);
|
| 604 |
+
box-shadow: 0 0 0 4px rgba(244, 191, 117, 0.1), 0 0 14px rgba(244, 191, 117, 0.38);
|
| 605 |
+
animation: msv-pulse 1.7s ease-in-out infinite;
|
| 606 |
+
}
|
| 607 |
+
|
| 608 |
+
.msv-root[data-state="complete"] .msv-status-dot,
|
| 609 |
+
.msv-root[data-state="completed"] .msv-status-dot,
|
| 610 |
+
.msv-root[data-state="ready"] .msv-status-dot {
|
| 611 |
+
background: var(--ms-success);
|
| 612 |
+
box-shadow: 0 0 0 4px rgba(99, 216, 162, 0.09), 0 0 13px rgba(99, 216, 162, 0.32);
|
| 613 |
+
}
|
| 614 |
+
|
| 615 |
+
.msv-root[data-state="error"] .msv-status-dot,
|
| 616 |
+
.msv-root[data-state="failed"] .msv-status-dot {
|
| 617 |
+
background: var(--ms-danger);
|
| 618 |
+
box-shadow: 0 0 0 4px rgba(255, 117, 142, 0.1);
|
| 619 |
+
}
|
| 620 |
+
|
| 621 |
+
.msv-title {
|
| 622 |
+
overflow: hidden;
|
| 623 |
+
max-width: 640px;
|
| 624 |
+
margin: 0;
|
| 625 |
+
color: var(--ms-text);
|
| 626 |
+
font-size: clamp(1.05rem, 2vw, 1.35rem);
|
| 627 |
+
font-weight: 610;
|
| 628 |
+
letter-spacing: -0.032em;
|
| 629 |
+
line-height: 1.22;
|
| 630 |
+
text-overflow: ellipsis;
|
| 631 |
+
white-space: nowrap;
|
| 632 |
+
}
|
| 633 |
+
|
| 634 |
+
.msv-subtitle {
|
| 635 |
+
margin: 7px 0 0;
|
| 636 |
+
color: var(--ms-muted);
|
| 637 |
+
font-size: 0.76rem;
|
| 638 |
+
line-height: 1.45;
|
| 639 |
+
}
|
| 640 |
+
|
| 641 |
+
.msv-actions {
|
| 642 |
+
display: flex;
|
| 643 |
+
flex: none;
|
| 644 |
+
align-items: center;
|
| 645 |
+
gap: 7px;
|
| 646 |
+
}
|
| 647 |
+
|
| 648 |
+
.msv-button,
|
| 649 |
+
.msv-download,
|
| 650 |
+
.msv-track-button,
|
| 651 |
+
.msv-track-download {
|
| 652 |
+
display: inline-flex;
|
| 653 |
+
align-items: center;
|
| 654 |
+
justify-content: center;
|
| 655 |
+
border: 1px solid var(--ms-border) !important;
|
| 656 |
+
border-radius: 9px;
|
| 657 |
+
background: rgba(255, 255, 255, 0.035);
|
| 658 |
+
box-shadow: none;
|
| 659 |
+
color: var(--ms-text-soft) !important;
|
| 660 |
+
font: 560 0.73rem/1 var(--ms-font);
|
| 661 |
+
text-decoration: none !important;
|
| 662 |
+
cursor: pointer;
|
| 663 |
+
transition: color 140ms ease, border-color 140ms ease, background 140ms ease, transform 140ms ease;
|
| 664 |
+
}
|
| 665 |
+
|
| 666 |
+
.msv-button {
|
| 667 |
+
width: 34px;
|
| 668 |
+
height: 34px;
|
| 669 |
+
padding: 0;
|
| 670 |
+
}
|
| 671 |
+
|
| 672 |
+
.msv-button svg,
|
| 673 |
+
.msv-download svg,
|
| 674 |
+
.msv-track-download svg {
|
| 675 |
+
width: 15px;
|
| 676 |
+
height: 15px;
|
| 677 |
+
fill: none;
|
| 678 |
+
stroke: currentColor;
|
| 679 |
+
stroke-linecap: round;
|
| 680 |
+
stroke-linejoin: round;
|
| 681 |
+
stroke-width: 1.8;
|
| 682 |
+
}
|
| 683 |
+
|
| 684 |
+
.msv-button:hover:not(:disabled),
|
| 685 |
+
.msv-download:hover:not([aria-disabled="true"]),
|
| 686 |
+
.msv-track-button:hover,
|
| 687 |
+
.msv-track-download:hover:not([aria-disabled="true"]) {
|
| 688 |
+
border-color: rgba(139, 124, 255, 0.32) !important;
|
| 689 |
+
background: var(--ms-accent-soft);
|
| 690 |
+
color: #e4e1ff !important;
|
| 691 |
+
transform: translateY(-1px);
|
| 692 |
+
}
|
| 693 |
+
|
| 694 |
+
.msv-button:focus-visible,
|
| 695 |
+
.msv-download:focus-visible,
|
| 696 |
+
.msv-track-button:focus-visible,
|
| 697 |
+
.msv-track-download:focus-visible {
|
| 698 |
+
outline: 2px solid rgba(139, 124, 255, 0.7);
|
| 699 |
+
outline-offset: 2px;
|
| 700 |
+
}
|
| 701 |
+
|
| 702 |
+
.msv-button:disabled,
|
| 703 |
+
.msv-download[aria-disabled="true"],
|
| 704 |
+
.msv-track-download[aria-disabled="true"] {
|
| 705 |
+
pointer-events: none;
|
| 706 |
+
opacity: 0.34;
|
| 707 |
+
cursor: not-allowed;
|
| 708 |
+
}
|
| 709 |
+
|
| 710 |
+
.msv-button[aria-pressed="true"] {
|
| 711 |
+
border-color: rgba(139, 124, 255, 0.34) !important;
|
| 712 |
+
background: rgba(139, 124, 255, 0.18);
|
| 713 |
+
color: #e7e4ff !important;
|
| 714 |
+
}
|
| 715 |
+
|
| 716 |
+
.msv-download {
|
| 717 |
+
height: 34px;
|
| 718 |
+
gap: 7px;
|
| 719 |
+
padding: 0 11px;
|
| 720 |
+
}
|
| 721 |
+
|
| 722 |
+
.msv-progress-wrap {
|
| 723 |
+
display: grid;
|
| 724 |
+
gap: 9px;
|
| 725 |
+
padding: 12px 14px;
|
| 726 |
+
border: 1px solid var(--ms-border);
|
| 727 |
+
border-radius: 12px;
|
| 728 |
+
background: rgba(8, 10, 14, 0.46);
|
| 729 |
+
}
|
| 730 |
+
|
| 731 |
+
.msv-progress-wrap[hidden] {
|
| 732 |
+
display: none;
|
| 733 |
+
}
|
| 734 |
+
|
| 735 |
+
.msv-progress-meta {
|
| 736 |
+
display: flex;
|
| 737 |
+
align-items: center;
|
| 738 |
+
justify-content: space-between;
|
| 739 |
+
gap: 12px;
|
| 740 |
+
}
|
| 741 |
+
|
| 742 |
+
.msv-progress-copy {
|
| 743 |
+
overflow: hidden;
|
| 744 |
+
color: var(--ms-text-soft);
|
| 745 |
+
font-size: 0.74rem;
|
| 746 |
+
line-height: 1.3;
|
| 747 |
+
text-overflow: ellipsis;
|
| 748 |
+
white-space: nowrap;
|
| 749 |
+
}
|
| 750 |
+
|
| 751 |
+
.msv-progress-value {
|
| 752 |
+
flex: none;
|
| 753 |
+
color: var(--ms-muted);
|
| 754 |
+
font-family: var(--ms-mono);
|
| 755 |
+
font-size: 0.67rem;
|
| 756 |
+
}
|
| 757 |
+
|
| 758 |
+
.msv-progress-track {
|
| 759 |
+
position: relative;
|
| 760 |
+
overflow: hidden;
|
| 761 |
+
height: 4px;
|
| 762 |
+
border-radius: 999px;
|
| 763 |
+
background: rgba(255, 255, 255, 0.055);
|
| 764 |
+
}
|
| 765 |
+
|
| 766 |
+
.msv-progress-bar {
|
| 767 |
+
width: 0;
|
| 768 |
+
height: 100%;
|
| 769 |
+
border-radius: inherit;
|
| 770 |
+
background: linear-gradient(90deg, var(--ms-accent), #6b9bff);
|
| 771 |
+
box-shadow: 0 0 18px rgba(119, 102, 255, 0.45);
|
| 772 |
+
transition: width 360ms cubic-bezier(0.2, 0.8, 0.2, 1);
|
| 773 |
+
}
|
| 774 |
+
|
| 775 |
+
.msv-progress-wrap[data-indeterminate="true"] .msv-progress-bar {
|
| 776 |
+
width: 34% !important;
|
| 777 |
+
animation: msv-indeterminate 1.45s ease-in-out infinite;
|
| 778 |
+
}
|
| 779 |
+
|
| 780 |
+
.msv-stats {
|
| 781 |
+
display: grid;
|
| 782 |
+
grid-template-columns: repeat(4, minmax(0, 1fr));
|
| 783 |
+
overflow: hidden;
|
| 784 |
+
border: 1px solid var(--ms-border);
|
| 785 |
+
border-radius: 12px;
|
| 786 |
+
background: rgba(8, 10, 14, 0.35);
|
| 787 |
+
}
|
| 788 |
+
|
| 789 |
+
.msv-stat {
|
| 790 |
+
min-width: 0;
|
| 791 |
+
padding: 11px 13px;
|
| 792 |
+
}
|
| 793 |
+
|
| 794 |
+
.msv-stat + .msv-stat {
|
| 795 |
+
border-left: 1px solid var(--ms-border);
|
| 796 |
+
}
|
| 797 |
+
|
| 798 |
+
.msv-stat-label {
|
| 799 |
+
display: block;
|
| 800 |
+
margin-bottom: 4px;
|
| 801 |
+
color: var(--ms-faint);
|
| 802 |
+
font-family: var(--ms-mono);
|
| 803 |
+
font-size: 0.58rem;
|
| 804 |
+
font-weight: 650;
|
| 805 |
+
letter-spacing: 0.075em;
|
| 806 |
+
text-transform: uppercase;
|
| 807 |
+
}
|
| 808 |
+
|
| 809 |
+
.msv-stat-value {
|
| 810 |
+
display: block;
|
| 811 |
+
overflow: hidden;
|
| 812 |
+
color: var(--ms-text-soft);
|
| 813 |
+
font-size: 0.78rem;
|
| 814 |
+
font-weight: 570;
|
| 815 |
+
letter-spacing: -0.01em;
|
| 816 |
+
text-overflow: ellipsis;
|
| 817 |
+
white-space: nowrap;
|
| 818 |
+
}
|
| 819 |
+
|
| 820 |
+
.msv-roll-shell {
|
| 821 |
+
position: relative;
|
| 822 |
+
overflow: hidden;
|
| 823 |
+
min-width: 0;
|
| 824 |
+
border: 1px solid var(--ms-border);
|
| 825 |
+
border-radius: 14px;
|
| 826 |
+
background: #0a0d12;
|
| 827 |
+
box-shadow: 0 1px 0 rgba(255, 255, 255, 0.025) inset;
|
| 828 |
+
}
|
| 829 |
+
|
| 830 |
+
.msv-roll-labelbar {
|
| 831 |
+
display: flex;
|
| 832 |
+
align-items: center;
|
| 833 |
+
justify-content: space-between;
|
| 834 |
+
height: 38px;
|
| 835 |
+
padding: 0 12px 0 14px;
|
| 836 |
+
border-bottom: 1px solid var(--ms-border);
|
| 837 |
+
background: rgba(255, 255, 255, 0.018);
|
| 838 |
+
}
|
| 839 |
+
|
| 840 |
+
.msv-roll-label {
|
| 841 |
+
color: var(--ms-muted);
|
| 842 |
+
font-family: var(--ms-mono);
|
| 843 |
+
font-size: 0.62rem;
|
| 844 |
+
font-weight: 650;
|
| 845 |
+
letter-spacing: 0.075em;
|
| 846 |
+
text-transform: uppercase;
|
| 847 |
+
}
|
| 848 |
+
|
| 849 |
+
.msv-roll-hint {
|
| 850 |
+
color: var(--ms-faint);
|
| 851 |
+
font-size: 0.66rem;
|
| 852 |
+
}
|
| 853 |
+
|
| 854 |
+
.msv-roll-scroll {
|
| 855 |
+
position: relative;
|
| 856 |
+
overflow-x: auto;
|
| 857 |
+
overflow-y: hidden;
|
| 858 |
+
width: 100%;
|
| 859 |
+
height: var(--roll-height);
|
| 860 |
+
scrollbar-color: rgba(255, 255, 255, 0.13) transparent;
|
| 861 |
+
scrollbar-width: thin;
|
| 862 |
+
}
|
| 863 |
+
|
| 864 |
+
.msv-roll-scroll::-webkit-scrollbar {
|
| 865 |
+
height: 8px;
|
| 866 |
+
}
|
| 867 |
+
|
| 868 |
+
.msv-roll-scroll::-webkit-scrollbar-track {
|
| 869 |
+
background: transparent;
|
| 870 |
+
}
|
| 871 |
+
|
| 872 |
+
.msv-roll-scroll::-webkit-scrollbar-thumb {
|
| 873 |
+
border: 2px solid #0a0d12;
|
| 874 |
+
border-radius: 999px;
|
| 875 |
+
background: rgba(255, 255, 255, 0.14);
|
| 876 |
+
}
|
| 877 |
+
|
| 878 |
+
.msv-roll-stage {
|
| 879 |
+
position: relative;
|
| 880 |
+
width: 100%;
|
| 881 |
+
height: 100%;
|
| 882 |
+
min-width: 100%;
|
| 883 |
+
}
|
| 884 |
+
|
| 885 |
+
.msv-roll-canvas,
|
| 886 |
+
.msv-cursor-canvas {
|
| 887 |
+
position: absolute;
|
| 888 |
+
inset: 0;
|
| 889 |
+
display: block;
|
| 890 |
+
width: 100%;
|
| 891 |
+
height: 100%;
|
| 892 |
+
}
|
| 893 |
+
|
| 894 |
+
.msv-cursor-canvas {
|
| 895 |
+
z-index: 2;
|
| 896 |
+
pointer-events: none;
|
| 897 |
+
}
|
| 898 |
+
|
| 899 |
+
.msv-empty {
|
| 900 |
+
position: absolute;
|
| 901 |
+
z-index: 3;
|
| 902 |
+
inset: 0;
|
| 903 |
+
display: flex;
|
| 904 |
+
align-items: center;
|
| 905 |
+
justify-content: center;
|
| 906 |
+
padding: 24px;
|
| 907 |
+
pointer-events: none;
|
| 908 |
+
background: radial-gradient(circle at 50% 40%, rgba(139, 124, 255, 0.055), transparent 42%);
|
| 909 |
+
}
|
| 910 |
+
|
| 911 |
+
.msv-empty[hidden] {
|
| 912 |
+
display: none;
|
| 913 |
+
}
|
| 914 |
+
|
| 915 |
+
.msv-empty-inner {
|
| 916 |
+
max-width: 360px;
|
| 917 |
+
text-align: center;
|
| 918 |
+
}
|
| 919 |
+
|
| 920 |
+
.msv-empty-icon {
|
| 921 |
+
display: grid;
|
| 922 |
+
width: 42px;
|
| 923 |
+
height: 42px;
|
| 924 |
+
margin: 0 auto 13px;
|
| 925 |
+
border: 1px solid var(--ms-border);
|
| 926 |
+
border-radius: 12px;
|
| 927 |
+
background: rgba(255, 255, 255, 0.025);
|
| 928 |
+
color: var(--ms-muted);
|
| 929 |
+
place-items: center;
|
| 930 |
+
}
|
| 931 |
+
|
| 932 |
+
.msv-empty-icon svg {
|
| 933 |
+
width: 20px;
|
| 934 |
+
height: 20px;
|
| 935 |
+
fill: none;
|
| 936 |
+
stroke: currentColor;
|
| 937 |
+
stroke-linecap: round;
|
| 938 |
+
stroke-linejoin: round;
|
| 939 |
+
stroke-width: 1.55;
|
| 940 |
+
}
|
| 941 |
+
|
| 942 |
+
.msv-empty-title {
|
| 943 |
+
margin: 0;
|
| 944 |
+
color: var(--ms-text-soft);
|
| 945 |
+
font-size: 0.83rem;
|
| 946 |
+
font-weight: 580;
|
| 947 |
+
}
|
| 948 |
+
|
| 949 |
+
.msv-empty-copy {
|
| 950 |
+
margin: 6px 0 0;
|
| 951 |
+
color: var(--ms-faint);
|
| 952 |
+
font-size: 0.7rem;
|
| 953 |
+
line-height: 1.55;
|
| 954 |
+
}
|
| 955 |
+
|
| 956 |
+
.msv-root[data-state="queued"] .msv-empty-icon,
|
| 957 |
+
.msv-root[data-state="loading"] .msv-empty-icon,
|
| 958 |
+
.msv-root[data-state="cold_start"] .msv-empty-icon,
|
| 959 |
+
.msv-root[data-state="transcribing"] .msv-empty-icon,
|
| 960 |
+
.msv-root[data-state="processing"] .msv-empty-icon {
|
| 961 |
+
color: #bbb4ff;
|
| 962 |
+
animation: msv-float 2.5s ease-in-out infinite;
|
| 963 |
+
}
|
| 964 |
+
|
| 965 |
+
.msv-section-heading {
|
| 966 |
+
display: flex;
|
| 967 |
+
align-items: center;
|
| 968 |
+
justify-content: space-between;
|
| 969 |
+
padding: 2px 3px 0;
|
| 970 |
+
}
|
| 971 |
+
|
| 972 |
+
.msv-section-heading h3 {
|
| 973 |
+
margin: 0;
|
| 974 |
+
color: var(--ms-text-soft);
|
| 975 |
+
font-size: 0.76rem;
|
| 976 |
+
font-weight: 610;
|
| 977 |
+
letter-spacing: -0.01em;
|
| 978 |
+
}
|
| 979 |
+
|
| 980 |
+
.msv-track-count {
|
| 981 |
+
color: var(--ms-faint);
|
| 982 |
+
font-family: var(--ms-mono);
|
| 983 |
+
font-size: 0.62rem;
|
| 984 |
+
}
|
| 985 |
+
|
| 986 |
+
.msv-track-list {
|
| 987 |
+
display: grid;
|
| 988 |
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
| 989 |
+
gap: 8px;
|
| 990 |
+
}
|
| 991 |
+
|
| 992 |
+
.msv-track-empty {
|
| 993 |
+
grid-column: 1 / -1;
|
| 994 |
+
padding: 15px;
|
| 995 |
+
border: 1px dashed var(--ms-border);
|
| 996 |
+
border-radius: 11px;
|
| 997 |
+
color: var(--ms-faint);
|
| 998 |
+
font-size: 0.72rem;
|
| 999 |
+
text-align: center;
|
| 1000 |
+
}
|
| 1001 |
+
|
| 1002 |
+
.msv-track-card {
|
| 1003 |
+
display: grid;
|
| 1004 |
+
grid-template-columns: auto minmax(0, 1fr) auto;
|
| 1005 |
+
align-items: center;
|
| 1006 |
+
min-width: 0;
|
| 1007 |
+
min-height: 62px;
|
| 1008 |
+
gap: 11px;
|
| 1009 |
+
padding: 10px 11px;
|
| 1010 |
+
border: 1px solid var(--ms-border);
|
| 1011 |
+
border-radius: 11px;
|
| 1012 |
+
background: rgba(255, 255, 255, 0.022);
|
| 1013 |
+
transition: border-color 150ms ease, background 150ms ease, opacity 150ms ease;
|
| 1014 |
+
}
|
| 1015 |
+
|
| 1016 |
+
.msv-track-card:hover {
|
| 1017 |
+
border-color: var(--ms-border-strong);
|
| 1018 |
+
background: rgba(255, 255, 255, 0.032);
|
| 1019 |
+
}
|
| 1020 |
+
|
| 1021 |
+
.msv-track-card[data-muted="true"] {
|
| 1022 |
+
opacity: 0.48;
|
| 1023 |
+
}
|
| 1024 |
+
|
| 1025 |
+
.msv-track-card[data-inactive="true"] {
|
| 1026 |
+
opacity: 0.56;
|
| 1027 |
+
}
|
| 1028 |
+
|
| 1029 |
+
.msv-track-swatch {
|
| 1030 |
+
width: 9px;
|
| 1031 |
+
height: 34px;
|
| 1032 |
+
border-radius: 999px;
|
| 1033 |
+
background: var(--track-color, var(--ms-accent));
|
| 1034 |
+
box-shadow: 0 0 17px color-mix(in srgb, var(--track-color, var(--ms-accent)) 27%, transparent);
|
| 1035 |
+
}
|
| 1036 |
+
|
| 1037 |
+
.msv-track-copy {
|
| 1038 |
+
min-width: 0;
|
| 1039 |
+
}
|
| 1040 |
+
|
| 1041 |
+
.msv-track-name {
|
| 1042 |
+
overflow: hidden;
|
| 1043 |
+
margin: 0 0 5px;
|
| 1044 |
+
color: var(--ms-text-soft);
|
| 1045 |
+
font-size: 0.76rem;
|
| 1046 |
+
font-weight: 590;
|
| 1047 |
+
line-height: 1.15;
|
| 1048 |
+
text-overflow: ellipsis;
|
| 1049 |
+
white-space: nowrap;
|
| 1050 |
+
}
|
| 1051 |
+
|
| 1052 |
+
.msv-track-meta {
|
| 1053 |
+
overflow: hidden;
|
| 1054 |
+
color: var(--ms-faint);
|
| 1055 |
+
font-family: var(--ms-mono);
|
| 1056 |
+
font-size: 0.58rem;
|
| 1057 |
+
letter-spacing: 0.015em;
|
| 1058 |
+
text-overflow: ellipsis;
|
| 1059 |
+
white-space: nowrap;
|
| 1060 |
+
}
|
| 1061 |
+
|
| 1062 |
+
.msv-track-controls {
|
| 1063 |
+
display: flex;
|
| 1064 |
+
align-items: center;
|
| 1065 |
+
gap: 5px;
|
| 1066 |
+
}
|
| 1067 |
+
|
| 1068 |
+
.msv-track-button,
|
| 1069 |
+
.msv-track-download {
|
| 1070 |
+
width: 27px;
|
| 1071 |
+
height: 27px;
|
| 1072 |
+
padding: 0;
|
| 1073 |
+
font-family: var(--ms-mono);
|
| 1074 |
+
font-size: 0.62rem;
|
| 1075 |
+
}
|
| 1076 |
+
|
| 1077 |
+
.msv-track-button[aria-pressed="true"] {
|
| 1078 |
+
border-color: color-mix(in srgb, var(--track-color, var(--ms-accent)) 48%, transparent) !important;
|
| 1079 |
+
background: color-mix(in srgb, var(--track-color, var(--ms-accent)) 15%, transparent);
|
| 1080 |
+
color: #fff !important;
|
| 1081 |
+
}
|
| 1082 |
+
|
| 1083 |
+
.msv-track-download svg {
|
| 1084 |
+
width: 13px;
|
| 1085 |
+
height: 13px;
|
| 1086 |
+
}
|
| 1087 |
+
|
| 1088 |
+
.msv-sr-only {
|
| 1089 |
+
position: absolute !important;
|
| 1090 |
+
overflow: hidden !important;
|
| 1091 |
+
width: 1px !important;
|
| 1092 |
+
height: 1px !important;
|
| 1093 |
+
padding: 0 !important;
|
| 1094 |
+
border: 0 !important;
|
| 1095 |
+
margin: -1px !important;
|
| 1096 |
+
clip: rect(0, 0, 0, 0) !important;
|
| 1097 |
+
white-space: nowrap !important;
|
| 1098 |
+
}
|
| 1099 |
+
|
| 1100 |
+
@keyframes msv-pulse {
|
| 1101 |
+
0%, 100% { opacity: 0.62; transform: scale(0.88); }
|
| 1102 |
+
50% { opacity: 1; transform: scale(1); }
|
| 1103 |
+
}
|
| 1104 |
+
|
| 1105 |
+
@keyframes msv-indeterminate {
|
| 1106 |
+
0% { transform: translateX(-120%); }
|
| 1107 |
+
55% { transform: translateX(190%); }
|
| 1108 |
+
100% { transform: translateX(310%); }
|
| 1109 |
+
}
|
| 1110 |
+
|
| 1111 |
+
@keyframes msv-float {
|
| 1112 |
+
0%, 100% { transform: translateY(0); }
|
| 1113 |
+
50% { transform: translateY(-4px); }
|
| 1114 |
+
}
|
| 1115 |
+
|
| 1116 |
+
@media (max-width: 800px) {
|
| 1117 |
+
#app-shell {
|
| 1118 |
+
width: min(100% - 24px, 1180px);
|
| 1119 |
+
padding-top: 22px;
|
| 1120 |
+
}
|
| 1121 |
+
|
| 1122 |
+
#masthead {
|
| 1123 |
+
padding-bottom: 18px;
|
| 1124 |
+
}
|
| 1125 |
+
|
| 1126 |
+
#intro {
|
| 1127 |
+
padding: 22px 3px 27px;
|
| 1128 |
+
}
|
| 1129 |
+
|
| 1130 |
+
#masthead h1 {
|
| 1131 |
+
letter-spacing: -0.052em;
|
| 1132 |
+
}
|
| 1133 |
+
|
| 1134 |
+
.msv-track-list {
|
| 1135 |
+
grid-template-columns: 1fr;
|
| 1136 |
+
}
|
| 1137 |
+
|
| 1138 |
+
.msv-root {
|
| 1139 |
+
--roll-height: 308px;
|
| 1140 |
+
}
|
| 1141 |
+
}
|
| 1142 |
+
|
| 1143 |
+
@media (max-width: 600px) {
|
| 1144 |
+
#app-shell {
|
| 1145 |
+
width: min(100% - 16px, 1180px);
|
| 1146 |
+
padding-bottom: 30px;
|
| 1147 |
+
gap: 12px;
|
| 1148 |
+
}
|
| 1149 |
+
|
| 1150 |
+
#upload-card,
|
| 1151 |
+
#result-panel {
|
| 1152 |
+
border-radius: 17px !important;
|
| 1153 |
+
}
|
| 1154 |
+
|
| 1155 |
+
#upload-card {
|
| 1156 |
+
padding: 12px !important;
|
| 1157 |
+
}
|
| 1158 |
+
|
| 1159 |
+
#masthead .masthead-inner,
|
| 1160 |
+
.footnote {
|
| 1161 |
+
align-items: flex-start;
|
| 1162 |
+
flex-direction: column;
|
| 1163 |
+
}
|
| 1164 |
+
|
| 1165 |
+
.header-meta {
|
| 1166 |
+
width: 100%;
|
| 1167 |
+
}
|
| 1168 |
+
|
| 1169 |
+
.intro-copy h1 {
|
| 1170 |
+
font-size: clamp(2.65rem, 15vw, 4.2rem);
|
| 1171 |
+
letter-spacing: -0.062em;
|
| 1172 |
+
}
|
| 1173 |
+
|
| 1174 |
+
#result-panel {
|
| 1175 |
+
padding: 10px !important;
|
| 1176 |
+
}
|
| 1177 |
+
|
| 1178 |
+
#audio-input {
|
| 1179 |
+
min-height: 180px;
|
| 1180 |
+
}
|
| 1181 |
+
|
| 1182 |
+
.msv-root {
|
| 1183 |
+
--roll-height: 276px;
|
| 1184 |
+
gap: 12px;
|
| 1185 |
+
}
|
| 1186 |
+
|
| 1187 |
+
.msv-topbar {
|
| 1188 |
+
display: grid;
|
| 1189 |
+
gap: 12px;
|
| 1190 |
+
}
|
| 1191 |
+
|
| 1192 |
+
.msv-title {
|
| 1193 |
+
font-size: 1.05rem;
|
| 1194 |
+
}
|
| 1195 |
+
|
| 1196 |
+
.msv-actions {
|
| 1197 |
+
width: 100%;
|
| 1198 |
+
}
|
| 1199 |
+
|
| 1200 |
+
.msv-download {
|
| 1201 |
+
flex: 1;
|
| 1202 |
+
}
|
| 1203 |
+
|
| 1204 |
+
.msv-stats {
|
| 1205 |
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
| 1206 |
+
}
|
| 1207 |
+
|
| 1208 |
+
.msv-stat:nth-child(3) {
|
| 1209 |
+
border-left: 0;
|
| 1210 |
+
border-top: 1px solid var(--ms-border);
|
| 1211 |
+
}
|
| 1212 |
+
|
| 1213 |
+
.msv-stat:nth-child(4) {
|
| 1214 |
+
border-top: 1px solid var(--ms-border);
|
| 1215 |
+
}
|
| 1216 |
+
|
| 1217 |
+
.msv-roll-hint {
|
| 1218 |
+
display: none;
|
| 1219 |
+
}
|
| 1220 |
+
|
| 1221 |
+
.msv-progress-copy {
|
| 1222 |
+
max-width: calc(100vw - 118px);
|
| 1223 |
+
}
|
| 1224 |
+
}
|
| 1225 |
+
|
| 1226 |
+
@media (prefers-reduced-motion: reduce) {
|
| 1227 |
+
*,
|
| 1228 |
+
*::before,
|
| 1229 |
+
*::after {
|
| 1230 |
+
scroll-behavior: auto !important;
|
| 1231 |
+
animation-duration: 0.01ms !important;
|
| 1232 |
+
animation-iteration-count: 1 !important;
|
| 1233 |
+
transition-duration: 0.01ms !important;
|
| 1234 |
+
}
|
| 1235 |
+
}
|
assets/visualizer.html
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<section class="msv-root" data-state="idle" aria-label="MuScriptor transcription result">
|
| 2 |
+
<header class="msv-topbar">
|
| 3 |
+
<div class="msv-heading">
|
| 4 |
+
<p class="msv-eyebrow">
|
| 5 |
+
<span class="msv-status-dot" aria-hidden="true"></span>
|
| 6 |
+
<span data-field="state-label">Ready for audio</span>
|
| 7 |
+
</p>
|
| 8 |
+
<h2 class="msv-title" data-field="audio-name">Your transcription</h2>
|
| 9 |
+
<p class="msv-subtitle" data-field="status">Upload a recording to reveal its notes and instruments.</p>
|
| 10 |
+
</div>
|
| 11 |
+
|
| 12 |
+
<div class="msv-actions" aria-label="Playback and download controls">
|
| 13 |
+
<button class="msv-button" type="button" data-action="play" aria-label="Play transcription" title="Play transcription" disabled>
|
| 14 |
+
<svg viewBox="0 0 20 20" aria-hidden="true">
|
| 15 |
+
<path d="M6.8 4.7 15 10l-8.2 5.3V4.7Z"></path>
|
| 16 |
+
</svg>
|
| 17 |
+
</button>
|
| 18 |
+
<button class="msv-button" type="button" data-action="stop" aria-label="Stop playback" title="Stop playback" disabled>
|
| 19 |
+
<svg viewBox="0 0 20 20" aria-hidden="true">
|
| 20 |
+
<rect x="5.5" y="5.5" width="9" height="9" rx="1"></rect>
|
| 21 |
+
</svg>
|
| 22 |
+
</button>
|
| 23 |
+
<a class="msv-download" data-action="download-full" aria-label="Download full MIDI" aria-disabled="true" tabindex="-1">
|
| 24 |
+
<svg viewBox="0 0 20 20" aria-hidden="true">
|
| 25 |
+
<path d="M10 3.5v9m0 0 3.2-3.2M10 12.5 6.8 9.3M4.5 15.7h11"></path>
|
| 26 |
+
</svg>
|
| 27 |
+
<span>Full MIDI</span>
|
| 28 |
+
</a>
|
| 29 |
+
</div>
|
| 30 |
+
</header>
|
| 31 |
+
|
| 32 |
+
<div class="msv-progress-wrap" data-region="progress" hidden>
|
| 33 |
+
<div class="msv-progress-meta">
|
| 34 |
+
<span class="msv-progress-copy" data-field="progress-copy">Preparing inference…</span>
|
| 35 |
+
<span class="msv-progress-value" data-field="progress-value">0%</span>
|
| 36 |
+
</div>
|
| 37 |
+
<div class="msv-progress-track" role="progressbar" aria-label="Transcription progress" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0">
|
| 38 |
+
<div class="msv-progress-bar" data-field="progress-bar"></div>
|
| 39 |
+
</div>
|
| 40 |
+
</div>
|
| 41 |
+
|
| 42 |
+
<div class="msv-stats" aria-label="Transcription statistics">
|
| 43 |
+
<div class="msv-stat">
|
| 44 |
+
<span class="msv-stat-label">Duration</span>
|
| 45 |
+
<span class="msv-stat-value" data-field="duration">—</span>
|
| 46 |
+
</div>
|
| 47 |
+
<div class="msv-stat">
|
| 48 |
+
<span class="msv-stat-label">Notes</span>
|
| 49 |
+
<span class="msv-stat-value" data-field="note-count">—</span>
|
| 50 |
+
</div>
|
| 51 |
+
<div class="msv-stat">
|
| 52 |
+
<span class="msv-stat-label">Tracks</span>
|
| 53 |
+
<span class="msv-stat-value" data-field="track-count">—</span>
|
| 54 |
+
</div>
|
| 55 |
+
<div class="msv-stat">
|
| 56 |
+
<span class="msv-stat-label">Elapsed</span>
|
| 57 |
+
<span class="msv-stat-value" data-field="elapsed">—</span>
|
| 58 |
+
</div>
|
| 59 |
+
</div>
|
| 60 |
+
|
| 61 |
+
<div class="msv-roll-shell">
|
| 62 |
+
<div class="msv-roll-labelbar">
|
| 63 |
+
<span class="msv-roll-label">Piano roll</span>
|
| 64 |
+
<span class="msv-roll-hint">Scroll horizontally to explore</span>
|
| 65 |
+
</div>
|
| 66 |
+
<div class="msv-roll-scroll" data-region="roll-scroll" tabindex="0" aria-label="Scrollable piano roll">
|
| 67 |
+
<div class="msv-roll-stage" data-region="roll-stage">
|
| 68 |
+
<canvas class="msv-roll-canvas" data-canvas="notes" role="img" aria-label="Color-coded transcription piano roll"></canvas>
|
| 69 |
+
<canvas class="msv-cursor-canvas" data-canvas="cursor" aria-hidden="true"></canvas>
|
| 70 |
+
</div>
|
| 71 |
+
<div class="msv-empty" data-region="empty">
|
| 72 |
+
<div class="msv-empty-inner">
|
| 73 |
+
<div class="msv-empty-icon" aria-hidden="true">
|
| 74 |
+
<svg viewBox="0 0 24 24">
|
| 75 |
+
<path d="M5 15.5V8.7m3 9.1V5.6m3 11.1V9.2m3 7.8V3.8m3 12.4V7.4m3 7.3v-4.1"></path>
|
| 76 |
+
</svg>
|
| 77 |
+
</div>
|
| 78 |
+
<p class="msv-empty-title" data-field="empty-title">A score will appear here</p>
|
| 79 |
+
<p class="msv-empty-copy" data-field="empty-copy">MuScriptor separates notes into individual, color-coded instrument tracks.</p>
|
| 80 |
+
</div>
|
| 81 |
+
</div>
|
| 82 |
+
</div>
|
| 83 |
+
</div>
|
| 84 |
+
|
| 85 |
+
<div class="msv-section-heading">
|
| 86 |
+
<h3>Instrument tracks</h3>
|
| 87 |
+
<span class="msv-track-count" data-field="track-summary">No tracks yet</span>
|
| 88 |
+
</div>
|
| 89 |
+
<div class="msv-track-list" data-region="tracks" aria-label="Instrument tracks">
|
| 90 |
+
<div class="msv-track-empty">Tracks will appear after transcription.</div>
|
| 91 |
+
</div>
|
| 92 |
+
|
| 93 |
+
<p class="msv-sr-only" data-region="announcer" role="status" aria-live="polite"></p>
|
| 94 |
+
</section>
|
assets/visualizer.js
ADDED
|
@@ -0,0 +1,898 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const root = element.querySelector(".msv-root");
|
| 2 |
+
|
| 3 |
+
if (!root) {
|
| 4 |
+
throw new Error("MuScriptor visualizer template is missing .msv-root");
|
| 5 |
+
}
|
| 6 |
+
|
| 7 |
+
const $ = (selector) => root.querySelector(selector);
|
| 8 |
+
const notesCanvas = $('[data-canvas="notes"]');
|
| 9 |
+
const cursorCanvas = $('[data-canvas="cursor"]');
|
| 10 |
+
const rollScroll = $('[data-region="roll-scroll"]');
|
| 11 |
+
const rollStage = $('[data-region="roll-stage"]');
|
| 12 |
+
const emptyRegion = $('[data-region="empty"]');
|
| 13 |
+
const tracksRegion = $('[data-region="tracks"]');
|
| 14 |
+
const announcer = $('[data-region="announcer"]');
|
| 15 |
+
const progressRegion = $('[data-region="progress"]');
|
| 16 |
+
const progressTrack = progressRegion.querySelector('[role="progressbar"]');
|
| 17 |
+
const playButton = $('[data-action="play"]');
|
| 18 |
+
const stopButton = $('[data-action="stop"]');
|
| 19 |
+
const fullDownload = $('[data-action="download-full"]');
|
| 20 |
+
|
| 21 |
+
const fallbackColors = [
|
| 22 |
+
"#8b7cff",
|
| 23 |
+
"#5f9cff",
|
| 24 |
+
"#54c7a4",
|
| 25 |
+
"#f2b96f",
|
| 26 |
+
"#e878a6",
|
| 27 |
+
"#58c4dc",
|
| 28 |
+
"#b48cf2",
|
| 29 |
+
"#91c96f",
|
| 30 |
+
];
|
| 31 |
+
|
| 32 |
+
const stateLabels = {
|
| 33 |
+
idle: "Ready for audio",
|
| 34 |
+
queued: "ZeroGPU queued",
|
| 35 |
+
loading: "Loading model",
|
| 36 |
+
cold_start: "Starting ZeroGPU",
|
| 37 |
+
transcribing: "Transcribing",
|
| 38 |
+
processing: "Transcribing",
|
| 39 |
+
complete: "Transcription complete",
|
| 40 |
+
completed: "Transcription complete",
|
| 41 |
+
ready: "Transcription complete",
|
| 42 |
+
error: "Transcription failed",
|
| 43 |
+
failed: "Transcription failed",
|
| 44 |
+
};
|
| 45 |
+
|
| 46 |
+
const activeStates = new Set(["queued", "loading", "cold_start", "transcribing", "processing"]);
|
| 47 |
+
const terminalStates = new Set(["complete", "completed", "ready", "error", "failed", "idle"]);
|
| 48 |
+
const trackState = new Map();
|
| 49 |
+
let snapshot = normalizePayload({});
|
| 50 |
+
let geometry = null;
|
| 51 |
+
let playback = null;
|
| 52 |
+
let resizeFrame = 0;
|
| 53 |
+
let announceTimer = 0;
|
| 54 |
+
|
| 55 |
+
function asFinite(value, fallback = 0) {
|
| 56 |
+
const number = Number(value);
|
| 57 |
+
return Number.isFinite(number) ? number : fallback;
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
function clamp(value, minimum, maximum) {
|
| 61 |
+
return Math.min(maximum, Math.max(minimum, value));
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
function safeColor(value, index) {
|
| 65 |
+
const candidate = typeof value === "string" ? value.trim() : "";
|
| 66 |
+
if (candidate && typeof CSS !== "undefined" && CSS.supports("color", candidate)) {
|
| 67 |
+
return candidate;
|
| 68 |
+
}
|
| 69 |
+
return fallbackColors[index % fallbackColors.length];
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
function titleFromId(value) {
|
| 73 |
+
return String(value || "Instrument")
|
| 74 |
+
.replace(/[_-]+/g, " ")
|
| 75 |
+
.replace(/\b\w/g, (letter) => letter.toUpperCase());
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
function normalizeNote(note) {
|
| 79 |
+
const pitch = clamp(Math.round(asFinite(note && note.pitch, 60)), 0, 127);
|
| 80 |
+
const start = Math.max(0, asFinite(note && note.start, 0));
|
| 81 |
+
const end = Math.max(start + 0.015, asFinite(note && note.end, start + 0.12));
|
| 82 |
+
const velocity = clamp(Math.round(asFinite(note && note.velocity, 90)), 1, 127);
|
| 83 |
+
return { pitch, start, end, velocity };
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
function normalizeTrack(track, index) {
|
| 87 |
+
const id = String(track && track.id != null ? track.id : `track-${index + 1}`);
|
| 88 |
+
const notes = Array.isArray(track && track.notes)
|
| 89 |
+
? track.notes.filter((note) => note && typeof note === "object").map(normalizeNote)
|
| 90 |
+
: [];
|
| 91 |
+
notes.sort((left, right) => left.start - right.start || left.pitch - right.pitch);
|
| 92 |
+
return {
|
| 93 |
+
id,
|
| 94 |
+
name: String((track && track.name) || titleFromId(id)),
|
| 95 |
+
color: safeColor(track && track.color, index),
|
| 96 |
+
note_count: Math.max(0, Math.round(asFinite(track && track.note_count, notes.length))),
|
| 97 |
+
program: track && track.program != null ? Math.round(asFinite(track.program, 0)) : null,
|
| 98 |
+
is_drum: Boolean(track && track.is_drum),
|
| 99 |
+
midi: typeof (track && track.midi) === "string" ? track.midi : "",
|
| 100 |
+
notes,
|
| 101 |
+
};
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
function parseIncoming(value) {
|
| 105 |
+
if (value && typeof value === "object") {
|
| 106 |
+
return value;
|
| 107 |
+
}
|
| 108 |
+
if (typeof value !== "string" || !value.trim()) {
|
| 109 |
+
return {};
|
| 110 |
+
}
|
| 111 |
+
try {
|
| 112 |
+
const parsed = JSON.parse(value);
|
| 113 |
+
return parsed && typeof parsed === "object" ? parsed : {};
|
| 114 |
+
} catch (error) {
|
| 115 |
+
return {
|
| 116 |
+
state: "error",
|
| 117 |
+
status: "The transcription response could not be read.",
|
| 118 |
+
_parse_error: true,
|
| 119 |
+
};
|
| 120 |
+
}
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
function normalizePayload(raw) {
|
| 124 |
+
const tracks = Array.isArray(raw && raw.tracks) ? raw.tracks.map(normalizeTrack) : [];
|
| 125 |
+
const notesFromTracks = tracks.reduce((total, track) => total + track.notes.length, 0);
|
| 126 |
+
const noteEnd = tracks.reduce(
|
| 127 |
+
(latest, track) => track.notes.reduce((trackEnd, note) => Math.max(trackEnd, note.end), latest),
|
| 128 |
+
0,
|
| 129 |
+
);
|
| 130 |
+
const suppliedDuration = Math.max(0, asFinite(raw && raw.duration, 0));
|
| 131 |
+
const inferredState = tracks.length || (raw && raw.full_midi) ? "complete" : "idle";
|
| 132 |
+
const state = String((raw && raw.state) || inferredState)
|
| 133 |
+
.trim()
|
| 134 |
+
.toLowerCase()
|
| 135 |
+
.replace(/[\s-]+/g, "_");
|
| 136 |
+
const rawProgress = Number(raw && raw.progress);
|
| 137 |
+
let progress = Number.isFinite(rawProgress) ? rawProgress : null;
|
| 138 |
+
if (progress != null && progress > 1) {
|
| 139 |
+
progress /= 100;
|
| 140 |
+
}
|
| 141 |
+
if (progress != null) {
|
| 142 |
+
progress = clamp(progress, 0, 1);
|
| 143 |
+
}
|
| 144 |
+
return {
|
| 145 |
+
state,
|
| 146 |
+
status: typeof (raw && raw.status) === "string" ? raw.status.trim() : "",
|
| 147 |
+
progress,
|
| 148 |
+
audio_name: typeof (raw && raw.audio_name) === "string" ? raw.audio_name.trim() : "",
|
| 149 |
+
elapsed: Math.max(0, asFinite(raw && raw.elapsed, 0)),
|
| 150 |
+
duration: Math.max(suppliedDuration, noteEnd),
|
| 151 |
+
note_count: Math.max(0, Math.round(asFinite(raw && raw.note_count, notesFromTracks))),
|
| 152 |
+
full_midi: typeof (raw && raw.full_midi) === "string" ? raw.full_midi : "",
|
| 153 |
+
tracks,
|
| 154 |
+
_parse_error: Boolean(raw && raw._parse_error),
|
| 155 |
+
};
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
function stateCopy(payload) {
|
| 159 |
+
if (payload.status) {
|
| 160 |
+
return payload.status;
|
| 161 |
+
}
|
| 162 |
+
switch (payload.state) {
|
| 163 |
+
case "queued":
|
| 164 |
+
return "Waiting briefly for a ZeroGPU worker…";
|
| 165 |
+
case "loading":
|
| 166 |
+
case "cold_start":
|
| 167 |
+
return "Waking the model. A cold start can take a little longer.";
|
| 168 |
+
case "transcribing":
|
| 169 |
+
case "processing":
|
| 170 |
+
return "Listening in five-second windows and separating instruments…";
|
| 171 |
+
case "complete":
|
| 172 |
+
case "completed":
|
| 173 |
+
case "ready":
|
| 174 |
+
return "Every color is an independently playable instrument track.";
|
| 175 |
+
case "error":
|
| 176 |
+
case "failed":
|
| 177 |
+
return "Something interrupted the transcription. Please try again.";
|
| 178 |
+
default:
|
| 179 |
+
return "Upload a recording to reveal its notes and instruments.";
|
| 180 |
+
}
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
function formatClock(seconds) {
|
| 184 |
+
if (!Number.isFinite(Number(seconds)) || Number(seconds) <= 0) {
|
| 185 |
+
return "—";
|
| 186 |
+
}
|
| 187 |
+
const total = Math.round(Number(seconds));
|
| 188 |
+
const minutes = Math.floor(total / 60);
|
| 189 |
+
const remainder = total % 60;
|
| 190 |
+
return `${minutes}:${String(remainder).padStart(2, "0")}`;
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
function formatElapsed(seconds) {
|
| 194 |
+
if (!Number.isFinite(Number(seconds)) || Number(seconds) <= 0) {
|
| 195 |
+
return "—";
|
| 196 |
+
}
|
| 197 |
+
if (Number(seconds) < 60) {
|
| 198 |
+
return `${Number(seconds).toFixed(Number(seconds) < 10 ? 1 : 0)}s`;
|
| 199 |
+
}
|
| 200 |
+
return formatClock(seconds);
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
function fileStem(name) {
|
| 204 |
+
const raw = String(name || "muscriptor").replace(/\.[a-z0-9]{1,5}$/i, "");
|
| 205 |
+
const safe = raw
|
| 206 |
+
.normalize("NFKD")
|
| 207 |
+
.replace(/[^a-z0-9]+/gi, "-")
|
| 208 |
+
.replace(/^-+|-+$/g, "")
|
| 209 |
+
.toLowerCase();
|
| 210 |
+
return safe || "muscriptor";
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
function normalizeMidiHref(value) {
|
| 214 |
+
const midi = String(value || "").trim();
|
| 215 |
+
if (!midi) {
|
| 216 |
+
return "";
|
| 217 |
+
}
|
| 218 |
+
if (/^(data:|blob:|https?:\/\/|\/)/i.test(midi)) {
|
| 219 |
+
return midi;
|
| 220 |
+
}
|
| 221 |
+
return `data:audio/midi;base64,${midi.replace(/\s+/g, "")}`;
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
function setText(selector, value) {
|
| 225 |
+
const target = $(selector);
|
| 226 |
+
if (target) {
|
| 227 |
+
target.textContent = value;
|
| 228 |
+
}
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
function announce(message) {
|
| 232 |
+
window.clearTimeout(announceTimer);
|
| 233 |
+
announcer.textContent = "";
|
| 234 |
+
announceTimer = window.setTimeout(() => {
|
| 235 |
+
announcer.textContent = message;
|
| 236 |
+
}, 60);
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
function hasSolo() {
|
| 240 |
+
return snapshot.tracks.some((track) => trackState.get(track.id)?.solo);
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
function isAudible(trackId) {
|
| 244 |
+
const state = trackState.get(trackId) || { solo: false, muted: false };
|
| 245 |
+
return !state.muted && (!hasSolo() || state.solo);
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
+
function syncTrackState(tracks) {
|
| 249 |
+
const currentIds = new Set(tracks.map((track) => track.id));
|
| 250 |
+
for (const id of trackState.keys()) {
|
| 251 |
+
if (!currentIds.has(id)) {
|
| 252 |
+
trackState.delete(id);
|
| 253 |
+
}
|
| 254 |
+
}
|
| 255 |
+
tracks.forEach((track) => {
|
| 256 |
+
if (!trackState.has(track.id)) {
|
| 257 |
+
trackState.set(track.id, { solo: false, muted: false });
|
| 258 |
+
}
|
| 259 |
+
});
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
function updateDownload(anchor, midi, filename) {
|
| 263 |
+
const href = normalizeMidiHref(midi);
|
| 264 |
+
if (href) {
|
| 265 |
+
anchor.href = href;
|
| 266 |
+
anchor.download = filename;
|
| 267 |
+
anchor.removeAttribute("aria-disabled");
|
| 268 |
+
anchor.removeAttribute("tabindex");
|
| 269 |
+
} else {
|
| 270 |
+
anchor.removeAttribute("href");
|
| 271 |
+
anchor.removeAttribute("download");
|
| 272 |
+
anchor.setAttribute("aria-disabled", "true");
|
| 273 |
+
anchor.setAttribute("tabindex", "-1");
|
| 274 |
+
}
|
| 275 |
+
}
|
| 276 |
+
|
| 277 |
+
function trackMeta(track) {
|
| 278 |
+
const notes = `${track.note_count.toLocaleString()} ${track.note_count === 1 ? "note" : "notes"}`;
|
| 279 |
+
if (track.is_drum) {
|
| 280 |
+
return `${notes} · percussion`;
|
| 281 |
+
}
|
| 282 |
+
if (track.program != null) {
|
| 283 |
+
return `${notes} · program ${track.program}`;
|
| 284 |
+
}
|
| 285 |
+
return notes;
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
function downloadIcon() {
|
| 289 |
+
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
| 290 |
+
svg.setAttribute("viewBox", "0 0 20 20");
|
| 291 |
+
svg.setAttribute("aria-hidden", "true");
|
| 292 |
+
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
| 293 |
+
path.setAttribute("d", "M10 3.5v9m0 0 3.2-3.2M10 12.5 6.8 9.3M4.5 15.7h11");
|
| 294 |
+
svg.append(path);
|
| 295 |
+
return svg;
|
| 296 |
+
}
|
| 297 |
+
|
| 298 |
+
function renderTracks() {
|
| 299 |
+
tracksRegion.replaceChildren();
|
| 300 |
+
const stem = fileStem(snapshot.audio_name);
|
| 301 |
+
|
| 302 |
+
if (!snapshot.tracks.length) {
|
| 303 |
+
const empty = document.createElement("div");
|
| 304 |
+
empty.className = "msv-track-empty";
|
| 305 |
+
empty.textContent = activeStates.has(snapshot.state)
|
| 306 |
+
? "Instrument tracks will arrive as transcription completes."
|
| 307 |
+
: "Tracks will appear after transcription.";
|
| 308 |
+
tracksRegion.append(empty);
|
| 309 |
+
return;
|
| 310 |
+
}
|
| 311 |
+
|
| 312 |
+
snapshot.tracks.forEach((track) => {
|
| 313 |
+
const state = trackState.get(track.id);
|
| 314 |
+
const card = document.createElement("article");
|
| 315 |
+
card.className = "msv-track-card";
|
| 316 |
+
card.dataset.trackId = track.id;
|
| 317 |
+
card.dataset.muted = String(Boolean(state.muted));
|
| 318 |
+
card.dataset.inactive = String(!isAudible(track.id));
|
| 319 |
+
card.style.setProperty("--track-color", track.color);
|
| 320 |
+
card.setAttribute("aria-label", `${track.name}, ${trackMeta(track)}`);
|
| 321 |
+
|
| 322 |
+
const swatch = document.createElement("span");
|
| 323 |
+
swatch.className = "msv-track-swatch";
|
| 324 |
+
swatch.setAttribute("aria-hidden", "true");
|
| 325 |
+
|
| 326 |
+
const copy = document.createElement("div");
|
| 327 |
+
copy.className = "msv-track-copy";
|
| 328 |
+
const name = document.createElement("h4");
|
| 329 |
+
name.className = "msv-track-name";
|
| 330 |
+
name.textContent = track.name;
|
| 331 |
+
const meta = document.createElement("div");
|
| 332 |
+
meta.className = "msv-track-meta";
|
| 333 |
+
meta.textContent = trackMeta(track);
|
| 334 |
+
copy.append(name, meta);
|
| 335 |
+
|
| 336 |
+
const controls = document.createElement("div");
|
| 337 |
+
controls.className = "msv-track-controls";
|
| 338 |
+
controls.setAttribute("aria-label", `${track.name} controls`);
|
| 339 |
+
|
| 340 |
+
const solo = document.createElement("button");
|
| 341 |
+
solo.className = "msv-track-button";
|
| 342 |
+
solo.type = "button";
|
| 343 |
+
solo.textContent = "S";
|
| 344 |
+
solo.title = `Solo ${track.name}`;
|
| 345 |
+
solo.setAttribute("aria-label", `Solo ${track.name}`);
|
| 346 |
+
solo.setAttribute("aria-pressed", String(Boolean(state.solo)));
|
| 347 |
+
solo.addEventListener("click", () => {
|
| 348 |
+
state.solo = !state.solo;
|
| 349 |
+
renderTracks();
|
| 350 |
+
refreshTrackMix();
|
| 351 |
+
drawPianoRoll();
|
| 352 |
+
announce(`${track.name} solo ${state.solo ? "on" : "off"}.`);
|
| 353 |
+
});
|
| 354 |
+
|
| 355 |
+
const mute = document.createElement("button");
|
| 356 |
+
mute.className = "msv-track-button";
|
| 357 |
+
mute.type = "button";
|
| 358 |
+
mute.textContent = "M";
|
| 359 |
+
mute.title = `Mute ${track.name}`;
|
| 360 |
+
mute.setAttribute("aria-label", `Mute ${track.name}`);
|
| 361 |
+
mute.setAttribute("aria-pressed", String(Boolean(state.muted)));
|
| 362 |
+
mute.addEventListener("click", () => {
|
| 363 |
+
state.muted = !state.muted;
|
| 364 |
+
renderTracks();
|
| 365 |
+
refreshTrackMix();
|
| 366 |
+
drawPianoRoll();
|
| 367 |
+
announce(`${track.name} mute ${state.muted ? "on" : "off"}.`);
|
| 368 |
+
});
|
| 369 |
+
|
| 370 |
+
const download = document.createElement("a");
|
| 371 |
+
download.className = "msv-track-download";
|
| 372 |
+
download.title = `Download ${track.name} MIDI`;
|
| 373 |
+
download.setAttribute("aria-label", `Download ${track.name} MIDI`);
|
| 374 |
+
download.append(downloadIcon());
|
| 375 |
+
updateDownload(download, track.midi, `${stem}-${fileStem(track.name)}.mid`);
|
| 376 |
+
|
| 377 |
+
controls.append(solo, mute, download);
|
| 378 |
+
card.append(swatch, copy, controls);
|
| 379 |
+
tracksRegion.append(card);
|
| 380 |
+
});
|
| 381 |
+
}
|
| 382 |
+
|
| 383 |
+
function refreshTrackMix() {
|
| 384 |
+
if (!playback) {
|
| 385 |
+
return;
|
| 386 |
+
}
|
| 387 |
+
const now = playback.context.currentTime;
|
| 388 |
+
playback.trackGains.forEach((gain, id) => {
|
| 389 |
+
gain.gain.cancelScheduledValues(now);
|
| 390 |
+
gain.gain.setTargetAtTime(isAudible(id) ? 1 : 0.0001, now, 0.018);
|
| 391 |
+
});
|
| 392 |
+
}
|
| 393 |
+
|
| 394 |
+
function renderProgress() {
|
| 395 |
+
const active = activeStates.has(snapshot.state);
|
| 396 |
+
progressRegion.hidden = !active;
|
| 397 |
+
if (!active) {
|
| 398 |
+
return;
|
| 399 |
+
}
|
| 400 |
+
|
| 401 |
+
const determinate = snapshot.progress != null;
|
| 402 |
+
const percent = Math.round((snapshot.progress || 0) * 100);
|
| 403 |
+
progressRegion.dataset.indeterminate = String(!determinate);
|
| 404 |
+
setText('[data-field="progress-copy"]', stateCopy(snapshot));
|
| 405 |
+
setText('[data-field="progress-value"]', determinate ? `${percent}%` : "Working");
|
| 406 |
+
$('[data-field="progress-bar"]').style.width = `${percent}%`;
|
| 407 |
+
progressTrack.setAttribute("aria-valuenow", String(percent));
|
| 408 |
+
progressTrack.setAttribute("aria-valuetext", determinate ? `${percent}%` : "In progress");
|
| 409 |
+
}
|
| 410 |
+
|
| 411 |
+
function renderEmptyState(visibleTracks) {
|
| 412 |
+
const noteTotal = snapshot.tracks.reduce((total, track) => total + track.notes.length, 0);
|
| 413 |
+
const hasAudibleNotes = visibleTracks.some((track) => track.notes.length);
|
| 414 |
+
let title = "A score will appear here";
|
| 415 |
+
let copy = "MuScriptor separates notes into individual, color-coded instrument tracks.";
|
| 416 |
+
|
| 417 |
+
if (snapshot.state === "queued" || snapshot.state === "loading" || snapshot.state === "cold_start") {
|
| 418 |
+
title = "Waking ZeroGPU";
|
| 419 |
+
copy = "The first run can take a little longer while the model is allocated and loaded.";
|
| 420 |
+
} else if (snapshot.state === "transcribing" || snapshot.state === "processing") {
|
| 421 |
+
title = "Listening in five-second windows";
|
| 422 |
+
copy = "Notes and instrument tracks will appear as soon as transcription finishes.";
|
| 423 |
+
} else if (snapshot.state === "error" || snapshot.state === "failed") {
|
| 424 |
+
title = "Transcription could not finish";
|
| 425 |
+
copy = stateCopy(snapshot);
|
| 426 |
+
} else if (noteTotal && !hasAudibleNotes) {
|
| 427 |
+
title = "All tracks are muted";
|
| 428 |
+
copy = "Unmute a track or change the solo selection to bring its notes back.";
|
| 429 |
+
} else if (snapshot.tracks.length && !noteTotal) {
|
| 430 |
+
title = "No pitched notes detected";
|
| 431 |
+
copy = "The MIDI tracks are available below even though the piano roll is empty.";
|
| 432 |
+
}
|
| 433 |
+
|
| 434 |
+
setText('[data-field="empty-title"]', title);
|
| 435 |
+
setText('[data-field="empty-copy"]', copy);
|
| 436 |
+
emptyRegion.hidden = hasAudibleNotes;
|
| 437 |
+
}
|
| 438 |
+
|
| 439 |
+
function niceTimeStep(duration) {
|
| 440 |
+
if (duration <= 12) return 1;
|
| 441 |
+
if (duration <= 45) return 2;
|
| 442 |
+
if (duration <= 120) return 5;
|
| 443 |
+
if (duration <= 360) return 10;
|
| 444 |
+
return 30;
|
| 445 |
+
}
|
| 446 |
+
|
| 447 |
+
function setupCanvas(canvas, width, height, ratio) {
|
| 448 |
+
const pixelWidth = Math.max(1, Math.round(width * ratio));
|
| 449 |
+
const pixelHeight = Math.max(1, Math.round(height * ratio));
|
| 450 |
+
if (canvas.width !== pixelWidth || canvas.height !== pixelHeight) {
|
| 451 |
+
canvas.width = pixelWidth;
|
| 452 |
+
canvas.height = pixelHeight;
|
| 453 |
+
}
|
| 454 |
+
canvas.style.width = `${width}px`;
|
| 455 |
+
canvas.style.height = `${height}px`;
|
| 456 |
+
const context = canvas.getContext("2d");
|
| 457 |
+
context.setTransform(ratio, 0, 0, ratio, 0, 0);
|
| 458 |
+
return context;
|
| 459 |
+
}
|
| 460 |
+
|
| 461 |
+
function roundedRect(context, x, y, width, height, radius) {
|
| 462 |
+
const r = Math.min(radius, width / 2, height / 2);
|
| 463 |
+
context.beginPath();
|
| 464 |
+
context.moveTo(x + r, y);
|
| 465 |
+
context.arcTo(x + width, y, x + width, y + height, r);
|
| 466 |
+
context.arcTo(x + width, y + height, x, y + height, r);
|
| 467 |
+
context.arcTo(x, y + height, x, y, r);
|
| 468 |
+
context.arcTo(x, y, x + width, y, r);
|
| 469 |
+
context.closePath();
|
| 470 |
+
}
|
| 471 |
+
|
| 472 |
+
function computePitchRange(tracks) {
|
| 473 |
+
let minimumPitch = 128;
|
| 474 |
+
let maximumPitch = -1;
|
| 475 |
+
tracks.forEach((track) => {
|
| 476 |
+
track.notes.forEach((note) => {
|
| 477 |
+
minimumPitch = Math.min(minimumPitch, note.pitch);
|
| 478 |
+
maximumPitch = Math.max(maximumPitch, note.pitch);
|
| 479 |
+
});
|
| 480 |
+
});
|
| 481 |
+
if (maximumPitch < 0) {
|
| 482 |
+
return { minimum: 36, maximum: 84 };
|
| 483 |
+
}
|
| 484 |
+
let minimum = clamp(minimumPitch - 2, 0, 127);
|
| 485 |
+
let maximum = clamp(maximumPitch + 2, 0, 127);
|
| 486 |
+
if (maximum - minimum < 24) {
|
| 487 |
+
const missing = 24 - (maximum - minimum);
|
| 488 |
+
minimum = clamp(minimum - Math.ceil(missing / 2), 0, 127);
|
| 489 |
+
maximum = clamp(minimum + 24, 0, 127);
|
| 490 |
+
minimum = Math.max(0, maximum - 24);
|
| 491 |
+
}
|
| 492 |
+
return { minimum, maximum };
|
| 493 |
+
}
|
| 494 |
+
|
| 495 |
+
function drawPianoRoll() {
|
| 496 |
+
const visibleTracks = snapshot.tracks.filter((track) => isAudible(track.id));
|
| 497 |
+
renderEmptyState(visibleTracks);
|
| 498 |
+
|
| 499 |
+
const viewportWidth = Math.max(320, rollScroll.clientWidth || root.clientWidth || 720);
|
| 500 |
+
const height = Math.max(220, rollScroll.clientHeight || 334);
|
| 501 |
+
const duration = Math.max(snapshot.duration, 1);
|
| 502 |
+
const timePixels = duration <= 15 ? 72 : duration <= 60 ? 54 : duration <= 180 ? 38 : 25;
|
| 503 |
+
const width = Math.round(Math.max(viewportWidth, Math.min(9000, 68 + duration * timePixels)));
|
| 504 |
+
const ratio = width > 4200 ? 1 : Math.min(2, window.devicePixelRatio || 1);
|
| 505 |
+
const keyWidth = 52;
|
| 506 |
+
const rulerHeight = 24;
|
| 507 |
+
const rightPadding = 14;
|
| 508 |
+
const plotWidth = Math.max(1, width - keyWidth - rightPadding);
|
| 509 |
+
const rangeSource = visibleTracks.some((track) => track.notes.length) ? visibleTracks : snapshot.tracks;
|
| 510 |
+
const pitchRange = computePitchRange(rangeSource);
|
| 511 |
+
const pitchCount = pitchRange.maximum - pitchRange.minimum + 1;
|
| 512 |
+
const rowHeight = (height - rulerHeight) / pitchCount;
|
| 513 |
+
|
| 514 |
+
rollStage.style.width = `${width}px`;
|
| 515 |
+
const context = setupCanvas(notesCanvas, width, height, ratio);
|
| 516 |
+
setupCanvas(cursorCanvas, width, height, ratio).clearRect(0, 0, width, height);
|
| 517 |
+
context.clearRect(0, 0, width, height);
|
| 518 |
+
context.fillStyle = "#0a0d12";
|
| 519 |
+
context.fillRect(0, 0, width, height);
|
| 520 |
+
|
| 521 |
+
context.fillStyle = "#0e1118";
|
| 522 |
+
context.fillRect(0, 0, width, rulerHeight);
|
| 523 |
+
|
| 524 |
+
const blackNotes = new Set([1, 3, 6, 8, 10]);
|
| 525 |
+
for (let pitch = pitchRange.minimum; pitch <= pitchRange.maximum; pitch += 1) {
|
| 526 |
+
const row = pitchRange.maximum - pitch;
|
| 527 |
+
const y = rulerHeight + row * rowHeight;
|
| 528 |
+
if (blackNotes.has(pitch % 12)) {
|
| 529 |
+
context.fillStyle = "rgba(255,255,255,0.013)";
|
| 530 |
+
context.fillRect(keyWidth, y, width - keyWidth, rowHeight);
|
| 531 |
+
}
|
| 532 |
+
context.beginPath();
|
| 533 |
+
context.moveTo(keyWidth, Math.round(y) + 0.5);
|
| 534 |
+
context.lineTo(width, Math.round(y) + 0.5);
|
| 535 |
+
context.strokeStyle = pitch % 12 === 0 ? "rgba(255,255,255,0.068)" : "rgba(255,255,255,0.026)";
|
| 536 |
+
context.lineWidth = 1;
|
| 537 |
+
context.stroke();
|
| 538 |
+
}
|
| 539 |
+
|
| 540 |
+
const step = niceTimeStep(duration);
|
| 541 |
+
context.font = '9px "SFMono-Regular", Consolas, monospace';
|
| 542 |
+
context.textBaseline = "middle";
|
| 543 |
+
for (let second = 0; second <= duration + 0.001; second += step) {
|
| 544 |
+
const x = keyWidth + (second / duration) * plotWidth;
|
| 545 |
+
context.beginPath();
|
| 546 |
+
context.moveTo(Math.round(x) + 0.5, 0);
|
| 547 |
+
context.lineTo(Math.round(x) + 0.5, height);
|
| 548 |
+
context.strokeStyle = second === 0 ? "rgba(255,255,255,0.1)" : "rgba(255,255,255,0.045)";
|
| 549 |
+
context.lineWidth = 1;
|
| 550 |
+
context.stroke();
|
| 551 |
+
context.fillStyle = "rgba(180,185,198,0.55)";
|
| 552 |
+
const label = second < 60 ? `${Math.round(second)}s` : formatClock(second);
|
| 553 |
+
context.fillText(label, x + 5, rulerHeight / 2 + 0.5);
|
| 554 |
+
}
|
| 555 |
+
|
| 556 |
+
context.fillStyle = "#10141b";
|
| 557 |
+
context.fillRect(0, rulerHeight, keyWidth, height - rulerHeight);
|
| 558 |
+
context.beginPath();
|
| 559 |
+
context.moveTo(keyWidth - 0.5, 0);
|
| 560 |
+
context.lineTo(keyWidth - 0.5, height);
|
| 561 |
+
context.strokeStyle = "rgba(255,255,255,0.1)";
|
| 562 |
+
context.stroke();
|
| 563 |
+
|
| 564 |
+
context.font = '8px "SFMono-Regular", Consolas, monospace';
|
| 565 |
+
context.textAlign = "right";
|
| 566 |
+
for (let pitch = pitchRange.minimum; pitch <= pitchRange.maximum; pitch += 1) {
|
| 567 |
+
const row = pitchRange.maximum - pitch;
|
| 568 |
+
const y = rulerHeight + row * rowHeight;
|
| 569 |
+
const isBlack = blackNotes.has(pitch % 12);
|
| 570 |
+
if (isBlack) {
|
| 571 |
+
context.fillStyle = "#090b10";
|
| 572 |
+
context.fillRect(0, y, keyWidth * 0.66, rowHeight);
|
| 573 |
+
}
|
| 574 |
+
if (pitch % 12 === 0 && rowHeight >= 4.5) {
|
| 575 |
+
context.fillStyle = "rgba(191,196,209,0.52)";
|
| 576 |
+
context.textBaseline = "middle";
|
| 577 |
+
context.fillText(`C${Math.floor(pitch / 12) - 1}`, keyWidth - 7, y + rowHeight / 2);
|
| 578 |
+
}
|
| 579 |
+
}
|
| 580 |
+
context.textAlign = "left";
|
| 581 |
+
|
| 582 |
+
visibleTracks.forEach((track) => {
|
| 583 |
+
track.notes.forEach((note) => {
|
| 584 |
+
const x = keyWidth + (clamp(note.start, 0, duration) / duration) * plotWidth;
|
| 585 |
+
const endX = keyWidth + (clamp(note.end, 0, duration) / duration) * plotWidth;
|
| 586 |
+
const widthPx = Math.max(2.2, endX - x);
|
| 587 |
+
const row = pitchRange.maximum - clamp(note.pitch, pitchRange.minimum, pitchRange.maximum);
|
| 588 |
+
const y = rulerHeight + row * rowHeight + Math.max(0.65, rowHeight * 0.12);
|
| 589 |
+
const heightPx = Math.max(2, rowHeight * 0.76);
|
| 590 |
+
context.globalAlpha = 0.56 + (note.velocity / 127) * 0.38;
|
| 591 |
+
context.fillStyle = track.color;
|
| 592 |
+
roundedRect(context, x, y, widthPx, heightPx, Math.min(2.5, heightPx * 0.34));
|
| 593 |
+
context.fill();
|
| 594 |
+
if (heightPx >= 4) {
|
| 595 |
+
context.globalAlpha = 0.24;
|
| 596 |
+
context.fillStyle = "#ffffff";
|
| 597 |
+
roundedRect(context, x + 0.7, y + 0.7, Math.max(1, widthPx - 1.4), 1, 0.5);
|
| 598 |
+
context.fill();
|
| 599 |
+
}
|
| 600 |
+
});
|
| 601 |
+
});
|
| 602 |
+
context.globalAlpha = 1;
|
| 603 |
+
|
| 604 |
+
geometry = { width, height, ratio, duration, keyWidth, plotWidth };
|
| 605 |
+
const audibleNames = visibleTracks.map((track) => track.name).join(", ");
|
| 606 |
+
notesCanvas.setAttribute(
|
| 607 |
+
"aria-label",
|
| 608 |
+
visibleTracks.length
|
| 609 |
+
? `Piano roll with ${visibleTracks.length} audible tracks: ${audibleNames}`
|
| 610 |
+
: "Piano roll with no audible tracks",
|
| 611 |
+
);
|
| 612 |
+
}
|
| 613 |
+
|
| 614 |
+
function clearCursor() {
|
| 615 |
+
if (!geometry) return;
|
| 616 |
+
const context = setupCanvas(cursorCanvas, geometry.width, geometry.height, geometry.ratio);
|
| 617 |
+
context.clearRect(0, 0, geometry.width, geometry.height);
|
| 618 |
+
}
|
| 619 |
+
|
| 620 |
+
function drawCursor(seconds, follow = false) {
|
| 621 |
+
if (!geometry) return;
|
| 622 |
+
const context = setupCanvas(cursorCanvas, geometry.width, geometry.height, geometry.ratio);
|
| 623 |
+
context.clearRect(0, 0, geometry.width, geometry.height);
|
| 624 |
+
const x = geometry.keyWidth + (clamp(seconds, 0, geometry.duration) / geometry.duration) * geometry.plotWidth;
|
| 625 |
+
const gradient = context.createLinearGradient(x, 0, x + 10, 0);
|
| 626 |
+
gradient.addColorStop(0, "rgba(238,235,255,0.92)");
|
| 627 |
+
gradient.addColorStop(0.18, "rgba(150,133,255,0.22)");
|
| 628 |
+
gradient.addColorStop(1, "rgba(150,133,255,0)");
|
| 629 |
+
context.fillStyle = gradient;
|
| 630 |
+
context.fillRect(x, 0, 12, geometry.height);
|
| 631 |
+
context.fillStyle = "#f3f1ff";
|
| 632 |
+
context.fillRect(Math.round(x), 0, 1.25, geometry.height);
|
| 633 |
+
|
| 634 |
+
if (follow && x > rollScroll.scrollLeft + rollScroll.clientWidth * 0.82) {
|
| 635 |
+
rollScroll.scrollTo({ left: Math.max(0, x - rollScroll.clientWidth * 0.28), behavior: "smooth" });
|
| 636 |
+
}
|
| 637 |
+
}
|
| 638 |
+
|
| 639 |
+
function noteFrequency(pitch) {
|
| 640 |
+
return clamp(440 * 2 ** ((pitch - 69) / 12), 28, 4200);
|
| 641 |
+
}
|
| 642 |
+
|
| 643 |
+
function scheduleNote(track, note, currentElapsed) {
|
| 644 |
+
if (!playback) return;
|
| 645 |
+
const context = playback.context;
|
| 646 |
+
const trackGain = playback.trackGains.get(track.id);
|
| 647 |
+
if (!trackGain) return;
|
| 648 |
+
|
| 649 |
+
const scheduledStart = playback.startedAt + note.start;
|
| 650 |
+
const start = Math.max(context.currentTime + 0.006, scheduledStart);
|
| 651 |
+
const naturalLength = Math.max(0.025, note.end - Math.max(note.start, currentElapsed));
|
| 652 |
+
const length = track.is_drum ? Math.min(0.13, naturalLength) : Math.min(8, naturalLength);
|
| 653 |
+
const end = start + length;
|
| 654 |
+
const oscillator = context.createOscillator();
|
| 655 |
+
const envelope = context.createGain();
|
| 656 |
+
const amplitude = (note.velocity / 127) * (track.is_drum ? 0.05 : 0.075);
|
| 657 |
+
|
| 658 |
+
oscillator.type = track.is_drum ? "square" : ["sine", "triangle", "sine", "triangle"][
|
| 659 |
+
Math.abs(track.id.split("").reduce((sum, letter) => sum + letter.charCodeAt(0), 0)) % 4
|
| 660 |
+
];
|
| 661 |
+
oscillator.frequency.setValueAtTime(track.is_drum ? 52 + (note.pitch % 24) * 7 : noteFrequency(note.pitch), start);
|
| 662 |
+
if (track.is_drum) {
|
| 663 |
+
oscillator.frequency.exponentialRampToValueAtTime(34, end);
|
| 664 |
+
}
|
| 665 |
+
|
| 666 |
+
envelope.gain.setValueAtTime(0.0001, start);
|
| 667 |
+
envelope.gain.exponentialRampToValueAtTime(Math.max(0.0002, amplitude), start + Math.min(0.012, length * 0.25));
|
| 668 |
+
envelope.gain.exponentialRampToValueAtTime(0.0001, end);
|
| 669 |
+
oscillator.connect(envelope);
|
| 670 |
+
envelope.connect(trackGain);
|
| 671 |
+
oscillator.start(start);
|
| 672 |
+
oscillator.stop(end + 0.01);
|
| 673 |
+
playback.nodes.add(oscillator);
|
| 674 |
+
oscillator.addEventListener("ended", () => playback && playback.nodes.delete(oscillator), { once: true });
|
| 675 |
+
}
|
| 676 |
+
|
| 677 |
+
function runScheduler() {
|
| 678 |
+
if (!playback) return;
|
| 679 |
+
const elapsed = playback.context.currentTime - playback.startedAt;
|
| 680 |
+
const horizon = elapsed + 0.42;
|
| 681 |
+
snapshot.tracks.forEach((track) => {
|
| 682 |
+
let index = playback.nextNote.get(track.id) || 0;
|
| 683 |
+
while (index < track.notes.length && track.notes[index].start <= horizon) {
|
| 684 |
+
const note = track.notes[index];
|
| 685 |
+
if (note.end > elapsed) {
|
| 686 |
+
scheduleNote(track, note, elapsed);
|
| 687 |
+
}
|
| 688 |
+
index += 1;
|
| 689 |
+
}
|
| 690 |
+
playback.nextNote.set(track.id, index);
|
| 691 |
+
});
|
| 692 |
+
}
|
| 693 |
+
|
| 694 |
+
function playbackFrame() {
|
| 695 |
+
if (!playback) return;
|
| 696 |
+
const elapsed = Math.max(0, playback.context.currentTime - playback.startedAt);
|
| 697 |
+
drawCursor(elapsed, true);
|
| 698 |
+
if (elapsed >= playback.duration + 0.08) {
|
| 699 |
+
stopPlayback(true);
|
| 700 |
+
announce("Playback finished.");
|
| 701 |
+
return;
|
| 702 |
+
}
|
| 703 |
+
playback.frame = window.requestAnimationFrame(playbackFrame);
|
| 704 |
+
}
|
| 705 |
+
|
| 706 |
+
async function startPlayback() {
|
| 707 |
+
const AudioContextClass = window.AudioContext || window.webkitAudioContext;
|
| 708 |
+
const hasNotes = snapshot.tracks.some((track) => track.notes.length);
|
| 709 |
+
if (!AudioContextClass || !hasNotes) {
|
| 710 |
+
announce(AudioContextClass ? "There are no notes to play." : "WebAudio is not available in this browser.");
|
| 711 |
+
return;
|
| 712 |
+
}
|
| 713 |
+
stopPlayback(false);
|
| 714 |
+
|
| 715 |
+
const context = new AudioContextClass({ latencyHint: "interactive" });
|
| 716 |
+
if (context.state === "suspended") {
|
| 717 |
+
await context.resume();
|
| 718 |
+
}
|
| 719 |
+
const master = context.createGain();
|
| 720 |
+
const compressor = context.createDynamicsCompressor();
|
| 721 |
+
master.gain.value = 0.62;
|
| 722 |
+
compressor.threshold.value = -14;
|
| 723 |
+
compressor.knee.value = 14;
|
| 724 |
+
compressor.ratio.value = 8;
|
| 725 |
+
compressor.attack.value = 0.004;
|
| 726 |
+
compressor.release.value = 0.12;
|
| 727 |
+
master.connect(compressor);
|
| 728 |
+
compressor.connect(context.destination);
|
| 729 |
+
|
| 730 |
+
const trackGains = new Map();
|
| 731 |
+
snapshot.tracks.forEach((track) => {
|
| 732 |
+
const gain = context.createGain();
|
| 733 |
+
gain.gain.value = isAudible(track.id) ? 1 : 0.0001;
|
| 734 |
+
gain.connect(master);
|
| 735 |
+
trackGains.set(track.id, gain);
|
| 736 |
+
});
|
| 737 |
+
|
| 738 |
+
playback = {
|
| 739 |
+
context,
|
| 740 |
+
master,
|
| 741 |
+
trackGains,
|
| 742 |
+
nodes: new Set(),
|
| 743 |
+
nextNote: new Map(snapshot.tracks.map((track) => [track.id, 0])),
|
| 744 |
+
startedAt: context.currentTime + 0.07,
|
| 745 |
+
duration: Math.max(snapshot.duration, 0.1),
|
| 746 |
+
timer: 0,
|
| 747 |
+
frame: 0,
|
| 748 |
+
};
|
| 749 |
+
runScheduler();
|
| 750 |
+
playback.timer = window.setInterval(runScheduler, 90);
|
| 751 |
+
playback.frame = window.requestAnimationFrame(playbackFrame);
|
| 752 |
+
playButton.disabled = true;
|
| 753 |
+
playButton.setAttribute("aria-pressed", "true");
|
| 754 |
+
stopButton.disabled = false;
|
| 755 |
+
announce("Playback started.");
|
| 756 |
+
}
|
| 757 |
+
|
| 758 |
+
function stopPlayback(resetCursor = true) {
|
| 759 |
+
if (playback) {
|
| 760 |
+
window.clearInterval(playback.timer);
|
| 761 |
+
window.cancelAnimationFrame(playback.frame);
|
| 762 |
+
playback.nodes.forEach((node) => {
|
| 763 |
+
try {
|
| 764 |
+
node.stop();
|
| 765 |
+
} catch (error) {
|
| 766 |
+
// A node that has already ended cannot be stopped again.
|
| 767 |
+
}
|
| 768 |
+
});
|
| 769 |
+
playback.context.close().catch(() => {});
|
| 770 |
+
playback = null;
|
| 771 |
+
}
|
| 772 |
+
const hasNotes = snapshot.tracks.some((track) => track.notes.length);
|
| 773 |
+
playButton.disabled = !hasNotes;
|
| 774 |
+
playButton.setAttribute("aria-pressed", "false");
|
| 775 |
+
stopButton.disabled = true;
|
| 776 |
+
if (resetCursor) {
|
| 777 |
+
clearCursor();
|
| 778 |
+
}
|
| 779 |
+
}
|
| 780 |
+
|
| 781 |
+
function render(nextSnapshot, options = {}) {
|
| 782 |
+
const previousState = snapshot.state;
|
| 783 |
+
const previousSignature = snapshot.tracks
|
| 784 |
+
.map((track) => `${track.id}:${track.notes.length}:${track.notes.at(-1)?.end || 0}`)
|
| 785 |
+
.join("|");
|
| 786 |
+
const nextSignature = nextSnapshot.tracks
|
| 787 |
+
.map((track) => `${track.id}:${track.notes.length}:${track.notes.at(-1)?.end || 0}`)
|
| 788 |
+
.join("|");
|
| 789 |
+
if (playback && previousSignature !== nextSignature) {
|
| 790 |
+
stopPlayback(true);
|
| 791 |
+
}
|
| 792 |
+
|
| 793 |
+
snapshot = nextSnapshot;
|
| 794 |
+
syncTrackState(snapshot.tracks);
|
| 795 |
+
root.dataset.state = snapshot.state || "idle";
|
| 796 |
+
setText('[data-field="state-label"]', stateLabels[snapshot.state] || titleFromId(snapshot.state));
|
| 797 |
+
setText('[data-field="audio-name"]', snapshot.audio_name || "Your transcription");
|
| 798 |
+
setText('[data-field="status"]', stateCopy(snapshot));
|
| 799 |
+
setText('[data-field="duration"]', formatClock(snapshot.duration));
|
| 800 |
+
setText('[data-field="note-count"]', snapshot.note_count ? snapshot.note_count.toLocaleString() : "—");
|
| 801 |
+
setText('[data-field="track-count"]', snapshot.tracks.length ? snapshot.tracks.length.toLocaleString() : "—");
|
| 802 |
+
setText('[data-field="elapsed"]', formatElapsed(snapshot.elapsed));
|
| 803 |
+
setText(
|
| 804 |
+
'[data-field="track-summary"]',
|
| 805 |
+
snapshot.tracks.length
|
| 806 |
+
? `${snapshot.tracks.length} ${snapshot.tracks.length === 1 ? "track" : "tracks"}`
|
| 807 |
+
: "No tracks yet",
|
| 808 |
+
);
|
| 809 |
+
|
| 810 |
+
updateDownload(fullDownload, snapshot.full_midi, `${fileStem(snapshot.audio_name)}-full.mid`);
|
| 811 |
+
renderProgress();
|
| 812 |
+
renderTracks();
|
| 813 |
+
drawPianoRoll();
|
| 814 |
+
|
| 815 |
+
const hasNotes = snapshot.tracks.some((track) => track.notes.length);
|
| 816 |
+
if (!playback) {
|
| 817 |
+
playButton.disabled = !hasNotes;
|
| 818 |
+
stopButton.disabled = true;
|
| 819 |
+
}
|
| 820 |
+
|
| 821 |
+
const submitButton = findSubmitButton();
|
| 822 |
+
if (submitButton && terminalStates.has(snapshot.state)) {
|
| 823 |
+
submitButton.removeAttribute("aria-busy");
|
| 824 |
+
}
|
| 825 |
+
|
| 826 |
+
if (!options.silent && (previousState !== snapshot.state || options.local)) {
|
| 827 |
+
announce(`${stateLabels[snapshot.state] || titleFromId(snapshot.state)}. ${stateCopy(snapshot)}`);
|
| 828 |
+
}
|
| 829 |
+
}
|
| 830 |
+
|
| 831 |
+
function updateFromProps() {
|
| 832 |
+
render(normalizePayload(parseIncoming(props.value)));
|
| 833 |
+
}
|
| 834 |
+
|
| 835 |
+
function findSubmitButton() {
|
| 836 |
+
const localRoot = element.getRootNode && element.getRootNode();
|
| 837 |
+
return (localRoot && localRoot.querySelector && localRoot.querySelector("#transcribe-button"))
|
| 838 |
+
|| document.querySelector("#transcribe-button");
|
| 839 |
+
}
|
| 840 |
+
|
| 841 |
+
function bindSubmitButton() {
|
| 842 |
+
const button = findSubmitButton();
|
| 843 |
+
if (!button || button.__muscriptorVisualizerBound) {
|
| 844 |
+
return Boolean(button);
|
| 845 |
+
}
|
| 846 |
+
button.__muscriptorVisualizerBound = true;
|
| 847 |
+
button.addEventListener("click", () => {
|
| 848 |
+
if (button.matches(":disabled") || button.getAttribute("aria-disabled") === "true") {
|
| 849 |
+
return;
|
| 850 |
+
}
|
| 851 |
+
button.setAttribute("aria-busy", "true");
|
| 852 |
+
render(
|
| 853 |
+
normalizePayload({
|
| 854 |
+
...snapshot,
|
| 855 |
+
state: "queued",
|
| 856 |
+
status: "Request sent. Allocating a ZeroGPU worker…",
|
| 857 |
+
progress: 0.01,
|
| 858 |
+
elapsed: 0,
|
| 859 |
+
duration: 0,
|
| 860 |
+
note_count: 0,
|
| 861 |
+
full_midi: "",
|
| 862 |
+
tracks: [],
|
| 863 |
+
}),
|
| 864 |
+
{ local: true },
|
| 865 |
+
);
|
| 866 |
+
}, { capture: true });
|
| 867 |
+
return true;
|
| 868 |
+
}
|
| 869 |
+
|
| 870 |
+
playButton.addEventListener("click", () => {
|
| 871 |
+
startPlayback().catch(() => {
|
| 872 |
+
stopPlayback(true);
|
| 873 |
+
announce("Playback could not start. Check this browser's audio permissions.");
|
| 874 |
+
});
|
| 875 |
+
});
|
| 876 |
+
stopButton.addEventListener("click", () => {
|
| 877 |
+
stopPlayback(true);
|
| 878 |
+
announce("Playback stopped.");
|
| 879 |
+
});
|
| 880 |
+
|
| 881 |
+
const resizeObserver = new ResizeObserver(() => {
|
| 882 |
+
window.cancelAnimationFrame(resizeFrame);
|
| 883 |
+
resizeFrame = window.requestAnimationFrame(drawPianoRoll);
|
| 884 |
+
});
|
| 885 |
+
resizeObserver.observe(rollScroll);
|
| 886 |
+
|
| 887 |
+
if (!bindSubmitButton()) {
|
| 888 |
+
const buttonObserver = new MutationObserver(() => {
|
| 889 |
+
if (bindSubmitButton()) {
|
| 890 |
+
buttonObserver.disconnect();
|
| 891 |
+
}
|
| 892 |
+
});
|
| 893 |
+
buttonObserver.observe(document.body, { childList: true, subtree: true });
|
| 894 |
+
window.setTimeout(() => buttonObserver.disconnect(), 15000);
|
| 895 |
+
}
|
| 896 |
+
|
| 897 |
+
updateFromProps();
|
| 898 |
+
watch("value", updateFromProps);
|
requirements.txt
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio==6.10.0
|
| 2 |
+
spaces==0.51.0
|
| 3 |
+
muscriptor==0.2.1
|
| 4 |
+
numpy==2.3.5
|
| 5 |
+
einops==0.8.2
|
| 6 |
+
mido==1.3.3
|
| 7 |
+
soundfile==0.14.0
|
| 8 |
+
safetensors==0.8.0
|
| 9 |
+
huggingface_hub==1.21.0
|