cwLeeDev commited on
Commit
33080a1
·
verified ·
1 Parent(s): 9be8fa9

Fix Python composite runtime and atomic export

Browse files
MANIFEST.json CHANGED
@@ -1,11 +1,11 @@
1
  {
2
- "schema": "aiflow-hf-research-snapshot-v9",
3
- "generated_at": "2026-07-23T20:15:19.8122156Z",
4
  "track": "R_noncommercial_plus_rejected_P_proxy",
5
  "product_validation": false,
6
  "public_release": true,
7
  "contains_raw_dataset": false,
8
- "tests": "284 passed",
9
  "retracted_paths": [
10
  "models/auxiliary/boundary_auxiliary_head.pt",
11
  "models/auxiliary/seed17/boundary_joint_delta.pt",
@@ -30,8 +30,8 @@
30
  },
31
  {
32
  "path": "reports/RESEARCH_REPORT.md",
33
- "bytes": 34116,
34
- "sha256": "c2c1f9fc166525912d34f421b80b3f2662b18ec4a4d47934990c37b9432223e8"
35
  },
36
  {
37
  "path": "scripts/evaluate_crohme_tray_joint_selector.py",
@@ -90,8 +90,8 @@
90
  },
91
  {
92
  "path": "src/math_ink_06.py",
93
- "bytes": 54708,
94
- "sha256": "6c41a7ff9958fe6bd396b3321586d7a935ef0206553ce633327962c2d27ffff4"
95
  },
96
  {
97
  "path": "src/ink06_export.py",
@@ -135,8 +135,8 @@
135
  },
136
  {
137
  "path": "scripts/export_math_ink_06_litert.py",
138
- "bytes": 11876,
139
- "sha256": "3d5362341c29ef31493969ccf833c21cb1d4e02d8ccf9ccd217b038091f9e43c"
140
  },
141
  {
142
  "path": "scripts/evaluate_math_ink_06_p_boundary_device_stress.py",
@@ -170,13 +170,13 @@
170
  },
171
  {
172
  "path": "colab/aiflow_math_ink_06_litert_bundle.zip",
173
- "bytes": 17276380,
174
- "sha256": "e20ed37a7c35daabaae72b6108204ae08da3b909a19f230528bb40ab198c7ca8"
175
  },
176
  {
177
  "path": "colab/aiflow_math_ink_06_litert_bundle.manifest.json",
178
  "bytes": 2978,
179
- "sha256": "03a7ebfe56cc8855494ecfafb463290d4bbe9b0fb0582153e17b62ea6726bdd6"
180
  },
181
  {
182
  "path": "exports/seed17/export_manifest.json",
@@ -220,8 +220,8 @@
220
  },
221
  {
222
  "path": "README.md",
223
- "bytes": 11411,
224
- "sha256": "5e8ee6eef8985af8404540edc7fd000d5b03a095ed1eac05cabcfd8d346ab064"
225
  },
226
  {
227
  "path": "assets/stroke_encoding.svg",
 
1
  {
2
+ "schema": "aiflow-hf-research-snapshot-v10",
3
+ "generated_at": "2026-07-23T20:20:53.2135890Z",
4
  "track": "R_noncommercial_plus_rejected_P_proxy",
5
  "product_validation": false,
6
  "public_release": true,
7
  "contains_raw_dataset": false,
8
+ "tests": "285 passed",
9
  "retracted_paths": [
10
  "models/auxiliary/boundary_auxiliary_head.pt",
11
  "models/auxiliary/seed17/boundary_joint_delta.pt",
 
30
  },
31
  {
32
  "path": "reports/RESEARCH_REPORT.md",
33
+ "bytes": 34631,
34
+ "sha256": "46946707f85cd6860161e301c21970b885dfd742c122eb9d9f0f44c3173c208b"
35
  },
36
  {
37
  "path": "scripts/evaluate_crohme_tray_joint_selector.py",
 
90
  },
91
  {
92
  "path": "src/math_ink_06.py",
93
+ "bytes": 58690,
94
+ "sha256": "657a720f32bfe8548ec859986bc3b6bcafbf3ec70f0abfdbf50127946c655052"
95
  },
96
  {
97
  "path": "src/ink06_export.py",
 
135
  },
136
  {
137
  "path": "scripts/export_math_ink_06_litert.py",
138
+ "bytes": 11027,
139
+ "sha256": "68510ea84f778bc415c3b5b29e3445cd0c5102a5febb1bb126c92262c75ccebb"
140
  },
141
  {
142
  "path": "scripts/evaluate_math_ink_06_p_boundary_device_stress.py",
 
170
  },
171
  {
172
  "path": "colab/aiflow_math_ink_06_litert_bundle.zip",
173
+ "bytes": 17276876,
174
+ "sha256": "d5357732c9f1654d65664e3fe23acab88a7374e88e3f2bd5b60ab46120aefc47"
175
  },
176
  {
177
  "path": "colab/aiflow_math_ink_06_litert_bundle.manifest.json",
178
  "bytes": 2978,
179
+ "sha256": "d6876734bbb23394b40c3b484e2c44d9e06cf3f67de77d3f50205e554b41f758"
180
  },
181
  {
182
  "path": "exports/seed17/export_manifest.json",
 
220
  },
221
  {
222
  "path": "README.md",
223
+ "bytes": 11437,
224
+ "sha256": "5a160ef5bb7753ad36fb39f692aecb3171c243743d8fdd4f64def16a3714af42"
225
  },
226
  {
227
  "path": "assets/stroke_encoding.svg",
README.md CHANGED
@@ -95,11 +95,6 @@ models/
95
  ...
96
  seed47/
97
  ...
98
- auxiliary/
99
- boundary_auxiliary_head.pt
100
- seed17/boundary_joint_delta.pt
101
- seed31/boundary_joint_delta.pt
102
- seed47/boundary_joint_delta.pt
103
  artifacts/
104
  boundary_behavior_guard.joblib
105
  ```
@@ -110,8 +105,6 @@ artifacts/
110
  | `online_adapter.pt` | 실제 온라인 stroke 보정 | `dual_tcn_v3`, `top4-skeleton-128x19` |
111
  | `behavior_role_head.pt` | `x/X/×` 역할 문맥 | 128×19 stroke + 49 context feature |
112
  | `boundary_behavior_guard.joblib` | 잘못된 다기호 병합 억제 | geometry 17 feature, threshold 0.5, weight 6 |
113
- | `boundary_auxiliary_head.pt` | shared embedding용 P-track boundary smoke | 384→1, main exact/family head 비퇴행 |
114
- | `boundary_joint_delta.pt` | seed별 제한 joint fine-tuning delta | last TCN + attention + exact/family/boundary heads |
115
 
116
  세 seed teacher를 그대로 모바일에 넣는 것이 최종 목표는 아니다. release 경로는 seed ensemble을 하나의 student로 distillation한 뒤 LiteRT INT8/FP16을 비교하는 것이다.
117
 
@@ -136,7 +129,26 @@ print(adapter["adapter_architecture"], adapter["feature_contract"])
136
  print(role["role_labels"], role["context_features"])
137
  ```
138
 
139
- `state_dict`를 실제 module에 적재하려면 저장소의 snapshot 코드뿐 아니라 전체 AIFlow 연구 코드가 필요하다. checkpoint 내부 경로는 lineage 기록 로컬 절대경로 의존해 추론하지 않는다.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
 
141
  ## 현재 성능
142
 
 
95
  ...
96
  seed47/
97
  ...
 
 
 
 
 
98
  artifacts/
99
  boundary_behavior_guard.joblib
100
  ```
 
105
  | `online_adapter.pt` | 실제 온라인 stroke 보정 | `dual_tcn_v3`, `top4-skeleton-128x19` |
106
  | `behavior_role_head.pt` | `x/X/×` 역할 문맥 | 128×19 stroke + 49 context feature |
107
  | `boundary_behavior_guard.joblib` | 잘못된 다기호 병합 억제 | geometry 17 feature, threshold 0.5, weight 6 |
 
 
108
 
109
  세 seed teacher를 그대로 모바일에 넣는 것이 최종 목표는 아니다. release 경로는 seed ensemble을 하나의 student로 distillation한 뒤 LiteRT INT8/FP16을 비교하는 것이다.
110
 
 
129
  print(role["role_labels"], role["context_features"])
130
  ```
131
 
132
+ 실제 Python composite 추론은 Colab bundle을 디렉터리에서 다음처럼 실행한다. Adapter를 생략하면 정정된 메인 모델아니라 base-only 경로 되므로 반드시 함께 전달한다.
133
+
134
+ ```python
135
+ from pathlib import Path
136
+ from math_grid_drawer.research.math_ink_06 import MathInk06Engine
137
+
138
+ engine = MathInk06Engine(
139
+ Path("artifacts/base_378.pt"),
140
+ adapter_checkpoint=Path("artifacts/online_adapter.pt"),
141
+ )
142
+
143
+ result = engine.recognize_online(
144
+ strokes,
145
+ canvas_width=128,
146
+ canvas_height=128,
147
+ top_k=5,
148
+ )
149
+ ```
150
+
151
+ Checkpoint 내부 경로는 lineage 기록이며 로컬 절대경로에 의존해 추론하지 않는다.
152
 
153
  ## 현재 성능
154
 
colab/aiflow_math_ink_06_litert_bundle.manifest.json CHANGED
@@ -1,6 +1,6 @@
1
  {
2
  "schema": "aiflow-math-ink-06-litert-colab-bundle-v1",
3
- "generated_at": "2026-07-23T20:13:15.384782+00:00",
4
  "seed": 17,
5
  "litert_torch_version": "0.9.1",
6
  "representative_samples": 76,
@@ -32,8 +32,8 @@
32
  },
33
  {
34
  "path": "src/math_grid_drawer/research/math_ink_06.py",
35
- "bytes": 54708,
36
- "sha256": "6c41a7ff9958fe6bd396b3321586d7a935ef0206553ce633327962c2d27ffff4"
37
  },
38
  {
39
  "path": "src/math_grid_drawer/research/raster_skeleton06.py",
@@ -52,8 +52,8 @@
52
  },
53
  {
54
  "path": "scripts/export_math_ink_06_litert.py",
55
- "bytes": 11876,
56
- "sha256": "3d5362341c29ef31493969ccf833c21cb1d4e02d8ccf9ccd217b038091f9e43c"
57
  },
58
  {
59
  "path": "artifacts/base_378.pt",
@@ -73,8 +73,8 @@
73
  ],
74
  "product_validation": false,
75
  "bundle": "research\\runs\\math_ink_06_litert_colab_20260724\\aiflow_math_ink_06_litert_bundle.zip",
76
- "bundle_bytes": 17276380,
77
- "bundle_sha256": "e20ed37a7c35daabaae72b6108204ae08da3b909a19f230528bb40ab198c7ca8",
78
  "verification": {
79
  "schema": "aiflow-math-ink-06-litert-colab-bundle-v1",
80
  "files": 13,
 
1
  {
2
  "schema": "aiflow-math-ink-06-litert-colab-bundle-v1",
3
+ "generated_at": "2026-07-23T20:19:13.374433+00:00",
4
  "seed": 17,
5
  "litert_torch_version": "0.9.1",
6
  "representative_samples": 76,
 
32
  },
33
  {
34
  "path": "src/math_grid_drawer/research/math_ink_06.py",
35
+ "bytes": 58690,
36
+ "sha256": "657a720f32bfe8548ec859986bc3b6bcafbf3ec70f0abfdbf50127946c655052"
37
  },
38
  {
39
  "path": "src/math_grid_drawer/research/raster_skeleton06.py",
 
52
  },
53
  {
54
  "path": "scripts/export_math_ink_06_litert.py",
55
+ "bytes": 11027,
56
+ "sha256": "68510ea84f778bc415c3b5b29e3445cd0c5102a5febb1bb126c92262c75ccebb"
57
  },
58
  {
59
  "path": "artifacts/base_378.pt",
 
73
  ],
74
  "product_validation": false,
75
  "bundle": "research\\runs\\math_ink_06_litert_colab_20260724\\aiflow_math_ink_06_litert_bundle.zip",
76
+ "bundle_bytes": 17276876,
77
+ "bundle_sha256": "d5357732c9f1654d65664e3fe23acab88a7374e88e3f2bd5b60ab46120aefc47",
78
  "verification": {
79
  "schema": "aiflow-math-ink-06-litert-colab-bundle-v1",
80
  "files": 13,
colab/aiflow_math_ink_06_litert_bundle.zip CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:e20ed37a7c35daabaae72b6108204ae08da3b909a19f230528bb40ab198c7ca8
3
- size 17276380
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d5357732c9f1654d65664e3fe23acab88a7374e88e3f2bd5b60ab46120aefc47
3
+ size 17276876
reports/RESEARCH_REPORT.md CHANGED
@@ -409,7 +409,9 @@ Joint delta 없이 정정된 main과 새 auxiliary head만 대상으로 device s
409
 
410
  Seed 17·31·47 모두 online/raster 각각 대표 입력 76개에서 eager 대비 top-1 100% 일치, 최대 logit 절대오차 0.0으로 strict `torch.export` gate를 통과했다. 기존 출력 경로를 재사용할 때 오래된 `.pt2`가 남는 문제를 발견해 `.part → atomic replace`로 수정했고, fresh seed-17 두 graph 합계는 24,302,579 bytes로 25MiB 예산 안이다. 다만 `.pt2`는 Android LiteRT artifact가 아니다. 공식 LiteRT Torch 0.9.1은 Linux 환경을 요구하며 WSL 변환 환경 설치 중 WSL filesystem I/O 오류가 발생해 `.tflite` 변환과 runtime parity는 아직 미완료다.
411
 
412
- Linux/Colab 변환을 위해 label이 없는 실제 HWRT-derived online/raster 대표 입력 76개를 5,721,772-byte cache로 고정했다. 최소 bundle은 composite base·adapter, 필요한 source 7개, converter와 대표 cache만 포함하며 13개 entry·17,276,380 bytes다. Bundle 내부 SHA-256 전체 검증과 별도 디렉터리 추출 후 composite export smoke를 통과했다. Colab notebook은 공식 `litert-torch==0.9.1` 설치, 공개 Hugging Face bundle 다운로드, 해시 검증, online/raster `.tflite` 변환, 76개 전체 top-1 100%·logit 오차≤0.02·합계≤25MiB gate를 자동 실행한다.
 
 
413
 
414
  ### 실제 P 연속식 데이터 계약
415
 
@@ -422,7 +424,7 @@ Linux/Colab 변환을 위해 label이 없는 실제 HWRT-derived online/raster
422
  - 양수 canvas 크기
423
  - token과 원본 raw stroke가 포함된 정답 symbol group
424
 
425
- Origin·writer·device·source가 둘 이상의 split에 나타나면 제품 평가를 거부한다. Timestamp와 pressure가 없는 symbol은 삭제하거나 관측값으로 위장하지 않고 missing slice로 센다. 검증된 formula의 실제 symbol group은 boundary 음성, 인접한 두 symbol group 결합은 boundary 양성으로 만든다. 이 계약은 실제 성능값을 만들지는 않지만, 향후 P 입력이 CROHME 정답 group이나 합성 고립기호 proxy와 섞이는 것을 방지한다. 전체 회귀는 284개가 통과했다.
426
 
427
  ## 산출물
428
 
 
409
 
410
  Seed 17·31·47 모두 online/raster 각각 대표 입력 76개에서 eager 대비 top-1 100% 일치, 최대 logit 절대오차 0.0으로 strict `torch.export` gate를 통과했다. 기존 출력 경로를 재사용할 때 오래된 `.pt2`가 남는 문제를 발견해 `.part → atomic replace`로 수정했고, fresh seed-17 두 graph 합계는 24,302,579 bytes로 25MiB 예산 안이다. 다만 `.pt2`는 Android LiteRT artifact가 아니다. 공식 LiteRT Torch 0.9.1은 Linux 환경을 요구하며 WSL 변환 환경 설치 중 WSL filesystem I/O 오류가 발생해 `.tflite` 변환과 runtime parity는 아직 미완료다.
411
 
412
+ Linux/Colab 변환을 위해 label이 없는 실제 HWRT-derived online/raster 대표 입력 76개를 5,721,772-byte cache로 고정했다. 최소 bundle은 composite base·adapter, 필요한 source, converter와 대표 cache만 포함하며 13개 entry·17,276,876 bytes다. Bundle 내부 SHA-256 전체 검증과 별도 디렉터리 추출 후 composite export smoke를 통과했다. Colab notebook은 공식 `litert-torch==0.9.1` 설치, 공개 Hugging Face bundle 다운로드, 해시 검증, online/raster `.tflite` 변환, 76개 전체 top-1 100%·logit 오차≤0.02·합계≤25MiB gate를 자동 실행한다.
413
+
414
+ Python 공개 런타임의 `MathInk06Engine`도 base-only였으므로 `adapter_checkpoint`를 공식 생성자 인자로 추가했다. 엔진은 base→adapter `shared_state_dict`→dual modality adapter 순서로 구성하고 online은 `adapter.online`, raster virtual trajectory는 `adapter.raster`를 거친다. 실제 seed-17 representative 입력에서 엔진과 export wrapper의 online/raster 최대 logit 오차는 모두 0.0이었다. Adapter를 생략한 호출은 호환용 base-only 경로로만 남긴다.
415
 
416
  ### 실제 P 연속식 데이터 계약
417
 
 
424
  - 양수 canvas 크기
425
  - token과 원본 raw stroke가 포함된 정답 symbol group
426
 
427
+ Origin·writer·device·source가 둘 이상의 split에 나타나면 제품 평가를 거부한다. Timestamp와 pressure가 없는 symbol은 삭제하거나 관측값으로 위장하지 않고 missing slice로 센다. 검증된 formula의 실제 symbol group은 boundary 음성, 인접한 두 symbol group 결합은 boundary 양성으로 만든다. 이 계약은 실제 성능값을 만들지는 않지만, 향후 P 입력이 CROHME 정답 group이나 합성 고립기호 proxy와 섞이는 것을 방지한다. 전체 회귀는 285개가 통과했다.
428
 
429
  ## 산출물
430
 
scripts/export_math_ink_06_litert.py CHANGED
@@ -21,10 +21,7 @@ from math_grid_drawer.research.ink06_export import (
21
  OnlineExportWrapper06, RasterExportWrapper06, exported_equivalence06,
22
  )
23
  from math_grid_drawer.research.math_ink_06 import MathInk06Engine
24
- from math_grid_drawer.research.skeleton_adapter06 import (
25
- DualModalityTrajectoryAdapter06, SkeletonTrajectoryAdapter06,
26
- )
27
- from math_grid_drawer.research.math_ink_06 import VirtualTrajectoryAdapter06
28
 
29
 
30
  def _representative_inputs(baseline_report: Path, data_path: Path) -> tuple[list[tuple[torch.Tensor, ...]], list[tuple[torch.Tensor, ...]]]:
@@ -127,24 +124,9 @@ def _load_composite06(
127
  ) -> tuple[MathInk06Engine, torch.nn.Module, dict]:
128
  """필요 변수: base·adapter checkpoint. 작동 원리: base→shared state→modality adapter 순서로 배포 모델을 합성한다."""
129
 
130
- engine = MathInk06Engine(checkpoint)
131
  payload = torch.load(adapter_checkpoint, map_location="cpu", weights_only=False)
132
- architecture = str(payload["adapter_architecture"])
133
- if architecture == "local_v1":
134
- adapter: torch.nn.Module = VirtualTrajectoryAdapter06()
135
- elif architecture == "tcn_v2":
136
- adapter = SkeletonTrajectoryAdapter06()
137
- elif architecture == "dual_tcn_v3":
138
- adapter = DualModalityTrajectoryAdapter06()
139
- else:
140
- raise ValueError(f"지원하지 않는 adapter architecture입니다: {architecture}")
141
- adapter.load_state_dict(payload["state_dict"])
142
- shared_state = payload.get("shared_state_dict") or {}
143
- if shared_state:
144
- incompatible = engine.model.load_state_dict(shared_state, strict=False)
145
- if incompatible.unexpected_keys:
146
- raise ValueError(f"adapter shared state key 오류: {incompatible.unexpected_keys}")
147
- return engine, adapter.eval(), payload
148
 
149
 
150
  def _adapter_branches06(adapter: torch.nn.Module) -> tuple[torch.nn.Module, torch.nn.Module]:
 
21
  OnlineExportWrapper06, RasterExportWrapper06, exported_equivalence06,
22
  )
23
  from math_grid_drawer.research.math_ink_06 import MathInk06Engine
24
+ from math_grid_drawer.research.skeleton_adapter06 import DualModalityTrajectoryAdapter06
 
 
 
25
 
26
 
27
  def _representative_inputs(baseline_report: Path, data_path: Path) -> tuple[list[tuple[torch.Tensor, ...]], list[tuple[torch.Tensor, ...]]]:
 
124
  ) -> tuple[MathInk06Engine, torch.nn.Module, dict]:
125
  """필요 변수: base·adapter checkpoint. 작동 원리: base→shared state→modality adapter 순서로 배포 모델을 합성한다."""
126
 
127
+ engine = MathInk06Engine(checkpoint, adapter_checkpoint=adapter_checkpoint)
128
  payload = torch.load(adapter_checkpoint, map_location="cpu", weights_only=False)
129
+ return engine, engine.composite_adapter, payload
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
 
131
 
132
  def _adapter_branches06(adapter: torch.nn.Module) -> tuple[torch.nn.Module, torch.nn.Module]:
src/math_ink_06.py CHANGED
@@ -874,7 +874,11 @@ class SymbolCandidate06:
874
  class MathInk06Engine:
875
  """필요 변수: 0.6 checkpoint. 작동 원리: 원본 stroke 또는 raster에서 텍스트 후보만 반환한다."""
876
 
877
- def __init__(self, checkpoint: Path, *, device: str = "cpu") -> None:
 
 
 
 
878
  payload = torch.load(checkpoint, map_location=device, weights_only=False)
879
  self.labels = tuple(str(value) for value in payload["exact_labels"])
880
  self.family_labels = tuple(str(value) for value in payload["family_labels"])
@@ -896,6 +900,46 @@ class MathInk06Engine:
896
  "symmetry_weight": 0.0, "symmetry_mode": "logsumexp",
897
  **dict(payload.get("raster_fusion", {})),
898
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
899
  family_to_index = {label: index for index, label in enumerate(self.family_labels)}
900
  # checkpoint가 가진 ontology와 동일한 exact→family 사상을 기기 내 상수 tensor로 유지한다.
901
  from .trajectory_sequence import shape_family
@@ -933,6 +977,40 @@ class MathInk06Engine:
933
  contribution = joint.gather(2, predicted[:, None, None].expand(-1, joint.shape[1], 1)).squeeze(-1)
934
  return fused, contribution.argmax(dim=1)
935
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
936
  def _result(self, logits: Tensor, started: float, top_k: int) -> dict[str, Any]:
937
  """필요 변수: fused logits·시작시각·k. 작동 원리: stroke/image 없이 모바일 공개 SymbolResult를 만든다."""
938
 
@@ -952,7 +1030,7 @@ class MathInk06Engine:
952
  ink = canonicalize_ink06(strokes, canvas_width=canvas_width, canvas_height=canvas_height)
953
  sequence = torch.from_numpy(ink.features).unsqueeze(0).to(self.device)
954
  with torch.inference_mode():
955
- exact, _family = self.model.forward_online(sequence)
956
  return self._result(exact, started, top_k)
957
 
958
  def recognize_raster(self, image: Image.Image, *, top_k: int = 5, debug: bool = False) -> dict[str, Any]:
@@ -962,7 +1040,9 @@ class MathInk06Engine:
962
  normalized = image.convert("L").resize((128, 128), Image.Resampling.LANCZOS)
963
  raster = 1.0 - torch.from_numpy(np.asarray(normalized, dtype=np.float32) / 255.0)
964
  with torch.inference_mode():
965
- output = self.model.forward_raster(raster.view(1, 1, 128, 128).to(self.device))
 
 
966
  symmetry_weight = float(self.raster_fusion["symmetry_weight"])
967
  if symmetry_weight:
968
  symmetry_exact, symmetry_family = raster_symmetry_logits06(
 
874
  class MathInk06Engine:
875
  """필요 변수: 0.6 checkpoint. 작동 원리: 원본 stroke 또는 raster에서 텍스트 후보만 반환한다."""
876
 
877
+ def __init__(
878
+ self, checkpoint: Path, *, adapter_checkpoint: Path | None = None, device: str = "cpu",
879
+ ) -> None:
880
+ """필요 변수: base와 선택 composite adapter. 작동 원리: base→shared state→modality adapter 순서로 런타임을 구성한다."""
881
+
882
  payload = torch.load(checkpoint, map_location=device, weights_only=False)
883
  self.labels = tuple(str(value) for value in payload["exact_labels"])
884
  self.family_labels = tuple(str(value) for value in payload["family_labels"])
 
900
  "symmetry_weight": 0.0, "symmetry_mode": "logsumexp",
901
  **dict(payload.get("raster_fusion", {})),
902
  }
903
+ self.composite_adapter: nn.Module = nn.Identity()
904
+ self.online_adapter: nn.Module = nn.Identity()
905
+ self.raster_adapter: nn.Module = nn.Identity()
906
+ if adapter_checkpoint is not None:
907
+ adapter_payload = torch.load(
908
+ adapter_checkpoint, map_location=device, weights_only=False,
909
+ )
910
+ shared_state = adapter_payload.get("shared_state_dict") or {}
911
+ if shared_state:
912
+ incompatible = self.model.load_state_dict(shared_state, strict=False)
913
+ if incompatible.unexpected_keys:
914
+ raise ValueError(
915
+ f"adapter shared state key 오류: {incompatible.unexpected_keys}"
916
+ )
917
+ architecture = str(adapter_payload["adapter_architecture"])
918
+ if architecture == "local_v1":
919
+ adapter: nn.Module = VirtualTrajectoryAdapter06()
920
+ else:
921
+ from .skeleton_adapter06 import (
922
+ DualModalityTrajectoryAdapter06, SkeletonTrajectoryAdapter06,
923
+ )
924
+ if architecture == "tcn_v2":
925
+ adapter = SkeletonTrajectoryAdapter06()
926
+ elif architecture == "dual_tcn_v3":
927
+ adapter = DualModalityTrajectoryAdapter06()
928
+ else:
929
+ raise ValueError(
930
+ f"지원하지 않는 adapter architecture입니다: {architecture}"
931
+ )
932
+ adapter.load_state_dict(adapter_payload["state_dict"])
933
+ adapter = adapter.to(self.device).eval()
934
+ self.composite_adapter = adapter
935
+ if architecture == "dual_tcn_v3":
936
+ self.online_adapter = adapter.online # type: ignore[attr-defined]
937
+ self.raster_adapter = adapter.raster # type: ignore[attr-defined]
938
+ else:
939
+ self.online_adapter = self.raster_adapter = adapter
940
+ self.model_version = (
941
+ f"{self.model_version}+{adapter_payload.get('model_version', architecture)}"
942
+ )
943
  family_to_index = {label: index for index, label in enumerate(self.family_labels)}
944
  # checkpoint가 가진 ontology와 동일한 exact→family 사상을 기기 내 상수 tensor로 유지한다.
945
  from .trajectory_sequence import shape_family
 
977
  contribution = joint.gather(2, predicted[:, None, None].expand(-1, joint.shape[1], 1)).squeeze(-1)
978
  return fused, contribution.argmax(dim=1)
979
 
980
+ def _forward_raster_composite06(self, raster: Tensor) -> dict[str, Tensor]:
981
+ """필요 변수: 정규화 raster. 작동 원리: virtual top-4를 외부 raster adapter까지 거쳐 shared head로 분류한다."""
982
+
983
+ if isinstance(self.raster_adapter, nn.Identity):
984
+ return self.model.forward_raster(raster)
985
+ coordinates, states, progress, hypothesis_scores = self.model.decode_raster_trajectories(raster)
986
+ features = virtual_features06(
987
+ coordinates, states,
988
+ None if self.model.raster_architecture == "spatial_flat_v1" else progress,
989
+ contract=self.model.virtual_contract,
990
+ )
991
+ batch, hypotheses, steps, channels = features.shape
992
+ if self.model.use_virtual_adapter:
993
+ raw_features = features
994
+ internal = self.model.virtual_adapter(
995
+ features.reshape(batch * hypotheses, steps, channels),
996
+ ).reshape(batch, hypotheses, steps, channels)
997
+ features = (
998
+ raw_features
999
+ + self.model.virtual_adapter_weight * (internal - raw_features)
1000
+ )
1001
+ flat = self.raster_adapter(
1002
+ features.reshape(batch * hypotheses, steps, channels),
1003
+ )
1004
+ exact, family = self.model.classify_trajectory(flat)
1005
+ return {
1006
+ "coordinates": coordinates,
1007
+ "state_logits": states,
1008
+ "stroke_progress": progress,
1009
+ "hypothesis_scores": hypothesis_scores,
1010
+ "exact_logits": exact.reshape(batch, hypotheses, -1),
1011
+ "family_logits": family.reshape(batch, hypotheses, -1),
1012
+ }
1013
+
1014
  def _result(self, logits: Tensor, started: float, top_k: int) -> dict[str, Any]:
1015
  """필요 변수: fused logits·시작시각·k. 작동 원리: stroke/image 없이 모바일 공개 SymbolResult를 만든다."""
1016
 
 
1030
  ink = canonicalize_ink06(strokes, canvas_width=canvas_width, canvas_height=canvas_height)
1031
  sequence = torch.from_numpy(ink.features).unsqueeze(0).to(self.device)
1032
  with torch.inference_mode():
1033
+ exact, _family = self.model.forward_online(self.online_adapter(sequence))
1034
  return self._result(exact, started, top_k)
1035
 
1036
  def recognize_raster(self, image: Image.Image, *, top_k: int = 5, debug: bool = False) -> dict[str, Any]:
 
1040
  normalized = image.convert("L").resize((128, 128), Image.Resampling.LANCZOS)
1041
  raster = 1.0 - torch.from_numpy(np.asarray(normalized, dtype=np.float32) / 255.0)
1042
  with torch.inference_mode():
1043
+ output = self._forward_raster_composite06(
1044
+ raster.view(1, 1, 128, 128).to(self.device),
1045
+ )
1046
  symmetry_weight = float(self.raster_fusion["symmetry_weight"])
1047
  if symmetry_weight:
1048
  symmetry_exact, symmetry_family = raster_symmetry_logits06(