File size: 11,875 Bytes
5094348 | 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 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 | """์ฌ๋์ด ํ์ ํ AIFlow Ink v1 ์์๋ง P Formula v1 ํ์ต ํ๋ณด๋ก ๋ณํํ๋ค."""
from __future__ import annotations
from dataclasses import dataclass
import json
from pathlib import Path
from typing import Any, Iterable, Sequence
TRAINABLE_LABEL_STATUS06 = frozenset({"human_verified"})
SPLIT_MAP06 = {
"train": "training",
"training": "training",
"validation": "validation",
"test": "test",
}
@dataclass(frozen=True, slots=True)
class PFormulaAnnotation06:
"""ํ์ ๋ณ์: sampleยท๊ธฐ๊ธฐยทcell token. ์๋ ์๋ฆฌ: raw ink์ ๋ถ๋ฆฌ๋ ์ฌ๋ ํ์ label ๊ณ์ฝ์ ๋ณด์กดํ๋ค."""
sample_id: str
device_id: str
tokens: dict[str, str]
label_status: str = "human_verified"
def parse_p_formula_annotation06(value: dict[str, Any]) -> PFormulaAnnotation06:
"""ํ์ ๋ณ์: annotation JSON object. ์๋ ์๋ฆฌ: ๋น์ด ์๋ IDยทtoken๊ณผ ์๋ label์ fail-closed๋ก ๊ฑฐ๋ถํ๋ค."""
sample_id = str(value.get("sample_id") or "").strip()
device_id = str(value.get("device_id") or "").strip()
label_status = str(value.get("label_status") or "").strip()
raw_tokens = value.get("tokens")
if not sample_id or not device_id:
raise ValueError("annotation์๋ sample_id์ ์ค์ device_id๊ฐ ํ์ํฉ๋๋ค.")
if label_status not in TRAINABLE_LABEL_STATUS06:
raise ValueError("P Formula token์ human_verified ์ํ๋ง ํ์ฉํฉ๋๋ค.")
if not isinstance(raw_tokens, dict) or not raw_tokens:
raise ValueError("annotation tokens๋ ๋น์ด ์์ง ์์ cell_idโtoken ๊ฐ์ฒด์ฌ์ผ ํฉ๋๋ค.")
tokens = {
str(cell_id).strip(): str(token).strip()
for cell_id, token in raw_tokens.items()
}
if any(not cell_id or not token for cell_id, token in tokens.items()):
raise ValueError("annotation cell ID์ token์ ๋น์ด ์์ ์ ์์ต๋๋ค.")
if len(tokens) != len(raw_tokens):
raise ValueError("์ ๊ทํ ํ ์ค๋ณต๋๋ annotation cell ID๊ฐ ์์ต๋๋ค.")
return PFormulaAnnotation06(
sample_id=sample_id,
device_id=device_id,
tokens=tokens,
label_status=label_status,
)
def read_p_formula_annotations06(path: Path) -> dict[str, PFormulaAnnotation06]:
"""ํ์ ๋ณ์: UTF-8 annotation JSONL. ์๋ ์๋ฆฌ: ํ ์์น ์ค๋ฅ์ sample ์ค๋ณต์ ๋ช
์ํ๋ฉฐ ์ ์ฒด๋ฅผ ์ฝ๋๋ค."""
annotations: dict[str, PFormulaAnnotation06] = {}
for line_number, raw in enumerate(
path.read_text(encoding="utf-8").splitlines(),
start=1,
):
if not raw.strip():
continue
try:
value = json.loads(raw)
except json.JSONDecodeError as error:
raise ValueError(
f"{path}:{line_number} UTF-8 JSON ํ์ฑ ์คํจ: {error.msg}",
) from error
if not isinstance(value, dict):
raise ValueError(f"{path}:{line_number} annotation์ด JSON object๊ฐ ์๋๋๋ค.")
try:
annotation = parse_p_formula_annotation06(value)
except ValueError as error:
raise ValueError(f"{path}:{line_number} {error}") from error
if annotation.sample_id in annotations:
raise ValueError(f"์ค๋ณต annotation sample_id์
๋๋ค: {annotation.sample_id}")
annotations[annotation.sample_id] = annotation
if not annotations:
raise ValueError("P Formula annotation์ด ์์ต๋๋ค.")
return annotations
def _point06(point: dict[str, Any]) -> dict[str, float | None]:
"""ํ์ ๋ณ์: Ink v1 point. ์๋ ์๋ฆฌ: ์ขํยท๊ด์ธก timestampยทpressure๋ฅผ P Formula ์ด๋ฆ์ผ๋ก ์์ค ์์ด ์ฎ๊ธด๋ค."""
try:
x, y = float(point["x"]), float(point["y"])
except (KeyError, TypeError, ValueError) as error:
raise ValueError("๋ชจ๋ point์๋ ์ ํจํ x/y๊ฐ ํ์ํฉ๋๋ค.") from error
timestamp = point.get("t_ms")
pressure = point.get("pressure")
return {
"x": x,
"y": y,
"t": None if timestamp is None else float(timestamp),
"pressure": None if pressure is None else float(pressure),
}
def build_p_formula_record06(
intake_record: dict[str, Any],
annotation: PFormulaAnnotation06,
*,
allowed_labels: Sequence[str] | None = None,
) -> dict[str, Any]:
"""ํ์ ๋ณ์: curated Ink v1ยท์ฌ๋ annotationยท์ ํ vocabulary. ์๋ ์๋ฆฌ: cell/stroke/token ์ ๋จ์ฌ๋ง P Formula v1๋ก ๋ณํํ๋ค."""
sample_id = str(intake_record.get("sample_id") or "").strip()
if sample_id != annotation.sample_id:
raise ValueError("intake sample_id์ annotation sample_id๊ฐ ๋ค๋ฆ
๋๋ค.")
if str(intake_record.get("format")) != "aiflow-ink/v1":
raise ValueError("AIFlow Ink v1 ์๋ณธ๋ง P Formula v1๋ก ๋ณํํ ์ ์์ต๋๋ค.")
if annotation.label_status not in TRAINABLE_LABEL_STATUS06:
raise ValueError("์ฌ๋์ด ํ์ ํ์ง ์์ token์ ํ์ต ํ๋ณด๋ก ๋ณํํ ์ ์์ต๋๋ค.")
consent = str(intake_record.get("consent_scope") or "")
if not consent.startswith("model_training"):
raise ValueError("model_training ๋์๊ฐ ์๋ intake record์
๋๋ค.")
source_id = str(intake_record.get("source") or "").strip()
writer_id = str(intake_record.get("writer_hash") or "").strip()
split = SPLIT_MAP06.get(str(intake_record.get("split") or ""))
canvas = intake_record.get("canvas")
if not source_id or not writer_id or split is None:
raise ValueError("sourceยทwriterยทtraining/validation/test split์ด ํ์ํฉ๋๋ค.")
if not isinstance(canvas, dict):
raise ValueError("canvas ๊ฐ์ฒด๊ฐ ํ์ํฉ๋๋ค.")
try:
canvas_width = float(canvas["width"])
canvas_height = float(canvas["height"])
except (KeyError, TypeError, ValueError) as error:
raise ValueError("์ ํจํ canvas width/height๊ฐ ํ์ํฉ๋๋ค.") from error
if canvas_width <= 0 or canvas_height <= 0:
raise ValueError("canvas width/height๋ ์์์ฌ์ผ ํฉ๋๋ค.")
raw_strokes = intake_record.get("strokes")
cells = intake_record.get("formula_cells")
if not isinstance(raw_strokes, list) or not raw_strokes:
raise ValueError("๋น์ด ์์ง ์์ raw strokes๊ฐ ํ์ํฉ๋๋ค.")
if not isinstance(cells, list) or not cells:
raise ValueError("๋น์ด ์์ง ์์ formula_cells๊ฐ ํ์ํฉ๋๋ค.")
stroke_map: dict[int, dict[str, Any]] = {}
for stroke in raw_strokes:
if not isinstance(stroke, dict):
raise ValueError("stroke๋ JSON object์ฌ์ผ ํฉ๋๋ค.")
stroke_id = int(stroke["stroke_id"])
if stroke_id in stroke_map:
raise ValueError(f"์ค๋ณต stroke_id์
๋๋ค: {stroke_id}")
stroke_map[stroke_id] = stroke
cell_ids = [str(cell.get("formula_id") or "").strip() for cell in cells]
if any(not cell_id for cell_id in cell_ids) or len(set(cell_ids)) != len(cell_ids):
raise ValueError("formula cell ID๋ ๋น์ด ์์ง ์๊ณ ๊ณ ์ ํด์ผ ํฉ๋๋ค.")
if set(cell_ids) != set(annotation.tokens):
missing = sorted(set(cell_ids) - set(annotation.tokens))
extra = sorted(set(annotation.tokens) - set(cell_ids))
raise ValueError(f"cell token ์ ๋จ์ฌ๊ฐ ์๋๋๋ค: missing={missing}, extra={extra}")
allowed = None if allowed_labels is None else set(str(label) for label in allowed_labels)
unknown = sorted({
annotation.tokens[cell_id]
for cell_id in cell_ids
if allowed is not None and annotation.tokens[cell_id] not in allowed
})
if unknown:
raise ValueError(f"0.6 vocabulary ๋ฐ token์
๋๋ค: {unknown}")
assigned: list[int] = []
symbols = []
for cell, cell_id in zip(cells, cell_ids, strict=True):
raw_ids = cell.get("stroke_ids")
if not isinstance(raw_ids, list) or not raw_ids:
raise ValueError(f"cell {cell_id}์ stroke๊ฐ ์์ต๋๋ค.")
stroke_ids = [int(value) for value in raw_ids]
missing_strokes = sorted(set(stroke_ids) - set(stroke_map))
if missing_strokes:
raise ValueError(f"cell {cell_id}๊ฐ ์๋ stroke๋ฅผ ์ฐธ์กฐํฉ๋๋ค: {missing_strokes}")
assigned.extend(stroke_ids)
ordered = sorted(
(stroke_map[stroke_id] for stroke_id in stroke_ids),
key=lambda stroke: (int(stroke.get("order", 0)), int(stroke["stroke_id"])),
)
converted_strokes = []
for stroke in ordered:
points = stroke.get("points")
if not isinstance(points, list) or not points:
raise ValueError(f"stroke {stroke['stroke_id']}์ point๊ฐ ์์ต๋๋ค.")
converted_strokes.append([_point06(point) for point in points])
symbols.append({
"token": annotation.tokens[cell_id],
"strokes": converted_strokes,
"source_cell_id": cell_id,
})
if len(assigned) != len(set(assigned)) or set(assigned) != set(stroke_map):
raise ValueError("๋ชจ๋ raw stroke๋ ์ ํํ ํ symbol cell์ ์ํด์ผ ํฉ๋๋ค.")
return {
"formula_id": sample_id,
"origin_id": f"{source_id}:{sample_id}",
"writer_id": writer_id,
"device_id": annotation.device_id,
"source_id": source_id,
"split": split,
"canvas_width": canvas_width,
"canvas_height": canvas_height,
"symbols": symbols,
"rights_track": "P",
"commercial_training_allowed": True,
"license_id": str(intake_record.get("license_id") or ""),
"label_status": annotation.label_status,
}
def materialize_p_formula_records06(
intake_records: Iterable[dict[str, Any]],
annotations: dict[str, PFormulaAnnotation06],
*,
allowed_labels: Sequence[str] | None = None,
) -> list[dict[str, Any]]:
"""ํ์ ๋ณ์: intake ์ํ์ดยทsample๋ณ annotation. ์๋ ์๋ฆฌ: ์์ชฝ sample ์งํฉ์ด ์ ํํ ๊ฐ์ ๋๋ง ์์๋ฅผ ๋ณด์กดํด ๋ณํํ๋ค."""
records = list(intake_records)
sample_ids = [str(record.get("sample_id") or "").strip() for record in records]
if any(not sample_id for sample_id in sample_ids):
raise ValueError("๋ชจ๋ intake record์ sample_id๊ฐ ํ์ํฉ๋๋ค.")
if len(set(sample_ids)) != len(sample_ids):
raise ValueError("์ค๋ณต intake sample_id๊ฐ ์์ต๋๋ค.")
if set(sample_ids) != set(annotations):
missing = sorted(set(sample_ids) - set(annotations))
extra = sorted(set(annotations) - set(sample_ids))
raise ValueError(f"intake/annotation sample ์งํฉ์ด ๋ค๋ฆ
๋๋ค: missing={missing}, extra={extra}")
return [
build_p_formula_record06(
record,
annotations[sample_id],
allowed_labels=allowed_labels,
)
for record, sample_id in zip(records, sample_ids, strict=True)
]
def write_p_formula_jsonl06(
path: Path,
records: Sequence[dict[str, Any]],
*,
overwrite: bool = False,
) -> None:
"""ํ์ ๋ณ์: ์ถ๋ ฅ ๊ฒฝ๋กยท๊ฒ์ฆ๋ record. ์๋ ์๋ฆฌ: UTF-8 ์์ ํ์ผ์ ์์ ๊ต์ฒดํด ๋ถ๋ถ JSONL์ ๋จ๊ธฐ์ง ์๋๋ค."""
if not records:
raise ValueError("์ ์ฅํ P Formula record๊ฐ ์์ต๋๋ค.")
if path.exists() and not overwrite:
raise FileExistsError(f"๊ธฐ์กด P Formula ํ์ผ์ ๋ฎ์ด์ฐ์ง ์์ต๋๋ค: {path}")
path.parent.mkdir(parents=True, exist_ok=True)
temporary = path.with_suffix(path.suffix + ".part")
payload = "".join(
json.dumps(record, ensure_ascii=False, separators=(",", ":")) + "\n"
for record in records
)
try:
temporary.write_text(payload, encoding="utf-8", newline="\n")
temporary.replace(path)
finally:
if temporary.exists():
temporary.unlink()
|