| """์ฌ๋์ด ํ์ ํ 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() |
|
|