-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathlogic.py
More file actions
210 lines (174 loc) · 6.71 KB
/
Copy pathlogic.py
File metadata and controls
210 lines (174 loc) · 6.71 KB
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
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
# logic.py
from __future__ import annotations
import re
from typing import Dict, List, Optional, Tuple
def classify_value(value: int, target: int, tol: int) -> str:
tol = max(0, int(tol))
low = target - tol
high = target + tol
if low <= value <= high:
return "good"
if (low - 1) <= value <= (high + 1):
return "mid"
return "bad"
# ---------------------------------------------------------------------------
# .rapnote format (human-readable TXT with metadata in comments)
# ---------------------------------------------------------------------------
# # RapNote Project
# # target: 16
# # tol: 2
# # lang: ru
#
# [Verse]
# # instruction: generate verse | описание
# привет как дела
# я пишу рэп про жизнь
#
# [Chorus]
# это припев
#
# [Instrumental: Break]
# ---------------------------------------------------------------------------
_INSTRUCTION_RE = re.compile(
r"^#\s*instruction:\s*(?P<cmd>[^|]*?)(?:\s*\|\s*(?P<desc>.*))?$"
)
_META_RE = re.compile(r"^#\s*(?P<key>\w+)\s*:\s*(?P<value>.+)$")
_HEADER_RE = re.compile(r"^\[(?P<name>[^\[\]]+)\]$")
_INSTRUMENTAL_HEADER_RE = re.compile(r"^\[Instrumental:\s*(?P<type>[^\[\]]+)\]$")
def export_txt(project: object, path: str) -> None:
"""Write project to .rapnote / TXT format with metadata and instructions."""
from model import INSTRUMENTAL_SECTIONS, Project
assert isinstance(project, Project)
out: List[str] = []
out.append("# RapNote Project")
out.append(f"# target: {project.target}")
out.append(f"# tol: {project.tol}")
out.append(f"# lang: {project.lang}")
out.append("")
for idx, b in enumerate(project.blocks):
# --- section header ---
if b.kind == "instrumental":
out.append(f"[Instrumental: {b.instrumental_type}]")
elif b.section:
out.append(f"[{b.section}]")
# --- instruction (if any) ---
if b.instruction_command:
desc_part = f" | {b.instruction_description}" if b.instruction_description else ""
out.append(f"# instruction: {b.instruction_command}{desc_part}")
# --- lines ---
for s in b.lines:
cleaned = (s or "").replace("|", "")
out.append(cleaned.rstrip())
# --- blank line between blocks ---
if idx != len(project.blocks) - 1:
out.append("")
# trim trailing blanks
while out and out[-1] == "":
out.pop()
with open(path, "w", encoding="utf-8") as f:
f.write("\n".join(out) + "\n")
# ---------------------------------------------------------------------------
# Import / parse
# ---------------------------------------------------------------------------
def _read_text_file(path: str) -> str:
last_error: Optional[UnicodeDecodeError] = None
for encoding in ("utf-8-sig", "utf-8", "cp1251"):
try:
with open(path, "r", encoding=encoding) as handle:
return handle.read()
except UnicodeDecodeError as exc:
last_error = exc
if last_error is not None:
raise last_error
raise last_error # type: ignore[misc]
def _parse_header(name: str) -> Tuple[Optional[str], Optional[str], bool]:
"""Return (section, instrumental_type, is_instrumental) for a header name."""
from model import INSTRUMENTAL_SECTIONS, SECTION_CHOICES
instr_match = _INSTRUMENTAL_HEADER_RE.match(f"[{name}]")
if instr_match:
itype = instr_match.group("type").strip()
if itype not in INSTRUMENTAL_SECTIONS:
itype = INSTRUMENTAL_SECTIONS[0]
return None, itype, True
section = name.strip()
if section == "None" or section not in SECTION_CHOICES:
section = None
return section, None, False
def import_txt(path: str) -> List["Block"]:
"""Parse a .rapnote / TXT file into a list of Blocks."""
from model import Block
blocks: List[Block] = []
current_section: Optional[str] = None
current_instrumental: Optional[str] = None
current_is_instrumental: bool = False
current_lines: List[str] = []
pending_instruction_cmd: str = ""
pending_instruction_desc: str = ""
def flush_current() -> None:
nonlocal current_lines, pending_instruction_cmd, pending_instruction_desc
if not current_lines and not pending_instruction_cmd:
return
if current_is_instrumental:
blocks.append(Block(
kind="instrumental",
instrumental_type=current_instrumental,
lines=list(current_lines) or [""],
instruction_command=pending_instruction_cmd,
instruction_description=pending_instruction_desc,
))
else:
blocks.append(Block(
kind="lyrics",
section=current_section,
lines=list(current_lines) or [""],
instruction_command=pending_instruction_cmd,
instruction_description=pending_instruction_desc,
))
current_lines = []
pending_instruction_cmd = ""
pending_instruction_desc = ""
text = _read_text_file(path)
for raw_line in text.splitlines():
line = raw_line.strip()
# blank line → flush block
if not line:
flush_current()
current_section = None
current_instrumental = None
current_is_instrumental = False
continue
# skip project-level metadata comments
if line.startswith("#"):
# check for instruction comment
instr_match = _INSTRUCTION_RE.match(line)
if instr_match:
pending_instruction_cmd = instr_match.group("cmd").strip()
pending_instruction_desc = (instr_match.group("desc") or "").strip()
# all other # comments (metadata, etc.) are ignored
continue
# section header
header_match = _HEADER_RE.match(line)
if header_match:
flush_current()
name = header_match.group("name").strip()
current_section, current_instrumental, current_is_instrumental = _parse_header(name)
continue
# regular text line
current_lines.append(line)
flush_current()
return blocks
def parse_metadata(path: str) -> Dict[str, str]:
"""Extract key: value metadata from a .rapnote file header."""
meta: Dict[str, str] = {}
try:
text = _read_text_file(path)
except Exception:
return meta
for raw_line in text.splitlines():
line = raw_line.strip()
if not line:
break # stop at first blank line
m = _META_RE.match(line)
if m:
meta[m.group("key").lower()] = m.group("value").strip()
return meta