File size: 6,242 Bytes
daa0bdd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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
63
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
"""
core/models.py

๋‚ด๋ถ€ ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ ์ •์˜.
๋ชจ๋“  core ๋ชจ๋“ˆ ๊ฐ„์— ๊ณต์œ ๋˜๋Š” ๋ฐ์ดํ„ฐ ํด๋ž˜์Šค.
"""

from __future__ import annotations

from dataclasses import dataclass, field
from typing import Optional


@dataclass
class NoteEvent:
    """
    ์•…๋ณด์—์„œ ์ถ”์ถœํ•œ ๋‹จ์ผ ์Œํ‘œ/์‰ผํ‘œ ์ด๋ฒคํŠธ.

    pitch=0 ์ด๋ฉด ์‰ผํ‘œ(rest)๋กœ ์ทจ๊ธ‰.
    duration์€ 4๋ถ„์Œํ‘œ ๊ธฐ์ค€์˜ ๋น„์œจ (์˜ˆ: 1.0 = 4๋ถ„์Œํ‘œ, 0.5 = 8๋ถ„์Œํ‘œ, 2.0 = 2๋ถ„์Œํ‘œ).
    """
    pitch: int          # MIDI ๋ฒˆํ˜ธ (0 = rest, 60 = C4)
    start: float        # ์‹œ์ž‘ ์‹œ๊ฐ (4๋ถ„์Œํ‘œ ๊ธฐ์ค€ beat)
    duration: float     # ์ง€์† ์‹œ๊ฐ„ (4๋ถ„์Œํ‘œ ๊ธฐ์ค€ beat)
    staff: int = 1      # ๋ณดํ‘œ ๋ฒˆํ˜ธ (1 = ์ƒ๋‹จ, 2 = ํ•˜๋‹จ)
    voice: int = 1      # ์„ฑ๋ถ€ ๋ฒˆํ˜ธ
    part_hint: Optional[int] = None  # ํŒŒํŠธ ๋ฐฐ๋ถ„ ํžŒํŠธ (None = ๋ฏธ์ง€์ •)


@dataclass
class ConvertOptions:
    """
    ๋ณ€ํ™˜ ํŒŒ์ดํ”„๋ผ์ธ ์˜ต์…˜.
    """
    part_count: int = 0          # ์ถœ๋ ฅ ํŒŒํŠธ ์ˆ˜ (0 = ๋™์‹œ ๋ฐœ์Œ ์ˆ˜ ๊ธฐ์ค€ ์ž๋™ ๊ฐ์ง€)
    strict_mode: bool = False    # True์ด๋ฉด ๊ฒฝ๊ณ ๋ฅผ ์—๋Ÿฌ๋กœ ์ฒ˜๋ฆฌ
    prefer_sharps: bool = True   # True = ์˜ฌ๋ฆผํ‘œ(#), False = ๋‚ด๋ฆผํ‘œ(b)
    mock_mode: bool = True       # True์ด๋ฉด MockOMRAdapter ์‚ฌ์šฉ
    default_tempo: int = 120     # ๊ธฐ๋ณธ ํ…œํฌ BPM
    pdf_dpi: int = 150           # PDF ๋ Œ๋”๋ง ํ•ด์ƒ๋„
                                 # 150dpi: ๋น ๋ฅธ ์ฒ˜๋ฆฌ, ๊ธฐ๋ณธ๊ฐ’
                                 # 300dpi: Audiveris ์‹ค์ œ ์‚ฌ์šฉ ์‹œ ๊ถŒ์žฅ (์ธ์‹๋ฅ  ํ–ฅ์ƒ)
    preprocess_enabled: bool = True   # OpenCV ์ „์ฒ˜๋ฆฌ ์‚ฌ์šฉ ์—ฌ๋ถ€ (audiveris ๋ชจ๋“œ๋งŒ ์ ์šฉ)
    blur_enabled: bool = True         # GaussianBlur ๋…ธ์ด์ฆˆ ์ œ๊ฑฐ ์‚ฌ์šฉ ์—ฌ๋ถ€
    binarize_enabled: bool = False    # ์ด์ง„ํ™” ์‚ฌ์šฉ ์—ฌ๋ถ€ (๊ธฐ๋ณธ off: Audiveris ์ž์ฒด ์ด์ง„ํ™” ์‹ ๋ขฐ)
    binarize_method: str = "otsu"     # ์ด์ง„ํ™” ๋ฐฉ์‹: "otsu" | "adaptive" (binarize_enabled=True ์‹œ)
    deskew_enabled: bool = False      # ๊ธฐ์šธ๊ธฐ ๋ณด์ • (์‹คํ—˜์ , ๊ธฐ๋ณธ off)
    debug_dir: str = ""               # ์ค‘๊ฐ„ ๊ฒฐ๊ณผ๋ฌผ ์ €์žฅ ๋””๋ ‰ํ† ๋ฆฌ (๋นˆ ๋ฌธ์ž์—ด = ์ €์žฅ ์•ˆ ํ•จ)
    engine: str = ""                  # OMR ์—”์ง„ ์ง€์ •: "" | "audiveris" | "homr" | "oemer" | "clarity"
                                      # ๋นˆ ๋ฌธ์ž์—ด์ด๋ฉด mock_mode์— ๋”ฐ๋ผ ์ž๋™ ์„ ํƒ
    pdf_pages: list = field(default_factory=list)
                                      # ์ฒ˜๋ฆฌํ•  PDF ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ ๋ชฉ๋ก (1-based). ๋นˆ ๋ฆฌ์ŠคํŠธ = ์ „์ฒด ์ฒ˜๋ฆฌ
                                      # ์˜ˆ) [1, 3] โ†’ 1, 3ํŽ˜์ด์ง€๋งŒ ์ฒ˜๋ฆฌ


@dataclass
class ConvertResult:
    """
    ๋ณ€ํ™˜ ๊ฒฐ๊ณผ.

    mml: ๋งˆ๋น„๋…ธ๊ธฐ MML ์™„์„ฑ ๋ฌธ์ž์—ด "MML@p1,p2,p3;"
    part1/2/3: ํŒŒํŠธ๋ณ„ ๋ณธ๋ฌธ ๋ฌธ์ž์—ด (MML@, ; ์ œ์™ธ, ๋‚ด๋ถ€ ๊ฒ€์‚ฌ์šฉ)
    """
    success: bool
    mml: str = ""              # ์ตœ์ข… ๋งˆ๋น„๋…ธ๊ธฐ MML "MML@[t<BPM>]p1,p2,p3;"
    part1: str = ""            # Part 1 ๋ณธ๋ฌธ (๋‚ด๋ถ€ ๊ฒ€์‚ฌ/๋””๋ฒ„๊ทธ์šฉ)
    part2: str = ""
    part3: str = ""
    warnings: list[str] = field(default_factory=list)
    debug_info: dict = field(default_factory=dict)

    def parts(self) -> list[str]:
        """ํŒŒํŠธ ๋ณธ๋ฌธ ๋ชฉ๋ก์„ ๋ฆฌ์ŠคํŠธ๋กœ ๋ฐ˜ํ™˜."""
        return [self.part1, self.part2, self.part3]

    def format_output(self) -> str:
        """์ฝ˜์†”/ํŒŒ์ผ ์ถœ๋ ฅ์šฉ ํฌ๋งท. ํŒŒํŠธ๋ณ„ ๊ฐœ๋ณ„ MML@...;๋กœ ์ถœ๋ ฅ."""
        lines = []
        for i, mml in enumerate([self.part1, self.part2, self.part3], start=1):
            lines.append(f"Part {i}")
            lines.append(mml if mml else "MML@r1;")
            lines.append("")
        if self.warnings:
            lines.append("--- Warnings ---")
            for w in self.warnings:
                lines.append(f"  [WARN] {w}")
        return "\n".join(lines).strip()


@dataclass
class WarningMessage:
    """๊ฒฝ๊ณ  ๋ฉ”์‹œ์ง€ (ํ•„์š”์‹œ ๊ตฌ์กฐํ™”๋œ ๊ฒฝ๊ณ ๋กœ ์‚ฌ์šฉ)."""
    code: str
    message: str
    context: Optional[str] = None


# ---------------------------------------------------------------------------
# ๋ฉ€ํ‹ฐ ์—”์ง„ ๋น„๊ต์šฉ ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ
# ---------------------------------------------------------------------------

@dataclass
class EngineRunResult:
    """
    ๋‹จ์ผ OMR ์—”์ง„ ์‹คํ–‰ ๊ฒฐ๊ณผ.

    ์ž๋™ ์ ์ˆ˜๋งŒ์œผ๋กœ ์ตœ์ข… ํŒ์ •์„ ๋‚ด๋ฆฌ์ง€ ์•Š๋Š”๋‹ค.
    heuristic_summary๋Š” ์ฐธ๊ณ  ์ง€ํ‘œ์ผ ๋ฟ, ์ตœ์ข… ํ’ˆ์งˆ ํŒ๋‹จ์€ ์‚ฌ์šฉ์ž๊ฐ€ ์ง์ ‘ ํ™•์ธํ•ด์•ผ ํ•œ๋‹ค.
    """
    engine_name: str
    success: bool
    stage: str = ""             # ์‹คํŒจ ๋‹จ๊ณ„: "init" | "preprocess" | "omr" | "parse" | "convert"
    warnings: list[str] = field(default_factory=list)
    error_message: str = ""

    # ์ถœ๋ ฅ ํŒŒ์ผ ๊ฒฝ๋กœ (์ €์žฅ๋œ ๊ฒฝ์šฐ)
    output_xml_path: str = ""   # ์—”์ง„ ์ถœ๋ ฅ MusicXML ๊ฒฝ๋กœ
    output_mml_path: str = ""   # ์ €์žฅ๋œ MML ํ…์ŠคํŠธ ํŒŒ์ผ ๊ฒฝ๋กœ
    output_notes_json_path: str = ""  # ์ €์žฅ๋œ notes.json ๊ฒฝ๋กœ
    output_notes_txt_path: str = ""   # ์ €์žฅ๋œ notes.txt ๊ฒฝ๋กœ
    output_debug_path: str = ""       # ์ €์žฅ๋œ debug.json ๊ฒฝ๋กœ

    # ์ •๋Ÿ‰ ์ง€ํ‘œ (์ฐธ๊ณ ์šฉ)
    note_count: int = 0
    chord_count: int = 0          # ๋™์‹œ ๋ฐœ์Œ ๋…ธํŠธ ๊ทธ๋ฃน ์ˆ˜
    part_note_counts: list[int] = field(default_factory=list)
    warning_count: int = 0

    # ์ƒ์„ธ ๋ฐ์ดํ„ฐ
    debug_info: dict = field(default_factory=dict)
    heuristic_summary: dict = field(default_factory=dict)
    mml_parts: list[str] = field(default_factory=list)
    notes_dump: list[dict] = field(default_factory=list)  # ์‚ฌ๋žŒ์ด ๊ฒ€ํ† ํ•˜๊ธฐ ์ข‹์€ ๋…ธํŠธ ๋ชฉ๋ก


@dataclass
class ComparisonReport:
    """
    ์—ฌ๋Ÿฌ OMR ์—”์ง„ ๋น„๊ต ์‹คํ–‰ ๊ฒฐ๊ณผ ์ง‘๊ณ„.

    ์ค‘์š”: user_review_priority=True ๋Š” ํ•ญ์ƒ True์—ฌ์•ผ ํ•œ๋‹ค.
    ์ตœ์ข… ์—”์ง„ ์„ ํƒ์€ ์‚ฌ์šฉ์ž๊ฐ€ ์ง์ ‘ ๊ฒฐ๊ณผ๋ฅผ ๋“ค์–ด๋ณด๊ณ  ํŒ๋‹จํ•œ๋‹ค.
    suggested_engine์€ ๋‹จ์ˆœ ์ฐธ๊ณ ์šฉ์ด๋ฉฐ ์ž๋™ ๊ฒฐ์ •์ด ์•„๋‹ˆ๋‹ค.
    """
    input_file: str
    timestamp: str = ""
    runs: list[EngineRunResult] = field(default_factory=list)
    user_review_priority: bool = True   # ํ•ญ์ƒ True โ€” ์ตœ์ข… ํŒ์ •์€ ์‚ฌ์šฉ์ž ์ง์ ‘ ํ™•์ธ
    comparison_summary: str = ""
    notes_for_manual_review: list[str] = field(default_factory=list)
    suggested_engine: str = ""  # ์ž๋™ ์ฐธ๊ณ  ์ถ”์ฒœ (์ตœ์ข… ํŒ์ • ์•„๋‹˜)