feat: add minimal Gradio web UI (app.py)
Single-page form wrapping src.generate.generate_period: pick model, mode, key, style, function, time, sampling params and optional prefix; returns the chord grid plus downloadable .chord and .mid files. Russian usage instructions are embedded on the same page. Auto-length output is capped at 16 bars (the period maximum) so a model that never emits EOS can't run away into dozens of NC/hold bars. Added per the author's explicit request — web UI was previously out of scope; updated CLAUDE.md and README accordingly. Choices for style/ function/time are derived from VOCAB so the form can't drift from the tokenizer. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -158,7 +158,7 @@ When generating commit messages or code comments, write in English. When generat
|
|||||||
- **Do not change the `.chord` format** without first updating `docs/chord_format_spec.md` and bumping its version number. The format is the contract between the human-readable data and the model; changing one side silently breaks everything.
|
- **Do not change the `.chord` format** without first updating `docs/chord_format_spec.md` and bumping its version number. The format is the contract between the human-readable data and the model; changing one side silently breaks everything.
|
||||||
- **Do not modify files in `data/holdout/` or use them during training.** Holdout is held out.
|
- **Do not modify files in `data/holdout/` or use them during training.** Holdout is held out.
|
||||||
- **Do not add new model architectures "to compare"** unless explicitly asked. One model, done well, beats four half-done.
|
- **Do not add new model architectures "to compare"** unless explicitly asked. One model, done well, beats four half-done.
|
||||||
- **Do not implement bells and whistles** (web UI, real-time audio synthesis, beam search, voicing models). They are explicitly out of scope.
|
- **Do not implement bells and whistles** (real-time audio synthesis, beam search, voicing models). They are explicitly out of scope. (Exception: a minimal Gradio web UI in `app.py` was added at the author's explicit request — maintain it, but do not expand it into a full editor/DAW.)
|
||||||
- **Do not silently round or coerce unrecognized chord symbols.** If a chord can't be parsed, raise an error with the file name, bar number, and position. Silent corruption of training data is the worst failure mode here.
|
- **Do not silently round or coerce unrecognized chord symbols.** If a chord can't be parsed, raise an error with the file name, bar number, and position. Silent corruption of training data is the worst failure mode here.
|
||||||
|
|
||||||
## Things to always do
|
## Things to always do
|
||||||
@@ -175,7 +175,7 @@ When generating commit messages or code comments, write in English. When generat
|
|||||||
- Voice leading / voicing inside chords above the bass
|
- Voice leading / voicing inside chords above the bass
|
||||||
- Rhythmic patterns inside a held chord
|
- Rhythmic patterns inside a held chord
|
||||||
- Arrangement, timbre, dynamics
|
- Arrangement, timbre, dynamics
|
||||||
- Web interface / GUI
|
- Web interface / GUI beyond the minimal Gradio form in `app.py` (added by explicit request; no notation editor, playback, or version history)
|
||||||
- Real-time MIDI integration with REAPER
|
- Real-time MIDI integration with REAPER
|
||||||
- Modulation handling inside a single period
|
- Modulation handling inside a single period
|
||||||
- J-Pop fine-tuning experiment (future work after coursework deadline)
|
- J-Pop fine-tuning experiment (future work after coursework deadline)
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
- [3. Установка](#3-установка)
|
- [3. Установка](#3-установка)
|
||||||
- [4. Структура репозитория](#4-структура-репозитория)
|
- [4. Структура репозитория](#4-структура-репозитория)
|
||||||
- [5. Быстрый старт](#5-быстрый-старт)
|
- [5. Быстрый старт](#5-быстрый-старт)
|
||||||
|
- [5.1 Веб-интерфейс](#51-веб-интерфейс)
|
||||||
- [6. Подготовка датасета](#6-подготовка-датасета)
|
- [6. Подготовка датасета](#6-подготовка-датасета)
|
||||||
- [6.1 Собственный корпус](#61-собственный-корпус)
|
- [6.1 Собственный корпус](#61-собственный-корпус)
|
||||||
- [6.2 Публичный корпус](#62-публичный-корпус)
|
- [6.2 Публичный корпус](#62-публичный-корпус)
|
||||||
@@ -169,6 +170,31 @@ python scripts/generate.py \
|
|||||||
Модель достроит остаток периода в логике, выученной на собственном корпусе
|
Модель достроит остаток периода в логике, выученной на собственном корпусе
|
||||||
автора.
|
автора.
|
||||||
|
|
||||||
|
### 5.1 Веб-интерфейс
|
||||||
|
|
||||||
|
Для интерактивной работы без командной строки предусмотрен простой
|
||||||
|
веб-интерфейс на базе Gradio. Запуск:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python app.py
|
||||||
|
```
|
||||||
|
|
||||||
|
После запуска интерфейс открывается в браузере по адресу
|
||||||
|
`http://127.0.0.1:7860`. На одной странице расположены форма выбора
|
||||||
|
параметров (модель, лад, тональность, стиль, функция, тактовый размер,
|
||||||
|
параметры сэмплирования, опциональный префикс) и блок результата —
|
||||||
|
текстовая сетка аккордов и кнопки скачивания `.chord`- и `.mid`-файлов.
|
||||||
|
Краткая инструкция на русском языке встроена в саму страницу (раздел
|
||||||
|
«Инструкция»).
|
||||||
|
|
||||||
|
Интерфейс является надстройкой над тем же модулем `src/generate.py`,
|
||||||
|
что и CLI; вся логика генерации общая. Полезные флаги:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python app.py --port 8000 # другой порт
|
||||||
|
python app.py --share # временная публичная ссылка
|
||||||
|
```
|
||||||
|
|
||||||
## 6. Подготовка датасета
|
## 6. Подготовка датасета
|
||||||
|
|
||||||
Подготовка датасета — самая трудозатратная часть проекта (10–15 часов
|
Подготовка датасета — самая трудозатратная часть проекта (10–15 часов
|
||||||
@@ -340,8 +366,9 @@ python scripts/evaluate.py \
|
|||||||
- Ритмический паттерн внутри удержания аккорда (синкопы, проходящие фигуры,
|
- Ритмический паттерн внутри удержания аккорда (синкопы, проходящие фигуры,
|
||||||
альбертиевы басы).
|
альбертиевы басы).
|
||||||
- Аранжировка, тембр, динамика, артикуляция.
|
- Аранжировка, тембр, динамика, артикуляция.
|
||||||
- Графический пользовательский интерфейс. Взаимодействие осуществляется
|
- Развитый графический интерфейс. Реализован лишь минимальный веб-интерфейс
|
||||||
через командную строку.
|
(см. раздел 5.1) как надстройка над CLI; полноценного редактора с
|
||||||
|
визуализацией нотного стана, проигрывателем и историей версий нет.
|
||||||
- Прямая интеграция с REAPER в режиме реального времени. Обмен с DAW
|
- Прямая интеграция с REAPER в режиме реального времени. Обмен с DAW
|
||||||
происходит через файлы MIDI.
|
происходит через файлы MIDI.
|
||||||
- Обработка модуляций внутри одного периода. При наличии модуляции в
|
- Обработка модуляций внутри одного периода. При наличии модуляции в
|
||||||
|
|||||||
@@ -0,0 +1,319 @@
|
|||||||
|
"""Minimal Gradio web UI for hamori — generate a harmonic period from a browser.
|
||||||
|
|
||||||
|
A single-page form that wraps :func:`src.generate.generate_period`. Pick the
|
||||||
|
mode, key, style and sampling parameters; the app returns the chord grid plus
|
||||||
|
downloadable ``.chord`` and ``.mid`` files. Russian usage instructions are
|
||||||
|
embedded on the same page (see the "Инструкция" accordion).
|
||||||
|
|
||||||
|
This is a convenience front-end for demonstration only — all generation logic
|
||||||
|
lives in ``src/``. The CLI (``scripts/generate.py``) remains the canonical
|
||||||
|
entry point.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python app.py # serve on http://127.0.0.1:7860
|
||||||
|
python app.py --share # also create a temporary public link
|
||||||
|
python app.py --port 8000
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import tempfile
|
||||||
|
from dataclasses import replace
|
||||||
|
from functools import lru_cache
|
||||||
|
from pathlib import Path
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
import gradio as gr
|
||||||
|
import torch
|
||||||
|
|
||||||
|
from src.generate import generate_period
|
||||||
|
from src.midi_export import chord_file_to_midi
|
||||||
|
from src.model import ChordTransformer
|
||||||
|
from src.tokenizer import VOCAB, write_chord_file
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Constants — choices derived from the vocabulary so the form never drifts.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
CHECKPOINT_DIR = Path(__file__).resolve().parent / "checkpoints"
|
||||||
|
|
||||||
|
NOTE_NAMES = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]
|
||||||
|
|
||||||
|
# A period is 4–16 bars by definition; cap runaway "auto" output at this ceiling.
|
||||||
|
MAX_PERIOD_BARS = 16
|
||||||
|
|
||||||
|
STYLES = [t[len("STYLE_"):] for t in VOCAB if t.startswith("STYLE_")]
|
||||||
|
FUNCTIONS = [t[len("FUNC_"):] for t in VOCAB if t.startswith("FUNC_")]
|
||||||
|
TIMES = [t[len("TIME_"):] for t in VOCAB if t.startswith("TIME_")]
|
||||||
|
|
||||||
|
# Files generated for download live here for the lifetime of the process.
|
||||||
|
OUTPUT_DIR = Path(tempfile.mkdtemp(prefix="hamori_webui_"))
|
||||||
|
|
||||||
|
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Model loading (cached per checkpoint)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@lru_cache(maxsize=2)
|
||||||
|
def _load_model(checkpoint: str) -> ChordTransformer:
|
||||||
|
"""Load and cache a ChordTransformer by checkpoint stem ('pretrained' / 'finetuned')."""
|
||||||
|
path = CHECKPOINT_DIR / f"{checkpoint}.pt"
|
||||||
|
ckpt = torch.load(path, map_location=DEVICE, weights_only=True)
|
||||||
|
model = ChordTransformer(**ckpt["model_config"])
|
||||||
|
model.load_state_dict(ckpt["model_state"])
|
||||||
|
model.to(DEVICE)
|
||||||
|
model.eval()
|
||||||
|
return model
|
||||||
|
|
||||||
|
|
||||||
|
def _available_checkpoints() -> list[str]:
|
||||||
|
return sorted(p.stem for p in CHECKPOINT_DIR.glob("*.pt"))
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Generation callback
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _format_bars(period) -> str:
|
||||||
|
"""Render the bar grid as aligned text, one bar per line."""
|
||||||
|
width = max((len(s) for bar in period.bars for s in bar), default=1)
|
||||||
|
lines = []
|
||||||
|
for i, bar in enumerate(period.bars, 1):
|
||||||
|
cells = " ".join(s.rjust(width) for s in bar)
|
||||||
|
lines.append(f"Bar {i:2d} | {cells}")
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def generate(
|
||||||
|
checkpoint: str,
|
||||||
|
mode: str,
|
||||||
|
key: str,
|
||||||
|
style: str,
|
||||||
|
function: str,
|
||||||
|
time: str,
|
||||||
|
subdivision: int,
|
||||||
|
auto_bars: bool,
|
||||||
|
n_bars: int,
|
||||||
|
temperature: float,
|
||||||
|
top_p: float,
|
||||||
|
repetition_penalty: float,
|
||||||
|
tonic_anchor: bool,
|
||||||
|
prefix_text: str,
|
||||||
|
seed,
|
||||||
|
tempo: int,
|
||||||
|
):
|
||||||
|
"""Run one generation and return (status, bar grid, .chord path, .mid path)."""
|
||||||
|
try:
|
||||||
|
model = _load_model(checkpoint)
|
||||||
|
except Exception as exc: # noqa: BLE001 — surface any load error to the UI
|
||||||
|
return f"❌ Не удалось загрузить чекпойнт «{checkpoint}»: {exc}", "", None, None
|
||||||
|
|
||||||
|
target_key = f"{key}_{mode}"
|
||||||
|
|
||||||
|
# Prefix: explicit text wins; otherwise optionally anchor to the tonic.
|
||||||
|
prefix_chords: list[str] | None
|
||||||
|
prefix_text = (prefix_text or "").strip()
|
||||||
|
if prefix_text:
|
||||||
|
prefix_chords = prefix_text.split()
|
||||||
|
elif tonic_anchor:
|
||||||
|
prefix_chords = [key + ("m" if mode == "minor" else "")]
|
||||||
|
else:
|
||||||
|
prefix_chords = None
|
||||||
|
|
||||||
|
seed_val = int(seed) if seed is not None else None
|
||||||
|
bars_arg = None if auto_bars else int(n_bars)
|
||||||
|
|
||||||
|
try:
|
||||||
|
period = generate_period(
|
||||||
|
model=model,
|
||||||
|
mode=mode,
|
||||||
|
time=time,
|
||||||
|
subdivision=int(subdivision),
|
||||||
|
style=style,
|
||||||
|
function=function,
|
||||||
|
key=target_key,
|
||||||
|
prefix=prefix_chords,
|
||||||
|
temperature=float(temperature),
|
||||||
|
top_p=float(top_p),
|
||||||
|
n_bars=bars_arg,
|
||||||
|
seed=seed_val,
|
||||||
|
repetition_penalty=float(repetition_penalty),
|
||||||
|
)
|
||||||
|
except Exception as exc: # noqa: BLE001 — show generation errors verbatim
|
||||||
|
return f"❌ Ошибка генерации: {exc}", "", None, None
|
||||||
|
|
||||||
|
# "Auto" lets the model close via EOS, but if it never does it can run away
|
||||||
|
# into dozens of NC/hold bars. A period is 4–16 bars — cap the tail.
|
||||||
|
truncated = False
|
||||||
|
if len(period.bars) > MAX_PERIOD_BARS:
|
||||||
|
period = replace(period, bars=period.bars[:MAX_PERIOD_BARS])
|
||||||
|
truncated = True
|
||||||
|
|
||||||
|
period = replace(period, title=f"hamori — {key} {mode}, {function}")
|
||||||
|
|
||||||
|
stem = f"hamori_{key.replace('#', 'sharp')}_{mode}_{uuid4().hex[:6]}"
|
||||||
|
chord_path = OUTPUT_DIR / f"{stem}.chord"
|
||||||
|
midi_path = OUTPUT_DIR / f"{stem}.mid"
|
||||||
|
write_chord_file(period, chord_path)
|
||||||
|
chord_file_to_midi(chord_path, midi_path, tempo=int(tempo))
|
||||||
|
|
||||||
|
status = (
|
||||||
|
f"✅ Готово — {len(period.bars)} тактов · {target_key} · "
|
||||||
|
f"модель: {checkpoint} · seed: {seed_val if seed_val is not None else 'random'}"
|
||||||
|
)
|
||||||
|
if truncated:
|
||||||
|
status += f" · обрезано до {MAX_PERIOD_BARS} тактов (период ≤ 16)"
|
||||||
|
return status, _format_bars(period), str(chord_path), str(midi_path)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Russian instructions (rendered inline)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
INSTRUCTIONS_RU = """
|
||||||
|
## Как пользоваться
|
||||||
|
|
||||||
|
**hamori** генерирует одну гармоническую фразу (период, 4–16 тактов) в заданной
|
||||||
|
тональности и стиле. Это инструмент-подсказчик: он предлагает аккордовую
|
||||||
|
последовательность, которую вы дорабатываете в DAW.
|
||||||
|
|
||||||
|
### Параметры
|
||||||
|
|
||||||
|
| Поле | Что задаёт |
|
||||||
|
|------|------------|
|
||||||
|
| **Модель** | `finetuned` — обучена на вашем корпусе (рекомендуется). `pretrained` — только McGill Billboard, более «общий» звук. |
|
||||||
|
| **Лад / Тональность** | Мажор или минор и тоника результата (например, `F# major`). Модель генерирует в C/Am и транспонирует в выбранную тональность. |
|
||||||
|
| **Стиль / Функция** | Метки условия. `H1K0` — авторский стиль. Функция — роль фрагмента (куплет, припев…). |
|
||||||
|
| **Размер / Subdivision** | Тактовый размер и число позиций на такт. По умолчанию `4/4` и `4`. |
|
||||||
|
| **Число тактов** | Длина периода. «Авто» — модель сама решает, где закрыть фразу. |
|
||||||
|
| **Temperature** | Разброс. `1.0` — норма. Выше — смелее и хаотичнее, ниже — предсказуемее. |
|
||||||
|
| **Top-p** | Нуклеус-сэмплинг. `0.9` — норма. Ниже — консервативнее. |
|
||||||
|
| **Repetition penalty** | Борется с зацикливанием (I–V–I–V). `0.0` — выкл. Для `pretrained` попробуйте `0.5–1.0`; для `finetuned` обычно не нужно. |
|
||||||
|
| **Tonic anchor** | Если префикс пуст — начинать с тоники, чтобы фраза держалась в тональности. |
|
||||||
|
| **Префикс** | Свои стартовые аккорды через пробел в выбранной тональности, напр. `Cmaj7 . Am7 .`. `.` — держать, `NC` — без аккорда. Если задан, перекрывает tonic anchor. |
|
||||||
|
| **Seed** | Фиксирует случайность для воспроизводимости. Очистите поле для случайного результата. |
|
||||||
|
| **Tempo** | Темп MIDI-файла (BPM). На сами аккорды не влияет. |
|
||||||
|
|
||||||
|
### Результат
|
||||||
|
|
||||||
|
- **Сетка аккордов** — текстовый предпросмотр периода.
|
||||||
|
- **`.chord`** — исходный формат проекта (человекочитаемый).
|
||||||
|
- **`.mid`** — импортируйте в REAPER перетаскиванием на дорожку.
|
||||||
|
|
||||||
|
### Рекомендации для старта
|
||||||
|
Модель `finetuned`, `temperature = 1.0`, `top-p = 0.9`, tonic anchor включён.
|
||||||
|
Если получается монотонно или зациклено — поднимите temperature или добавьте
|
||||||
|
repetition penalty.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# UI definition
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def build_ui() -> gr.Blocks:
|
||||||
|
checkpoints = _available_checkpoints()
|
||||||
|
default_ckpt = "finetuned" if "finetuned" in checkpoints else (
|
||||||
|
checkpoints[0] if checkpoints else "finetuned"
|
||||||
|
)
|
||||||
|
|
||||||
|
with gr.Blocks(title="hamori — генератор гармонии") as demo:
|
||||||
|
gr.Markdown(
|
||||||
|
"# hamori 🎶 — генератор гармонических периодов\n"
|
||||||
|
"Заполните форму и нажмите **Сгенерировать**. "
|
||||||
|
"Подробности — в разделе «Инструкция» внизу."
|
||||||
|
)
|
||||||
|
|
||||||
|
with gr.Row():
|
||||||
|
# ---- Left: form ------------------------------------------------
|
||||||
|
with gr.Column(scale=1):
|
||||||
|
checkpoint = gr.Radio(
|
||||||
|
choices=checkpoints or ["finetuned", "pretrained"],
|
||||||
|
value=default_ckpt, label="Модель",
|
||||||
|
)
|
||||||
|
with gr.Row():
|
||||||
|
mode = gr.Radio(["major", "minor"], value="major", label="Лад")
|
||||||
|
key = gr.Dropdown(NOTE_NAMES, value="C", label="Тональность")
|
||||||
|
with gr.Row():
|
||||||
|
style = gr.Dropdown(
|
||||||
|
STYLES, value="H1K0" if "H1K0" in STYLES else STYLES[0],
|
||||||
|
label="Стиль",
|
||||||
|
)
|
||||||
|
function = gr.Dropdown(
|
||||||
|
FUNCTIONS,
|
||||||
|
value="chorus" if "chorus" in FUNCTIONS else FUNCTIONS[0],
|
||||||
|
label="Функция",
|
||||||
|
)
|
||||||
|
with gr.Row():
|
||||||
|
time = gr.Dropdown(
|
||||||
|
TIMES, value="4/4" if "4/4" in TIMES else TIMES[0],
|
||||||
|
label="Размер",
|
||||||
|
)
|
||||||
|
subdivision = gr.Radio([4, 8], value=4, label="Subdivision")
|
||||||
|
|
||||||
|
with gr.Row():
|
||||||
|
auto_bars = gr.Checkbox(value=False, label="Авто (длина сама)")
|
||||||
|
n_bars = gr.Slider(4, 16, value=8, step=1, label="Число тактов")
|
||||||
|
|
||||||
|
with gr.Accordion("Сэмплирование", open=True):
|
||||||
|
temperature = gr.Slider(0.1, 2.0, value=1.0, step=0.05,
|
||||||
|
label="Temperature")
|
||||||
|
top_p = gr.Slider(0.1, 1.0, value=0.9, step=0.05, label="Top-p")
|
||||||
|
repetition_penalty = gr.Slider(
|
||||||
|
0.0, 2.0, value=0.0, step=0.1, label="Repetition penalty",
|
||||||
|
)
|
||||||
|
|
||||||
|
with gr.Accordion("Дополнительно", open=False):
|
||||||
|
tonic_anchor = gr.Checkbox(value=True, label="Tonic anchor")
|
||||||
|
prefix_text = gr.Textbox(
|
||||||
|
label="Префикс (аккорды через пробел)",
|
||||||
|
placeholder="напр. Cmaj7 . Am7 .",
|
||||||
|
)
|
||||||
|
seed = gr.Number(value=42, precision=0,
|
||||||
|
label="Seed (пусто = случайно)")
|
||||||
|
tempo = gr.Number(value=90, precision=0, label="Tempo (BPM)")
|
||||||
|
|
||||||
|
run = gr.Button("Сгенерировать", variant="primary")
|
||||||
|
|
||||||
|
# ---- Right: outputs -------------------------------------------
|
||||||
|
with gr.Column(scale=1):
|
||||||
|
status = gr.Markdown()
|
||||||
|
bars_out = gr.Textbox(label="Сетка аккордов", lines=10,
|
||||||
|
interactive=False)
|
||||||
|
chord_file = gr.File(label="Скачать .chord")
|
||||||
|
midi_file = gr.File(label="Скачать .mid")
|
||||||
|
|
||||||
|
with gr.Accordion("Инструкция", open=False):
|
||||||
|
gr.Markdown(INSTRUCTIONS_RU)
|
||||||
|
|
||||||
|
run.click(
|
||||||
|
fn=generate,
|
||||||
|
inputs=[
|
||||||
|
checkpoint, mode, key, style, function, time, subdivision,
|
||||||
|
auto_bars, n_bars, temperature, top_p, repetition_penalty,
|
||||||
|
tonic_anchor, prefix_text, seed, tempo,
|
||||||
|
],
|
||||||
|
outputs=[status, bars_out, chord_file, midi_file],
|
||||||
|
)
|
||||||
|
|
||||||
|
return demo
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
ap = argparse.ArgumentParser(description=__doc__,
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||||
|
ap.add_argument("--port", type=int, default=7860, help="Server port (default: 7860).")
|
||||||
|
ap.add_argument("--host", default="127.0.0.1", help="Bind address (default: 127.0.0.1).")
|
||||||
|
ap.add_argument("--share", action="store_true",
|
||||||
|
help="Create a temporary public Gradio link.")
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
demo = build_ui()
|
||||||
|
demo.launch(server_name=args.host, server_port=args.port, share=args.share)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -12,5 +12,8 @@ mido==1.3.3
|
|||||||
# Visualization
|
# Visualization
|
||||||
matplotlib==3.10.9
|
matplotlib==3.10.9
|
||||||
|
|
||||||
|
# Web UI (optional — only needed for app.py)
|
||||||
|
gradio==6.16.0
|
||||||
|
|
||||||
# Testing
|
# Testing
|
||||||
pytest==9.0.3
|
pytest==9.0.3
|
||||||
|
|||||||
Reference in New Issue
Block a user