Spaces:
Running
Running
Commit ·
cfc429a
1
Parent(s): 97fd84e
Add corrector tool as iframe tab in Gradio UI
Browse files- Embed corrector (HTML/JS/CSS) in new "교정 도구" tab via iframe
- Add loadFromData() + postMessage listener for programmatic data loading
- Convert function now returns corrector data (image paths + XML texts as JSON)
- Uses _norm.png images (Audiveris-processed) for coordinate accuracy
- "교정 도구 열기" button sends conversion results to iframe via postMessage
- Dockerfile updated to COPY static/ directory
- allowed_paths includes static dir + temp dir for file serving
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Dockerfile +1 -0
- app_gradio.py +190 -98
- static/corrector.css +144 -0
- static/corrector.html +130 -0
- static/corrector.js +0 -0
Dockerfile
CHANGED
|
@@ -48,6 +48,7 @@ RUN pip install --no-cache-dir -r /tmp/requirements.txt
|
|
| 48 |
# Copy application code
|
| 49 |
WORKDIR /app
|
| 50 |
COPY core/ ./core/
|
|
|
|
| 51 |
COPY app_gradio.py .
|
| 52 |
COPY convert_3part.py .
|
| 53 |
|
|
|
|
| 48 |
# Copy application code
|
| 49 |
WORKDIR /app
|
| 50 |
COPY core/ ./core/
|
| 51 |
+
COPY static/ ./static/
|
| 52 |
COPY app_gradio.py .
|
| 53 |
COPY convert_3part.py .
|
| 54 |
|
app_gradio.py
CHANGED
|
@@ -282,19 +282,19 @@ def _check_pdf_pages(file_path: str) -> str | None:
|
|
| 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[:
|
| 288 |
|
| 289 |
ext = Path(file_path).suffix.lower()
|
| 290 |
if ext not in SUPPORTED_EXTENSIONS:
|
| 291 |
-
return (*empty[:
|
| 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[:
|
| 298 |
|
| 299 |
dpi = int(dpi_str) if dpi_str else 300
|
| 300 |
|
|
@@ -319,7 +319,7 @@ def convert(file_path, preprocess, dpi_str, upscale, page_start, page_end, progr
|
|
| 319 |
with _active_lock:
|
| 320 |
_active_count -= 1
|
| 321 |
_record_error(str(e))
|
| 322 |
-
return (*empty[:
|
| 323 |
|
| 324 |
with _active_lock:
|
| 325 |
_active_count -= 1
|
|
@@ -367,6 +367,36 @@ def convert(file_path, preprocess, dpi_str, upscale, page_start, page_end, progr
|
|
| 367 |
info_parts.append(three_info)
|
| 368 |
warnings_text = "\n\n".join(info_parts)
|
| 369 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 370 |
return (
|
| 371 |
whole_text,
|
| 372 |
three_text,
|
|
@@ -375,6 +405,7 @@ def convert(file_path, preprocess, dpi_str, upscale, page_start, page_end, progr
|
|
| 375 |
mxl_paths,
|
| 376 |
xml_paths,
|
| 377 |
preview_paths,
|
|
|
|
| 378 |
warnings_text,
|
| 379 |
)
|
| 380 |
|
|
@@ -522,102 +553,128 @@ def build_ui() -> gr.Blocks:
|
|
| 522 |
size="lg",
|
| 523 |
)
|
| 524 |
|
| 525 |
-
with gr.
|
| 526 |
-
|
| 527 |
-
|
| 528 |
-
label="악보 파일 (PDF, PNG, JPG)",
|
| 529 |
-
file_types=[".pdf", ".png", ".jpg", ".jpeg"],
|
| 530 |
-
)
|
| 531 |
-
preprocess_radio = gr.Radio(
|
| 532 |
-
choices=["없음", "Otsu", "Adaptive", "대비강화"],
|
| 533 |
-
value="없음",
|
| 534 |
-
label="전처리 (없음=원본, Otsu=깨끗한스캔, Adaptive=조명불균일, 대비강화=흐린스캔)",
|
| 535 |
-
)
|
| 536 |
-
dpi = gr.Dropdown(
|
| 537 |
-
choices=["150", "300", "450", "600"],
|
| 538 |
-
value="300",
|
| 539 |
-
label="DPI (PDF만 해당, 300 권장)",
|
| 540 |
-
)
|
| 541 |
-
upscale_radio = gr.Radio(
|
| 542 |
-
choices=["없음", "PIL 2×", "PIL 3×"],
|
| 543 |
-
value="없음",
|
| 544 |
-
label="업스케일 (PIL 리사이즈 — 저해상도 스캔에 효과적)",
|
| 545 |
-
)
|
| 546 |
with gr.Row():
|
| 547 |
-
|
| 548 |
-
|
| 549 |
-
|
| 550 |
-
|
| 551 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 552 |
)
|
| 553 |
-
|
| 554 |
-
|
| 555 |
-
|
| 556 |
-
|
| 557 |
-
|
| 558 |
-
|
| 559 |
-
|
| 560 |
-
|
| 561 |
-
|
|
|
|
| 562 |
)
|
| 563 |
-
|
| 564 |
-
|
| 565 |
-
|
| 566 |
-
|
| 567 |
-
|
| 568 |
-
|
| 569 |
-
placeholder="전체화음 결과",
|
| 570 |
-
elem_id="mml-whole",
|
| 571 |
-
)
|
| 572 |
-
whole_file = gr.File(label="Whole Part 다운로드 (트랙별 파일)", file_count="multiple")
|
| 573 |
-
with gr.Row():
|
| 574 |
-
whole_play_btn = gr.Button("▶ Whole Part 재생", variant="primary", size="sm")
|
| 575 |
-
stop_btn = gr.Button("■ 정지", size="sm")
|
| 576 |
-
|
| 577 |
-
# 3 Part (멜로디/화음/베이스)
|
| 578 |
-
gr.Markdown("#### 3 Part (멜로디 / 화음 / 베이스)")
|
| 579 |
-
three_output = gr.Textbox(
|
| 580 |
-
label="3파트 MML (Part1=멜로디, Part2=화음, Part3=베이스)",
|
| 581 |
-
lines=12,
|
| 582 |
-
placeholder="3파트 결과",
|
| 583 |
-
elem_id="mml-three",
|
| 584 |
)
|
| 585 |
-
three_file = gr.File(label="3 Part 다운로드 (트랙별 파일)", file_count="multiple")
|
| 586 |
-
with gr.Row():
|
| 587 |
-
three_play_btn = gr.Button("▶ 3 Part 전체", size="sm")
|
| 588 |
-
melody_btn = gr.Button("▶ 멜로디", size="sm")
|
| 589 |
-
chord_btn = gr.Button("▶ 화음", size="sm")
|
| 590 |
-
bass_btn = gr.Button("▶ 베이스", size="sm")
|
| 591 |
-
|
| 592 |
-
# 박자 (Tempo) 조절
|
| 593 |
-
gr.Markdown("#### 박자 조절")
|
| 594 |
-
with gr.Row():
|
| 595 |
-
tempo_slider = gr.Slider(
|
| 596 |
-
minimum=32, maximum=255, value=120, step=1,
|
| 597 |
-
label="Tempo (32~255, 기본: 120)",
|
| 598 |
-
elem_id="tempo-slider",
|
| 599 |
-
)
|
| 600 |
-
tempo_btn = gr.Button("박자 적용", size="sm")
|
| 601 |
-
|
| 602 |
-
# 기타 다운로드
|
| 603 |
-
mxl_output = gr.File(label="MXL 다운로드", file_count="multiple")
|
| 604 |
-
xml_output = gr.File(label="XML 다운로드", file_count="multiple")
|
| 605 |
-
warnings_output = gr.Textbox(label="경고", lines=3, placeholder="경고 메시지")
|
| 606 |
-
|
| 607 |
-
# 테스트
|
| 608 |
-
with gr.Row():
|
| 609 |
-
test_btn = gr.Button("나비야 테스트", size="sm")
|
| 610 |
-
|
| 611 |
-
# Admin dashboard
|
| 612 |
-
with gr.Accordion("통계 대시보드", open=False):
|
| 613 |
-
dashboard_output = gr.Textbox(
|
| 614 |
-
label="서버 통계",
|
| 615 |
-
lines=20,
|
| 616 |
-
interactive=False,
|
| 617 |
-
elem_id="dashboard",
|
| 618 |
-
)
|
| 619 |
-
refresh_btn = gr.Button("새로고침", size="sm")
|
| 620 |
-
refresh_btn.click(fn=_get_dashboard, outputs=[dashboard_output])
|
| 621 |
|
| 622 |
# Wire up feedback button
|
| 623 |
feedback_btn.click(
|
|
@@ -625,6 +682,37 @@ def build_ui() -> gr.Blocks:
|
|
| 625 |
js="() => { window.open('https://docs.google.com/forms/d/e/1FAIpQLScDoM53RMjDLlftORYHXmZ5kmkN4TTZOIyFIRuVsZhp4RGjEA/viewform', '_blank'); }",
|
| 626 |
)
|
| 627 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 628 |
# Wire up playback JS
|
| 629 |
stop_btn.click(fn=None, js=_JS_STOP)
|
| 630 |
whole_play_btn.click(fn=None, js=_make_box_play_js("mml-whole", "Whole Part"))
|
|
@@ -646,7 +734,8 @@ def build_ui() -> gr.Blocks:
|
|
| 646 |
fn=convert,
|
| 647 |
inputs=[file_input, preprocess_radio, dpi, upscale_radio, page_start_input, page_end_input],
|
| 648 |
outputs=[whole_output, three_output, whole_file, three_file,
|
| 649 |
-
mxl_output, xml_output, preview_gallery,
|
|
|
|
| 650 |
)
|
| 651 |
|
| 652 |
# Record page visit
|
|
@@ -663,8 +752,11 @@ if __name__ == "__main__":
|
|
| 663 |
|
| 664 |
demo = build_ui()
|
| 665 |
demo.queue(max_size=10, default_concurrency_limit=1)
|
|
|
|
|
|
|
| 666 |
demo.launch(
|
| 667 |
share=args.share,
|
| 668 |
server_name="0.0.0.0",
|
| 669 |
server_port=args.port,
|
|
|
|
| 670 |
)
|
|
|
|
| 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[:8], "파일을 업로드해주세요.")
|
| 288 |
|
| 289 |
ext = Path(file_path).suffix.lower()
|
| 290 |
if ext not in SUPPORTED_EXTENSIONS:
|
| 291 |
+
return (*empty[:8], 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[:8], page_err)
|
| 298 |
|
| 299 |
dpi = int(dpi_str) if dpi_str else 300
|
| 300 |
|
|
|
|
| 319 |
with _active_lock:
|
| 320 |
_active_count -= 1
|
| 321 |
_record_error(str(e))
|
| 322 |
+
return (*empty[:8], f"변환 중 오류 발생:\n{e}")
|
| 323 |
|
| 324 |
with _active_lock:
|
| 325 |
_active_count -= 1
|
|
|
|
| 367 |
info_parts.append(three_info)
|
| 368 |
warnings_text = "\n\n".join(info_parts)
|
| 369 |
|
| 370 |
+
# Build corrector data: page images + XML texts as JSON for iframe postMessage
|
| 371 |
+
# Must use _norm images — Audiveris processes those, so XML coordinates match them.
|
| 372 |
+
# _normalize_png can resize, so dimensions may differ from _pre or original.
|
| 373 |
+
corrector_images = []
|
| 374 |
+
for mxl_p in mxl_paths:
|
| 375 |
+
stem = Path(mxl_p).stem # e.g., "page_01_x2_pre"
|
| 376 |
+
# Audiveris input is {stem}_norm.png
|
| 377 |
+
norm_path = pages_dir / f"{stem}_norm.png" if pages_dir.exists() else None
|
| 378 |
+
if norm_path and norm_path.exists():
|
| 379 |
+
corrector_images.append(str(norm_path))
|
| 380 |
+
else:
|
| 381 |
+
# fallback: try {stem}.png
|
| 382 |
+
fallback = pages_dir / f"{stem}.png" if pages_dir.exists() else None
|
| 383 |
+
if fallback and fallback.exists():
|
| 384 |
+
corrector_images.append(str(fallback))
|
| 385 |
+
elif preview_paths:
|
| 386 |
+
idx = mxl_paths.index(mxl_p)
|
| 387 |
+
if idx < len(preview_paths):
|
| 388 |
+
corrector_images.append(preview_paths[idx])
|
| 389 |
+
corrector_xmls = []
|
| 390 |
+
for xp in (xml_paths or []):
|
| 391 |
+
try:
|
| 392 |
+
corrector_xmls.append(Path(xp).read_text(encoding="utf-8"))
|
| 393 |
+
except Exception:
|
| 394 |
+
pass
|
| 395 |
+
corrector_data = json.dumps({
|
| 396 |
+
"images": corrector_images,
|
| 397 |
+
"xmls": corrector_xmls,
|
| 398 |
+
}, ensure_ascii=False)
|
| 399 |
+
|
| 400 |
return (
|
| 401 |
whole_text,
|
| 402 |
three_text,
|
|
|
|
| 405 |
mxl_paths,
|
| 406 |
xml_paths,
|
| 407 |
preview_paths,
|
| 408 |
+
corrector_data,
|
| 409 |
warnings_text,
|
| 410 |
)
|
| 411 |
|
|
|
|
| 553 |
size="lg",
|
| 554 |
)
|
| 555 |
|
| 556 |
+
with gr.Tabs():
|
| 557 |
+
# ── Tab 1: 변환 ──────────────────────────────────────
|
| 558 |
+
with gr.Tab("변환"):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 559 |
with gr.Row():
|
| 560 |
+
with gr.Column():
|
| 561 |
+
file_input = gr.File(
|
| 562 |
+
label="악보 파일 (PDF, PNG, JPG)",
|
| 563 |
+
file_types=[".pdf", ".png", ".jpg", ".jpeg"],
|
| 564 |
+
)
|
| 565 |
+
preprocess_radio = gr.Radio(
|
| 566 |
+
choices=["없음", "Otsu", "Adaptive", "대비강화"],
|
| 567 |
+
value="없음",
|
| 568 |
+
label="전처리 (없음=원본, Otsu=깨끗한스캔, Adaptive=조명불균일, 대비강화=흐린스캔)",
|
| 569 |
+
)
|
| 570 |
+
dpi = gr.Dropdown(
|
| 571 |
+
choices=["150", "300", "450", "600"],
|
| 572 |
+
value="300",
|
| 573 |
+
label="DPI (PDF만 해당, 300 권장)",
|
| 574 |
+
)
|
| 575 |
+
upscale_radio = gr.Radio(
|
| 576 |
+
choices=["없음", "PIL 2×", "PIL 3×"],
|
| 577 |
+
value="없음",
|
| 578 |
+
label="업스케일 (PIL 리사이즈 — 저해상도 스캔에 효과적)",
|
| 579 |
+
)
|
| 580 |
+
with gr.Row():
|
| 581 |
+
page_start_input = gr.Number(
|
| 582 |
+
value=0, label="시작 페이지 (0=처음부터)", minimum=0, precision=0,
|
| 583 |
+
)
|
| 584 |
+
page_end_input = gr.Number(
|
| 585 |
+
value=0, label="끝 페이지 (0=끝까지)", minimum=0, precision=0,
|
| 586 |
+
)
|
| 587 |
+
convert_btn = gr.Button("변환 시작", variant="primary")
|
| 588 |
+
|
| 589 |
+
with gr.Column():
|
| 590 |
+
# 입력 이미지 미리보기
|
| 591 |
+
preview_gallery = gr.Gallery(
|
| 592 |
+
label="입력 악보 미리보기",
|
| 593 |
+
columns=2,
|
| 594 |
+
height=200,
|
| 595 |
+
object_fit="contain",
|
| 596 |
+
)
|
| 597 |
+
|
| 598 |
+
# Whole Part (N파트 전체화음)
|
| 599 |
+
gr.Markdown("#### Whole Part (전체화음)")
|
| 600 |
+
whole_output = gr.Textbox(
|
| 601 |
+
label="전체화음 N파트 MML",
|
| 602 |
+
lines=12,
|
| 603 |
+
placeholder="전체화음 결과",
|
| 604 |
+
elem_id="mml-whole",
|
| 605 |
+
)
|
| 606 |
+
whole_file = gr.File(label="Whole Part 다운로드 (트랙별 파일)", file_count="multiple")
|
| 607 |
+
with gr.Row():
|
| 608 |
+
whole_play_btn = gr.Button("▶ Whole Part 재생", variant="primary", size="sm")
|
| 609 |
+
stop_btn = gr.Button("■ 정지", size="sm")
|
| 610 |
+
|
| 611 |
+
# 3 Part (멜로디/화음/베이스)
|
| 612 |
+
gr.Markdown("#### 3 Part (멜로디 / 화음 / 베이스)")
|
| 613 |
+
three_output = gr.Textbox(
|
| 614 |
+
label="3파트 MML (Part1=멜로디, Part2=화음, Part3=베이스)",
|
| 615 |
+
lines=12,
|
| 616 |
+
placeholder="3파트 결과",
|
| 617 |
+
elem_id="mml-three",
|
| 618 |
+
)
|
| 619 |
+
three_file = gr.File(label="3 Part 다운로드 (트랙별 파일)", file_count="multiple")
|
| 620 |
+
with gr.Row():
|
| 621 |
+
three_play_btn = gr.Button("▶ 3 Part 전체", size="sm")
|
| 622 |
+
melody_btn = gr.Button("▶ 멜로디", size="sm")
|
| 623 |
+
chord_btn = gr.Button("▶ 화음", size="sm")
|
| 624 |
+
bass_btn = gr.Button("▶ 베이스", size="sm")
|
| 625 |
+
|
| 626 |
+
# 박자 (Tempo) 조절
|
| 627 |
+
gr.Markdown("#### 박자 조절")
|
| 628 |
+
with gr.Row():
|
| 629 |
+
tempo_slider = gr.Slider(
|
| 630 |
+
minimum=32, maximum=255, value=120, step=1,
|
| 631 |
+
label="Tempo (32~255, 기본: 120)",
|
| 632 |
+
elem_id="tempo-slider",
|
| 633 |
+
)
|
| 634 |
+
tempo_btn = gr.Button("박자 적용", size="sm")
|
| 635 |
+
|
| 636 |
+
# 기타 다운로드
|
| 637 |
+
mxl_output = gr.File(label="MXL 다운로드", file_count="multiple")
|
| 638 |
+
xml_output = gr.File(label="XML 다운로드", file_count="multiple")
|
| 639 |
+
|
| 640 |
+
# Hidden: corrector data (JSON with image paths + XML texts)
|
| 641 |
+
corrector_data_box = gr.Textbox(
|
| 642 |
+
visible=False,
|
| 643 |
+
elem_id="corrector-data",
|
| 644 |
+
)
|
| 645 |
+
|
| 646 |
+
warnings_output = gr.Textbox(label="경고", lines=3, placeholder="경고 메시지")
|
| 647 |
+
|
| 648 |
+
# 테스트
|
| 649 |
+
with gr.Row():
|
| 650 |
+
test_btn = gr.Button("나비야 테스트", size="sm")
|
| 651 |
+
|
| 652 |
+
# Admin dashboard
|
| 653 |
+
with gr.Accordion("통계 대시보드", open=False):
|
| 654 |
+
dashboard_output = gr.Textbox(
|
| 655 |
+
label="서버 통계",
|
| 656 |
+
lines=20,
|
| 657 |
+
interactive=False,
|
| 658 |
+
elem_id="dashboard",
|
| 659 |
)
|
| 660 |
+
refresh_btn = gr.Button("새로고침", size="sm")
|
| 661 |
+
refresh_btn.click(fn=_get_dashboard, outputs=[dashboard_output])
|
| 662 |
+
|
| 663 |
+
# ── Tab 2: 교정 도구 (Corrector) ─────────────────────
|
| 664 |
+
with gr.Tab("교정 도구", elem_id="corrector-tab"):
|
| 665 |
+
gr.Markdown("변환 완료 후 **'교정 도구 열기'** 버튼을 누르면 악보 교정이 가능합니다.")
|
| 666 |
+
open_corrector_btn = gr.Button(
|
| 667 |
+
"교정 도구 열기 (변환 결과 자동 로드)",
|
| 668 |
+
variant="primary",
|
| 669 |
+
size="lg",
|
| 670 |
)
|
| 671 |
+
gr.HTML(
|
| 672 |
+
'<iframe id="corrector-iframe" '
|
| 673 |
+
'src="/file=/app/static/corrector.html" '
|
| 674 |
+
'style="width:100%;height:80vh;border:1px solid #444;border-radius:8px;" '
|
| 675 |
+
'allow="autoplay">'
|
| 676 |
+
'</iframe>'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 677 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 678 |
|
| 679 |
# Wire up feedback button
|
| 680 |
feedback_btn.click(
|
|
|
|
| 682 |
js="() => { window.open('https://docs.google.com/forms/d/e/1FAIpQLScDoM53RMjDLlftORYHXmZ5kmkN4TTZOIyFIRuVsZhp4RGjEA/viewform', '_blank'); }",
|
| 683 |
)
|
| 684 |
|
| 685 |
+
# Wire up corrector open button — reads hidden corrector-data and postMessages to iframe
|
| 686 |
+
_JS_OPEN_CORRECTOR = """
|
| 687 |
+
() => {
|
| 688 |
+
var box = document.getElementById('corrector-data');
|
| 689 |
+
if (!box) { alert('교정 데이터를 찾을 수 없습니다.'); return; }
|
| 690 |
+
var ta = box.querySelector('textarea');
|
| 691 |
+
var raw = ta ? ta.value : '';
|
| 692 |
+
if (!raw) { alert('먼저 악보를 변환하세요.'); return; }
|
| 693 |
+
try {
|
| 694 |
+
var data = JSON.parse(raw);
|
| 695 |
+
if (!data.images || !data.images.length || !data.xmls || !data.xmls.length) {
|
| 696 |
+
alert('변환 결과가 비어있습니다.'); return;
|
| 697 |
+
}
|
| 698 |
+
// Convert file paths to Gradio /file= URLs
|
| 699 |
+
var imageUrls = data.images.map(function(p) { return '/file=' + p; });
|
| 700 |
+
var iframe = document.getElementById('corrector-iframe');
|
| 701 |
+
if (!iframe || !iframe.contentWindow) {
|
| 702 |
+
alert('교정 도구 iframe을 찾을 수 없습니다.'); return;
|
| 703 |
+
}
|
| 704 |
+
iframe.contentWindow.postMessage({
|
| 705 |
+
type: 'corrector-load',
|
| 706 |
+
images: imageUrls,
|
| 707 |
+
xmls: data.xmls
|
| 708 |
+
}, '*');
|
| 709 |
+
} catch(e) {
|
| 710 |
+
alert('데이터 파싱 오류: ' + e.message);
|
| 711 |
+
}
|
| 712 |
+
}
|
| 713 |
+
"""
|
| 714 |
+
open_corrector_btn.click(fn=None, js=_JS_OPEN_CORRECTOR)
|
| 715 |
+
|
| 716 |
# Wire up playback JS
|
| 717 |
stop_btn.click(fn=None, js=_JS_STOP)
|
| 718 |
whole_play_btn.click(fn=None, js=_make_box_play_js("mml-whole", "Whole Part"))
|
|
|
|
| 734 |
fn=convert,
|
| 735 |
inputs=[file_input, preprocess_radio, dpi, upscale_radio, page_start_input, page_end_input],
|
| 736 |
outputs=[whole_output, three_output, whole_file, three_file,
|
| 737 |
+
mxl_output, xml_output, preview_gallery, corrector_data_box,
|
| 738 |
+
warnings_output],
|
| 739 |
)
|
| 740 |
|
| 741 |
# Record page visit
|
|
|
|
| 752 |
|
| 753 |
demo = build_ui()
|
| 754 |
demo.queue(max_size=10, default_concurrency_limit=1)
|
| 755 |
+
# allowed_paths: serve corrector static files + temp dirs for conversion results
|
| 756 |
+
_static_path = str(Path(__file__).resolve().parent / "static")
|
| 757 |
demo.launch(
|
| 758 |
share=args.share,
|
| 759 |
server_name="0.0.0.0",
|
| 760 |
server_port=args.port,
|
| 761 |
+
allowed_paths=[_static_path, tempfile.gettempdir()],
|
| 762 |
)
|
static/corrector.css
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
| 2 |
+
body { font-family: 'Segoe UI', sans-serif; font-size: 13px; background: #1a1a2e; color: #e0e0e0; display: flex; flex-direction: column; height: 100vh; }
|
| 3 |
+
|
| 4 |
+
/* Feedback Banner */
|
| 5 |
+
#feedback-bar {
|
| 6 |
+
display: flex; justify-content: center; padding: 6px 12px;
|
| 7 |
+
background: #1a0a2e; border-bottom: 1px solid #444;
|
| 8 |
+
}
|
| 9 |
+
#feedback-btn {
|
| 10 |
+
display: inline-block; padding: 8px 40px;
|
| 11 |
+
background: #e94560; color: #fff; font-size: 22px; font-weight: bold;
|
| 12 |
+
text-decoration: none; border-radius: 6px; letter-spacing: 1px;
|
| 13 |
+
}
|
| 14 |
+
#feedback-btn:hover { background: #ff6b6b; }
|
| 15 |
+
|
| 16 |
+
/* Upload Bar */
|
| 17 |
+
#upload-bar {
|
| 18 |
+
display: flex; align-items: center; gap: 15px; padding: 8px 12px;
|
| 19 |
+
background: #16213e; border-bottom: 1px solid #333;
|
| 20 |
+
font-size: 24px; flex-wrap: wrap;
|
| 21 |
+
}
|
| 22 |
+
#upload-bar label { display: flex; align-items: center; gap: 6px; font-size: 24px; }
|
| 23 |
+
#upload-bar input[type="file"] { width: 340px; font-size: 21px; }
|
| 24 |
+
#upload-bar input[type="number"] { width: 75px; background: #0f3460; border: 1px solid #555; color: #fff; padding: 4px 5px; border-radius: 4px; font-size: 24px; }
|
| 25 |
+
#load-btn { padding: 8px 22px; background: #e94560; color: #fff; border: none; border-radius: 5px; cursor: pointer; font-weight: bold; font-size: 24px; }
|
| 26 |
+
#load-btn:hover { background: #ff6b6b; }
|
| 27 |
+
#load-status { color: #aaa; font-size: 19px; }
|
| 28 |
+
#page-nav { display: inline-flex; align-items: center; gap: 6px; }
|
| 29 |
+
#page-nav button { padding: 4px 12px; background: #0f3460; color: #e0e0e0; border: 1px solid #555; border-radius: 4px; cursor: pointer; font-size: 24px; }
|
| 30 |
+
#page-nav button:hover { background: #1a1a5e; }
|
| 31 |
+
#page-indicator { font-size: 22px; color: #e0e0e0; min-width: 60px; text-align: center; }
|
| 32 |
+
|
| 33 |
+
/* Toolbar */
|
| 34 |
+
#toolbar {
|
| 35 |
+
display: flex; align-items: center; gap: 8px; padding: 6px 12px;
|
| 36 |
+
background: #0f3460; border-bottom: 1px solid #333; flex-wrap: wrap;
|
| 37 |
+
font-size: 24px;
|
| 38 |
+
}
|
| 39 |
+
.tool-group { display: flex; align-items: center; gap: 4px; padding: 0 6px; border-right: 1px solid #333; }
|
| 40 |
+
.tool-group:last-child { border-right: none; }
|
| 41 |
+
.tool-group.right { margin-left: auto; }
|
| 42 |
+
#toolbar button {
|
| 43 |
+
padding: 6px 11px; background: #16213e; color: #e0e0e0; border: 1px solid #555;
|
| 44 |
+
border-radius: 3px; cursor: pointer; font-size: 24px;
|
| 45 |
+
}
|
| 46 |
+
#toolbar button:hover { background: #1a1a5e; border-color: #888; }
|
| 47 |
+
#toolbar button:active { background: #e94560; }
|
| 48 |
+
#btn-download { background: #1b6b1b; border-color: #2a2; }
|
| 49 |
+
#btn-download:hover { background: #2a8a2a; }
|
| 50 |
+
#toolbar label { font-size: 24px; }
|
| 51 |
+
#toolbar input[type="number"] { font-size: 24px; width: 68px; }
|
| 52 |
+
#toolbar input[type="range"] { width: 112px; height: 12px; }
|
| 53 |
+
|
| 54 |
+
#zoom-slider { width: 112px; vertical-align: middle; }
|
| 55 |
+
#zoom-label { font-size: 21px; color: #aaa; min-width: 38px; display: inline-block; }
|
| 56 |
+
|
| 57 |
+
/* Shortcut Reference Bar */
|
| 58 |
+
#shortcut-bar {
|
| 59 |
+
display: flex; flex-wrap: wrap; gap: 6px 18px; padding: 6px 12px;
|
| 60 |
+
background: #12122a; border-bottom: 1px solid #2a2a4a;
|
| 61 |
+
font-size: 20px; color: #888;
|
| 62 |
+
}
|
| 63 |
+
#shortcut-bar b { color: #bbb; }
|
| 64 |
+
|
| 65 |
+
/* Progress Bar */
|
| 66 |
+
#progress-bar-container {
|
| 67 |
+
position: relative; height: 38px; background: #0a0a1a; border-bottom: 1px solid #333;
|
| 68 |
+
cursor: pointer; user-select: none;
|
| 69 |
+
}
|
| 70 |
+
#progress-bar-fill {
|
| 71 |
+
height: 100%; width: 0%; background: linear-gradient(90deg, #e94560, #ff6b6b);
|
| 72 |
+
transition: none; pointer-events: none;
|
| 73 |
+
}
|
| 74 |
+
#progress-time {
|
| 75 |
+
position: absolute; top: 0; right: 8px; line-height: 38px;
|
| 76 |
+
font-size: 15px; color: #aaa; pointer-events: none;
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
/* Main Canvas */
|
| 80 |
+
#canvas-wrapper {
|
| 81 |
+
flex: 1; overflow: auto; background: #111; position: relative;
|
| 82 |
+
}
|
| 83 |
+
#canvas-container {
|
| 84 |
+
position: relative; display: inline-block;
|
| 85 |
+
transform-origin: top left;
|
| 86 |
+
}
|
| 87 |
+
#score-image {
|
| 88 |
+
display: block; user-select: none; -webkit-user-drag: none;
|
| 89 |
+
}
|
| 90 |
+
#marker-svg {
|
| 91 |
+
position: absolute; top: 0; left: 0; pointer-events: none;
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
/* Markers */
|
| 95 |
+
.marker {
|
| 96 |
+
pointer-events: all; cursor: pointer;
|
| 97 |
+
transition: r 0.1s, stroke-width 0.1s;
|
| 98 |
+
}
|
| 99 |
+
.marker:hover { r: 10; }
|
| 100 |
+
.marker.selected { stroke: #ffd700; stroke-width: 3; r: 10; }
|
| 101 |
+
.marker.playback-highlight { stroke: #ff3333; stroke-width: 3; r: 11; fill: rgba(255,50,50,0.9); }
|
| 102 |
+
.marker.modified { stroke: #00ff88; stroke-width: 2.5; filter: drop-shadow(0 0 3px #00ff88); }
|
| 103 |
+
.marker.modified.selected { stroke: #ffd700; stroke-width: 3; filter: drop-shadow(0 0 4px #00ff88); }
|
| 104 |
+
.marker.voice1 { fill: rgba(68, 136, 255, 0.7); stroke: #4488ff; stroke-width: 1; }
|
| 105 |
+
.marker.voice2 { fill: rgba(255, 68, 68, 0.7); stroke: #ff4444; stroke-width: 1; }
|
| 106 |
+
.marker.voice3 { fill: rgba(68, 170, 68, 0.7); stroke: #44aa44; stroke-width: 1; }
|
| 107 |
+
.marker.voice4 { fill: rgba(255, 170, 0, 0.7); stroke: #ffaa00; stroke-width: 1; }
|
| 108 |
+
.marker.rest-marker { fill: rgba(150, 150, 150, 0.4); stroke: #888; stroke-width: 1; r: 6; }
|
| 109 |
+
|
| 110 |
+
/* Accidental label next to marker */
|
| 111 |
+
.acc-label {
|
| 112 |
+
pointer-events: none; font-size: 10px; fill: #ffd700; font-weight: bold;
|
| 113 |
+
}
|
| 114 |
+
/* Ghost marker for add mode */
|
| 115 |
+
.ghost-marker {
|
| 116 |
+
fill: rgba(255, 50, 50, 0.5); stroke: #ff3333; stroke-width: 2.5;
|
| 117 |
+
stroke-dasharray: 4 2;
|
| 118 |
+
}
|
| 119 |
+
.ghost-label {
|
| 120 |
+
fill: #ff3333; font-size: 14px; font-weight: bold; opacity: 0.9;
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
/* Chord Popup */
|
| 124 |
+
#chord-popup {
|
| 125 |
+
position: absolute; background: #16213e; border: 1px solid #e94560;
|
| 126 |
+
border-radius: 6px; padding: 6px 0; z-index: 100; min-width: 140px;
|
| 127 |
+
box-shadow: 0 4px 12px rgba(0,0,0,0.5);
|
| 128 |
+
}
|
| 129 |
+
#chord-popup.hidden { display: none; }
|
| 130 |
+
#chord-popup-title { padding: 4px 12px; font-size: 11px; color: #aaa; border-bottom: 1px solid #333; }
|
| 131 |
+
#chord-popup-list { list-style: none; }
|
| 132 |
+
#chord-popup-list li {
|
| 133 |
+
padding: 6px 12px; cursor: pointer; font-size: 13px;
|
| 134 |
+
}
|
| 135 |
+
#chord-popup-list li:hover { background: #0f3460; }
|
| 136 |
+
#chord-popup-list li .voice-dot {
|
| 137 |
+
display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 6px;
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
/* Status Bar */
|
| 141 |
+
#status-bar {
|
| 142 |
+
display: flex; justify-content: space-between; padding: 5px 12px;
|
| 143 |
+
background: #16213e; border-top: 1px solid #333; font-size: 19px; color: #aaa;
|
| 144 |
+
}
|
static/corrector.html
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="ko">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>OMR Corrector</title>
|
| 7 |
+
<link rel="stylesheet" href="corrector.css">
|
| 8 |
+
</head>
|
| 9 |
+
<body>
|
| 10 |
+
|
| 11 |
+
<!-- Feedback Banner -->
|
| 12 |
+
<div id="feedback-bar">
|
| 13 |
+
<a id="feedback-btn" href="https://docs.google.com/forms/d/e/1FAIpQLScDoM53RMjDLlftORYHXmZ5kmkN4TTZOIyFIRuVsZhp4RGjEA/viewform" target="_blank" rel="noopener">Feedback</a>
|
| 14 |
+
</div>
|
| 15 |
+
|
| 16 |
+
<!-- Upload Bar -->
|
| 17 |
+
<div id="upload-bar">
|
| 18 |
+
<label>Images: <input type="file" id="image-input" accept=".png,.jpg,.jpeg" multiple></label>
|
| 19 |
+
<label>MusicXML: <input type="file" id="xml-input" accept=".xml,.musicxml,.mxl" multiple></label>
|
| 20 |
+
<label>DPI: <input type="number" id="dpi-input" value="300" min="72" max="1200" step="1"></label>
|
| 21 |
+
<button id="load-btn">Load</button>
|
| 22 |
+
<span id="page-nav">
|
| 23 |
+
<button id="btn-prev-page" title="Previous Page">«</button>
|
| 24 |
+
<span id="page-indicator">-</span>
|
| 25 |
+
<button id="btn-next-page" title="Next Page">»</button>
|
| 26 |
+
</span>
|
| 27 |
+
<span id="load-status"></span>
|
| 28 |
+
</div>
|
| 29 |
+
|
| 30 |
+
<!-- Toolbar -->
|
| 31 |
+
<div id="toolbar">
|
| 32 |
+
<div class="tool-group">
|
| 33 |
+
<button id="btn-undo" title="Undo (Ctrl+Z)">↩</button>
|
| 34 |
+
<button id="btn-redo" title="Redo (Ctrl+Y)">↪</button>
|
| 35 |
+
</div>
|
| 36 |
+
<div class="tool-group">
|
| 37 |
+
<button id="btn-up" title="Pitch Up (Arrow Up)">▲</button>
|
| 38 |
+
<button id="btn-down" title="Pitch Down (Arrow Down)">▼</button>
|
| 39 |
+
</div>
|
| 40 |
+
<div class="tool-group">
|
| 41 |
+
<button id="btn-sharp" title="Sharp (#)">#</button>
|
| 42 |
+
<button id="btn-flat" title="Flat (b)">b</button>
|
| 43 |
+
<button id="btn-natural" title="Natural (n)">♮</button>
|
| 44 |
+
<button id="btn-delete" title="Delete Note (Del)">🗑</button>
|
| 45 |
+
</div>
|
| 46 |
+
<div class="tool-group">
|
| 47 |
+
<button id="btn-dur-whole" title="Whole Note (1)">𝅝</button>
|
| 48 |
+
<button id="btn-dur-half" title="Half Note (2)">𝅗𝅥</button>
|
| 49 |
+
<button id="btn-dur-quarter" title="Quarter Note (4)">𝅘𝅥</button>
|
| 50 |
+
<button id="btn-dur-eighth" title="Eighth Note (5)">𝅘𝅥𝅮</button>
|
| 51 |
+
<button id="btn-dur-16th" title="16th Note (6)">𝅘𝅥𝅯</button>
|
| 52 |
+
<button id="btn-dur-dot" title="Dot Toggle (.)">•</button>
|
| 53 |
+
</div>
|
| 54 |
+
<div class="tool-group">
|
| 55 |
+
<button id="btn-prev" title="Previous Note (Shift+Tab)"><</button>
|
| 56 |
+
<button id="btn-next" title="Next Note (Tab)">></button>
|
| 57 |
+
</div>
|
| 58 |
+
<div class="tool-group">
|
| 59 |
+
<button id="btn-play" title="Play Note Sound">♫</button>
|
| 60 |
+
<button id="btn-playall" title="Play All (Space)">▶</button>
|
| 61 |
+
<button id="btn-stop" title="Stop">■</button>
|
| 62 |
+
<label>BPM: <input type="number" id="bpm-input" value="120" min="20" max="300" step="5" style="width:94px"></label>
|
| 63 |
+
</div>
|
| 64 |
+
<div class="tool-group">
|
| 65 |
+
<label>Zoom: <input type="range" id="zoom-slider" min="25" max="200" value="100" step="5"><span id="zoom-label">100%</span></label>
|
| 66 |
+
</div>
|
| 67 |
+
<div class="tool-group">
|
| 68 |
+
<label>X off: <input type="number" id="offset-x" value="0" step="1" style="width:84px"></label>
|
| 69 |
+
<label>Y off: <input type="number" id="offset-y" value="0" step="1" style="width:84px"></label>
|
| 70 |
+
</div>
|
| 71 |
+
<div class="tool-group">
|
| 72 |
+
<label>Staff dist: <input type="number" id="staff-dist-input" value="65" step="1" style="width:84px"></label>
|
| 73 |
+
<label>Sys dist adj: <input type="number" id="sys-dist-adj" value="0" step="1" style="width:84px"></label>
|
| 74 |
+
</div>
|
| 75 |
+
<div class="tool-group">
|
| 76 |
+
<button id="btn-debug-lines" title="Show/hide staff lines">Staff Lines</button>
|
| 77 |
+
</div>
|
| 78 |
+
<div class="tool-group right">
|
| 79 |
+
<button id="btn-download">Download XML</button>
|
| 80 |
+
</div>
|
| 81 |
+
</div>
|
| 82 |
+
|
| 83 |
+
<!-- Progress Bar -->
|
| 84 |
+
<div id="progress-bar-container">
|
| 85 |
+
<div id="progress-bar-fill"></div>
|
| 86 |
+
<span id="progress-time">0:00 / 0:00</span>
|
| 87 |
+
</div>
|
| 88 |
+
|
| 89 |
+
<!-- Shortcut Reference -->
|
| 90 |
+
<div id="shortcut-bar">
|
| 91 |
+
<span><b>Select:</b> Click / Tab</span>
|
| 92 |
+
<span><b>Pitch:</b> ↑↓ / Drag</span>
|
| 93 |
+
<span><b>#/b/n:</b> Accidental</span>
|
| 94 |
+
<span><b>1/2/4/5/6:</b> Duration</span>
|
| 95 |
+
<span><b>.</b> Dot</span>
|
| 96 |
+
<span><b>Del:</b> Delete</span>
|
| 97 |
+
<span><b>r:</b> Note↔Rest</span>
|
| 98 |
+
<span><b>Shift+N:</b> Add Mode</span>
|
| 99 |
+
<span><b>Shift+A:</b> Add Chord</span>
|
| 100 |
+
<span><b>Space:</b> Play</span>
|
| 101 |
+
<span><b>Shift+Click:</b> Seek</span>
|
| 102 |
+
<span><b>DblClick:</b> Seek+Play</span>
|
| 103 |
+
<span><b>PgUp/Dn:</b> Page</span>
|
| 104 |
+
<span><b>Ctrl+Z/Y:</b> Undo/Redo</span>
|
| 105 |
+
</div>
|
| 106 |
+
|
| 107 |
+
<!-- Main Canvas Area -->
|
| 108 |
+
<div id="canvas-wrapper">
|
| 109 |
+
<div id="canvas-container">
|
| 110 |
+
<img id="score-image" alt="Score image">
|
| 111 |
+
<svg id="marker-svg" xmlns="http://www.w3.org/2000/svg"></svg>
|
| 112 |
+
</div>
|
| 113 |
+
</div>
|
| 114 |
+
|
| 115 |
+
<!-- Chord Popup -->
|
| 116 |
+
<div id="chord-popup" class="hidden">
|
| 117 |
+
<div id="chord-popup-title">Select note:</div>
|
| 118 |
+
<ul id="chord-popup-list"></ul>
|
| 119 |
+
</div>
|
| 120 |
+
|
| 121 |
+
<!-- Status Bar -->
|
| 122 |
+
<div id="status-bar">
|
| 123 |
+
<span id="status-mode" style="color:#ffff66;font-weight:bold;"></span>
|
| 124 |
+
<span id="status-selection">No selection</span>
|
| 125 |
+
<span id="status-total"></span>
|
| 126 |
+
</div>
|
| 127 |
+
|
| 128 |
+
<script src="corrector.js"></script>
|
| 129 |
+
</body>
|
| 130 |
+
</html>
|
static/corrector.js
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|