{"version":3,"sources":["/Users/mong-e/workspace/kordoc/dist/chunk-H7OWEYH2.cjs","../src/pdf/formula/models.ts"],"names":[],"mappings":"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","file":"/Users/mong-e/workspace/kordoc/dist/chunk-H7OWEYH2.cjs","sourcesContent":[null,"/**\n * 수식 OCR 모델 자동 다운로드 + SHA-256 검증\n *\n * 캐시 위치: `~/.cache/kordoc/models/pix2text/`\n * 총 ~155MB (MFD 44MB + MFR encoder 87MB + MFR decoder 30MB + tokenizer 40KB)\n *\n * HF URL + SHA 는 Docufinder archive/rust-formula-ocr-wip 에서 검증 완료됨.\n * 모델 버전 갱신 시 URL + SHA 를 함께 갱신해야 한다.\n */\n\nimport { createHash } from \"crypto\"\nimport { createReadStream } from \"fs\"\nimport { mkdir, stat, unlink, rename } from \"fs/promises\"\nimport { createWriteStream } from \"fs\"\nimport { homedir } from \"os\"\nimport { join, dirname } from \"path\"\nimport { pipeline } from \"stream/promises\"\nimport { Readable } from \"stream\"\n\nexport interface ModelSpec {\n  name: string\n  filename: string\n  url: string\n  sha256: string\n  sizeMb: number\n}\n\n/** 모델 스펙 (SHA-256 은 실제 다운로드로 검증됨 — 변경 금지) */\nexport const MFD_MODEL: ModelSpec = {\n  name: \"Pix2Text MFD\",\n  filename: \"mfd.onnx\",\n  url: \"https://huggingface.co/breezedeus/pix2text-mfd/resolve/main/mfd-v20240618.onnx\",\n  sha256: \"51a8854743b17ae654729af8db82a630c1ccfa06debf4856c8b28055f87d02c1\",\n  sizeMb: 42,\n}\n\nexport const MFR_ENCODER_MODEL: ModelSpec = {\n  name: \"Pix2Text MFR encoder\",\n  filename: \"encoder_model.onnx\",\n  url: \"https://huggingface.co/breezedeus/pix2text-mfr/resolve/main/encoder_model.onnx\",\n  sha256: \"bd8d5c322792e9ec45793af5569e9748f82a3d728a9e00213dbfc56c1486f37d\",\n  sizeMb: 87,\n}\n\nexport const MFR_DECODER_MODEL: ModelSpec = {\n  name: \"Pix2Text MFR decoder\",\n  filename: \"decoder_model.onnx\",\n  url: \"https://huggingface.co/breezedeus/pix2text-mfr/resolve/main/decoder_model.onnx\",\n  sha256: \"fd0f92d7a012f3dae41e1ac79421aea0ea888b5a66cb3f9a004e424f82f3daed\",\n  sizeMb: 30,\n}\n\nexport const MFR_TOKENIZER: ModelSpec = {\n  name: \"Pix2Text MFR tokenizer\",\n  filename: \"tokenizer.json\",\n  url: \"https://huggingface.co/breezedeus/pix2text-mfr/resolve/main/tokenizer.json\",\n  sha256: \"3e2ab757277d22639bec28c9d7972e352d3d1dba223051fa674002dc5ab64df3\",\n  sizeMb: 1,\n}\n\nexport const ALL_FORMULA_MODELS: ReadonlyArray<ModelSpec> = [\n  MFD_MODEL,\n  MFR_ENCODER_MODEL,\n  MFR_DECODER_MODEL,\n  MFR_TOKENIZER,\n]\n\n/**\n * 모델 캐시 하위 디렉토리 경로. 환경 변수 `KORDOC_MODEL_CACHE` 가 설정되면\n * 해당 경로를 우선 사용, 없으면 `~/.cache/kordoc/models/<subdir>/`.\n * (수식 OCR 외 텍스트 OCR 등 다른 모델군도 공용 — subdir 로 격리)\n */\nexport function getModelsDir(subdir: string): string {\n  const override = process.env.KORDOC_MODEL_CACHE\n  if (override && override.trim()) {\n    return join(override, subdir)\n  }\n  return join(homedir(), \".cache\", \"kordoc\", \"models\", subdir)\n}\n\nexport function getFormulaModelsDir(): string {\n  return getModelsDir(\"pix2text\")\n}\n\nexport interface ModelStatus {\n  spec: ModelSpec\n  localPath: string\n  exists: boolean\n  /** SHA 검증까지 끝났는가 (exists && valid) */\n  verified: boolean\n  /** 검증 실패 시 이유 */\n  invalidReason?: string\n}\n\n/** 지정 모델군의 존재/유효성 상태 반환 (다운로드 없이 확인만) */\nexport async function getModelStatusIn(dir: string, specs: ReadonlyArray<ModelSpec>): Promise<ModelStatus[]> {\n  const result: ModelStatus[] = []\n  for (const spec of specs) {\n    const localPath = join(dir, spec.filename)\n    let exists = false\n    try {\n      const s = await stat(localPath)\n      exists = s.isFile() && s.size > 0\n    } catch {\n      exists = false\n    }\n    if (!exists) {\n      result.push({ spec, localPath, exists: false, verified: false })\n      continue\n    }\n    try {\n      const actual = await sha256OfFile(localPath)\n      if (actual === spec.sha256) {\n        result.push({ spec, localPath, exists: true, verified: true })\n      } else {\n        result.push({\n          spec,\n          localPath,\n          exists: true,\n          verified: false,\n          invalidReason: `SHA256 mismatch: expected ${spec.sha256}, got ${actual}`,\n        })\n      }\n    } catch (e) {\n      result.push({\n        spec,\n        localPath,\n        exists: true,\n        verified: false,\n        invalidReason: `SHA compute failed: ${(e as Error).message}`,\n      })\n    }\n  }\n  return result\n}\n\n/** 모든 수식 모델의 존재/유효성 상태 반환 (다운로드 없이 확인만) */\nexport async function getFormulaModelStatus(): Promise<ModelStatus[]> {\n  return getModelStatusIn(getFormulaModelsDir(), ALL_FORMULA_MODELS)\n}\n\nexport interface DownloadProgress {\n  spec: ModelSpec\n  /** 현재까지 다운로드한 바이트 */\n  downloaded: number\n  /** 전체 바이트 (알 수 있으면) */\n  total: number | null\n  /** \"download\" | \"verify\" | \"done\" | \"skip\" */\n  phase: \"download\" | \"verify\" | \"done\" | \"skip\" | \"error\"\n  message?: string\n}\n\nexport type ProgressHandler = (p: DownloadProgress) => void\n\n/**\n * 지정 모델군을 다운로드/검증한다.\n * - 이미 있고 SHA 일치 → skip\n * - 없음 → 다운로드 후 SHA 검증. 실패 시 파일 삭제.\n */\nexport async function ensureModelsIn(dir: string, specs: ReadonlyArray<ModelSpec>, onProgress?: ProgressHandler): Promise<void> {\n  await mkdir(dir, { recursive: true })\n\n  for (const spec of specs) {\n    const localPath = join(dir, spec.filename)\n\n    if (await isExistingValid(localPath, spec.sha256)) {\n      onProgress?.({\n        spec,\n        downloaded: 0,\n        total: null,\n        phase: \"skip\",\n        message: \"이미 존재 + SHA 일치\",\n      })\n      continue\n    }\n\n    // 기존 파일 있지만 SHA 불일치 → 삭제\n    try {\n      await unlink(localPath)\n    } catch {\n      // 없을 수 있음\n    }\n\n    await downloadToFile(spec, localPath, onProgress)\n  }\n}\n\n/** 필요한 모든 수식 모델을 다운로드/검증한다. */\nexport async function ensureFormulaModels(onProgress?: ProgressHandler): Promise<void> {\n  return ensureModelsIn(getFormulaModelsDir(), ALL_FORMULA_MODELS, onProgress)\n}\n\n/** 단일 모델만 확인/다운로드 (진단 UI 에서 개별 상태 새로고침용) */\nexport async function ensureSingleModel(spec: ModelSpec, onProgress?: ProgressHandler): Promise<void> {\n  const dir = getFormulaModelsDir()\n  await mkdir(dir, { recursive: true })\n  const localPath = join(dir, spec.filename)\n  if (await isExistingValid(localPath, spec.sha256)) {\n    onProgress?.({ spec, downloaded: 0, total: null, phase: \"skip\" })\n    return\n  }\n  try {\n    await unlink(localPath)\n  } catch {}\n  await downloadToFile(spec, localPath, onProgress)\n}\n\nasync function isExistingValid(localPath: string, sha256Expected: string): Promise<boolean> {\n  try {\n    const s = await stat(localPath)\n    if (!s.isFile() || s.size === 0) return false\n  } catch {\n    return false\n  }\n  try {\n    const actual = await sha256OfFile(localPath)\n    return actual === sha256Expected\n  } catch {\n    return false\n  }\n}\n\nasync function downloadToFile(\n  spec: ModelSpec,\n  localPath: string,\n  onProgress?: ProgressHandler,\n): Promise<void> {\n  // 먼저 .part 로 받고 검증 후 rename — 중단된 다운로드가 \"정상 파일\"로 오인되는 걸 방지\n  const partPath = `${localPath}.part`\n  await mkdir(dirname(localPath), { recursive: true })\n\n  const resp = await fetch(spec.url, {\n    headers: {\n      // HF CDN 은 UA 없으면 가끔 403 을 뱉는다\n      \"User-Agent\": \"kordoc-formula-ocr/1.0 (+https://github.com/chrisryugj/kordoc)\",\n    },\n  })\n  if (!resp.ok || !resp.body) {\n    throw new Error(\n      `${spec.name} 다운로드 실패: HTTP ${resp.status} ${resp.statusText} (${spec.url})`,\n    )\n  }\n\n  const lenHeader = resp.headers.get(\"content-length\")\n  const total = lenHeader ? Number.parseInt(lenHeader, 10) : null\n  let downloaded = 0\n\n  const ws = createWriteStream(partPath)\n  try {\n    const reader = Readable.fromWeb(resp.body as unknown as import(\"stream/web\").ReadableStream)\n    reader.on(\"data\", (chunk: Buffer | Uint8Array) => {\n      downloaded += chunk.length\n      onProgress?.({\n        spec,\n        downloaded,\n        total,\n        phase: \"download\",\n      })\n    })\n    await pipeline(reader, ws)\n  } catch (e) {\n    try {\n      await unlink(partPath)\n    } catch {}\n    throw new Error(`${spec.name} 스트리밍 실패: ${(e as Error).message}`)\n  }\n\n  onProgress?.({\n    spec,\n    downloaded,\n    total,\n    phase: \"verify\",\n  })\n\n  // SHA 검증\n  let actual: string\n  try {\n    actual = await sha256OfFile(partPath)\n  } catch (e) {\n    try {\n      await unlink(partPath)\n    } catch {}\n    throw new Error(`${spec.name} SHA 계산 실패: ${(e as Error).message}`)\n  }\n\n  if (actual !== spec.sha256) {\n    try {\n      await unlink(partPath)\n    } catch {}\n    throw new Error(\n      `${spec.name} SHA256 mismatch: expected ${spec.sha256}, got ${actual} — 모델 URL 이 오염되었거나 전송 중 손상되었습니다.`,\n    )\n  }\n\n  await rename(partPath, localPath)\n  onProgress?.({\n    spec,\n    downloaded,\n    total,\n    phase: \"done\",\n  })\n}\n\nasync function sha256OfFile(p: string): Promise<string> {\n  const h = createHash(\"sha256\")\n  const stream = createReadStream(p)\n  await pipeline(stream, async function* (src) {\n    for await (const chunk of src) {\n      h.update(chunk)\n      // yield 안 함 (소비만)\n    }\n  })\n  return h.digest(\"hex\")\n}\n"]}