aiflow-math-ink-06-intermediate / src /p_formula_intake06.py
cwLeeDev's picture
Add fail-closed local P Formula intake contract
5094348 verified
Raw
History Blame Contribute Delete
11.9 kB
"""์‚ฌ๋žŒ์ด ํ™•์ •ํ•œ 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()