Spaces:
Running
Running
Restore full UI + add page range selection
Browse files- app_gradio.py +659 -170
app_gradio.py
CHANGED
|
@@ -1,170 +1,659 @@
|
|
| 1 |
-
"""
|
| 2 |
-
app_gradio.py
|
| 3 |
-
|
| 4 |
-
Gradio ์น UI โ ์
๋ณด(PDF/PNG/JPG) ์
๋ก๋ โ MML ๋ณํ ๊ฒฐ๊ณผ ๋ฐํ.
|
| 5 |
-
|
| 6 |
-
์คํ:
|
| 7 |
-
python app_gradio.py
|
| 8 |
-
|
| 9 |
-
์ธ๋ถ ์ ์:
|
| 10 |
-
python app_gradio.py --share
|
| 11 |
-
|
| 12 |
-
Hugging Face Spaces ๋ฐฐํฌ:
|
| 13 |
-
Dockerfile์์ ENTRYPOINT๋ก ์คํ๋จ
|
| 14 |
-
"""
|
| 15 |
-
|
| 16 |
-
import argparse
|
| 17 |
-
import datetime
|
| 18 |
-
import
|
| 19 |
-
import
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
import
|
| 23 |
-
|
| 24 |
-
from
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
if
|
| 63 |
-
return
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
app_gradio.py
|
| 3 |
+
|
| 4 |
+
Gradio ์น UI โ ์
๋ณด(PDF/PNG/JPG) ์
๋ก๋ โ MML ๋ณํ ๊ฒฐ๊ณผ ๋ฐํ.
|
| 5 |
+
|
| 6 |
+
์คํ:
|
| 7 |
+
python app_gradio.py
|
| 8 |
+
|
| 9 |
+
์ธ๋ถ ์ ์:
|
| 10 |
+
python app_gradio.py --share
|
| 11 |
+
|
| 12 |
+
Hugging Face Spaces ๋ฐฐํฌ:
|
| 13 |
+
Dockerfile์์ ENTRYPOINT๋ก ์คํ๋จ
|
| 14 |
+
"""
|
| 15 |
+
|
| 16 |
+
import argparse
|
| 17 |
+
import datetime
|
| 18 |
+
import json
|
| 19 |
+
import logging
|
| 20 |
+
import os
|
| 21 |
+
import re
|
| 22 |
+
import tempfile
|
| 23 |
+
import threading
|
| 24 |
+
from pathlib import Path
|
| 25 |
+
from typing import List
|
| 26 |
+
|
| 27 |
+
import gradio as gr
|
| 28 |
+
|
| 29 |
+
from core.convert_pipeline import run_score_pipeline
|
| 30 |
+
|
| 31 |
+
logger = logging.getLogger(__name__)
|
| 32 |
+
|
| 33 |
+
SUPPORTED_EXTENSIONS = [".pdf", ".png", ".jpg", ".jpeg"]
|
| 34 |
+
MAX_PDF_PAGES = int(os.environ.get("MAX_PDF_PAGES", "5"))
|
| 35 |
+
_SEP = "-" * 40
|
| 36 |
+
|
| 37 |
+
# ---------------------------------------------------------------------------
|
| 38 |
+
# Persistent stats via Hugging Face Dataset repo
|
| 39 |
+
# ---------------------------------------------------------------------------
|
| 40 |
+
_STATS_REPO = os.environ.get("STATS_REPO", "Coconuttttt/score-to-mml-stats")
|
| 41 |
+
_STATS_FILE = "stats.json"
|
| 42 |
+
_stats_lock = threading.Lock()
|
| 43 |
+
|
| 44 |
+
# Real-time active conversion counter
|
| 45 |
+
_active_lock = threading.Lock()
|
| 46 |
+
_active_count = 0
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def _hf_api():
|
| 50 |
+
"""Get HfApi instance (lazy import โ huggingface_hub ships with gradio)."""
|
| 51 |
+
try:
|
| 52 |
+
from huggingface_hub import HfApi
|
| 53 |
+
token = os.environ.get("HF_TOKEN")
|
| 54 |
+
return HfApi(token=token) if token else None
|
| 55 |
+
except ImportError:
|
| 56 |
+
return None
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def _load_stats() -> dict:
|
| 60 |
+
"""Download stats.json from HF Dataset repo. Returns dict or empty."""
|
| 61 |
+
api = _hf_api()
|
| 62 |
+
if not api:
|
| 63 |
+
return {}
|
| 64 |
+
try:
|
| 65 |
+
from huggingface_hub import hf_hub_download
|
| 66 |
+
token = os.environ.get("HF_TOKEN")
|
| 67 |
+
path = hf_hub_download(
|
| 68 |
+
repo_id=_STATS_REPO, filename=_STATS_FILE,
|
| 69 |
+
repo_type="dataset", token=token,
|
| 70 |
+
force_download=True,
|
| 71 |
+
)
|
| 72 |
+
with open(path, encoding="utf-8") as f:
|
| 73 |
+
return json.load(f)
|
| 74 |
+
except Exception:
|
| 75 |
+
return {}
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def _save_stats(data: dict):
|
| 79 |
+
"""Upload stats.json to HF Dataset repo."""
|
| 80 |
+
api = _hf_api()
|
| 81 |
+
if not api:
|
| 82 |
+
return
|
| 83 |
+
try:
|
| 84 |
+
tmp = Path(tempfile.gettempdir()) / _STATS_FILE
|
| 85 |
+
tmp.write_text(json.dumps(data, indent=2, ensure_ascii=False), encoding="utf-8")
|
| 86 |
+
api.upload_file(
|
| 87 |
+
path_or_fileobj=str(tmp),
|
| 88 |
+
path_in_repo=_STATS_FILE,
|
| 89 |
+
repo_id=_STATS_REPO,
|
| 90 |
+
repo_type="dataset",
|
| 91 |
+
)
|
| 92 |
+
except Exception as e:
|
| 93 |
+
logger.warning("stats save failed: %s", e)
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def _bump_stat(key: str, extra: dict | None = None):
|
| 97 |
+
"""Increment today's counter + hourly bucket (thread-safe).
|
| 98 |
+
extra: optional dict to merge into today's record (e.g. error details).
|
| 99 |
+
"""
|
| 100 |
+
now = datetime.datetime.now()
|
| 101 |
+
today = now.strftime("%Y-%m-%d")
|
| 102 |
+
hour = now.strftime("%H")
|
| 103 |
+
with _stats_lock:
|
| 104 |
+
data = _load_stats()
|
| 105 |
+
if today not in data:
|
| 106 |
+
data[today] = {"visits": 0, "conversions": 0, "errors": 0, "hourly": {}}
|
| 107 |
+
# Migrate old format
|
| 108 |
+
if "hourly" not in data[today]:
|
| 109 |
+
data[today]["hourly"] = {}
|
| 110 |
+
if "errors" not in data[today]:
|
| 111 |
+
data[today]["errors"] = 0
|
| 112 |
+
# Daily total
|
| 113 |
+
data[today][key] = data[today].get(key, 0) + 1
|
| 114 |
+
# Hourly bucket
|
| 115 |
+
if hour not in data[today]["hourly"]:
|
| 116 |
+
data[today]["hourly"][hour] = {"visits": 0, "conversions": 0, "errors": 0}
|
| 117 |
+
data[today]["hourly"][hour][key] = data[today]["hourly"][hour].get(key, 0) + 1
|
| 118 |
+
# Track peak concurrent
|
| 119 |
+
with _active_lock:
|
| 120 |
+
cur = _active_count
|
| 121 |
+
peak = data[today].get("peak_concurrent", 0)
|
| 122 |
+
if cur > peak:
|
| 123 |
+
data[today]["peak_concurrent"] = cur
|
| 124 |
+
# Merge extra (e.g. recent error messages)
|
| 125 |
+
if extra:
|
| 126 |
+
if "recent_errors" not in data[today]:
|
| 127 |
+
data[today]["recent_errors"] = []
|
| 128 |
+
data[today]["recent_errors"].append(extra)
|
| 129 |
+
# Keep only last 10 errors per day
|
| 130 |
+
data[today]["recent_errors"] = data[today]["recent_errors"][-10:]
|
| 131 |
+
_save_stats(data)
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
def _record_visit():
|
| 135 |
+
"""Called on page load via demo.load()."""
|
| 136 |
+
threading.Thread(target=_bump_stat, args=("visits",), daemon=True).start()
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
def _record_conversion():
|
| 140 |
+
"""Called after successful conversion."""
|
| 141 |
+
threading.Thread(target=_bump_stat, args=("conversions",), daemon=True).start()
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
def _record_error(error_msg: str):
|
| 145 |
+
"""Called on conversion failure โ logs error type + message."""
|
| 146 |
+
now = datetime.datetime.now().strftime("%H:%M:%S")
|
| 147 |
+
short = str(error_msg)[:200]
|
| 148 |
+
extra = {"time": now, "msg": short}
|
| 149 |
+
threading.Thread(target=_bump_stat, args=("errors", extra), daemon=True).start()
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
def _get_dashboard() -> str:
|
| 153 |
+
"""Build admin dashboard text from stats."""
|
| 154 |
+
with _active_lock:
|
| 155 |
+
active = _active_count
|
| 156 |
+
data = _load_stats()
|
| 157 |
+
today = datetime.date.today().isoformat()
|
| 158 |
+
lines = []
|
| 159 |
+
lines.append(f"=== ์ค์๊ฐ ===")
|
| 160 |
+
lines.append(f"ํ์ฌ ๋ณํ ์ค: {active}๊ฑด")
|
| 161 |
+
lines.append("")
|
| 162 |
+
|
| 163 |
+
# Today
|
| 164 |
+
td = data.get(today, {})
|
| 165 |
+
lines.append(f"=== ์ค๋ ({today}) ===")
|
| 166 |
+
lines.append(f"๋ฐฉ๋ฌธ: {td.get('visits', 0)} | ๋ณํ: {td.get('conversions', 0)} | ์๋ฌ: {td.get('errors', 0)} | ํผํฌ ๋์: {td.get('peak_concurrent', 0)}")
|
| 167 |
+
hourly = td.get("hourly", {})
|
| 168 |
+
if hourly:
|
| 169 |
+
lines.append("")
|
| 170 |
+
lines.append("์๊ฐ๋๋ณ:")
|
| 171 |
+
for h in sorted(hourly.keys()):
|
| 172 |
+
hd = hourly[h]
|
| 173 |
+
err_str = f" ์๋ฌ: {hd.get('errors',0):>3}" if hd.get('errors', 0) else ""
|
| 174 |
+
lines.append(f" {h}์ ๋ฐฉ๋ฌธ: {hd.get('visits',0):>3} ๋ณํ: {hd.get('conversions',0):>3}{err_str}")
|
| 175 |
+
|
| 176 |
+
# Recent errors today
|
| 177 |
+
recent_errs = td.get("recent_errors", [])
|
| 178 |
+
if recent_errs:
|
| 179 |
+
lines.append("")
|
| 180 |
+
lines.append("์ต๊ทผ ์๋ฌ:")
|
| 181 |
+
for e in recent_errs[-5:]:
|
| 182 |
+
lines.append(f" [{e.get('time','')}] {e.get('msg','')[:100]}")
|
| 183 |
+
lines.append("")
|
| 184 |
+
|
| 185 |
+
# Recent 7 days summary
|
| 186 |
+
lines.append("=== ์ต๊ทผ 7์ผ ===")
|
| 187 |
+
lines.append(f"{'๋ ์ง':<12} {'๋ฐฉ๋ฌธ':>5} {'๋ณํ':>5} {'์๋ฌ':>4} {'ํผํฌ':>4}")
|
| 188 |
+
lines.append("-" * 38)
|
| 189 |
+
dates = sorted(data.keys(), reverse=True)[:7]
|
| 190 |
+
total_v, total_c, total_e = 0, 0, 0
|
| 191 |
+
for d in dates:
|
| 192 |
+
dd = data[d]
|
| 193 |
+
v = dd.get("visits", 0)
|
| 194 |
+
c = dd.get("conversions", 0)
|
| 195 |
+
e = dd.get("errors", 0)
|
| 196 |
+
p = dd.get("peak_concurrent", 0)
|
| 197 |
+
total_v += v
|
| 198 |
+
total_c += c
|
| 199 |
+
total_e += e
|
| 200 |
+
lines.append(f"{d:<12} {v:>5} {c:>5} {e:>4} {p:>4}")
|
| 201 |
+
lines.append("-" * 38)
|
| 202 |
+
lines.append(f"{'ํฉ๊ณ':<12} {total_v:>5} {total_c:>5} {total_e:>4}")
|
| 203 |
+
|
| 204 |
+
return "\n".join(lines)
|
| 205 |
+
|
| 206 |
+
_PREPROCESS_MAP = {"์์": "none", "Otsu": "otsu", "Adaptive": "adaptive", "๋๋น๊ฐํ": "contrast"}
|
| 207 |
+
_UPSCALE_MAP = {
|
| 208 |
+
"์์": "none",
|
| 209 |
+
"PIL 2ร": "pil_2", "PIL 3ร": "pil_3",
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
|
| 213 |
+
def _extract_mml_parts(text: str) -> List[str]:
|
| 214 |
+
"""Extract MML content from MML@...; patterns."""
|
| 215 |
+
matches = re.findall(r'MML@([^;]*);', text, re.IGNORECASE)
|
| 216 |
+
return matches
|
| 217 |
+
|
| 218 |
+
|
| 219 |
+
def _to_mabi_tracks(text: str) -> List[str]:
|
| 220 |
+
"""Convert Part-labeled MML text to MabiIcco track strings.
|
| 221 |
+
|
| 222 |
+
Mabinogi: 1 track = max 3 channels (melody, chord, bass).
|
| 223 |
+
N parts โ ceil(N/3) tracks, each: MML@ch1,ch2,ch3;
|
| 224 |
+
Empty channels filled with empty string: MML@p7,,;
|
| 225 |
+
"""
|
| 226 |
+
parts = _extract_mml_parts(text)
|
| 227 |
+
if not parts:
|
| 228 |
+
return []
|
| 229 |
+
tracks = []
|
| 230 |
+
for i in range(0, len(parts), 3):
|
| 231 |
+
chunk = parts[i:i + 3]
|
| 232 |
+
while len(chunk) < 3:
|
| 233 |
+
chunk.append("")
|
| 234 |
+
tracks.append("MML@" + ",".join(chunk) + ";")
|
| 235 |
+
return tracks
|
| 236 |
+
|
| 237 |
+
|
| 238 |
+
def _make_track_files(text: str, prefix: str, ts: str, tmpdir: Path) -> List[str]:
|
| 239 |
+
"""Generate one .mml file per track, return list of file paths."""
|
| 240 |
+
tracks = _to_mabi_tracks(text)
|
| 241 |
+
if not tracks:
|
| 242 |
+
return []
|
| 243 |
+
paths = []
|
| 244 |
+
for i, track in enumerate(tracks, 1):
|
| 245 |
+
p = tmpdir / f"{prefix}_track{i}_{ts}.mml"
|
| 246 |
+
p.write_text(track, encoding="utf-8")
|
| 247 |
+
paths.append(str(p))
|
| 248 |
+
return paths
|
| 249 |
+
|
| 250 |
+
|
| 251 |
+
_MABI_CHAR_LIMIT = 1500 # ๋ง๋น๋
ธ๊ธฐ MML ์ฑ๋๋น ๊ธ์์ ์ ํ (๊ทผ์ฌ๊ฐ)
|
| 252 |
+
|
| 253 |
+
|
| 254 |
+
def _mml_char_info(text: str, label: str) -> str:
|
| 255 |
+
"""Generate per-part character count summary. Warns if over limit."""
|
| 256 |
+
parts = _extract_mml_parts(text)
|
| 257 |
+
if not parts:
|
| 258 |
+
return ""
|
| 259 |
+
lines = [f"[{label} ๊ธ์์]"]
|
| 260 |
+
for i, p in enumerate(parts, 1):
|
| 261 |
+
full = f"MML@{p};"
|
| 262 |
+
n = len(full)
|
| 263 |
+
warn = f" *** {_MABI_CHAR_LIMIT}์ ์ด๊ณผ!" if n > _MABI_CHAR_LIMIT else ""
|
| 264 |
+
lines.append(f" Part {i}: {n}์{warn}")
|
| 265 |
+
return "\n".join(lines)
|
| 266 |
+
|
| 267 |
+
|
| 268 |
+
def _check_pdf_pages(file_path: str) -> str | None:
|
| 269 |
+
if Path(file_path).suffix.lower() != ".pdf":
|
| 270 |
+
return None
|
| 271 |
+
try:
|
| 272 |
+
import fitz
|
| 273 |
+
doc = fitz.open(file_path)
|
| 274 |
+
count = doc.page_count
|
| 275 |
+
doc.close()
|
| 276 |
+
if count > MAX_PDF_PAGES:
|
| 277 |
+
return f"PDF๊ฐ {count}ํ์ด์ง์
๋๋ค. ์ต๋ {MAX_PDF_PAGES}ํ์ด์ง๊น์ง ์ง์ํฉ๋๋ค."
|
| 278 |
+
except Exception:
|
| 279 |
+
pass
|
| 280 |
+
return None
|
| 281 |
+
|
| 282 |
+
|
| 283 |
+
def convert(file_path, preprocess, dpi_str, upscale, page_start, page_end, progress=gr.Progress()):
|
| 284 |
+
global _active_count
|
| 285 |
+
empty = ("", "", [], [], [], [], [], "")
|
| 286 |
+
if file_path is None:
|
| 287 |
+
return (*empty[:7], "ํ์ผ์ ์
๋ก๋ํด์ฃผ์ธ์.")
|
| 288 |
+
|
| 289 |
+
ext = Path(file_path).suffix.lower()
|
| 290 |
+
if ext not in SUPPORTED_EXTENSIONS:
|
| 291 |
+
return (*empty[:7], f"์ง์ํ์ง ์๋ ํ์ผ ํ์์
๋๋ค: {ext}\n์ง์ ํ์: PDF, PNG, JPG")
|
| 292 |
+
|
| 293 |
+
page_err = _check_pdf_pages(file_path)
|
| 294 |
+
ps = int(page_start) if page_start else 0
|
| 295 |
+
pe = int(page_end) if page_end else 0
|
| 296 |
+
if page_err and ps == 0 and pe == 0:
|
| 297 |
+
return (*empty[:7], page_err)
|
| 298 |
+
|
| 299 |
+
dpi = int(dpi_str) if dpi_str else 300
|
| 300 |
+
|
| 301 |
+
# Use a persistent work_dir so we can retrieve page PNGs for preview
|
| 302 |
+
work_dir = tempfile.mkdtemp(prefix="sml_ui_")
|
| 303 |
+
|
| 304 |
+
with _active_lock:
|
| 305 |
+
_active_count += 1
|
| 306 |
+
try:
|
| 307 |
+
combined, mxl_paths, xml_paths, warnings = run_score_pipeline(
|
| 308 |
+
input_path=file_path,
|
| 309 |
+
preprocess_mode=_PREPROCESS_MAP.get(preprocess, "none"),
|
| 310 |
+
dpi=dpi,
|
| 311 |
+
upscale_mode=_UPSCALE_MAP.get(upscale, "none"),
|
| 312 |
+
on_progress=lambda frac, desc: progress(frac, desc=desc),
|
| 313 |
+
correct_xml=False,
|
| 314 |
+
save_dir=work_dir,
|
| 315 |
+
page_start=ps,
|
| 316 |
+
page_end=pe,
|
| 317 |
+
)
|
| 318 |
+
except Exception as e:
|
| 319 |
+
with _active_lock:
|
| 320 |
+
_active_count -= 1
|
| 321 |
+
_record_error(str(e))
|
| 322 |
+
return (*empty[:7], f"๋ณํ ์ค ์ค๋ฅ ๋ฐ์:\n{e}")
|
| 323 |
+
|
| 324 |
+
with _active_lock:
|
| 325 |
+
_active_count -= 1
|
| 326 |
+
|
| 327 |
+
# Collect page preview images from work_dir/pages/
|
| 328 |
+
pages_dir = Path(work_dir) / "pages"
|
| 329 |
+
preview_images = sorted(pages_dir.glob("*.png")) if pages_dir.exists() else []
|
| 330 |
+
# Filter: show preprocessed versions if they exist, otherwise originals
|
| 331 |
+
pre_imgs = [p for p in preview_images if "_pre" in p.name or "_norm" in p.name]
|
| 332 |
+
if not pre_imgs:
|
| 333 |
+
pre_imgs = [p for p in preview_images if not p.name.endswith(("_norm.png",))]
|
| 334 |
+
# Convert to string paths for gr.Gallery
|
| 335 |
+
preview_paths = [str(p) for p in (pre_imgs if pre_imgs else preview_images)]
|
| 336 |
+
|
| 337 |
+
# Split combined into Whole Part and 3 Part
|
| 338 |
+
if _SEP in combined:
|
| 339 |
+
parts = combined.split(_SEP, 1)
|
| 340 |
+
whole_text = parts[0].strip()
|
| 341 |
+
three_text = parts[1].strip()
|
| 342 |
+
else:
|
| 343 |
+
whole_text = combined.strip()
|
| 344 |
+
three_text = ""
|
| 345 |
+
|
| 346 |
+
# Generate MabiIcco-compatible MML files (1 file per track)
|
| 347 |
+
stem = Path(file_path).stem
|
| 348 |
+
ts = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 349 |
+
tmpdir = Path(tempfile.gettempdir())
|
| 350 |
+
|
| 351 |
+
whole_files = _make_track_files(whole_text, f"{stem}_whole", ts, tmpdir)
|
| 352 |
+
three_files = _make_track_files(three_text, f"{stem}_3part", ts, tmpdir) if three_text else []
|
| 353 |
+
|
| 354 |
+
_record_conversion()
|
| 355 |
+
|
| 356 |
+
# Build warnings + char count info
|
| 357 |
+
info_parts = []
|
| 358 |
+
if warnings:
|
| 359 |
+
info_parts.append("\n".join(f"[WARN] {w}" for w in warnings))
|
| 360 |
+
else:
|
| 361 |
+
info_parts.append("๊ฒฝ๊ณ ์์")
|
| 362 |
+
whole_info = _mml_char_info(whole_text, "Whole Part")
|
| 363 |
+
three_info = _mml_char_info(three_text, "3 Part")
|
| 364 |
+
if whole_info:
|
| 365 |
+
info_parts.append(whole_info)
|
| 366 |
+
if three_info:
|
| 367 |
+
info_parts.append(three_info)
|
| 368 |
+
warnings_text = "\n\n".join(info_parts)
|
| 369 |
+
|
| 370 |
+
return (
|
| 371 |
+
whole_text,
|
| 372 |
+
three_text,
|
| 373 |
+
whole_files,
|
| 374 |
+
three_files,
|
| 375 |
+
mxl_paths,
|
| 376 |
+
xml_paths,
|
| 377 |
+
preview_paths,
|
| 378 |
+
warnings_text,
|
| 379 |
+
)
|
| 380 |
+
|
| 381 |
+
|
| 382 |
+
def _apply_tempo(whole_text: str, three_text: str, tempo: int):
|
| 383 |
+
"""๊ฐ MML@...; ํํธ์ ํ
ํฌ๋ฅผ T{tempo}๋ก ๊ต์ฒด/์ฝ์
ํ๊ณ ๋ค์ด๋ก๋ ํ์ผ ์ฌ์์ฑ."""
|
| 384 |
+
tempo = max(32, min(255, int(tempo)))
|
| 385 |
+
|
| 386 |
+
def replace_tempo(text):
|
| 387 |
+
if not text:
|
| 388 |
+
return text
|
| 389 |
+
# MML@T120... โ MML@T{tempo}... or MML@c4... โ MML@T{tempo}c4...
|
| 390 |
+
return re.sub(r'(MML@)(T\d+)?', rf'\1T{tempo}', text, flags=re.IGNORECASE)
|
| 391 |
+
|
| 392 |
+
new_whole = replace_tempo(whole_text)
|
| 393 |
+
new_three = replace_tempo(three_text)
|
| 394 |
+
|
| 395 |
+
# Regenerate download files (1 per track)
|
| 396 |
+
ts = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 397 |
+
tmpdir = Path(tempfile.gettempdir())
|
| 398 |
+
|
| 399 |
+
whole_files = _make_track_files(new_whole, f"tempo{tempo}_whole", ts, tmpdir) if new_whole else []
|
| 400 |
+
three_files = _make_track_files(new_three, f"tempo{tempo}_3part", ts, tmpdir) if new_three else []
|
| 401 |
+
|
| 402 |
+
return (
|
| 403 |
+
new_whole,
|
| 404 |
+
new_three,
|
| 405 |
+
whole_files,
|
| 406 |
+
three_files,
|
| 407 |
+
)
|
| 408 |
+
|
| 409 |
+
|
| 410 |
+
# ---------------------------------------------------------------------------
|
| 411 |
+
# MML Player JS โ gr.Button.click(fn=None, js="() => {...}") is the only
|
| 412 |
+
# reliable way to execute JS in Gradio (innerHTML/head/js all fail).
|
| 413 |
+
# ---------------------------------------------------------------------------
|
| 414 |
+
|
| 415 |
+
_JS_INIT = """
|
| 416 |
+
if(!window._MP){
|
| 417 |
+
window._MP={actx:null,playing:[],stop:false};
|
| 418 |
+
window._MP.parse=function(mml){
|
| 419 |
+
var notes=[],tempo=120,oct=4,dL=4,vol=8,i=0;
|
| 420 |
+
var s=mml.toUpperCase().replace(/\\s+/g,'');
|
| 421 |
+
var nm={C:0,D:2,E:4,F:5,G:7,A:9,B:11};
|
| 422 |
+
function rn(){var n='';while(i<s.length&&s[i]>='0'&&s[i]<='9'){n+=s[i];i++;}return n?parseInt(n):0;}
|
| 423 |
+
function ls(l){var b=60/tempo*4/l,d=0;while(i<s.length&&s[i]==='.'){d++;i++;}var x=b;for(var j=0;j<d;j++){x/=2;b+=x;}return b;}
|
| 424 |
+
while(i<s.length){
|
| 425 |
+
var c=s[i];
|
| 426 |
+
if(c==='T'){i++;tempo=rn()||120;}
|
| 427 |
+
else if(c==='O'){i++;oct=rn()||4;}
|
| 428 |
+
else if(c==='L'){i++;dL=rn()||4;}
|
| 429 |
+
else if(c==='V'){i++;vol=rn();if(vol>15)vol=15;}
|
| 430 |
+
else if(c==='>'){i++;oct=Math.min(oct+1,8);}
|
| 431 |
+
else if(c==='<'){i++;oct=Math.max(oct-1,1);}
|
| 432 |
+
else if(c==='&'){i++;}
|
| 433 |
+
else if(c==='N'){i++;var nn=rn();var ll=rn()||dL;notes.push({m:nn,d:ls(ll),v:vol});}
|
| 434 |
+
else if(nm[c]!==undefined){
|
| 435 |
+
i++;var sm=nm[c];
|
| 436 |
+
if(i<s.length&&s[i]==='+'){sm++;i++;}
|
| 437 |
+
else if(i<s.length&&(s[i]==='-'||s[i]==='#')){if(s[i]==='#')sm++;else sm--;i++;}
|
| 438 |
+
var ll2=rn()||dL;var dur=ls(ll2);notes.push({m:(oct+1)*12+sm,d:dur,v:vol});
|
| 439 |
+
}
|
| 440 |
+
else if(c==='R'){i++;var ll3=rn()||dL;notes.push({m:-1,d:ls(ll3),v:0});}
|
| 441 |
+
else{i++;}
|
| 442 |
+
}
|
| 443 |
+
return notes;
|
| 444 |
+
};
|
| 445 |
+
window._MP.freq=function(m){return 440*Math.pow(2,(m-69)/12);};
|
| 446 |
+
window._MP.playN=function(nl){
|
| 447 |
+
var P=window._MP;
|
| 448 |
+
if(!P.actx)P.actx=new(window.AudioContext||window.webkitAudioContext)();
|
| 449 |
+
if(P.actx.state==='suspended')P.actx.resume();
|
| 450 |
+
var t=P.actx.currentTime+0.05;
|
| 451 |
+
for(var i=0;i<nl.length;i++){
|
| 452 |
+
var n=nl[i];if(P.stop)break;if(n.m<0){t+=n.d;continue;}
|
| 453 |
+
var o=P.actx.createOscillator(),g=P.actx.createGain(),v=n.v/15*0.3;
|
| 454 |
+
o.type='triangle';o.frequency.value=P.freq(n.m);
|
| 455 |
+
g.gain.setValueAtTime(v,t);g.gain.exponentialRampToValueAtTime(0.001,t+n.d*0.95);
|
| 456 |
+
o.connect(g);g.connect(P.actx.destination);o.start(t);o.stop(t+n.d);
|
| 457 |
+
P.playing.push(o);t+=n.d;
|
| 458 |
+
}
|
| 459 |
+
return t-P.actx.currentTime;
|
| 460 |
+
};
|
| 461 |
+
window._MP.getFrom=function(elemId){
|
| 462 |
+
var box=document.getElementById(elemId);
|
| 463 |
+
if(box){var ta=box.querySelector('textarea');if(ta&&ta.value)return ta.value.trim();}
|
| 464 |
+
return '';
|
| 465 |
+
};
|
| 466 |
+
window._MP.extract=function(t){var m=t.match(/MML@[^;]*;/gi)||[];return m.map(function(s){return s.replace(/^MML@/i,'').replace(/;$/,'');});};
|
| 467 |
+
window._MP.doStop=function(){
|
| 468 |
+
var P=window._MP;P.stop=true;
|
| 469 |
+
P.playing.forEach(function(o){try{o.stop();}catch(e){}});P.playing=[];
|
| 470 |
+
};
|
| 471 |
+
window._MP.go=function(mmlArr,label){
|
| 472 |
+
var P=window._MP;P.doStop();P.stop=false;
|
| 473 |
+
if(!mmlArr.length){alert('์ฌ์ํ MML์ด ์์ต๋๋ค');return;}
|
| 474 |
+
var total=0,maxD=0;
|
| 475 |
+
for(var i=0;i<mmlArr.length;i++){
|
| 476 |
+
var notes=P.parse(mmlArr[i]);total+=notes.length;
|
| 477 |
+
var d=P.playN(notes);if(d>maxD)maxD=d;
|
| 478 |
+
}
|
| 479 |
+
if(total===0){alert('ํ์ฑ๋ ๋
ธํธ๊ฐ ์์ต๋๋ค');return;}
|
| 480 |
+
alert(label+' ์ฌ์ ์์ ('+total+'๊ฐ ๋
ธํธ, '+mmlArr.length+'ํธ๋)');
|
| 481 |
+
};
|
| 482 |
+
}
|
| 483 |
+
"""
|
| 484 |
+
|
| 485 |
+
|
| 486 |
+
def _make_box_play_js(elem_id: str, label: str, part_index: int | None = None) -> str:
|
| 487 |
+
"""Play all MML parts from a specific textbox, or a single part by index."""
|
| 488 |
+
if part_index is not None:
|
| 489 |
+
action = (
|
| 490 |
+
f"var parts=P.extract(text);"
|
| 491 |
+
f"if(parts.length>{part_index})P.go([parts[{part_index}]],'{label}');"
|
| 492 |
+
f"else alert('{label} ํํธ๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค');"
|
| 493 |
+
)
|
| 494 |
+
else:
|
| 495 |
+
action = f"var parts=P.extract(text);P.go(parts,'{label}');"
|
| 496 |
+
return "() => {" + _JS_INIT + f"""
|
| 497 |
+
var P=window._MP;
|
| 498 |
+
var text=P.getFrom('{elem_id}');
|
| 499 |
+
if(!text){{alert('MML ๊ฒฐ๊ณผ๊ฐ ์์ต๋๋ค. ๋จผ์ ์
๋ณด๋ฅผ ๋ณํํ์ธ์.');}}
|
| 500 |
+
else{{{action}}}
|
| 501 |
+
}}"""
|
| 502 |
+
|
| 503 |
+
|
| 504 |
+
_JS_STOP = "() => {" + _JS_INIT + "window._MP.doStop();alert('์ ์ง');}"
|
| 505 |
+
|
| 506 |
+
_JS_TEST = "() => {" + _JS_INIT + """
|
| 507 |
+
var P=window._MP;P.doStop();P.stop=false;
|
| 508 |
+
var mml='T120O5G8E8E4F8D8D4C8D8E8F8G4G4G8E8E8E8F8D8D8D8C8E8G8G8E2';
|
| 509 |
+
var notes=P.parse(mml);
|
| 510 |
+
P.playN(notes);
|
| 511 |
+
alert('๋๋น์ผ ํ
์คํธ ์ฌ์ ('+notes.length+'๊ฐ ๋
ธํธ)');
|
| 512 |
+
}"""
|
| 513 |
+
|
| 514 |
+
|
| 515 |
+
def build_ui() -> gr.Blocks:
|
| 516 |
+
with gr.Blocks(title="์
๋ณด โ MML ๋ณํ๊ธฐ") as demo:
|
| 517 |
+
gr.Markdown("# ์
๋ณด โ MML ๋ณํ๊ธฐ")
|
| 518 |
+
gr.Markdown("PDF ๋๋ ์ด๋ฏธ์ง ์
๋ณด๋ฅผ ์
๋ก๋ํ๋ฉด ๋ง๋น๋
ธ๊ธฐ MML๋ก ๋ณํํด๋๋ฆฝ๋๋ค.")
|
| 519 |
+
|
| 520 |
+
with gr.Row():
|
| 521 |
+
with gr.Column():
|
| 522 |
+
file_input = gr.File(
|
| 523 |
+
label="์
๋ณด ํ์ผ (PDF, PNG, JPG)",
|
| 524 |
+
file_types=[".pdf", ".png", ".jpg", ".jpeg"],
|
| 525 |
+
)
|
| 526 |
+
preprocess_radio = gr.Radio(
|
| 527 |
+
choices=["์์", "Otsu", "Adaptive", "๋๋น๊ฐํ"],
|
| 528 |
+
value="์์",
|
| 529 |
+
label="์ ์ฒ๋ฆฌ (์์=์๋ณธ, Otsu=๊นจ๋ํ์ค์บ, Adaptive=์กฐ๋ช
๋ถ๊ท ์ผ, ๋๋น๊ฐํ=ํ๋ฆฐ์ค์บ)",
|
| 530 |
+
)
|
| 531 |
+
dpi = gr.Dropdown(
|
| 532 |
+
choices=["150", "300", "450", "600"],
|
| 533 |
+
value="300",
|
| 534 |
+
label="DPI (PDF๋ง ํด๋น, 300 ๊ถ์ฅ)",
|
| 535 |
+
)
|
| 536 |
+
upscale_radio = gr.Radio(
|
| 537 |
+
choices=["์์", "PIL 2ร", "PIL 3ร"],
|
| 538 |
+
value="์์",
|
| 539 |
+
label="์
์ค์ผ์ผ (PIL ๋ฆฌ์ฌ์ด์ฆ โ ์ ํด์๋ ์ค์บ์ ํจ๊ณผ์ )",
|
| 540 |
+
)
|
| 541 |
+
with gr.Row():
|
| 542 |
+
page_start_input = gr.Number(
|
| 543 |
+
value=0, label="์์ ํ์ด์ง (0=์ฒ์๋ถํฐ)", minimum=0, precision=0,
|
| 544 |
+
)
|
| 545 |
+
page_end_input = gr.Number(
|
| 546 |
+
value=0, label="๋ ํ์ด์ง (0=๋๊น์ง)", minimum=0, precision=0,
|
| 547 |
+
)
|
| 548 |
+
convert_btn = gr.Button("๋ณํ ์์", variant="primary")
|
| 549 |
+
|
| 550 |
+
with gr.Column():
|
| 551 |
+
# ์
๋ ฅ ์ด๋ฏธ์ง ๋ฏธ๋ฆฌ๋ณด๊ธฐ
|
| 552 |
+
preview_gallery = gr.Gallery(
|
| 553 |
+
label="์
๋ ฅ ์
๋ณด ๋ฏธ๋ฆฌ๋ณด๊ธฐ",
|
| 554 |
+
columns=2,
|
| 555 |
+
height=200,
|
| 556 |
+
object_fit="contain",
|
| 557 |
+
)
|
| 558 |
+
|
| 559 |
+
# Whole Part (Nํํธ ์ ์ฒดํ์)
|
| 560 |
+
gr.Markdown("#### Whole Part (์ ์ฒดํ์)")
|
| 561 |
+
whole_output = gr.Textbox(
|
| 562 |
+
label="์ ์ฒดํ์ Nํํธ MML",
|
| 563 |
+
lines=12,
|
| 564 |
+
placeholder="์ ์ฒดํ์ ๊ฒฐ๊ณผ",
|
| 565 |
+
elem_id="mml-whole",
|
| 566 |
+
)
|
| 567 |
+
whole_file = gr.File(label="Whole Part ๋ค์ด๋ก๋ (ํธ๋๋ณ ํ์ผ)", file_count="multiple")
|
| 568 |
+
with gr.Row():
|
| 569 |
+
whole_play_btn = gr.Button("โถ Whole Part ์ฌ์", variant="primary", size="sm")
|
| 570 |
+
stop_btn = gr.Button("โ ์ ์ง", size="sm")
|
| 571 |
+
|
| 572 |
+
# 3 Part (๋ฉ๋ก๋/ํ์/๋ฒ ์ด์ค)
|
| 573 |
+
gr.Markdown("#### 3 Part (๋ฉ๋ก๋ / ํ์ / ๋ฒ ์ด์ค)")
|
| 574 |
+
three_output = gr.Textbox(
|
| 575 |
+
label="3ํํธ MML (Part1=๋ฉ๋ก๋, Part2=ํ์, Part3=๋ฒ ์ด์ค)",
|
| 576 |
+
lines=12,
|
| 577 |
+
placeholder="3ํํธ ๊ฒฐ๊ณผ",
|
| 578 |
+
elem_id="mml-three",
|
| 579 |
+
)
|
| 580 |
+
three_file = gr.File(label="3 Part ๋ค์ด๋ก๋ (ํธ๋๋ณ ํ์ผ)", file_count="multiple")
|
| 581 |
+
with gr.Row():
|
| 582 |
+
three_play_btn = gr.Button("โถ 3 Part ์ ์ฒด", size="sm")
|
| 583 |
+
melody_btn = gr.Button("โถ ๋ฉ๋ก๋", size="sm")
|
| 584 |
+
chord_btn = gr.Button("โถ ํ์", size="sm")
|
| 585 |
+
bass_btn = gr.Button("โถ ๋ฒ ์ด์ค", size="sm")
|
| 586 |
+
|
| 587 |
+
# ๋ฐ์ (Tempo) ์กฐ์
|
| 588 |
+
gr.Markdown("#### ๋ฐ์ ์กฐ์ ")
|
| 589 |
+
with gr.Row():
|
| 590 |
+
tempo_slider = gr.Slider(
|
| 591 |
+
minimum=32, maximum=255, value=120, step=1,
|
| 592 |
+
label="Tempo (32~255, ๊ธฐ๋ณธ: 120)",
|
| 593 |
+
elem_id="tempo-slider",
|
| 594 |
+
)
|
| 595 |
+
tempo_btn = gr.Button("๋ฐ์ ์ ์ฉ", size="sm")
|
| 596 |
+
|
| 597 |
+
# ๊ธฐํ ๋ค์ด๋ก๋
|
| 598 |
+
mxl_output = gr.File(label="MXL ๋ค์ด๋ก๋", file_count="multiple")
|
| 599 |
+
xml_output = gr.File(label="XML ๋ค์ด๋ก๋", file_count="multiple")
|
| 600 |
+
warnings_output = gr.Textbox(label="๊ฒฝ๊ณ ", lines=3, placeholder="๊ฒฝ๊ณ ๋ฉ์์ง")
|
| 601 |
+
|
| 602 |
+
# ํ
์คํธ
|
| 603 |
+
with gr.Row():
|
| 604 |
+
test_btn = gr.Button("๋๋น์ผ ํ
์คํธ", size="sm")
|
| 605 |
+
|
| 606 |
+
# Admin dashboard
|
| 607 |
+
with gr.Accordion("ํต๊ณ ๋์๋ณด๋", open=False):
|
| 608 |
+
dashboard_output = gr.Textbox(
|
| 609 |
+
label="์๋ฒ ํต๊ณ",
|
| 610 |
+
lines=20,
|
| 611 |
+
interactive=False,
|
| 612 |
+
elem_id="dashboard",
|
| 613 |
+
)
|
| 614 |
+
refresh_btn = gr.Button("์๋ก๊ณ ์นจ", size="sm")
|
| 615 |
+
refresh_btn.click(fn=_get_dashboard, outputs=[dashboard_output])
|
| 616 |
+
|
| 617 |
+
# Wire up playback JS
|
| 618 |
+
stop_btn.click(fn=None, js=_JS_STOP)
|
| 619 |
+
whole_play_btn.click(fn=None, js=_make_box_play_js("mml-whole", "Whole Part"))
|
| 620 |
+
three_play_btn.click(fn=None, js=_make_box_play_js("mml-three", "3 Part"))
|
| 621 |
+
melody_btn.click(fn=None, js=_make_box_play_js("mml-three", "๋ฉ๋ก๋", 0))
|
| 622 |
+
chord_btn.click(fn=None, js=_make_box_play_js("mml-three", "ํ์", 1))
|
| 623 |
+
bass_btn.click(fn=None, js=_make_box_play_js("mml-three", "๋ฒ ์ด์ค", 2))
|
| 624 |
+
test_btn.click(fn=None, js=_JS_TEST)
|
| 625 |
+
|
| 626 |
+
# Wire up tempo
|
| 627 |
+
tempo_btn.click(
|
| 628 |
+
fn=_apply_tempo,
|
| 629 |
+
inputs=[whole_output, three_output, tempo_slider],
|
| 630 |
+
outputs=[whole_output, three_output, whole_file, three_file],
|
| 631 |
+
)
|
| 632 |
+
|
| 633 |
+
# Wire up conversion
|
| 634 |
+
convert_btn.click(
|
| 635 |
+
fn=convert,
|
| 636 |
+
inputs=[file_input, preprocess_radio, dpi, upscale_radio, page_start_input, page_end_input],
|
| 637 |
+
outputs=[whole_output, three_output, whole_file, three_file,
|
| 638 |
+
mxl_output, xml_output, preview_gallery, warnings_output],
|
| 639 |
+
)
|
| 640 |
+
|
| 641 |
+
# Record page visit
|
| 642 |
+
demo.load(fn=_record_visit)
|
| 643 |
+
|
| 644 |
+
return demo
|
| 645 |
+
|
| 646 |
+
|
| 647 |
+
if __name__ == "__main__":
|
| 648 |
+
parser = argparse.ArgumentParser()
|
| 649 |
+
parser.add_argument("--share", action="store_true", help="์ธ๋ถ ๊ณต์ ๋งํฌ ์์ฑ")
|
| 650 |
+
parser.add_argument("--port", type=int, default=7860, help="ํฌํธ ๋ฒํธ (๊ธฐ๋ณธ: 7860)")
|
| 651 |
+
args = parser.parse_args()
|
| 652 |
+
|
| 653 |
+
demo = build_ui()
|
| 654 |
+
demo.queue(max_size=10, default_concurrency_limit=1)
|
| 655 |
+
demo.launch(
|
| 656 |
+
share=args.share,
|
| 657 |
+
server_name="0.0.0.0",
|
| 658 |
+
server_port=args.port,
|
| 659 |
+
)
|