-
Notifications
You must be signed in to change notification settings - Fork 1
ui: GAME MIDI Studio (Electron) — oudep import, vocal→MIDI, viz, export #20
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
76dd672
cc963d7
ea415c5
d2d70fa
7307e30
5d2e840
96a1be5
9230228
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| name: build-ui | ||
|
|
||
| # Separate from the oudep-packaging ci.yml: builds the GAME MIDI Studio | ||
| # Electron app (ui/) for Windows/macOS/Linux. Independently triggerable | ||
| # via workflow_dispatch, and auto-runs on pushes touching ui/. | ||
| on: | ||
| workflow_dispatch: {} | ||
| push: | ||
| paths: ['ui/**', '.github/workflows/build-ui.yml'] | ||
|
|
||
| jobs: | ||
| build: | ||
| name: ${{ matrix.name }} | ||
| runs-on: ${{ matrix.os }} | ||
| strategy: | ||
| fail-fast: false | ||
| matrix: | ||
| include: | ||
| - os: windows-latest | ||
| name: win | ||
| dist: '--win' | ||
| - os: macos-latest | ||
| name: mac | ||
| dist: '--mac' | ||
| - os: ubuntu-latest | ||
| name: linux | ||
| dist: '--linux' | ||
| defaults: | ||
| run: | ||
| shell: bash | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
|
|
||
| - uses: actions/setup-node@v4 | ||
| with: | ||
| node-version: 20 | ||
| cache: npm | ||
| cache-dependency-path: ui/package-lock.json | ||
|
|
||
| - name: Install deps | ||
| working-directory: ui | ||
| run: npm ci | ||
|
|
||
| - name: Package (${{ matrix.dist }}) | ||
| working-directory: ui | ||
| run: npx electron-builder ${{ matrix.dist }} | ||
| env: | ||
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
|
|
||
| - name: Upload artifacts | ||
| uses: actions/upload-artifact@v4 | ||
| with: | ||
| name: game-midi-studio-${{ matrix.name }} | ||
| path: ui/dist/* | ||
| if-no-files-found: error | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| #!/usr/bin/env python3 | ||
| """Re-run the GGML rows of the 7-channel benchmark under the original conditions. | ||
|
|
||
| Mirrors dbcache_ablation/{bench_ggml.ps1, ggml_n8.py}: 60 s wav, no slice, | ||
| tempo 120, seed 42, nsteps ∈ {1,8}, DBCache = {threshold 0.25, fn_blocks 1, | ||
| warmup 1} on / threshold 0 off. Prints wall + per-stage profile + notes + | ||
| DBCache hit/miss per run. | ||
|
|
||
| Usage: | ||
| run_ggml_bench.py --exe <game_ggml_cli.exe> --tag <name> [--wav <w>] \ | ||
| [--model-dir <dir-with-game_medium[.gguf|_q8.gguf]>] [--gpu] | ||
| """ | ||
| from __future__ import annotations | ||
| import argparse, os, re, subprocess, sys, time, threading, glob | ||
|
|
||
| WAV_DEFAULT = r"J:\GGML-GAME\bench_dml_compare\input\w44k_60.wav" | ||
| MODEL_DIR_DEFAULT = r"J:\GGML-GAME\game_ggml_cli\build-local-cpu\ml_qkv" | ||
| VK_CACHE = r"J:\GGML-GAME\_vkpc\pipeline.cache" | ||
|
|
||
| def vram_mib(): | ||
| try: | ||
| t = subprocess.run(["nvidia-smi", "--query-gpu=memory.used", | ||
| "--format=csv,noheader,nounits"], capture_output=True, text=True, timeout=2) | ||
| return float(t.stdout.strip().splitlines()[0].strip()) if t.stdout and t.returncode == 0 else 0.0 | ||
| except Exception: | ||
| return 0.0 | ||
|
Comment on lines
+20
to
+26
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Report unavailable GPU monitoring as unavailable. Line 24 returns Return 🧰 Tools🪛 ast-grep (0.45.1)[error] 21-22: Command coming from incoming request (subprocess-from-request) 🪛 Ruff (0.16.1)[error] 22-23: Starting a process with a partial executable path (S607) [warning] 25-25: Do not catch blind exception: (BLE001) 🤖 Prompt for AI Agents |
||
|
|
||
| def run(exe, model, wav, outdir, nsteps, cache, gpu): | ||
| cmd = [exe, "extract", wav, "-m", model, | ||
| "--output-formats", "csv", "--output-dir", outdir, | ||
| "--tempo", "120", "--seed", "42", "--no-slice", | ||
| "--nsteps", "8" if nsteps else "1"] | ||
| if nsteps: | ||
| if cache: | ||
| cmd += ["--cache-threshold", "0.25", "--cache-fn-blocks", "1", "--cache-warmup", "1"] | ||
| else: | ||
| cmd += ["--cache-threshold", "0"] | ||
| env = dict(os.environ) | ||
| env["GAME_GGML_PROFILE"] = "1" | ||
| env["GAME_GGML_THREADS"] = "16" | ||
| env["GAME_GGML_DUMP_DBCACHE"] = "1" | ||
| env["GGML_VK_PIPELINE_CACHE_PATH"] = VK_CACHE | ||
| base = vram_mib() if gpu else 0.0 | ||
| peak = {"v": base} | ||
| stop = threading.Event() | ||
| def sampler(): | ||
| while not stop.is_set(): | ||
| if gpu: | ||
| v = vram_mib() | ||
| if v > peak["v"]: peak["v"] = v | ||
| time.sleep(0.04) | ||
| th = threading.Thread(target=sampler, daemon=True); th.start() | ||
| t0 = time.perf_counter() | ||
| p = subprocess.Popen(cmd, env=env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, | ||
| encoding="utf-8", errors="replace") | ||
| log = "".join(l for l in p.stdout) | ||
| p.wait() | ||
| wall = time.perf_counter() - t0 | ||
| stop.set(); th.join(1.0) | ||
| total = re.search(r"total\s+([\d.]+)\s*s", log) | ||
| notes = re.search(r"total notes:\s*(\d+)", log) | ||
| hits = len(re.findall(r"\bHIT\b", log)); misses = len(re.findall(r"\bMISS\b", log)) | ||
| stage = {} | ||
| for m in re.finditer(r"\s+(\S+)\s+([\d.]+)\s*s\s+\(\s*([\d.]+)%\s*\)", log): | ||
| stage[m.group(1)] = (m.group(2), m.group(3)) | ||
| dv = (peak["v"] - base) if gpu else None | ||
| return dict(wall=wall, total=total.group(1) if total else "?", notes=notes.group(1) if notes else "?", | ||
| hits=hits, misses=misses, vram=("N/A" if dv is None else f"+{dv:.0f}MiB"), stage=stage, rc=p.returncode) | ||
|
|
||
| def main(): | ||
| ap = argparse.ArgumentParser() | ||
| ap.add_argument("--exe", required=True) | ||
| ap.add_argument("--tag", required=True) | ||
| ap.add_argument("--wav", default=WAV_DEFAULT) | ||
| ap.add_argument("--model-dir", default=MODEL_DIR_DEFAULT) | ||
| ap.add_argument("--gpu", action="store_true") | ||
| ap.add_argument("--only-n8", action="store_true") | ||
| a = ap.parse_args() | ||
| models = {"F32": os.path.join(a.model_dir, "game_medium.gguf"), | ||
| "Q8": os.path.join(a.model_dir, "game_medium_q8.gguf")} | ||
| for mname, mod in models.items(): | ||
| if not os.path.isfile(mod): | ||
| print(f"!! missing {mod}"); continue | ||
| for nsteps in (0, 8) if not a.only_n8 else (8,): | ||
| for cache in ((False, True) if nsteps else (False,)): | ||
| tag = f"{a.tag} {mname} n{'8' if nsteps else '1'}{' cache' if cache else ''}" | ||
| out = os.path.join("bench_out", f"{a.tag}-{mname}-n{nsteps if nsteps else 1}-{1 if cache else 0}") | ||
| os.makedirs(out, exist_ok=True) | ||
| r = run(a.exe, mod, a.wav, out, nsteps, cache, a.gpu) | ||
| st = " | ".join(f"{k}={v}s({p}%)" for k, (v, p) in r["stage"].items()) | ||
| print(f"GGML {tag}: wall={r['wall']:.3f}s total={r['total']}s notes={r['notes']} " | ||
| f"dbc={r['hits']}/{r['misses']} vram={r['vram']} rc={r['rc']}") | ||
| if st: print(f" {st}") | ||
|
|
||
| if __name__ == "__main__": | ||
| main() | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| node_modules/ | ||
| dist/ | ||
| out/ | ||
| *.log | ||
| .DS_Store |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| # GAME MIDI Studio (Electron UI) | ||
|
|
||
| A simple cross-platform desktop UI for the GAME singing-voice-to-MIDI engine | ||
| (`game.cpp` / `game_ggml_cli`). It imports an `.oudep` package, decodes/resamples | ||
| an vocal audio file to the engine input format, renders MIDI, visualizes notes, | ||
| and exports `.mid`. | ||
|
|
||
| ## Features | ||
|
|
||
| - **Import .oudep** — unpack a `game.cpp` CI-produced package | ||
| (`game_ggml-<platform>.oudep`): discovers the `game_ggml_cli` executable, the | ||
| `.gguf` model(s), `config.json` (samplerate / languages), and `oudep.yaml`. | ||
| - **Audio input** — any format Chromium can decode (wav / mp3 / flac / m4a / | ||
| aac / ogg / opus / mp4 / webm…); auto **resamples to 44.1 kHz mono** | ||
| (the engine requirement) via Web Audio, and shows a waveform. | ||
| - **Render MIDI** — runs `game_ggml_cli extract` with the imported model, | ||
| selected language, tempo, nsteps (1 or 8) and seed; DBCache defaults for n8. | ||
| - **Visualize** — piano-roll canvas of the rendered notes (pitch vs time). | ||
| - **Export .mid** — save the rendered `.mid` anywhere; or open the output dir. | ||
|
|
||
| ## Build & run | ||
|
|
||
| ```bash | ||
| cd ui | ||
| npm install # installs electron + builder + adm-zip | ||
| npm start # run in dev | ||
| npm run dist:win # or dist:mac / dist:linux (electron-builder) | ||
| ``` | ||
|
|
||
| CI: `.github/workflows/build-ui.yml` builds the app for Windows / macOS / | ||
| Linux, independently from the oudep-packaging `ci.yml` (triggered by | ||
| `workflow_dispatch`, or any push touching `ui/**`). | ||
|
|
||
| ## Notes | ||
|
|
||
| - The engine (`game_ggml_cli`) only accepts **44.1 kHz mono 16-bit WAV**; the UI | ||
| handles conversion. Source formats with unusual sample rates or stereo are | ||
| downmixed/resampled automatically. | ||
| - `langId` comes from the package's `config.json` `languages` map; if absent, | ||
| the CLI default is used. | ||
| - The `.oudep` is unpacked under the app's `userData/oudep/`; packages can be | ||
| removed from the UI. | ||
| - `nsteps=8` enables the segmenter DBCache (default threshold 0.25) for quality, | ||
| matching the repo's default high-quality path. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: KakaruHayate/game.cpp
Length of output: 860
🏁 Script executed:
Repository: KakaruHayate/game.cpp
Length of output: 7265
Do not expose repository credentials to build commands.
Set
permissions: contents: read, addpersist-credentials: falsetoactions/checkout@v4, and removeGH_TOKENfrom the artifact-only packaging step. The Electron Builder configuration does not publish releases. If publishing is required, isolate it in a separate release job with only the required write permission.🧰 Tools
🪛 zizmor (1.29.0)
[warning] 32-32: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false
(artipacked)
🤖 Prompt for AI Agents
Source: Linters/SAST tools