@heripo/pdf-parser 0.1.11 → 0.1.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +589 -38
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +598 -38
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/core/pdf-parser.ts","../src/config/constants.ts","../../shared/src/utils/batch-processor.ts","../../shared/src/utils/concurrent-pool.ts","../../shared/src/utils/spawn-utils.ts","../../shared/src/utils/llm-caller.ts","../../shared/src/utils/provider-detector.ts","../../shared/src/utils/llm-token-usage-aggregator.ts","../src/environment/docling-environment.ts","../src/utils/python-version.ts","../src/core/pdf-converter.ts","../src/errors/image-pdf-fallback-error.ts","../src/processors/image-extractor.ts","../src/utils/jq.ts","../src/processors/page-renderer.ts","../src/processors/pdf-text-extractor.ts","../src/processors/vlm-text-corrector.ts","../src/types/vlm-text-correction-schema.ts","../src/samplers/ocr-strategy-sampler.ts","../src/utils/local-file-server.ts","../src/core/image-pdf-converter.ts","../src/validators/vlm-response-validator.ts"],"sourcesContent":["export { PDFParser } from './core/pdf-parser';\nexport type {\n ConversionCompleteCallback,\n ConvertWithStrategyResult,\n PDFConvertOptions,\n} from './core/pdf-converter';\nexport { ImagePdfFallbackError } from './errors/image-pdf-fallback-error';\nexport { VlmResponseValidator } from './validators/vlm-response-validator';\nexport type {\n VlmValidationResult,\n VlmQualityIssue,\n} from './validators/vlm-response-validator';\nexport type {\n VlmPageQuality,\n VlmQualityIssueType,\n} from './types/vlm-page-result';\n","import type { LoggerMethods } from '@heripo/logger';\nimport type { TokenUsageReport } from '@heripo/model';\nimport type { DoclingAPIClient } from 'docling-sdk';\n\nimport { Docling } from 'docling-sdk';\nimport { execSync } from 'node:child_process';\nimport { platform } from 'node:os';\nimport { join } from 'node:path';\n\nimport { PDF_PARSER } from '../config/constants';\nimport { DoclingEnvironment } from '../environment/docling-environment';\nimport {\n type ConversionCompleteCallback,\n type PDFConvertOptions,\n PDFConverter,\n} from './pdf-converter';\n\ntype Options = {\n logger: LoggerMethods;\n timeout?: number;\n venvPath?: string;\n killExistingProcess?: boolean;\n /**\n * Enable fallback to image-based PDF when conversion fails.\n * Only works with local server mode (port option).\n * Requires ImageMagick and Ghostscript to be installed.\n */\n enableImagePdfFallback?: boolean;\n} & ({ port?: number } | { baseUrl: string });\n\n/**\n * PDFParser - A PDF document parser using docling-serve\n *\n * ## System Requirements\n * Before using PDFParser, ensure your system meets these requirements:\n *\n * ### Operating System\n * - macOS 10.15 (Catalina) or later\n *\n * ### Required Software\n * - `python3` (version 3.9 - 3.12)\n * - Python 3.13+ is NOT compatible with docling-serve\n * - Recommended: Python 3.11 or 3.12\n * - Install specific version: `pyenv install 3.12.0 && pyenv global 3.12.0`\n * - `jq` - JSON processor\n * - Install: `brew install jq`\n * - `poppler` - PDF text extraction tools (pdftotext, pdfinfo)\n * - Install: `brew install poppler`\n * - `lsof` - List open files (usually pre-installed on macOS)\n *\n * ## Initialization Process\n * When `init()` is called, the following setup occurs automatically:\n *\n * ### If using external server (baseUrl provided):\n * 1. Connects to the provided baseUrl\n * 2. Waits for server health check (up to 120 seconds)\n *\n * ### If using local server (default):\n * 1. **Python Environment Setup**\n * - Verifies Python version compatibility (3.9-3.12)\n * - Creates Python virtual environment at `venvPath` (default: `.venv`)\n * - Verifies virtual environment Python version\n *\n * 2. **Dependency Installation**\n * - Upgrades pip to latest version\n * - Installs setuptools and wheel\n * - Installs pyarrow (binary-only to avoid compilation)\n * - Installs docling-serve package\n *\n * 3. **Server Management**\n * - Checks if specified port is already in use\n * - If `killExistingProcess` is true, kills any process using the port\n * - If port is in use and `killExistingProcess` is false, reuses existing server\n * - Otherwise, starts new docling-serve instance on specified port\n * - Waits for server to become ready (health check, up to 120 seconds)\n *\n * ## Notes\n * - First initialization may take several minutes due to Python package downloads\n * - Subsequent initializations are faster if virtual environment already exists\n * - The virtual environment and packages are reused across sessions\n * - Server process runs in background and needs to be managed separately if needed\n */\nexport class PDFParser {\n private readonly logger: LoggerMethods;\n private readonly port?: number;\n private readonly baseUrl?: string;\n private readonly timeout: number;\n private readonly venvPath: string;\n private readonly killExistingProcess: boolean;\n private readonly enableImagePdfFallback: boolean;\n private client: DoclingAPIClient | null = null;\n private environment?: DoclingEnvironment;\n\n constructor(options: Options) {\n const {\n logger,\n timeout = PDF_PARSER.DEFAULT_TIMEOUT_MS,\n venvPath,\n killExistingProcess = false,\n enableImagePdfFallback = false,\n } = options;\n\n this.logger = logger;\n\n if ('baseUrl' in options) {\n this.baseUrl = options.baseUrl;\n this.port = undefined;\n } else {\n this.port = options.port;\n this.baseUrl = undefined;\n }\n\n this.timeout = timeout;\n this.venvPath = venvPath || join(process.cwd(), '.venv');\n this.killExistingProcess = killExistingProcess;\n this.enableImagePdfFallback = enableImagePdfFallback;\n }\n\n async init(): Promise<void> {\n this.logger.info('[PDFParser] Initializing...');\n\n this.checkOperatingSystem();\n this.checkJqInstalled();\n this.checkPopplerInstalled();\n this.checkMacOSVersion();\n\n // Check ImageMagick/Ghostscript only for local server mode with fallback enabled\n if (this.enableImagePdfFallback && !this.baseUrl) {\n this.checkImageMagickInstalled();\n this.checkGhostscriptInstalled();\n } else if (this.enableImagePdfFallback && this.baseUrl) {\n this.logger.warn(\n '[PDFParser] enableImagePdfFallback is ignored when using external server (baseUrl)',\n );\n }\n\n if (this.baseUrl) {\n this.logger.info('[PDFParser] Using external server:', this.baseUrl);\n this.client = new Docling({\n api: { baseUrl: this.baseUrl, timeout: this.timeout },\n });\n await this.waitForServerReady();\n return;\n }\n\n this.logger.info('[PDFParser] Setting up local server...');\n try {\n this.environment = new DoclingEnvironment({\n logger: this.logger,\n venvPath: this.venvPath,\n port: this.port as number,\n killExistingProcess: this.killExistingProcess,\n });\n\n await this.environment.setup();\n\n const clientUrl = `http://localhost:${this.port}`;\n this.client = new Docling({\n api: {\n baseUrl: clientUrl,\n timeout: this.timeout,\n },\n });\n\n await this.waitForServerReady();\n this.logger.info('[PDFParser] Ready');\n } catch (error) {\n this.logger.error('[PDFParser] Initialization failed:', error);\n throw new Error(`Failed to initialize PDFParser: ${error}`);\n }\n }\n\n private checkOperatingSystem(): void {\n if (platform() !== 'darwin') {\n throw new Error(\n 'PDFParser is only supported on macOS. Current platform: ' + platform(),\n );\n }\n }\n\n private checkJqInstalled(): void {\n try {\n execSync('which jq', { stdio: 'ignore' });\n } catch {\n throw new Error(\n 'jq is not installed. Please install jq using: brew install jq',\n );\n }\n }\n\n private checkPopplerInstalled(): void {\n try {\n execSync('which pdftotext', { stdio: 'ignore' });\n } catch {\n throw new Error(\n 'poppler is not installed. Please install poppler using: brew install poppler',\n );\n }\n }\n\n private checkMacOSVersion(): void {\n try {\n const versionOutput = execSync('sw_vers -productVersion', {\n encoding: 'utf-8',\n }).trim();\n const versionMatch = versionOutput.match(/^(\\d+)\\.(\\d+)/);\n if (versionMatch) {\n const major = parseInt(versionMatch[1]);\n const minor = parseInt(versionMatch[2]);\n if (major < 10 || (major === 10 && minor < 15)) {\n throw new Error(\n `macOS 10.15 or later is required. Current version: ${versionOutput}`,\n );\n }\n }\n } catch (error) {\n if (error instanceof Error && error.message.includes('macOS 10.15')) {\n throw error;\n }\n throw new Error('Failed to check macOS version');\n }\n }\n\n private checkImageMagickInstalled(): void {\n try {\n execSync('which magick', { stdio: 'ignore' });\n } catch {\n throw new Error(\n 'ImageMagick is not installed but enableImagePdfFallback is enabled. ' +\n 'Please install ImageMagick using: brew install imagemagick',\n );\n }\n }\n\n private checkGhostscriptInstalled(): void {\n try {\n execSync('which gs', { stdio: 'ignore' });\n } catch {\n throw new Error(\n 'Ghostscript is not installed but enableImagePdfFallback is enabled. ' +\n 'Please install Ghostscript using: brew install ghostscript',\n );\n }\n }\n\n /**\n * Check if an error is a connection refused error (ECONNREFUSED).\n * This typically indicates the Docling server has crashed.\n */\n private isConnectionRefusedError(error: unknown): boolean {\n if (error instanceof Error) {\n // Check message and cause chain for ECONNREFUSED\n if (error.message.includes('ECONNREFUSED')) {\n return true;\n }\n if (\n error.cause instanceof Error &&\n error.cause.message.includes('ECONNREFUSED')\n ) {\n return true;\n }\n }\n return false;\n }\n\n /**\n * Restart the Docling server after it has crashed.\n * This kills any existing process on the port, starts a new server,\n * and waits for it to become ready.\n *\n * Note: This method is only called when canRecover is true,\n * which guarantees this.port is defined.\n */\n private async restartServer(): Promise<void> {\n this.logger.info('[PDFParser] Restarting server...');\n\n // Kill existing process on port\n // Note: this.port is guaranteed to be defined by the caller (canRecover check)\n await DoclingEnvironment.killProcessOnPort(this.logger, this.port!);\n\n // Start new server\n const environment = new DoclingEnvironment({\n logger: this.logger,\n venvPath: this.venvPath,\n port: this.port!,\n killExistingProcess: false, // Already killed above\n });\n\n await environment.startServer();\n\n // Recreate client\n this.client?.destroy();\n this.client = new Docling({\n api: {\n baseUrl: `http://localhost:${this.port}`,\n timeout: this.timeout,\n },\n });\n\n await this.waitForServerReady();\n this.logger.info('[PDFParser] Server restarted successfully');\n }\n\n private async waitForServerReady(): Promise<void> {\n const maxAttempts = PDF_PARSER.MAX_HEALTH_CHECK_ATTEMPTS;\n const checkInterval = PDF_PARSER.HEALTH_CHECK_INTERVAL_MS;\n const logInterval = PDF_PARSER.HEALTH_CHECK_LOG_INTERVAL_MS;\n let lastLogTime = 0;\n\n for (let attempt = 1; attempt <= maxAttempts; attempt++) {\n try {\n await this.client!.health();\n this.logger.info('[PDFParser] Server is ready');\n return;\n } catch {\n const now = Date.now();\n if (now - lastLogTime >= logInterval) {\n this.logger.info(\n '[PDFParser] Waiting for server... (attempt',\n attempt,\n '/',\n maxAttempts,\n ')',\n );\n lastLogTime = now;\n }\n\n if (attempt < maxAttempts) {\n await new Promise((resolve) => setTimeout(resolve, checkInterval));\n }\n }\n }\n\n throw new Error('Server failed to become ready after maximum attempts');\n }\n\n public async parse(\n url: string,\n reportId: string,\n onComplete: ConversionCompleteCallback,\n cleanupAfterCallback: boolean,\n options: PDFConvertOptions,\n abortSignal?: AbortSignal,\n ): Promise<TokenUsageReport | null> {\n if (!this.client) {\n throw new Error(\n 'PDFParser is not initialized. Call init() before using parse()',\n );\n }\n\n // Check ImageMagick/Ghostscript for forceImagePdf (lazy check at parse time)\n if (options.forceImagePdf && !this.baseUrl) {\n this.checkImageMagickInstalled();\n this.checkGhostscriptInstalled();\n }\n\n // Use strategy-based flow when new options are provided\n const useStrategyFlow =\n options.strategySamplerModel !== undefined ||\n options.forcedMethod !== undefined;\n\n if (useStrategyFlow) {\n return this.parseWithStrategy(\n url,\n reportId,\n onComplete,\n cleanupAfterCallback,\n options,\n abortSignal,\n );\n }\n\n // Enable recovery only for local server mode\n const canRecover = !this.baseUrl && this.port !== undefined;\n const maxAttempts = PDF_PARSER.MAX_SERVER_RECOVERY_ATTEMPTS;\n let attempt = 0;\n\n while (attempt <= maxAttempts) {\n try {\n // Enable fallback only for local server mode\n const effectiveFallbackEnabled =\n this.enableImagePdfFallback && !this.baseUrl;\n const converter = new PDFConverter(\n this.logger,\n this.client,\n effectiveFallbackEnabled,\n this.timeout,\n );\n return await converter.convert(\n url,\n reportId,\n onComplete,\n cleanupAfterCallback,\n options,\n abortSignal,\n );\n } catch (error) {\n // If aborted, don't retry - re-throw immediately\n if (abortSignal?.aborted) {\n throw error;\n }\n\n // Attempt server recovery on ECONNREFUSED (server crashed)\n if (\n canRecover &&\n this.isConnectionRefusedError(error) &&\n attempt < maxAttempts\n ) {\n this.logger.warn(\n '[PDFParser] Connection refused, attempting server recovery...',\n );\n await this.restartServer();\n attempt++;\n continue;\n }\n throw error;\n }\n }\n\n /* v8 ignore start */\n return null;\n /* v8 ignore stop */\n }\n\n /**\n * Parse a PDF using OCR strategy sampling to decide between ocrmac and VLM.\n * Delegates to PDFConverter.convertWithStrategy() and returns the token usage report.\n *\n * Server recovery (restart on ECONNREFUSED) is preserved because\n * the ocrmac path still uses the Docling server.\n */\n private async parseWithStrategy(\n url: string,\n reportId: string,\n onComplete: ConversionCompleteCallback,\n cleanupAfterCallback: boolean,\n options: PDFConvertOptions,\n abortSignal?: AbortSignal,\n ): Promise<TokenUsageReport | null> {\n // Enable recovery only for local server mode\n const canRecover = !this.baseUrl && this.port !== undefined;\n const maxAttempts = PDF_PARSER.MAX_SERVER_RECOVERY_ATTEMPTS;\n let attempt = 0;\n\n while (attempt <= maxAttempts) {\n try {\n const effectiveFallbackEnabled =\n this.enableImagePdfFallback && !this.baseUrl;\n const converter = new PDFConverter(\n this.logger,\n this.client!,\n effectiveFallbackEnabled,\n this.timeout,\n );\n const result = await converter.convertWithStrategy(\n url,\n reportId,\n onComplete,\n cleanupAfterCallback,\n options,\n abortSignal,\n );\n return result.tokenUsageReport;\n } catch (error) {\n // If aborted, don't retry - re-throw immediately\n if (abortSignal?.aborted) {\n throw error;\n }\n\n // Attempt server recovery on ECONNREFUSED (server crashed)\n if (\n canRecover &&\n this.isConnectionRefusedError(error) &&\n attempt < maxAttempts\n ) {\n this.logger.warn(\n '[PDFParser] Connection refused, attempting server recovery...',\n );\n await this.restartServer();\n attempt++;\n continue;\n }\n throw error;\n }\n }\n\n /* v8 ignore start */\n return null;\n /* v8 ignore stop */\n }\n\n /**\n * Dispose the parser instance.\n * - Sets the internal client to null\n * - If a local docling server was started (no baseUrl), kills the process on the configured port\n */\n public async dispose(): Promise<void> {\n this.logger.info('[PDFParser] Disposing...');\n\n try {\n // Only manage local server lifecycle when we started it (i.e., no external baseUrl)\n if (!this.baseUrl && this.port) {\n await DoclingEnvironment.killProcessOnPort(this.logger, this.port);\n }\n } catch (error) {\n this.logger.error('[PDFParser] Error while disposing:', error);\n } finally {\n // Always clear the client reference\n this.client?.destroy();\n this.client = null;\n this.logger.info('[PDFParser] Disposed');\n }\n }\n}\n","/**\n * Configuration constants for PDFParser\n */\nexport const PDF_PARSER = {\n /**\n * Default timeout for API calls in milliseconds\n */\n DEFAULT_TIMEOUT_MS: 100000,\n\n /**\n * Maximum number of health check attempts before giving up\n */\n MAX_HEALTH_CHECK_ATTEMPTS: 60,\n\n /**\n * Interval between health check attempts in milliseconds\n */\n HEALTH_CHECK_INTERVAL_MS: 2000,\n\n /**\n * Interval between log messages during health check in milliseconds\n */\n HEALTH_CHECK_LOG_INTERVAL_MS: 5000,\n\n /**\n * Maximum retry attempts for server recovery on ECONNREFUSED\n */\n MAX_SERVER_RECOVERY_ATTEMPTS: 1,\n} as const;\n\n/**\n * Configuration constants for PDFConverter\n */\nexport const PDF_CONVERTER = {\n /**\n * Interval for progress polling in milliseconds\n */\n POLL_INTERVAL_MS: 1000,\n\n /**\n * Default timeout for task completion in milliseconds (30 minutes)\n */\n DEFAULT_TIMEOUT_MS: 1_800_000,\n} as const;\n\n/**\n * Configuration constants for DoclingEnvironment\n */\nexport const DOCLING_ENVIRONMENT = {\n /**\n * Delay after starting docling-serve to allow startup\n */\n STARTUP_DELAY_MS: 2000,\n} as const;\n\n/**\n * Configuration constants for page rendering DPI\n */\nexport const PAGE_RENDERING = {\n /** Default rendering DPI for VLM text recognition quality */\n DEFAULT_DPI: 200,\n /** Low-resolution DPI for OCR strategy sampling */\n SAMPLE_DPI: 150,\n} as const;\n\n/**\n * Configuration constants for ImagePdfConverter\n */\nexport const IMAGE_PDF_CONVERTER = {\n /**\n * ImageMagick density option (DPI) for PDF to image conversion\n */\n DENSITY: PAGE_RENDERING.DEFAULT_DPI,\n\n /**\n * ImageMagick quality option (1-100)\n */\n QUALITY: 100,\n} as const;\n","/**\n * BatchProcessor - Batch processing utility\n *\n * Provides functionality to split large arrays into batches for parallel processing.\n */\nexport class BatchProcessor {\n /**\n * Splits an array into batches of specified size.\n *\n * @param items - Array to split\n * @param batchSize - Size of each batch\n * @returns Array of batches\n *\n * @example\n * ```typescript\n * const items = [1, 2, 3, 4, 5];\n * const batches = BatchProcessor.createBatches(items, 2);\n * // [[1, 2], [3, 4], [5]]\n * ```\n */\n static createBatches<T>(items: T[], batchSize: number): T[][] {\n const batches: T[][] = [];\n for (let i = 0; i < items.length; i += batchSize) {\n batches.push(items.slice(i, i + batchSize));\n }\n return batches;\n }\n\n /**\n * Splits an array into batches and executes async function in parallel.\n *\n * @param items - Array to process\n * @param batchSize - Size of each batch\n * @param processFn - Async function to process each batch\n * @returns Flattened array of processed results\n *\n * @example\n * ```typescript\n * const texts = ['a', 'b', 'c', 'd', 'e'];\n * const results = await BatchProcessor.processBatch(\n * texts,\n * 2,\n * async (batch) => {\n * return batch.map(t => t.toUpperCase());\n * }\n * );\n * // ['A', 'B', 'C', 'D', 'E']\n * ```\n */\n static async processBatch<T, R>(\n items: T[],\n batchSize: number,\n processFn: (batch: T[]) => Promise<R[]>,\n ): Promise<R[]> {\n const batches = this.createBatches(items, batchSize);\n const results = await Promise.all(batches.map((batch) => processFn(batch)));\n return results.flat();\n }\n\n /**\n * Splits an array into batches and executes sync function in parallel.\n *\n * @param items - Array to process\n * @param batchSize - Size of each batch\n * @param processFn - Sync function to process each batch\n * @returns Flattened array of processed results\n *\n * @example\n * ```typescript\n * const numbers = [1, 2, 3, 4, 5];\n * const results = BatchProcessor.processBatchSync(\n * numbers,\n * 2,\n * (batch) => batch.map(n => n * 2)\n * );\n * // [2, 4, 6, 8, 10]\n * ```\n */\n static processBatchSync<T, R>(\n items: T[],\n batchSize: number,\n processFn: (batch: T[]) => R[],\n ): R[] {\n const batches = this.createBatches(items, batchSize);\n const results = batches.map((batch) => processFn(batch));\n return results.flat();\n }\n}\n","/**\n * ConcurrentPool - Worker pool utility for concurrent task execution.\n *\n * Unlike batch processing where all items in a batch must complete before\n * the next batch starts, the pool keeps N workers active at all times.\n * When a worker finishes, it immediately picks up the next available item.\n */\nexport class ConcurrentPool {\n /**\n * Process items concurrently using a worker pool pattern.\n *\n * Spawns up to `concurrency` workers that pull items from a shared queue.\n * Each worker processes one item at a time; when it finishes, it immediately\n * takes the next available item. Results maintain the original item order.\n *\n * @param items - Array of items to process\n * @param concurrency - Maximum number of concurrent workers\n * @param processFn - Async function to process each item\n * @param onItemComplete - Optional callback fired after each item completes\n * @returns Array of results in the same order as the input items\n */\n static async run<T, R>(\n items: T[],\n concurrency: number,\n processFn: (item: T, index: number) => Promise<R>,\n onItemComplete?: (result: R, index: number) => void,\n ): Promise<R[]> {\n const results: R[] = new Array(items.length);\n let nextIndex = 0;\n\n async function worker(): Promise<void> {\n while (nextIndex < items.length) {\n const index = nextIndex++;\n results[index] = await processFn(items[index], index);\n onItemComplete?.(results[index], index);\n }\n }\n\n const workers = Array.from(\n { length: Math.min(concurrency, items.length) },\n () => worker(),\n );\n await Promise.all(workers);\n return results;\n }\n}\n","import type { SpawnOptions } from 'node:child_process';\n\nimport { spawn } from 'node:child_process';\n\n/**\n * Result of a spawn operation\n */\nexport interface SpawnResult {\n stdout: string;\n stderr: string;\n code: number;\n}\n\n/**\n * Extended spawn options with output capture control\n */\nexport interface SpawnAsyncOptions extends SpawnOptions {\n /**\n * Whether to capture stdout (default: true)\n */\n captureStdout?: boolean;\n\n /**\n * Whether to capture stderr (default: true)\n */\n captureStderr?: boolean;\n}\n\n/**\n * Execute a command asynchronously and return the result\n *\n * Eliminates the repetitive Promise wrapper pattern used throughout\n * DoclingEnvironment for spawn operations.\n *\n * @param command - The command to execute\n * @param args - Arguments to pass to the command\n * @param options - Spawn options with optional output capture control\n * @returns Promise resolving to stdout, stderr, and exit code\n *\n * @example\n * ```typescript\n * // Simple usage\n * const result = await spawnAsync('python3', ['--version']);\n * console.log(result.stdout); // \"Python 3.12.0\"\n *\n * // With options\n * const result = await spawnAsync('pip', ['install', 'package'], {\n * cwd: '/path/to/venv',\n * captureStderr: true,\n * });\n * ```\n */\nexport function spawnAsync(\n command: string,\n args: string[],\n options: SpawnAsyncOptions = {},\n): Promise<SpawnResult> {\n const {\n captureStdout = true,\n captureStderr = true,\n ...spawnOptions\n } = options;\n\n return new Promise((resolve, reject) => {\n const proc = spawn(command, args, spawnOptions);\n\n let stdout = '';\n let stderr = '';\n\n if (captureStdout && proc.stdout) {\n proc.stdout.on('data', (data) => {\n stdout += data.toString();\n });\n }\n\n if (captureStderr && proc.stderr) {\n proc.stderr.on('data', (data) => {\n stderr += data.toString();\n });\n }\n\n proc.on('close', (code) => {\n resolve({ stdout, stderr, code: code ?? 0 });\n });\n\n proc.on('error', reject);\n });\n}\n","import type { z } from 'zod';\n\nimport {\n type LanguageModel,\n NoObjectGeneratedError,\n Output,\n generateText,\n hasToolCall,\n tool,\n} from 'ai';\n\nimport { detectProvider } from './provider-detector';\n\n/**\n * Configuration for LLM API call with retry and fallback support\n */\nexport interface LLMCallConfig<TSchema extends z.ZodType> {\n /**\n * Zod schema for response validation\n */\n schema: TSchema;\n\n /**\n * System prompt for LLM\n */\n systemPrompt: string;\n\n /**\n * User prompt for LLM\n */\n userPrompt: string;\n\n /**\n * Primary model for the call (required)\n */\n primaryModel: LanguageModel;\n\n /**\n * Fallback model for retry after primary model exhausts maxRetries (optional)\n */\n fallbackModel?: LanguageModel;\n\n /**\n * Maximum retry count per model (default: 3)\n */\n maxRetries: number;\n\n /**\n * Temperature for generation (optional, 0-1)\n */\n temperature?: number;\n\n /**\n * Abort signal for cancellation support\n */\n abortSignal?: AbortSignal;\n\n /**\n * Component name for tracking (e.g., 'TocExtractor', 'PageRangeParser')\n */\n component: string;\n\n /**\n * Phase name for tracking (e.g., 'extraction', 'validation', 'sampling')\n */\n phase: string;\n}\n\n/**\n * Configuration for LLM vision call with message format\n */\nexport interface LLMVisionCallConfig<TSchema extends z.ZodType> {\n /**\n * Zod schema for response validation\n */\n schema: TSchema;\n\n /**\n * Messages array for vision LLM (instead of systemPrompt/userPrompt)\n */\n messages: Array<{ role: 'user' | 'assistant'; content: any[] | string }>;\n\n /**\n * Primary model for the call (required)\n */\n primaryModel: LanguageModel;\n\n /**\n * Fallback model for retry after primary model exhausts maxRetries (optional)\n */\n fallbackModel?: LanguageModel;\n\n /**\n * Maximum retry count per model (default: 3)\n */\n maxRetries: number;\n\n /**\n * Temperature for generation (optional, 0-1)\n */\n temperature?: number;\n\n /**\n * Abort signal for cancellation support\n */\n abortSignal?: AbortSignal;\n\n /**\n * Component name for tracking (e.g., 'TocExtractor', 'PageRangeParser')\n */\n component: string;\n\n /**\n * Phase name for tracking (e.g., 'extraction', 'validation', 'sampling')\n */\n phase: string;\n}\n\n/**\n * Token usage information with model tracking\n */\nexport interface ExtendedTokenUsage {\n component: string;\n phase: string;\n model: 'primary' | 'fallback';\n modelName: string;\n inputTokens: number;\n outputTokens: number;\n totalTokens: number;\n}\n\n/**\n * Result of LLM call including usage information\n */\nexport interface LLMCallResult<T> {\n output: T;\n usage: ExtendedTokenUsage;\n usedFallback: boolean;\n}\n\n/**\n * Base execution configuration for LLM calls\n */\ninterface ExecutionConfig {\n primaryModel: LanguageModel;\n fallbackModel?: LanguageModel;\n abortSignal?: AbortSignal;\n component: string;\n phase: string;\n}\n\n/**\n * LLMCaller - Centralized LLM API caller with retry and fallback support\n *\n * Wraps AI SDK's generateText with enhanced retry strategy:\n * 1. Try primary model with maxRetries\n * 2. If all attempts fail and fallbackModel provided, try fallback with maxRetries\n * 3. Return usage data with model type indicator\n *\n * @example\n * ```typescript\n * const result = await LLMCaller.call({\n * schema: MyZodSchema,\n * systemPrompt: 'You are a helpful assistant',\n * userPrompt: 'Extract the TOC from this markdown',\n * primaryModel: openai('gpt-5'),\n * fallbackModel: anthropic('claude-opus-4-5'),\n * maxRetries: 3,\n * component: 'TocExtractor',\n * phase: 'extraction',\n * });\n *\n * console.log(result.output); // Parsed result\n * console.log(result.usage); // Token usage with model info\n * console.log(result.usedFallback); // Whether fallback was used\n * ```\n */\nexport class LLMCaller {\n /**\n * Extract model name from LanguageModel object\n *\n * Attempts to get model ID from various possible fields in the LanguageModel object.\n */\n private static extractModelName(model: LanguageModel): string {\n const modelObj = model as Record<string, unknown>;\n\n // Try common field names\n if (typeof modelObj.modelId === 'string') return modelObj.modelId;\n if (typeof modelObj.id === 'string') return modelObj.id;\n if (typeof modelObj.model === 'string') return modelObj.model;\n if (typeof modelObj.name === 'string') return modelObj.name;\n\n // Fallback: return object representation\n return String(model);\n }\n\n /**\n * Build usage information from response\n */\n private static buildUsage(\n config: ExecutionConfig,\n modelName: string,\n response: {\n usage?: {\n inputTokens?: number;\n outputTokens?: number;\n totalTokens?: number;\n };\n },\n usedFallback: boolean,\n ): ExtendedTokenUsage {\n return {\n component: config.component,\n phase: config.phase,\n model: usedFallback ? 'fallback' : 'primary',\n modelName,\n inputTokens: response.usage?.inputTokens ?? 0,\n outputTokens: response.usage?.outputTokens ?? 0,\n totalTokens: response.usage?.totalTokens ?? 0,\n };\n }\n\n /**\n * Maximum number of retries when structured output generation fails.\n * Total attempts = MAX_STRUCTURED_OUTPUT_RETRIES + 1.\n *\n * Applied to both:\n * - `Output.object()` path: retries on NoObjectGeneratedError (schema mismatch)\n * - Tool call path: retries when model does not produce a tool call\n */\n private static readonly MAX_STRUCTURED_OUTPUT_RETRIES = 10;\n\n /**\n * Generate structured output via forced tool call.\n *\n * Used for providers (Together AI, unknown) that do not reliably support\n * `Output.object()`. Forces the model to call a tool whose inputSchema\n * is the target Zod schema, then extracts the parsed input.\n *\n * Retries up to MAX_STRUCTURED_OUTPUT_RETRIES times when the model does not\n * produce a tool call, for a total of MAX_STRUCTURED_OUTPUT_RETRIES + 1 attempts.\n *\n * @throws NoObjectGeneratedError when all attempts fail to produce a tool call\n */\n private static async generateViaToolCall<TOutput>(\n model: LanguageModel,\n schema: z.ZodType<TOutput>,\n promptParams: Record<string, unknown>,\n ): Promise<{\n output: TOutput;\n usage?: {\n inputTokens?: number;\n outputTokens?: number;\n totalTokens?: number;\n };\n }> {\n const submitTool = tool({\n description: 'Submit the structured result',\n inputSchema: schema,\n });\n\n let lastResult: any;\n\n for (\n let attempt = 0;\n attempt <= this.MAX_STRUCTURED_OUTPUT_RETRIES;\n attempt++\n ) {\n lastResult = await (generateText as any)({\n ...promptParams,\n model,\n tools: { submitResult: submitTool },\n toolChoice: { type: 'tool', toolName: 'submitResult' },\n stopWhen: hasToolCall('submitResult'),\n });\n\n const toolCall = lastResult.toolCalls?.[0] as\n | { input: unknown }\n | undefined;\n if (toolCall) {\n return {\n output: toolCall.input as TOutput,\n usage: lastResult.usage,\n };\n }\n }\n\n throw new NoObjectGeneratedError({\n message: 'Model did not produce a tool call for structured output',\n text: lastResult.text ?? '',\n response: lastResult.response,\n usage: lastResult.usage,\n finishReason: lastResult.finishReason,\n });\n }\n\n /**\n * Generate structured output with provider-aware strategy.\n *\n * Strategy per provider:\n * - OpenAI / Anthropic / Google Gemini: `Output.object()` with schema retry\n * - Together AI / unknown: forced tool call pattern\n *\n * Retries up to MAX_STRUCTURED_OUTPUT_RETRIES times on NoObjectGeneratedError\n * (schema mismatch), re-throwing the last error if all attempts fail.\n */\n private static async generateStructuredOutput<TOutput>(\n model: LanguageModel,\n schema: z.ZodType<TOutput>,\n promptParams: Record<string, unknown>,\n ): Promise<{\n output: TOutput;\n usage?: {\n inputTokens?: number;\n outputTokens?: number;\n totalTokens?: number;\n };\n }> {\n const providerType = detectProvider(model);\n\n if (providerType === 'togetherai' || providerType === 'unknown') {\n return this.generateViaToolCall(model, schema, promptParams);\n }\n\n let lastError: unknown;\n\n for (\n let attempt = 0;\n attempt <= this.MAX_STRUCTURED_OUTPUT_RETRIES;\n attempt++\n ) {\n try {\n return await (generateText as any)({\n model,\n output: Output.object({ schema }),\n ...promptParams,\n });\n } catch (error) {\n if (NoObjectGeneratedError.isInstance(error)) {\n lastError = error;\n continue;\n }\n throw error;\n }\n }\n\n throw lastError;\n }\n\n /**\n * Execute LLM call with fallback support\n *\n * Common execution logic for both text and vision calls.\n * Logs additional details when NoObjectGeneratedError occurs.\n */\n private static async executeWithFallback<TOutput>(\n config: ExecutionConfig,\n generateFn: (model: LanguageModel) => Promise<{\n output: TOutput;\n usage?: {\n inputTokens?: number;\n outputTokens?: number;\n totalTokens?: number;\n };\n }>,\n ): Promise<LLMCallResult<TOutput>> {\n const primaryModelName = this.extractModelName(config.primaryModel);\n\n // Attempt 1: Try primary model\n try {\n const response = await generateFn(config.primaryModel);\n\n return {\n output: response.output,\n usage: this.buildUsage(config, primaryModelName, response, false),\n usedFallback: false,\n };\n } catch (primaryError) {\n // If aborted, don't try fallback - re-throw immediately\n if (config.abortSignal?.aborted) {\n throw primaryError;\n }\n\n // If no fallback model, throw immediately\n if (!config.fallbackModel) {\n throw primaryError;\n }\n\n // Attempt 2: Try fallback model\n const fallbackModelName = this.extractModelName(config.fallbackModel);\n const response = await generateFn(config.fallbackModel);\n\n return {\n output: response.output,\n usage: this.buildUsage(config, fallbackModelName, response, true),\n usedFallback: true,\n };\n }\n }\n\n /**\n * Call LLM with retry and fallback support\n *\n * Retry Strategy:\n * 1. Try primary model up to maxRetries times\n * 2. If all fail and fallbackModel provided, try fallback up to maxRetries times\n * 3. Throw error if all attempts exhausted\n *\n * Provider-aware strategy is automatically applied based on the model's provider field.\n *\n * @template TOutput - Output type from schema validation\n * @param config - LLM call configuration\n * @returns Result with parsed object and usage information\n * @throws Error if all retry attempts fail\n */\n static async call<TOutput = unknown>(\n config: LLMCallConfig<z.ZodType<TOutput>>,\n ): Promise<LLMCallResult<TOutput>> {\n return this.executeWithFallback(config, (model) =>\n this.generateStructuredOutput(model, config.schema, {\n system: config.systemPrompt,\n prompt: config.userPrompt,\n temperature: config.temperature,\n maxRetries: config.maxRetries,\n abortSignal: config.abortSignal,\n }),\n );\n }\n\n /**\n * Call LLM for vision tasks with message format support\n *\n * Same retry and fallback logic as call(), but using message format instead of system/user prompts.\n * Provider-aware strategy is automatically applied based on the model's provider field.\n *\n * @template TOutput - Output type from schema validation\n * @param config - LLM vision call configuration\n * @returns Result with parsed object and usage information\n * @throws Error if all retry attempts fail\n */\n static async callVision<TOutput = unknown>(\n config: LLMVisionCallConfig<z.ZodType<TOutput>>,\n ): Promise<LLMCallResult<TOutput>> {\n return this.executeWithFallback(config, (model) =>\n this.generateStructuredOutput(model, config.schema, {\n messages: config.messages,\n temperature: config.temperature,\n maxRetries: config.maxRetries,\n abortSignal: config.abortSignal,\n }),\n );\n }\n}\n","import type { LanguageModel } from 'ai';\n\nexport type ProviderType =\n | 'openai'\n | 'google'\n | 'anthropic'\n | 'togetherai'\n | 'unknown';\n\n/**\n * Detect the provider type from a LanguageModel instance.\n *\n * Reads the `provider` field of the model object and matches it against\n * known provider identifiers. Falls back to 'unknown' if unrecognized.\n */\nexport function detectProvider(model: LanguageModel): ProviderType {\n const providerId = (model as { provider?: string }).provider;\n if (!providerId || typeof providerId !== 'string') return 'unknown';\n\n if (providerId.includes('openai')) return 'openai';\n if (providerId.includes('google')) return 'google';\n if (providerId.includes('anthropic')) return 'anthropic';\n if (providerId.includes('together')) return 'togetherai';\n\n return 'unknown';\n}\n","import type { LoggerMethods } from '@heripo/logger';\n\nimport type { ExtendedTokenUsage } from './llm-caller';\n\n/**\n * Token usage totals\n */\nexport interface TokenUsage {\n inputTokens: number;\n outputTokens: number;\n totalTokens: number;\n}\n\n/**\n * Format token usage as a human-readable string\n *\n * @param usage - Token usage object with input, output, and total counts\n * @returns Formatted string like \"1500 input, 300 output, 1800 total\"\n */\nfunction formatTokens(usage: TokenUsage): string {\n return `${usage.inputTokens} input, ${usage.outputTokens} output, ${usage.totalTokens} total`;\n}\n\n/**\n * Aggregated token usage for a specific component\n */\ninterface ComponentAggregate {\n component: string;\n phases: Record<\n string,\n {\n primary?: {\n modelName: string;\n inputTokens: number;\n outputTokens: number;\n totalTokens: number;\n };\n fallback?: {\n modelName: string;\n inputTokens: number;\n outputTokens: number;\n totalTokens: number;\n };\n total: {\n inputTokens: number;\n outputTokens: number;\n totalTokens: number;\n };\n }\n >;\n total: {\n inputTokens: number;\n outputTokens: number;\n totalTokens: number;\n };\n}\n\n/**\n * LLMTokenUsageAggregator - Aggregates token usage across all LLM calls\n *\n * Unlike LLMTokenUsageTracker which logs immediately after each component,\n * this aggregator collects usage data from all components and logs a comprehensive\n * summary at the end of document processing.\n *\n * Tracks usage by:\n * - Component (TocExtractor, PageRangeParser, etc.)\n * - Phase (extraction, validation, sampling, etc.)\n * - Model (primary vs fallback)\n *\n * @example\n * ```typescript\n * const aggregator = new LLMTokenUsageAggregator();\n *\n * // Track usage from each LLM call\n * aggregator.track({\n * component: 'TocExtractor',\n * phase: 'extraction',\n * model: 'primary',\n * modelName: 'gpt-5',\n * inputTokens: 1500,\n * outputTokens: 300,\n * totalTokens: 1800,\n * });\n *\n * aggregator.track({\n * component: 'PageRangeParser',\n * phase: 'sampling',\n * model: 'fallback',\n * modelName: 'claude-opus-4-5',\n * inputTokens: 2000,\n * outputTokens: 100,\n * totalTokens: 2100,\n * });\n *\n * // Log comprehensive summary\n * aggregator.logSummary(logger);\n * // Outputs:\n * // [DocumentProcessor] Token usage summary:\n * // TocExtractor:\n * // - extraction (primary: gpt-5): 1500 input, 300 output, 1800 total\n * // TocExtractor total: 1500 input, 300 output, 1800 total\n * // PageRangeParser:\n * // - sampling (fallback: claude-opus-4-5): 2000 input, 100 output, 2100 total\n * // PageRangeParser total: 2000 input, 100 output, 2100 total\n * // Grand total: 3500 input, 400 output, 3900 total\n * ```\n */\nexport class LLMTokenUsageAggregator {\n private usage: Record<string, ComponentAggregate> = {};\n\n /**\n * Track token usage from an LLM call\n *\n * @param usage - Extended token usage with component/phase/model information\n */\n track(usage: ExtendedTokenUsage): void {\n // Initialize component if not seen before\n if (!this.usage[usage.component]) {\n this.usage[usage.component] = {\n component: usage.component,\n phases: {},\n total: {\n inputTokens: 0,\n outputTokens: 0,\n totalTokens: 0,\n },\n };\n }\n\n const component = this.usage[usage.component];\n\n // Initialize phase if not seen before\n if (!component.phases[usage.phase]) {\n component.phases[usage.phase] = {\n total: {\n inputTokens: 0,\n outputTokens: 0,\n totalTokens: 0,\n },\n };\n }\n\n const phase = component.phases[usage.phase];\n\n // Track by model type\n if (usage.model === 'primary') {\n if (!phase.primary) {\n phase.primary = {\n modelName: usage.modelName,\n inputTokens: 0,\n outputTokens: 0,\n totalTokens: 0,\n };\n }\n\n phase.primary.inputTokens += usage.inputTokens;\n phase.primary.outputTokens += usage.outputTokens;\n phase.primary.totalTokens += usage.totalTokens;\n } else if (usage.model === 'fallback') {\n if (!phase.fallback) {\n phase.fallback = {\n modelName: usage.modelName,\n inputTokens: 0,\n outputTokens: 0,\n totalTokens: 0,\n };\n }\n\n phase.fallback.inputTokens += usage.inputTokens;\n phase.fallback.outputTokens += usage.outputTokens;\n phase.fallback.totalTokens += usage.totalTokens;\n }\n\n // Update phase total\n phase.total.inputTokens += usage.inputTokens;\n phase.total.outputTokens += usage.outputTokens;\n phase.total.totalTokens += usage.totalTokens;\n\n // Update component total\n component.total.inputTokens += usage.inputTokens;\n component.total.outputTokens += usage.outputTokens;\n component.total.totalTokens += usage.totalTokens;\n }\n\n /**\n * Get aggregated usage grouped by component\n *\n * @returns Array of component aggregates with phase breakdown\n */\n getByComponent(): ComponentAggregate[] {\n return Object.values(this.usage);\n }\n\n /**\n * Get token usage report in structured JSON format\n *\n * Converts internal usage data to external TokenUsageReport format suitable\n * for serialization and reporting. The report includes component breakdown,\n * phase-level details, and both primary and fallback model usage.\n *\n * @returns Structured token usage report with components and total\n */\n getReport(): {\n components: Array<{\n component: string;\n phases: Array<{\n phase: string;\n primary?: {\n modelName: string;\n inputTokens: number;\n outputTokens: number;\n totalTokens: number;\n };\n fallback?: {\n modelName: string;\n inputTokens: number;\n outputTokens: number;\n totalTokens: number;\n };\n total: {\n inputTokens: number;\n outputTokens: number;\n totalTokens: number;\n };\n }>;\n total: {\n inputTokens: number;\n outputTokens: number;\n totalTokens: number;\n };\n }>;\n total: TokenUsage;\n } {\n const components: Array<{\n component: string;\n phases: Array<{\n phase: string;\n primary?: {\n modelName: string;\n inputTokens: number;\n outputTokens: number;\n totalTokens: number;\n };\n fallback?: {\n modelName: string;\n inputTokens: number;\n outputTokens: number;\n totalTokens: number;\n };\n total: {\n inputTokens: number;\n outputTokens: number;\n totalTokens: number;\n };\n }>;\n total: {\n inputTokens: number;\n outputTokens: number;\n totalTokens: number;\n };\n }> = [];\n\n for (const component of Object.values(this.usage)) {\n const phases: Array<{\n phase: string;\n primary?: {\n modelName: string;\n inputTokens: number;\n outputTokens: number;\n totalTokens: number;\n };\n fallback?: {\n modelName: string;\n inputTokens: number;\n outputTokens: number;\n totalTokens: number;\n };\n total: {\n inputTokens: number;\n outputTokens: number;\n totalTokens: number;\n };\n }> = [];\n\n for (const [phaseName, phaseData] of Object.entries(component.phases)) {\n const phaseReport: {\n phase: string;\n primary?: {\n modelName: string;\n inputTokens: number;\n outputTokens: number;\n totalTokens: number;\n };\n fallback?: {\n modelName: string;\n inputTokens: number;\n outputTokens: number;\n totalTokens: number;\n };\n total: {\n inputTokens: number;\n outputTokens: number;\n totalTokens: number;\n };\n } = {\n phase: phaseName,\n total: {\n inputTokens: phaseData.total.inputTokens,\n outputTokens: phaseData.total.outputTokens,\n totalTokens: phaseData.total.totalTokens,\n },\n };\n\n if (phaseData.primary) {\n phaseReport.primary = {\n modelName: phaseData.primary.modelName,\n inputTokens: phaseData.primary.inputTokens,\n outputTokens: phaseData.primary.outputTokens,\n totalTokens: phaseData.primary.totalTokens,\n };\n }\n\n if (phaseData.fallback) {\n phaseReport.fallback = {\n modelName: phaseData.fallback.modelName,\n inputTokens: phaseData.fallback.inputTokens,\n outputTokens: phaseData.fallback.outputTokens,\n totalTokens: phaseData.fallback.totalTokens,\n };\n }\n\n phases.push(phaseReport);\n }\n\n components.push({\n component: component.component,\n phases,\n total: {\n inputTokens: component.total.inputTokens,\n outputTokens: component.total.outputTokens,\n totalTokens: component.total.totalTokens,\n },\n });\n }\n\n const totalUsage = this.getTotalUsage();\n\n return {\n components,\n total: {\n inputTokens: totalUsage.inputTokens,\n outputTokens: totalUsage.outputTokens,\n totalTokens: totalUsage.totalTokens,\n },\n };\n }\n\n /**\n * Get total usage across all components and phases\n *\n * @returns Aggregated token usage totals\n */\n getTotalUsage(): TokenUsage {\n let totalInput = 0;\n let totalOutput = 0;\n let totalTokens = 0;\n\n for (const component of Object.values(this.usage)) {\n totalInput += component.total.inputTokens;\n totalOutput += component.total.outputTokens;\n totalTokens += component.total.totalTokens;\n }\n\n return {\n inputTokens: totalInput,\n outputTokens: totalOutput,\n totalTokens: totalTokens,\n };\n }\n\n /**\n * Log comprehensive token usage summary\n *\n * Outputs usage grouped by component, with phase and model breakdown.\n * Shows primary and fallback token usage separately for each phase.\n * Call this once at the end of document processing.\n *\n * @param logger - Logger instance for output\n */\n logSummary(logger: LoggerMethods): void {\n const components = this.getByComponent();\n\n if (components.length === 0) {\n logger.info('[DocumentProcessor] No token usage to report');\n return;\n }\n\n logger.info('[DocumentProcessor] Token usage summary:');\n logger.info('');\n\n let grandInputTokens = 0;\n let grandOutputTokens = 0;\n let grandTotalTokens = 0;\n let grandPrimaryInputTokens = 0;\n let grandPrimaryOutputTokens = 0;\n let grandPrimaryTotalTokens = 0;\n let grandFallbackInputTokens = 0;\n let grandFallbackOutputTokens = 0;\n let grandFallbackTotalTokens = 0;\n\n for (const component of components) {\n logger.info(`${component.component}:`);\n\n for (const [phase, phaseData] of Object.entries(component.phases)) {\n logger.info(` - ${phase}:`);\n\n // Show primary model usage\n if (phaseData.primary) {\n logger.info(\n ` primary (${phaseData.primary.modelName}): ${formatTokens(phaseData.primary)}`,\n );\n grandPrimaryInputTokens += phaseData.primary.inputTokens;\n grandPrimaryOutputTokens += phaseData.primary.outputTokens;\n grandPrimaryTotalTokens += phaseData.primary.totalTokens;\n }\n\n // Show fallback model usage\n if (phaseData.fallback) {\n logger.info(\n ` fallback (${phaseData.fallback.modelName}): ${formatTokens(phaseData.fallback)}`,\n );\n grandFallbackInputTokens += phaseData.fallback.inputTokens;\n grandFallbackOutputTokens += phaseData.fallback.outputTokens;\n grandFallbackTotalTokens += phaseData.fallback.totalTokens;\n }\n\n // Show phase subtotal\n logger.info(` subtotal: ${formatTokens(phaseData.total)}`);\n }\n\n logger.info(\n ` ${component.component} total: ${formatTokens(component.total)}`,\n );\n logger.info('');\n\n grandInputTokens += component.total.inputTokens;\n grandOutputTokens += component.total.outputTokens;\n grandTotalTokens += component.total.totalTokens;\n }\n\n // Show grand total with primary/fallback breakdown\n logger.info('--- Summary ---');\n if (grandPrimaryTotalTokens > 0) {\n logger.info(\n `Primary total: ${formatTokens({\n inputTokens: grandPrimaryInputTokens,\n outputTokens: grandPrimaryOutputTokens,\n totalTokens: grandPrimaryTotalTokens,\n })}`,\n );\n }\n if (grandFallbackTotalTokens > 0) {\n logger.info(\n `Fallback total: ${formatTokens({\n inputTokens: grandFallbackInputTokens,\n outputTokens: grandFallbackOutputTokens,\n totalTokens: grandFallbackTotalTokens,\n })}`,\n );\n }\n logger.info(\n `Grand total: ${formatTokens({\n inputTokens: grandInputTokens,\n outputTokens: grandOutputTokens,\n totalTokens: grandTotalTokens,\n })}`,\n );\n }\n\n /**\n * Reset all tracked usage\n *\n * Call this at the start of a new document processing run.\n */\n reset(): void {\n this.usage = {};\n }\n}\n","import type { LoggerMethods } from '@heripo/logger';\n\nimport { spawnAsync } from '@heripo/shared';\nimport { spawn } from 'node:child_process';\nimport { join } from 'node:path';\n\nimport { DOCLING_ENVIRONMENT } from '../config/constants';\nimport {\n PythonVersionError,\n type PythonVersionInfo,\n parsePythonVersion,\n validatePythonVersion,\n} from '../utils/python-version';\n\nexport class DoclingEnvironment {\n private readonly logger: LoggerMethods;\n private readonly venvPath: string;\n private readonly port: number;\n private readonly killExistingProcess: boolean;\n\n constructor(options: {\n logger: LoggerMethods;\n venvPath: string;\n port: number;\n killExistingProcess: boolean;\n }) {\n this.logger = options.logger;\n this.venvPath = options.venvPath;\n this.port = options.port;\n this.killExistingProcess = options.killExistingProcess;\n }\n\n async setup(): Promise<void> {\n this.logger.info('[DoclingEnvironment] Setting up Python environment...');\n\n await this.checkPythonVersion();\n await this.setupPythonEnvironment();\n await this.upgradePip();\n await this.installSetuptools();\n await this.installPyArrow();\n await this.installDoclingServe();\n\n // Check if server is already running\n const portInUse = await this.isPortInUse(this.port);\n\n if (portInUse && !this.killExistingProcess) {\n this.logger.info(\n '[DoclingEnvironment] Reusing existing server on port',\n this.port,\n );\n } else {\n await this.startDoclingServe();\n }\n\n this.logger.info('[DoclingEnvironment] Setup completed');\n }\n\n private async checkPythonVersion(): Promise<PythonVersionInfo> {\n const result = await spawnAsync('python3', ['--version']);\n\n if (result.code !== 0) {\n throw new Error('Failed to check Python version');\n }\n\n const output = result.stdout + result.stderr;\n const version = parsePythonVersion(output);\n\n if (!version) {\n throw new Error('Could not parse Python version');\n }\n\n this.logger.info(\n '[DoclingEnvironment] Python version:',\n version.versionString,\n );\n\n try {\n validatePythonVersion(version, 'system');\n } catch (error) {\n if (error instanceof PythonVersionError && version.minor >= 13) {\n this.logger.error(\n '[DoclingEnvironment] Python 3.13+ is not compatible. Install 3.11 or 3.12 with: pyenv install 3.12.0 && pyenv global 3.12.0',\n );\n }\n throw error;\n }\n\n return version;\n }\n\n private async setupPythonEnvironment(): Promise<void> {\n const result = await spawnAsync('python3', ['-m', 'venv', this.venvPath]);\n\n if (result.code !== 0) {\n throw new Error('Failed to create Python virtual environment');\n }\n\n await this.verifyVenvPythonVersion();\n }\n\n private async verifyVenvPythonVersion(): Promise<void> {\n const pythonPath = join(this.venvPath, 'bin', 'python');\n const result = await spawnAsync(pythonPath, ['--version']);\n\n if (result.code !== 0) {\n throw new Error('Failed to verify venv Python version');\n }\n\n const output = result.stdout + result.stderr;\n const version = parsePythonVersion(output);\n\n if (!version) {\n throw new Error('Could not parse venv Python version');\n }\n\n validatePythonVersion(version, 'venv');\n }\n\n private async upgradePip(): Promise<void> {\n const pipPath = join(this.venvPath, 'bin', 'pip');\n const result = await spawnAsync(pipPath, ['install', '--upgrade', 'pip']);\n\n if (result.code !== 0) {\n this.logger.error(\n '[DoclingEnvironment] Failed to upgrade pip:',\n result.stderr,\n );\n throw new Error(`Failed to upgrade pip. Exit code: ${result.code}`);\n }\n }\n\n private async installSetuptools(): Promise<void> {\n const pipPath = join(this.venvPath, 'bin', 'pip');\n const result = await spawnAsync(pipPath, [\n 'install',\n '--upgrade',\n 'setuptools',\n 'wheel',\n ]);\n\n if (result.code !== 0) {\n this.logger.error(\n '[DoclingEnvironment] Failed to install setuptools:',\n result.stderr,\n );\n throw new Error(\n `Failed to install setuptools. Exit code: ${result.code}`,\n );\n }\n }\n\n private async installPyArrow(): Promise<void> {\n const pipPath = join(this.venvPath, 'bin', 'pip');\n const result = await spawnAsync(pipPath, [\n 'install',\n '--only-binary',\n ':all:',\n 'pyarrow',\n ]);\n\n if (result.code !== 0) {\n this.logger.error(\n '[DoclingEnvironment] Failed to install pyarrow:',\n result.stderr,\n );\n throw new Error(`Failed to install pyarrow. Exit code: ${result.code}`);\n }\n }\n\n private async installDoclingServe(): Promise<void> {\n const pipPath = join(this.venvPath, 'bin', 'pip');\n const result = await spawnAsync(pipPath, [\n 'install',\n '--upgrade',\n 'docling-serve',\n ]);\n\n if (result.code !== 0) {\n this.logger.error(\n '[DoclingEnvironment] Failed to install docling-serve:',\n result.stderr,\n );\n throw new Error(\n `Failed to install docling-serve. Exit code: ${result.code}`,\n );\n }\n }\n\n private async isPortInUse(port: number): Promise<boolean> {\n try {\n const result = await spawnAsync('lsof', ['-ti', `:${port}`]);\n return result.code === 0 && !!result.stdout.trim();\n } catch {\n return false;\n }\n }\n\n /**\n * Start the docling-serve server without running full setup.\n * Useful for restarting the server after it has crashed.\n */\n public async startServer(): Promise<void> {\n await this.startDoclingServe();\n }\n\n // Process-killing logic is provided as a static method to allow reuse without instantiation\n public static async killProcessOnPort(\n logger: LoggerMethods,\n port: number,\n ): Promise<void> {\n return new Promise((resolve) => {\n const lsof = spawn('lsof', ['-ti', `:${port}`]);\n const pids: string[] = [];\n\n lsof.stdout?.on('data', (data) => {\n const txt: string = data.toString();\n pids.push(\n ...txt\n .split(/\\s+/)\n .map((s) => s.trim())\n .filter(Boolean),\n );\n });\n\n lsof.on('close', () => {\n if (pids.length === 0) return resolve();\n\n let remaining = pids.length;\n const done = () => {\n if (--remaining <= 0) resolve();\n };\n\n logger.info(\n '[DoclingEnvironment] Killing process',\n pids.join(', '),\n 'on port',\n port,\n );\n for (const pid of pids) {\n const killProc = spawn('kill', ['-9', pid]);\n\n killProc.on('close', (killCode) => {\n if (killCode !== 0) {\n logger.info('[DoclingEnvironment] Failed to kill process', pid);\n }\n done();\n });\n killProc.on('error', (Error) => {\n logger.info('[DoclingEnvironment] Failed to kill process', Error);\n done();\n });\n }\n });\n\n lsof.on('error', () => resolve());\n });\n }\n\n private async startDoclingServe(): Promise<void> {\n return new Promise(async (resolve, reject) => {\n // Kill any existing process on the port if option is enabled\n if (this.killExistingProcess) {\n await DoclingEnvironment.killProcessOnPort(this.logger, this.port);\n }\n\n const venvPath = this.venvPath;\n const doclingServePath = join(venvPath, 'bin', 'docling-serve');\n const args = ['run', '--port', this.port.toString()];\n\n this.logger.info(\n '[DoclingEnvironment] Starting docling-serve on port',\n this.port,\n );\n const doclingProcess = spawn(doclingServePath, args, {\n detached: true, // Detached from parent process\n stdio: 'ignore', // Remove stdio pipes to prevent event loop from hanging\n env: {\n ...process.env,\n // Enable remote API calls for API VLM models\n DOCLING_SERVE_ENABLE_REMOTE_SERVICES: 'true',\n },\n });\n\n doclingProcess.unref(); // Parent doesn't wait for child process to exit\n\n doclingProcess.on('error', (error) => {\n this.logger.error('[DoclingEnvironment] docling-serve error:', error);\n reject(error);\n });\n\n // Give docling-serve time to start\n setTimeout(() => {\n resolve();\n }, DOCLING_ENVIRONMENT.STARTUP_DELAY_MS);\n });\n }\n}\n","/**\n * Python version information\n */\nexport interface PythonVersionInfo {\n major: number;\n minor: number;\n versionString: string;\n}\n\n/**\n * Regex pattern to extract Python version from command output\n */\nexport const PYTHON_VERSION_REGEX = /Python (\\d+)\\.(\\d+)/;\n\n/**\n * Minimum supported Python version\n */\nexport const MIN_PYTHON_VERSION = { major: 3, minor: 9 };\n\n/**\n * Maximum supported Python version (exclusive upper bound)\n * Python 3.13+ is NOT compatible with docling-serve\n */\nexport const MAX_PYTHON_MINOR = 12;\n\n/**\n * Error thrown when Python version is invalid\n */\nexport class PythonVersionError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'PythonVersionError';\n }\n}\n\n/**\n * Parse Python version from command output\n *\n * @param output - Output from `python3 --version` command\n * @returns Parsed version info or null if parsing fails\n *\n * @example\n * ```typescript\n * const version = parsePythonVersion('Python 3.12.0');\n * // { major: 3, minor: 12, versionString: '3.12' }\n * ```\n */\nexport function parsePythonVersion(output: string): PythonVersionInfo | null {\n const match = output.match(PYTHON_VERSION_REGEX);\n if (!match) return null;\n\n const major = parseInt(match[1]);\n const minor = parseInt(match[2]);\n\n return {\n major,\n minor,\n versionString: `${major}.${minor}`,\n };\n}\n\n/**\n * Validate that Python version is within supported range\n *\n * @param version - Parsed Python version info\n * @param context - Context for error messages ('system' or 'venv')\n * @throws PythonVersionError if version is outside supported range\n *\n * @example\n * ```typescript\n * const version = parsePythonVersion('Python 3.12.0');\n * validatePythonVersion(version, 'system'); // OK\n *\n * const tooNew = parsePythonVersion('Python 3.13.0');\n * validatePythonVersion(tooNew, 'venv'); // throws PythonVersionError\n * ```\n */\nexport function validatePythonVersion(\n version: PythonVersionInfo,\n context: 'system' | 'venv' = 'system',\n): void {\n const { major, minor } = version;\n const prefix = context === 'venv' ? 'Venv Python' : 'Python';\n\n // Check if too new (3.13+)\n if (major === 3 && minor >= 13) {\n throw new PythonVersionError(\n `${prefix} ${major}.${minor} is too new. docling-serve requires Python 3.11 or 3.12.`,\n );\n }\n\n // Check if too old (< 3.9)\n if (major !== 3 || minor < MIN_PYTHON_VERSION.minor) {\n throw new PythonVersionError('Python 3.9 or higher is required');\n }\n}\n","import type { LoggerMethods } from '@heripo/logger';\nimport type { OcrStrategy, TokenUsageReport } from '@heripo/model';\nimport type { LanguageModel } from 'ai';\nimport type {\n AsyncConversionTask,\n ConversionOptions,\n DoclingAPIClient,\n} from 'docling-sdk';\n\nimport { LLMTokenUsageAggregator } from '@heripo/shared';\nimport { omit } from 'es-toolkit';\nimport { copyFileSync, createWriteStream, existsSync, rmSync } from 'node:fs';\nimport { rename, writeFile } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { pipeline } from 'node:stream/promises';\n\nimport { PAGE_RENDERING, PDF_CONVERTER } from '../config/constants';\nimport { ImagePdfFallbackError } from '../errors/image-pdf-fallback-error';\nimport { ImageExtractor } from '../processors/image-extractor';\nimport { PageRenderer } from '../processors/page-renderer';\nimport { PdfTextExtractor } from '../processors/pdf-text-extractor';\nimport { VlmTextCorrector } from '../processors/vlm-text-corrector';\nimport { OcrStrategySampler } from '../samplers/ocr-strategy-sampler';\nimport { runJqFileJson, runJqFileToFile } from '../utils/jq';\nimport { LocalFileServer } from '../utils/local-file-server';\nimport { ImagePdfConverter } from './image-pdf-converter';\n\n/**\n * Callback function invoked after PDF conversion completes\n * @param outputPath Absolute path to the output directory containing result files\n */\nexport type ConversionCompleteCallback = (\n outputPath: string,\n) => Promise<void> | void;\n\n/**\n * Extended options for PDF conversion.\n */\nexport type PDFConvertOptions = Omit<\n ConversionOptions,\n | 'to_formats'\n | 'image_export_mode'\n | 'ocr_engine'\n | 'accelerator_options'\n | 'ocr_options'\n | 'generate_picture_images'\n | 'generate_page_images'\n | 'images_scale'\n | 'force_ocr'\n | 'pipeline'\n | 'vlm_pipeline_model_local'\n | 'vlm_pipeline_model_api'\n> & {\n num_threads?: number;\n /**\n * Force pre-conversion to image-based PDF before processing.\n * Requires ImageMagick and Ghostscript.\n */\n forceImagePdf?: boolean;\n /** Vision model for OCR strategy sampling (enables new strategy-based flow) */\n strategySamplerModel?: LanguageModel;\n /** Vision model for VLM page processing (required when strategy selects VLM) */\n vlmProcessorModel?: LanguageModel;\n /** Concurrency for VLM page processing (default: 1) */\n vlmConcurrency?: number;\n /** Skip sampling and default to ocrmac */\n skipSampling?: boolean;\n /** Force a specific OCR method, bypassing sampling */\n forcedMethod?: 'ocrmac' | 'vlm';\n /** Token usage aggregator for tracking across sampling and VLM processing */\n aggregator?: LLMTokenUsageAggregator;\n /** Callback fired after each batch of VLM pages completes, with cumulative token usage */\n onTokenUsage?: (report: TokenUsageReport) => void;\n};\n\n/** Result of strategy-based conversion */\nexport interface ConvertWithStrategyResult {\n /** The OCR strategy that was determined */\n strategy: OcrStrategy;\n /** Token usage report from sampling and/or VLM processing (null when no LLM usage occurs) */\n tokenUsageReport: TokenUsageReport | null;\n}\n\nexport class PDFConverter {\n constructor(\n private readonly logger: LoggerMethods,\n private readonly client: DoclingAPIClient,\n private readonly enableImagePdfFallback: boolean = false,\n private readonly timeout: number = PDF_CONVERTER.DEFAULT_TIMEOUT_MS,\n ) {}\n\n async convert(\n url: string,\n reportId: string,\n onComplete: ConversionCompleteCallback,\n cleanupAfterCallback: boolean,\n options: PDFConvertOptions,\n abortSignal?: AbortSignal,\n ): Promise<TokenUsageReport | null> {\n this.logger.info('[PDFConverter] Converting:', url);\n\n // Force image PDF pre-conversion when explicitly requested\n if (options.forceImagePdf) {\n return this.convertViaImagePdf(\n url,\n reportId,\n onComplete,\n cleanupAfterCallback,\n options,\n abortSignal,\n );\n }\n\n // Standard pipeline: direct conversion with optional image PDF fallback\n return this.convertWithFallback(\n url,\n reportId,\n onComplete,\n cleanupAfterCallback,\n options,\n abortSignal,\n );\n }\n\n /**\n * Convert a PDF using OCR strategy sampling to decide between ocrmac and VLM.\n *\n * Flow:\n * 1. Determine strategy (forced, skipped, or sampled via VLM)\n * 2. If VLM → OCR pipeline + VlmTextCorrector (text correction)\n * 3. If ocrmac → existing Docling conversion\n *\n * @returns ConvertWithStrategyResult with the chosen strategy and token report\n */\n async convertWithStrategy(\n url: string,\n reportId: string,\n onComplete: ConversionCompleteCallback,\n cleanupAfterCallback: boolean,\n options: PDFConvertOptions,\n abortSignal?: AbortSignal,\n ): Promise<ConvertWithStrategyResult> {\n this.logger.info('[PDFConverter] Starting strategy-based conversion:', url);\n\n // Create an internal aggregator if none was provided so that\n // sampling + VLM processing token usage is always captured.\n const aggregator = options.aggregator ?? new LLMTokenUsageAggregator();\n const trackedOptions: PDFConvertOptions = { ...options, aggregator };\n\n const pdfPath = url.startsWith('file://') ? url.slice(7) : null;\n\n // Step 1: Determine OCR strategy\n const strategy = await this.determineStrategy(\n pdfPath,\n reportId,\n trackedOptions,\n abortSignal,\n );\n this.logger.info(\n `[PDFConverter] OCR strategy: ${strategy.method} (${strategy.reason})`,\n );\n\n // Emit token usage after sampling phase (so frontend sees sampling cost immediately)\n if (trackedOptions.onTokenUsage) {\n const samplingReport = this.buildTokenReport(aggregator);\n if (samplingReport) {\n trackedOptions.onTokenUsage(samplingReport);\n }\n }\n\n // Step 2: Execute conversion based on strategy\n if (strategy.method === 'vlm') {\n await this.convertWithVlm(\n pdfPath,\n reportId,\n onComplete,\n cleanupAfterCallback,\n trackedOptions,\n abortSignal,\n strategy.detectedLanguages,\n strategy.koreanHanjaMixPages,\n );\n return {\n strategy,\n tokenUsageReport: this.buildTokenReport(aggregator),\n };\n }\n\n // ocrmac path: delegate to existing Docling conversion\n const ocrmacOptions: PDFConvertOptions = strategy.detectedLanguages\n ? { ...trackedOptions, ocr_lang: strategy.detectedLanguages }\n : trackedOptions;\n await this.convert(\n url,\n reportId,\n onComplete,\n cleanupAfterCallback,\n ocrmacOptions,\n abortSignal,\n );\n return {\n strategy,\n tokenUsageReport: this.buildTokenReport(aggregator),\n };\n }\n\n /**\n * Build a token usage report from the aggregator.\n * Returns null when no LLM calls were tracked (e.g. forced ocrmac without sampling).\n */\n private buildTokenReport(\n aggregator: LLMTokenUsageAggregator,\n ): TokenUsageReport | null {\n const report = aggregator.getReport();\n if (report.components.length === 0) {\n return null;\n }\n return report;\n }\n\n /**\n * Determine the OCR strategy based on options and page sampling.\n *\n * When sampling is possible (strategySamplerModel + local file), it always\n * runs — even with forcedMethod — so that detectedLanguages are available\n * for OCR engine configuration. The forced method simply overrides the\n * sampled method choice.\n */\n private async determineStrategy(\n pdfPath: string | null,\n reportId: string,\n options: PDFConvertOptions,\n abortSignal?: AbortSignal,\n ): Promise<OcrStrategy> {\n // Cannot sample: skip, no sampler model, or non-local URL\n if (options.skipSampling || !options.strategySamplerModel || !pdfPath) {\n const method = options.forcedMethod ?? 'ocrmac';\n const reason = options.forcedMethod\n ? `Forced: ${options.forcedMethod}`\n : !pdfPath\n ? 'Non-local URL, sampling skipped'\n : 'Sampling skipped';\n return { method, reason, sampledPages: 0, totalPages: 0 };\n }\n\n // Sample pages to determine strategy (also detects languages)\n const samplingDir = join(process.cwd(), 'output', reportId, '_sampling');\n const sampler = new OcrStrategySampler(\n this.logger,\n new PageRenderer(this.logger),\n new PdfTextExtractor(this.logger),\n );\n\n try {\n const strategy = await sampler.sample(\n pdfPath,\n samplingDir,\n options.strategySamplerModel,\n {\n aggregator: options.aggregator,\n abortSignal,\n },\n );\n\n // Override method when forced, preserving detected languages from sampling\n if (options.forcedMethod) {\n return {\n ...strategy,\n method: options.forcedMethod,\n reason: `Forced: ${options.forcedMethod} (${strategy.reason})`,\n };\n }\n\n return strategy;\n } finally {\n // Always clean up sampling temp directory\n if (existsSync(samplingDir)) {\n rmSync(samplingDir, { recursive: true, force: true });\n }\n }\n }\n\n /**\n * Execute VLM-enhanced PDF conversion.\n *\n * Runs the standard OCR pipeline (Docling) first, then applies VLM text\n * correction to fix garbled Chinese characters (漢字/Hanja) in OCR output.\n */\n private async convertWithVlm(\n pdfPath: string | null,\n reportId: string,\n onComplete: ConversionCompleteCallback,\n cleanupAfterCallback: boolean,\n options: PDFConvertOptions,\n abortSignal?: AbortSignal,\n detectedLanguages?: string[],\n koreanHanjaMixPages?: number[],\n ): Promise<void> {\n if (!options.vlmProcessorModel) {\n throw new Error('vlmProcessorModel is required when OCR strategy is VLM');\n }\n if (!pdfPath) {\n throw new Error('VLM conversion requires a local file (file:// URL)');\n }\n\n const url = `file://${pdfPath}`;\n\n // Wrap the original callback with VLM text correction\n const wrappedCallback: ConversionCompleteCallback = async (outputDir) => {\n // Pre-extract text from PDF text layer for VLM reference\n let pageTexts: Map<number, string> | undefined;\n try {\n const resultPath = join(outputDir, 'result.json');\n // Use jq to extract only page count — avoids loading full JSON into memory\n const totalPages = await runJqFileJson<number>(\n '.pages | length',\n resultPath,\n );\n const textExtractor = new PdfTextExtractor(this.logger);\n pageTexts = await textExtractor.extractText(pdfPath, totalPages);\n } catch {\n this.logger.warn(\n '[PDFConverter] pdftotext extraction failed, proceeding without text reference',\n );\n }\n\n // Save OCR-only result before VLM correction for debugging\n const resultPath = join(outputDir, 'result.json');\n const ocrOriginPath = join(outputDir, 'result_ocr_origin.json');\n copyFileSync(resultPath, ocrOriginPath);\n\n const corrector = new VlmTextCorrector(this.logger);\n await corrector.correctAndSave(outputDir, options.vlmProcessorModel!, {\n concurrency: options.vlmConcurrency,\n aggregator: options.aggregator,\n abortSignal,\n onTokenUsage: options.onTokenUsage,\n documentLanguages: detectedLanguages,\n pageTexts,\n koreanHanjaMixPages,\n });\n await onComplete(outputDir);\n };\n\n // Run the standard OCR pipeline, then apply VLM correction via the wrapped callback\n const vlmOptions: PDFConvertOptions = detectedLanguages\n ? { ...options, ocr_lang: detectedLanguages }\n : options;\n await this.convert(\n url,\n reportId,\n wrappedCallback,\n cleanupAfterCallback,\n vlmOptions,\n abortSignal,\n );\n\n this.logger.info('[PDFConverter] VLM conversion completed successfully');\n }\n\n /**\n * Convert by first creating an image PDF, then running the conversion.\n * Used when forceImagePdf option is enabled.\n */\n private async convertViaImagePdf(\n url: string,\n reportId: string,\n onComplete: ConversionCompleteCallback,\n cleanupAfterCallback: boolean,\n options: PDFConvertOptions,\n abortSignal?: AbortSignal,\n ): Promise<TokenUsageReport | null> {\n this.logger.info(\n '[PDFConverter] Force image PDF mode: converting to image PDF first...',\n );\n const imagePdfConverter = new ImagePdfConverter(this.logger);\n let imagePdfPath: string | null = null;\n\n try {\n imagePdfPath = await imagePdfConverter.convert(url, reportId);\n const localUrl = `file://${imagePdfPath}`;\n this.logger.info(\n '[PDFConverter] Image PDF ready, starting conversion:',\n localUrl,\n );\n\n return await this.performConversion(\n localUrl,\n reportId,\n onComplete,\n cleanupAfterCallback,\n options,\n abortSignal,\n );\n } finally {\n if (imagePdfPath) {\n imagePdfConverter.cleanup(imagePdfPath);\n }\n }\n }\n\n /**\n * Convert directly with optional image PDF fallback on failure.\n * Used by standard (OCR) pipeline.\n */\n private async convertWithFallback(\n url: string,\n reportId: string,\n onComplete: ConversionCompleteCallback,\n cleanupAfterCallback: boolean,\n options: PDFConvertOptions,\n abortSignal?: AbortSignal,\n ): Promise<TokenUsageReport | null> {\n let originalError: Error | null = null;\n\n try {\n return await this.performConversion(\n url,\n reportId,\n onComplete,\n cleanupAfterCallback,\n options,\n abortSignal,\n );\n } catch (error) {\n // If aborted, don't try fallback - re-throw immediately\n if (abortSignal?.aborted) {\n throw error;\n }\n\n originalError = error as Error;\n this.logger.error('[PDFConverter] Conversion failed:', error);\n\n if (!this.enableImagePdfFallback) {\n throw error;\n }\n }\n\n // Fallback: Convert to image PDF and retry\n this.logger.info('[PDFConverter] Attempting image PDF fallback...');\n const imagePdfConverter = new ImagePdfConverter(this.logger);\n let imagePdfPath: string | null = null;\n\n try {\n imagePdfPath = await imagePdfConverter.convert(url, reportId);\n\n // Use file:// URL for local file\n const localUrl = `file://${imagePdfPath}`;\n this.logger.info('[PDFConverter] Retrying with image PDF:', localUrl);\n\n const report = await this.performConversion(\n localUrl,\n reportId,\n onComplete,\n cleanupAfterCallback,\n options,\n abortSignal,\n );\n\n this.logger.info('[PDFConverter] Fallback conversion succeeded');\n return report;\n } catch (fallbackError) {\n this.logger.error(\n '[PDFConverter] Fallback conversion also failed:',\n fallbackError,\n );\n throw new ImagePdfFallbackError(originalError!, fallbackError as Error);\n } finally {\n // Cleanup temp image PDF\n if (imagePdfPath) {\n imagePdfConverter.cleanup(imagePdfPath);\n }\n }\n }\n\n private async performConversion(\n url: string,\n reportId: string,\n onComplete: ConversionCompleteCallback,\n cleanupAfterCallback: boolean,\n options: PDFConvertOptions,\n abortSignal?: AbortSignal,\n ): Promise<TokenUsageReport | null> {\n const startTime = Date.now();\n const conversionOptions = this.buildConversionOptions(options);\n\n this.logger.info(\n `[PDFConverter] OCR languages: ${JSON.stringify(conversionOptions.ocr_options?.lang)}`,\n );\n this.logger.info(\n '[PDFConverter] Converting document with Async Source API...',\n );\n this.logger.info('[PDFConverter] Server will download from URL directly');\n this.logger.info(\n '[PDFConverter] Results will be returned as ZIP to avoid memory limits',\n );\n\n // Resolve URL (start local server for file:// URLs)\n const { httpUrl, server } = await this.resolveUrl(url);\n\n try {\n const task = await this.startConversionTask(httpUrl, conversionOptions);\n await this.trackTaskProgress(task);\n\n // Check abort after docling task completes\n if (abortSignal?.aborted) {\n this.logger.info(\n '[PDFConverter] Conversion aborted after docling completion',\n );\n const error = new Error('PDF conversion was aborted');\n error.name = 'AbortError';\n throw error;\n }\n\n await this.downloadResult(task.taskId);\n } finally {\n // Stop local file server if started\n if (server) {\n this.logger.info('[PDFConverter] Stopping local file server...');\n await server.stop();\n }\n }\n\n const cwd = process.cwd();\n const zipPath = join(cwd, 'result.zip');\n const extractDir = join(cwd, 'result_extracted');\n const outputDir = join(cwd, 'output', reportId);\n\n try {\n await this.processConvertedFiles(zipPath, extractDir, outputDir);\n\n // Render page images using ImageMagick (replaces Docling's page image generation)\n await this.renderPageImages(url, outputDir);\n\n // Check abort before callback\n if (abortSignal?.aborted) {\n this.logger.info('[PDFConverter] Conversion aborted before callback');\n const error = new Error('PDF conversion was aborted');\n error.name = 'AbortError';\n throw error;\n }\n\n // Execute callback with absolute output path\n this.logger.info('[PDFConverter] Executing completion callback...');\n await onComplete(outputDir);\n\n const duration = Date.now() - startTime;\n this.logger.info('[PDFConverter] Conversion completed successfully!');\n this.logger.info('[PDFConverter] Total time:', duration, 'ms');\n } finally {\n // Clean up temporary files (always cleanup temp files)\n this.logger.info('[PDFConverter] Cleaning up temporary files...');\n if (existsSync(zipPath)) {\n rmSync(zipPath, { force: true });\n }\n if (existsSync(extractDir)) {\n rmSync(extractDir, { recursive: true, force: true });\n }\n\n // Cleanup output directory only if requested\n if (cleanupAfterCallback) {\n this.logger.info(\n '[PDFConverter] Cleaning up output directory:',\n outputDir,\n );\n if (existsSync(outputDir)) {\n rmSync(outputDir, { recursive: true, force: true });\n }\n } else {\n this.logger.info('[PDFConverter] Output preserved at:', outputDir);\n }\n }\n\n return null;\n }\n\n private buildConversionOptions(\n options: PDFConvertOptions,\n ): ConversionOptions {\n return {\n ...omit(options, [\n 'num_threads',\n 'forceImagePdf',\n 'strategySamplerModel',\n 'vlmProcessorModel',\n 'skipSampling',\n 'forcedMethod',\n 'aggregator',\n 'onTokenUsage',\n ]),\n to_formats: ['json', 'html'],\n image_export_mode: 'embedded',\n ocr_engine: 'ocrmac',\n ocr_options: {\n kind: 'ocrmac',\n lang: options.ocr_lang ?? ['ko-KR', 'en-US'],\n recognition: 'accurate',\n framework: 'livetext',\n },\n generate_picture_images: true,\n generate_page_images: false, // Page images are rendered by PageRenderer (ImageMagick) after conversion\n images_scale: 2.0,\n /**\n * While disabling this option yields the most accurate text extraction for readable PDFs,\n * text layers overlaid on images or drawings can introduce noise when not merged properly.\n * In practice, archaeological report PDFs almost always contain such overlapping cases.\n * Enabling force_ocr mitigates this risk. Although OCR may introduce minor errors compared\n * to direct text extraction, the accuracy remains high since the source is digital, not scanned paper.\n */\n force_ocr: true,\n accelerator_options: {\n device: 'mps',\n num_threads: options.num_threads,\n },\n };\n }\n\n private async startConversionTask(\n url: string,\n conversionOptions: ConversionOptions,\n ): Promise<AsyncConversionTask> {\n const task = await this.client.convertSourceAsync({\n sources: [\n {\n kind: 'http',\n url,\n },\n ],\n options: conversionOptions,\n target: {\n kind: 'zip',\n },\n });\n\n this.logger.info(`[PDFConverter] Task created: ${task.taskId}`);\n this.logger.info('[PDFConverter] Polling for progress...');\n\n return task;\n }\n\n /**\n * Start a local file server for file:// URLs\n *\n * @param url URL to check (file:// or http://)\n * @returns Object with httpUrl and optional server to stop later\n */\n private async resolveUrl(\n url: string,\n ): Promise<{ httpUrl: string; server?: LocalFileServer }> {\n if (url.startsWith('file://')) {\n const filePath = url.slice(7); // Remove 'file://' prefix\n const server = new LocalFileServer();\n const httpUrl = await server.start(filePath);\n\n this.logger.info('[PDFConverter] Started local file server:', httpUrl);\n\n return { httpUrl, server };\n }\n\n return { httpUrl: url };\n }\n\n private async trackTaskProgress(task: AsyncConversionTask): Promise<void> {\n const conversionStartTime = Date.now();\n let lastProgressLine = '';\n\n const logProgress = (status: {\n task_status: string;\n task_position?: number;\n task_meta?: { total_documents?: number; processed_documents?: number };\n }) => {\n const parts: string[] = [`Status: ${status.task_status}`];\n\n if (status.task_position !== undefined) {\n parts.push(`position: ${status.task_position}`);\n }\n\n const meta = status.task_meta;\n if (meta) {\n if (\n meta.processed_documents !== undefined &&\n meta.total_documents !== undefined\n ) {\n parts.push(\n `progress: ${meta.processed_documents}/${meta.total_documents}`,\n );\n }\n }\n\n const progressLine = `\\r[PDFConverter] ${parts.join(' | ')}`;\n if (progressLine !== lastProgressLine) {\n lastProgressLine = progressLine;\n process.stdout.write(progressLine);\n }\n };\n\n while (true) {\n if (Date.now() - conversionStartTime > this.timeout) {\n throw new Error('Task timeout');\n }\n\n const status = await task.poll();\n\n logProgress(status);\n\n if (status.task_status === 'success') {\n this.logger.info('\\n[PDFConverter] Conversion completed!');\n return;\n }\n\n if (status.task_status === 'failure') {\n // Try to get detailed error info from the task result\n const errorDetails = await this.getTaskFailureDetails(task);\n const elapsed = Math.round((Date.now() - conversionStartTime) / 1000);\n this.logger.error(\n `\\n[PDFConverter] Task failed after ${elapsed}s: ${errorDetails}`,\n );\n throw new Error(`Task failed: ${errorDetails}`);\n }\n\n await new Promise((resolve) =>\n setTimeout(resolve, PDF_CONVERTER.POLL_INTERVAL_MS),\n );\n }\n }\n\n /**\n * Fetch detailed error information from a failed task result.\n */\n private async getTaskFailureDetails(\n task: AsyncConversionTask,\n ): Promise<string> {\n try {\n const result = await task.getResult();\n if (result.errors?.length) {\n return result.errors\n .map((e: { message: string }) => e.message)\n .join('; ');\n }\n /* v8 ignore start -- status is always present in ConvertDocumentResponse */\n return `status: ${result.status ?? 'unknown'}`;\n /* v8 ignore stop */\n } catch (err) {\n this.logger.error('[PDFConverter] Failed to retrieve task result:', err);\n return 'unable to retrieve error details';\n }\n }\n\n private async downloadResult(taskId: string): Promise<void> {\n this.logger.info(\n '\\n[PDFConverter] Task completed, downloading ZIP file...',\n );\n\n const zipResult = await this.client.getTaskResultFile(taskId);\n\n const zipPath = join(process.cwd(), 'result.zip');\n this.logger.info('[PDFConverter] Saving ZIP file to:', zipPath);\n\n if (zipResult.fileStream) {\n const writeStream = createWriteStream(zipPath);\n await pipeline(zipResult.fileStream, writeStream);\n return;\n }\n\n if (zipResult.data) {\n await writeFile(zipPath, zipResult.data);\n return;\n }\n\n // Fallback: direct HTTP download when SDK stream/data unavailable\n this.logger.warn(\n '[PDFConverter] SDK file result unavailable, falling back to direct download...',\n );\n const baseUrl = this.client.getConfig().baseUrl;\n const response = await fetch(`${baseUrl}/v1/result/${taskId}`, {\n headers: { Accept: 'application/zip' },\n });\n\n if (!response.ok) {\n throw new Error(\n `Failed to download ZIP file: ${response.status} ${response.statusText}`,\n );\n }\n\n const buffer = new Uint8Array(await response.arrayBuffer());\n await writeFile(zipPath, buffer);\n }\n\n private async processConvertedFiles(\n zipPath: string,\n extractDir: string,\n outputDir: string,\n ): Promise<void> {\n // Extract and save documents with images\n await ImageExtractor.extractAndSaveDocumentsFromZip(\n this.logger,\n zipPath,\n extractDir,\n outputDir,\n );\n }\n\n /**\n * Render page images from the source PDF using ImageMagick and update result.json.\n * Uses jq to update the JSON file without loading it into Node.js memory.\n * Replaces Docling's generate_page_images which fails on large PDFs\n * due to memory limits when embedding all page images as base64.\n */\n private async renderPageImages(\n url: string,\n outputDir: string,\n ): Promise<void> {\n if (!url.startsWith('file://')) {\n this.logger.warn(\n '[PDFConverter] Page image rendering skipped: only supported for local files (file:// URLs)',\n );\n return;\n }\n\n const pdfPath = url.slice(7);\n this.logger.info(\n '[PDFConverter] Rendering page images with ImageMagick...',\n );\n\n const renderer = new PageRenderer(this.logger);\n const renderResult = await renderer.renderPages(pdfPath, outputDir);\n\n // Update result.json with page image URIs using jq to avoid loading large JSON\n const resultPath = join(outputDir, 'result.json');\n const tmpPath = resultPath + '.tmp';\n const jqProgram = `\n .pages |= with_entries(\n if (.value.page_no - 1) >= 0 and (.value.page_no - 1) < ${renderResult.pageCount} then\n .value.image.uri = \"pages/page_\\\\(.value.page_no - 1).png\" |\n .value.image.mimetype = \"image/png\" |\n .value.image.dpi = ${PAGE_RENDERING.DEFAULT_DPI}\n else . end\n )\n `;\n await runJqFileToFile(jqProgram, resultPath, tmpPath);\n await rename(tmpPath, resultPath);\n\n this.logger.info(\n `[PDFConverter] Rendered ${renderResult.pageCount} page images`,\n );\n }\n}\n","/**\n * Error thrown when both original PDF conversion and image PDF fallback fail.\n * Contains both errors for debugging purposes.\n */\nexport class ImagePdfFallbackError extends Error {\n public readonly name = 'ImagePdfFallbackError';\n\n constructor(\n public readonly originalError: Error,\n public readonly fallbackError: Error,\n ) {\n super(\n `PDF conversion failed with fallback. ` +\n `Original: ${originalError.message}. ` +\n `Fallback: ${fallbackError.message}`,\n );\n }\n}\n","import type { LoggerMethods } from '@heripo/logger';\n\nimport {\n createReadStream,\n createWriteStream,\n existsSync,\n mkdirSync,\n readdirSync,\n rmSync,\n writeFileSync,\n} from 'node:fs';\nimport { extname, join } from 'node:path';\nimport { Transform } from 'node:stream';\nimport { pipeline } from 'node:stream/promises';\nimport * as yauzl from 'yauzl';\n\nimport {\n jqExtractBase64PngStringsStreaming,\n jqReplaceBase64WithPathsToFile,\n} from '../utils/jq';\n\n/**\n * ImageExtractor handles extraction and processing of images from PDF conversion results\n *\n * This class provides functionality to:\n * - Extract ZIP files containing converted PDF documents\n * - Extract base64-encoded images from JSON and HTML content\n * - Save images as separate PNG files\n * - Replace base64 data with relative file paths\n */\nexport class ImageExtractor {\n /**\n * Extract a ZIP file to a target directory\n */\n private static async extractZip(\n zipPath: string,\n targetDir: string,\n ): Promise<void> {\n return new Promise((resolve, reject) => {\n yauzl.open(zipPath, { lazyEntries: true }, (err, zipfile) => {\n if (err || !zipfile) {\n reject(err || new Error('Failed to open zip file'));\n return;\n }\n\n zipfile.readEntry();\n\n zipfile.on('entry', (entry) => {\n const entryPath = join(targetDir, entry.fileName);\n\n if (/\\/$/.test(entry.fileName)) {\n // Directory entry\n mkdirSync(entryPath, { recursive: true });\n zipfile.readEntry();\n } else {\n // File entry\n zipfile.openReadStream(entry, (err, readStream) => {\n if (err || !readStream) {\n reject(err || new Error('Failed to open read stream'));\n return;\n }\n\n mkdirSync(join(entryPath, '..'), { recursive: true });\n const writeStream = createWriteStream(entryPath);\n\n readStream.pipe(writeStream);\n writeStream.on('finish', () => {\n zipfile.readEntry();\n });\n writeStream.on('error', reject);\n });\n }\n });\n\n zipfile.on('end', () => {\n resolve();\n });\n\n zipfile.on('error', reject);\n });\n });\n }\n\n /**\n * Extract a base64-encoded image to a file and return the relative path\n */\n private static extractBase64ImageToFile(\n base64Data: string,\n imagesDir: string,\n index: number,\n prefix: string,\n dirName: string,\n ): string {\n const PREFIX = 'data:image/png;base64,';\n const base64Content = base64Data.startsWith(PREFIX)\n ? base64Data.slice(PREFIX.length)\n : base64Data;\n\n const filename = `${prefix}_${index}.png`;\n const filepath = join(imagesDir, filename);\n\n // Convert base64 to buffer and write to file\n const buffer = Buffer.from(base64Content, 'base64');\n writeFileSync(filepath, buffer);\n\n return `${dirName}/${filename}`;\n }\n\n /**\n * Extract base64 images from HTML using streaming.\n * Reads HTML file as a stream, extracts base64 images from src attributes,\n * saves them as PNG files, and replaces with file paths in the output HTML.\n * Returns the number of images extracted.\n */\n static async extractImagesFromHtmlStream(\n htmlInputPath: string,\n htmlOutputPath: string,\n imagesDir: string,\n ): Promise<number> {\n let imageIndex = 0;\n let pending = '';\n const MARKER = 'src=\"data:image/png;base64,';\n\n const transform = new Transform({\n decodeStrings: false,\n encoding: 'utf-8',\n transform(chunk: string, _encoding, callback) {\n pending += chunk;\n let result = '';\n\n while (true) {\n const markerIdx = pending.indexOf(MARKER);\n\n if (markerIdx === -1) {\n // Keep a tail that could be a partial marker match\n const safeEnd = Math.max(0, pending.length - MARKER.length);\n result += pending.slice(0, safeEnd);\n pending = pending.slice(safeEnd);\n break;\n }\n\n // Flush everything before the marker\n result += pending.slice(0, markerIdx);\n\n // Find the closing quote after base64 data\n const dataStart = markerIdx + MARKER.length;\n const quoteIdx = pending.indexOf('\"', dataStart);\n\n if (quoteIdx === -1) {\n // Closing quote not in buffer yet — keep everything from marker onward\n pending = pending.slice(markerIdx);\n break;\n }\n\n // Extract base64 content and save as image file\n const base64Content = pending.slice(dataStart, quoteIdx);\n const filename = `image_${imageIndex}.png`;\n const filepath = join(imagesDir, filename);\n const buf = Buffer.from(base64Content, 'base64');\n writeFileSync(filepath, buf);\n\n const relativePath = `images/${filename}`;\n result += `src=\"${relativePath}\"`;\n imageIndex++;\n\n pending = pending.slice(quoteIdx + 1);\n }\n\n if (result.length > 0) {\n this.push(result);\n }\n callback();\n },\n flush(callback) {\n if (pending.length > 0) {\n this.push(pending);\n }\n callback();\n },\n });\n\n const rs = createReadStream(htmlInputPath, { encoding: 'utf-8' });\n const ws = createWriteStream(htmlOutputPath, { encoding: 'utf-8' });\n\n await pipeline(rs, transform, ws);\n\n return imageIndex;\n }\n\n /**\n * Save JSON and HTML documents with base64 images extracted to separate files.\n * Uses jq for JSON processing and streaming for HTML to handle large files.\n *\n * This method:\n * 1. Extracts base64-encoded images from JSON and HTML content\n * 2. Saves images as separate PNG files\n * 3. Replaces base64 data with relative file paths\n * 4. Saves the transformed documents to the output directory\n */\n private static async saveDocumentsWithExtractedImages(\n logger: LoggerMethods,\n outputDir: string,\n filename: string,\n jsonSourcePath: string,\n htmlSourcePath: string,\n ): Promise<void> {\n // Clear output directory completely at the start, then recreate it\n try {\n if (existsSync(outputDir)) {\n rmSync(outputDir, { recursive: true, force: true });\n }\n } catch (e) {\n logger.warn('[PDFConverter] Failed to clear output directory:', e);\n }\n mkdirSync(outputDir, { recursive: true });\n\n // Get filename without extension\n const baseName = filename.replace(extname(filename), '');\n\n // Save JSON with extracted images (using jq streaming for large files)\n const jsonPath = join(outputDir, `${baseName}.json`);\n try {\n // Create images directory for picture images from JSON\n const imagesDir = join(outputDir, 'images');\n if (!existsSync(imagesDir)) {\n mkdirSync(imagesDir, { recursive: true });\n }\n\n // Step 1: Extract base64 images using streaming jq (one at a time, no accumulation)\n const imageCount = await jqExtractBase64PngStringsStreaming(\n jsonSourcePath,\n (base64Data, index) => {\n ImageExtractor.extractBase64ImageToFile(\n base64Data,\n imagesDir,\n index,\n 'pic',\n 'images',\n );\n },\n );\n\n logger.info(\n `[PDFConverter] Extracted ${imageCount} picture images from JSON to ${imagesDir}`,\n );\n\n // Step 2: Replace base64 images with file paths using jq, pipe directly to file\n await jqReplaceBase64WithPathsToFile(\n jsonSourcePath,\n jsonPath,\n 'images',\n 'pic',\n );\n\n logger.info(\n `[PDFConverter] Replaced ${imageCount} base64 images with file paths`,\n );\n } catch (e) {\n logger.warn(\n '[PDFConverter] Failed to extract images from JSON using jq. Error:',\n e,\n );\n throw e;\n }\n logger.info('[PDFConverter] Saved JSON:', jsonPath);\n\n // Save HTML with extracted images using streaming\n const htmlPath = join(outputDir, `${baseName}.html`);\n try {\n // Create images directory for HTML images\n const imagesDir = join(outputDir, 'images');\n if (!existsSync(imagesDir)) {\n mkdirSync(imagesDir, { recursive: true });\n }\n\n const htmlImageCount = await ImageExtractor.extractImagesFromHtmlStream(\n htmlSourcePath,\n htmlPath,\n imagesDir,\n );\n\n logger.info(\n `[PDFConverter] Extracted ${htmlImageCount} images from HTML to ${imagesDir}`,\n );\n } catch (e) {\n logger.warn(\n '[PDFConverter] Failed to extract images from HTML, copying original. Error:',\n e,\n );\n // Fallback: copy original HTML using streaming\n const rs = createReadStream(htmlSourcePath);\n const ws = createWriteStream(htmlPath);\n await pipeline(rs, ws);\n }\n logger.info('[PDFConverter] Saved HTML:', htmlPath);\n }\n\n /**\n * Extract documents from ZIP and save with extracted images\n * Uses jq for JSON processing and streaming for HTML to handle large files\n * without loading into Node.js memory\n *\n * Complete workflow:\n * 1. Extract ZIP file to temporary directory\n * 2. Find JSON and HTML files from extracted files\n * 3. Use jq to stream-extract base64 images from JSON and save as separate files\n * 4. Use jq to replace base64 with file paths in JSON (piped to file)\n * 5. Process HTML with streaming Transform to extract and replace images\n * 6. Save transformed documents to output directory (as result.json and result.html)\n */\n static async extractAndSaveDocumentsFromZip(\n logger: LoggerMethods,\n zipPath: string,\n extractDir: string,\n outputDir: string,\n ): Promise<void> {\n // Extract zip file\n logger.info('[PDFConverter] Extracting ZIP file...');\n await ImageExtractor.extractZip(zipPath, extractDir);\n\n // Find JSON and HTML files dynamically\n const files = readdirSync(extractDir);\n const jsonFile = files.find((f) => extname(f).toLowerCase() === '.json');\n const htmlFile = files.find((f) => extname(f).toLowerCase() === '.html');\n\n if (!jsonFile || !htmlFile) {\n throw new Error(\n `Expected one JSON and one HTML file in extracted directory. Found: ${files.join(', ')}`,\n );\n }\n\n // Get file paths\n const jsonPath = join(extractDir, jsonFile);\n const htmlPath = join(extractDir, htmlFile);\n\n // Save converted files to output directory with extracted images\n // Both JSON and HTML are processed with streaming to avoid loading large files into memory\n logger.info('[PDFConverter] Saving converted files to output...');\n await ImageExtractor.saveDocumentsWithExtractedImages(\n logger,\n outputDir,\n 'result',\n jsonPath,\n htmlPath,\n );\n\n logger.info('[PDFConverter] Files saved to:', outputDir);\n }\n}\n","import { spawn } from 'node:child_process';\nimport { createWriteStream } from 'node:fs';\nimport { rename } from 'node:fs/promises';\nimport { pipeline } from 'node:stream/promises';\n\n/**\n * Resolve jq executable path from environment or default to 'jq' in PATH.\n */\nfunction getJqPath(): string {\n const p = process.env.JQ_PATH?.trim();\n return p && p.length > 0 ? p : 'jq';\n}\n\n/**\n * Run a jq program against a JSON file and parse the JSON result.\n * - program: jq filter/program string\n * - filePath: path to the input JSON file\n * Returns parsed JSON as type T.\n */\nexport function runJqFileJson<T = unknown>(\n program: string,\n filePath: string,\n): Promise<T> {\n return new Promise<T>((resolve, reject) => {\n const jqPath = getJqPath();\n const args = [\n '-c', // compact output (single line when possible)\n program,\n filePath,\n ];\n\n const child = spawn(jqPath, args, {\n stdio: ['ignore', 'pipe', 'pipe'],\n env: process.env,\n });\n\n let stdout = '';\n let stderr = '';\n\n child.stdout.setEncoding('utf-8');\n child.stderr.setEncoding('utf-8');\n\n child.stdout.on('data', (chunk: string) => {\n stdout += chunk;\n });\n child.stderr.on('data', (chunk: string) => {\n stderr += chunk;\n });\n\n child.on('error', (err) => {\n reject(err);\n });\n\n child.on('close', (code) => {\n if (code !== 0) {\n const error = new Error(\n `jq exited with code ${code}. ${stderr ? 'Stderr: ' + stderr : ''}`,\n );\n return reject(error);\n }\n try {\n // jq may output trailing newlines; trim is safe for JSON\n const text = stdout.trim();\n const parsed = JSON.parse(text) as T;\n resolve(parsed);\n } catch (e) {\n reject(\n new Error(\n `Failed to parse jq output as JSON. Output length=${stdout.length}. Error: ${(e as Error).message}`,\n ),\n );\n }\n });\n });\n}\n\n/**\n * Run a jq program against a JSON file and pipe output directly to a file.\n * Avoids loading jq output into Node.js memory.\n */\nexport function runJqFileToFile(\n program: string,\n inputPath: string,\n outputPath: string,\n): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n const jqPath = getJqPath();\n const args = [program, inputPath];\n\n const child = spawn(jqPath, args, {\n stdio: ['ignore', 'pipe', 'pipe'],\n env: process.env,\n });\n\n let stderr = '';\n let exitCode: number | null = null;\n let pipelineDone = false;\n let settled = false;\n\n child.stderr.setEncoding('utf-8');\n child.stderr.on('data', (chunk: string) => {\n stderr += chunk;\n });\n\n const ws = createWriteStream(outputPath);\n\n function trySettle() {\n if (settled) return;\n if (!pipelineDone || exitCode === null) return;\n settled = true;\n if (exitCode !== 0) {\n reject(\n new Error(\n `jq exited with code ${exitCode}. ${stderr ? 'Stderr: ' + stderr : ''}`,\n ),\n );\n } else {\n resolve();\n }\n }\n\n child.on('error', (err) => {\n if (settled) return;\n settled = true;\n ws.destroy();\n reject(err);\n });\n\n pipeline(child.stdout, ws)\n .then(() => {\n pipelineDone = true;\n trySettle();\n })\n .catch((err) => {\n if (settled) return;\n settled = true;\n reject(err);\n });\n\n child.on('close', (code) => {\n exitCode = code ?? 1;\n trySettle();\n });\n });\n}\n\n/**\n * Run a jq program with -r (raw output) and process stdout line by line.\n * Each line is passed to the onLine callback immediately, avoiding memory accumulation.\n */\nexport function runJqFileLines(\n program: string,\n filePath: string,\n onLine: (line: string) => void,\n): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n const jqPath = getJqPath();\n const args = ['-r', program, filePath];\n\n const child = spawn(jqPath, args, {\n stdio: ['ignore', 'pipe', 'pipe'],\n env: process.env,\n });\n\n let stderr = '';\n let buffer = '';\n let callbackError = false;\n\n child.stdout.setEncoding('utf-8');\n child.stderr.setEncoding('utf-8');\n\n function safeOnLine(line: string): void {\n if (callbackError) return;\n try {\n onLine(line);\n } catch (err) {\n callbackError = true;\n child.kill();\n reject(err);\n }\n }\n\n child.stdout.on('data', (chunk: string) => {\n buffer += chunk;\n let newlineIdx: number;\n while ((newlineIdx = buffer.indexOf('\\n')) !== -1) {\n const line = buffer.slice(0, newlineIdx);\n buffer = buffer.slice(newlineIdx + 1);\n if (line.length > 0) {\n safeOnLine(line);\n }\n }\n });\n\n child.stderr.on('data', (chunk: string) => {\n stderr += chunk;\n });\n\n child.on('error', (err) => {\n if (!callbackError) reject(err);\n });\n\n child.on('close', (code) => {\n if (callbackError) return;\n // Process any remaining data in buffer\n if (buffer.length > 0) {\n safeOnLine(buffer);\n }\n if (callbackError) return;\n\n if (code !== 0) {\n reject(\n new Error(\n `jq exited with code ${code}. ${stderr ? 'Stderr: ' + stderr : ''}`,\n ),\n );\n } else {\n resolve();\n }\n });\n });\n}\n\n/**\n * Convenience: extract all base64 PNG data-URI strings from a JSON file.\n */\nexport function jqExtractBase64PngStrings(filePath: string): Promise<string[]> {\n const program = `\n [\n .. |\n select(type == \"string\" and startswith(\"data:image/png;base64\"))\n ]\n `;\n return runJqFileJson<string[]>(program, filePath);\n}\n\n/**\n * Streaming extraction of base64 PNG data-URI strings from a JSON file.\n * Each image is passed to the onImage callback immediately instead of accumulating all in memory.\n * Returns the total count of images found.\n */\nexport async function jqExtractBase64PngStringsStreaming(\n filePath: string,\n onImage: (base64Data: string, index: number) => void,\n): Promise<number> {\n let index = 0;\n await runJqFileLines(\n '.. | select(type == \"string\" and startswith(\"data:image/png;base64\"))',\n filePath,\n (line) => {\n onImage(line, index);\n index++;\n },\n );\n return index;\n}\n\n/**\n * Convenience: replace base64 PNG data-URIs with file paths like `${dirName}/${prefix}_<idx>.png`.\n * Returns an object: { data, count }\n */\nexport function jqReplaceBase64WithPaths(\n filePath: string,\n dirName: string,\n prefix: string,\n): Promise<{ data: unknown; count: number }> {\n const program = `\n reduce paths(type == \"string\" and startswith(\"data:image/png;base64\")) as $p (\n {data: ., counter: 0};\n .counter as $idx |\n .data |= setpath($p; \"${dirName}/${prefix}_\\\\($idx).png\") |\n .counter += 1\n ) | {data: .data, count: .counter}\n `;\n return runJqFileJson<{ data: unknown; count: number }>(program, filePath);\n}\n\n/**\n * Replace base64 PNG data-URIs with file paths and pipe result directly to an output file.\n * Avoids loading the entire transformed JSON into Node.js memory.\n * Uses a temporary file and atomic rename to avoid corrupting the output.\n */\nexport async function jqReplaceBase64WithPathsToFile(\n inputPath: string,\n outputPath: string,\n dirName: string,\n prefix: string,\n): Promise<void> {\n const program = `\n reduce paths(type == \"string\" and startswith(\"data:image/png;base64\")) as $p (\n {data: ., counter: 0};\n .counter as $idx |\n .data |= setpath($p; \"${dirName}/${prefix}_\\\\($idx).png\") |\n .counter += 1\n ) | .data\n `;\n const tmpPath = outputPath + '.tmp';\n await runJqFileToFile(program, inputPath, tmpPath);\n await rename(tmpPath, outputPath);\n}\n","import type { LoggerMethods } from '@heripo/logger';\n\nimport { spawnAsync } from '@heripo/shared';\nimport { existsSync, mkdirSync, readdirSync } from 'node:fs';\nimport { join } from 'node:path';\n\nimport { PAGE_RENDERING } from '../config/constants.js';\n\n/** Minimum percentage increment between progress log messages */\nconst PROGRESS_LOG_PERCENT_STEP = 10;\n\n/** Result of page rendering */\nexport interface PageRenderResult {\n /** Total number of pages rendered */\n pageCount: number;\n /** Absolute path to the pages directory */\n pagesDir: string;\n /** Sorted list of rendered page file paths (absolute) */\n pageFiles: string[];\n}\n\n/** Options for page rendering */\nexport interface PageRendererOptions {\n /** DPI for rendered images (default: 200) */\n dpi?: number;\n}\n\n/**\n * Renders PDF pages to individual PNG images using ImageMagick.\n *\n * ## System Requirements\n * - ImageMagick (`brew install imagemagick`)\n * - Ghostscript (`brew install ghostscript`)\n */\nexport class PageRenderer {\n private lastLoggedPercent = 0;\n\n constructor(private readonly logger: LoggerMethods) {}\n\n /**\n * Render all pages of a PDF to individual PNG files.\n *\n * Uses per-page rendering (`magick 'input.pdf[N]'`) when page count is known,\n * limiting peak memory to ~15MB/page instead of loading all pages at once.\n *\n * @param pdfPath - Absolute path to the source PDF file\n * @param outputDir - Directory where pages/ subdirectory will be created\n * @param options - Rendering options\n * @returns Render result with page count and file paths\n */\n async renderPages(\n pdfPath: string,\n outputDir: string,\n options?: PageRendererOptions,\n ): Promise<PageRenderResult> {\n const dpi = options?.dpi ?? PAGE_RENDERING.DEFAULT_DPI;\n const pagesDir = join(outputDir, 'pages');\n\n if (!existsSync(pagesDir)) {\n mkdirSync(pagesDir, { recursive: true });\n }\n\n const totalPages = await this.getPageCount(pdfPath);\n\n if (totalPages > 0) {\n this.logger.info(\n `[PageRenderer] Rendering ${totalPages} pages at ${dpi} DPI...`,\n );\n this.lastLoggedPercent = 0;\n\n for (let i = 0; i < totalPages; i++) {\n const result = await spawnAsync(\n 'magick',\n [\n '-density',\n dpi.toString(),\n `${pdfPath}[${i}]`,\n '-background',\n 'white',\n '-alpha',\n 'remove',\n '-alpha',\n 'off',\n join(pagesDir, `page_${i}.png`),\n ],\n { captureStdout: false },\n );\n\n if (result.code !== 0) {\n throw new Error(\n `[PageRenderer] Failed to render page ${i + 1}/${totalPages}: ${result.stderr || 'Unknown error'}`,\n );\n }\n\n this.logProgress(i + 1, totalPages);\n }\n } else {\n // Fallback: render all pages at once when pdfinfo is unavailable\n this.logger.info(`[PageRenderer] Rendering PDF at ${dpi} DPI...`);\n\n const result = await spawnAsync(\n 'magick',\n [\n '-density',\n dpi.toString(),\n pdfPath,\n '-background',\n 'white',\n '-alpha',\n 'remove',\n '-alpha',\n 'off',\n join(pagesDir, 'page_%d.png'),\n ],\n { captureStdout: false },\n );\n\n if (result.code !== 0) {\n throw new Error(\n `[PageRenderer] Failed to render PDF pages: ${result.stderr || 'Unknown error'}`,\n );\n }\n }\n\n const pageFiles = readdirSync(pagesDir)\n .filter((f) => f.startsWith('page_') && f.endsWith('.png'))\n .sort((a, b) => {\n const numA = parseInt(a.replace('page_', '').replace('.png', ''), 10);\n const numB = parseInt(b.replace('page_', '').replace('.png', ''), 10);\n return numA - numB;\n })\n .map((f) => join(pagesDir, f));\n\n this.logger.info(\n `[PageRenderer] Rendered ${pageFiles.length} pages to ${pagesDir}`,\n );\n\n return {\n pageCount: pageFiles.length,\n pagesDir,\n pageFiles,\n };\n }\n\n /**\n * Log rendering progress at appropriate intervals (every 10%).\n */\n private logProgress(current: number, total: number): void {\n const percent = Math.floor((current / total) * 100);\n if (\n percent >= this.lastLoggedPercent + PROGRESS_LOG_PERCENT_STEP ||\n current === total\n ) {\n this.lastLoggedPercent = percent;\n this.logger.info(\n `[PageRenderer] Rendering pages: ${current}/${total} (${percent}%)`,\n );\n }\n }\n\n /**\n * Get total page count using pdfinfo.\n * Returns 0 on failure (progress logging will be skipped).\n */\n private async getPageCount(pdfPath: string): Promise<number> {\n try {\n const result = await spawnAsync('pdfinfo', [pdfPath]);\n if (result.code !== 0) return 0;\n const match = result.stdout.match(/^Pages:\\s+(\\d+)/m);\n return match ? parseInt(match[1], 10) : 0;\n } catch {\n return 0;\n }\n }\n}\n","import type { LoggerMethods } from '@heripo/logger';\n\nimport { spawnAsync } from '@heripo/shared';\n\n/**\n * Extracts text from PDF pages using the pdftotext command-line tool.\n *\n * Uses the `-layout` flag to preserve the original page layout.\n * Failures are logged as warnings and produce empty strings,\n * allowing the pipeline to gracefully fall back to VLM-only OCR.\n *\n * ## System Requirements\n * - Poppler utils (`brew install poppler`)\n */\nexport class PdfTextExtractor {\n constructor(private readonly logger: LoggerMethods) {}\n\n /**\n * Extract text from all pages of a PDF.\n *\n * @param pdfPath - Absolute path to the source PDF file\n * @param totalPages - Total number of pages in the PDF\n * @returns Map of 1-based page numbers to extracted text strings\n */\n async extractText(\n pdfPath: string,\n totalPages: number,\n ): Promise<Map<number, string>> {\n this.logger.info(\n `[PdfTextExtractor] Extracting text from ${totalPages} pages...`,\n );\n\n const pageTexts = new Map<number, string>();\n\n for (let page = 1; page <= totalPages; page++) {\n const text = await this.extractPageText(pdfPath, page);\n pageTexts.set(page, text);\n }\n\n const nonEmptyCount = [...pageTexts.values()].filter(\n (t) => t.trim().length > 0,\n ).length;\n this.logger.info(\n `[PdfTextExtractor] Extracted text from ${nonEmptyCount}/${totalPages} pages`,\n );\n\n return pageTexts;\n }\n\n /**\n * Get total page count of a PDF using pdfinfo.\n * Returns 0 on failure.\n */\n async getPageCount(pdfPath: string): Promise<number> {\n const result = await spawnAsync('pdfinfo', [pdfPath]);\n if (result.code !== 0) {\n this.logger.warn(\n `[PdfTextExtractor] pdfinfo failed: ${result.stderr || 'Unknown error'}`,\n );\n return 0;\n }\n const match = result.stdout.match(/^Pages:\\s+(\\d+)/m);\n return match ? parseInt(match[1], 10) : 0;\n }\n\n /**\n * Extract text from the entire PDF in a single pdftotext invocation.\n * Returns empty string on failure (logged as warning).\n */\n async extractFullText(pdfPath: string): Promise<string> {\n const result = await spawnAsync('pdftotext', ['-layout', pdfPath, '-']);\n\n if (result.code !== 0) {\n this.logger.warn(\n `[PdfTextExtractor] pdftotext (full) failed: ${result.stderr || 'Unknown error'}`,\n );\n return '';\n }\n\n return result.stdout;\n }\n\n /**\n * Extract text from a single PDF page using pdftotext.\n * Returns empty string on failure (logged as warning).\n */\n async extractPageText(pdfPath: string, page: number): Promise<string> {\n const result = await spawnAsync('pdftotext', [\n '-f',\n page.toString(),\n '-l',\n page.toString(),\n '-layout',\n pdfPath,\n '-',\n ]);\n\n if (result.code !== 0) {\n this.logger.warn(\n `[PdfTextExtractor] pdftotext failed for page ${page}: ${result.stderr || 'Unknown error'}`,\n );\n return '';\n }\n\n return result.stdout;\n }\n}\n","import type { LoggerMethods } from '@heripo/logger';\nimport type {\n DoclingDocument,\n DoclingTableItem,\n DoclingTextItem,\n TokenUsageReport,\n} from '@heripo/model';\nimport type { LLMTokenUsageAggregator } from '@heripo/shared';\nimport type { LanguageModel } from 'ai';\n\nimport { ConcurrentPool, LLMCaller } from '@heripo/shared';\nimport { readFileSync, writeFileSync } from 'node:fs';\nimport { join } from 'node:path';\n\nimport {\n type VlmTextCorrectionOutput,\n vlmTextCorrectionSchema,\n} from '../types/vlm-text-correction-schema';\n\n/** Language display names for prompt context (keyed by ISO 639-1 base language code) */\nconst LANGUAGE_DISPLAY_NAMES: Record<string, string> = {\n ko: 'Korean (한국어)',\n ja: 'Japanese (日本語)',\n zh: 'Chinese (中文)',\n en: 'English',\n fr: 'French (Français)',\n de: 'German (Deutsch)',\n es: 'Spanish (Español)',\n pt: 'Portuguese (Português)',\n ru: 'Russian (Русский)',\n uk: 'Ukrainian (Українська)',\n it: 'Italian (Italiano)',\n};\n\n/** Minimum character overlap ratio to consider a pdftotext line as matching an OCR element */\nconst REFERENCE_MATCH_THRESHOLD = 0.4;\n\n/** Default concurrency for parallel page processing */\nconst DEFAULT_CONCURRENCY = 1;\n\n/** Default max retries per VLM call */\nconst DEFAULT_MAX_RETRIES = 3;\n\n/** Default temperature for VLM generation */\nconst DEFAULT_TEMPERATURE = 0;\n\n/** Type abbreviation codes for text element labels */\nconst LABEL_TO_TYPE_CODE: Record<string, string> = {\n section_header: 'sh',\n text: 'tx',\n caption: 'ca',\n footnote: 'fn',\n list_item: 'li',\n page_header: 'ph',\n page_footer: 'pf',\n};\n\n/** Text labels that should be included in VLM correction */\nconst TEXT_LABELS = new Set(Object.keys(LABEL_TO_TYPE_CODE));\n\n/**\n * System prompt for VLM text correction.\n * Instructs the VLM to compare OCR text against the page image and fix errors.\n */\nconst TEXT_CORRECTION_SYSTEM_PROMPT = `You are a text correction engine for OCR output from Korean archaeological (考古學) report PDFs. Compare OCR text against the page image and reference text to fix errors.\n\nThe OCR engine cannot read Chinese characters (漢字/Hanja) correctly. These errors appear as:\n- Random ASCII letters/symbols: 熊津 → \"M\", 小京制 → \"5☆\", 故址 → \"Bbt\"\n- Meaningless Korean syllables: 東明 → \"햇배\", 金憲昌 → \"숲\", 總管 → \"3씁\"\n- Number/symbol noise: 熊川州 → \"IEJIM\", 湯井郡 → \"3#\"\n- Hanja dropped entirely: (株)韓國纖維 → (주), (財)忠淸文化財硏究院 → (재)충남문화재연구원\n- Phonetic reading substitution (音讀): 漢字 replaced by Korean pronunciation, e.g. 忠淸文化財硏究院 → 충남문화재연구원, 實玉洞遺蹟 → 실옥동유적\n\nFIX: garbled/wrong Chinese characters, mojibake, encoding artifacts, random ASCII/Korean replacing Hanja, dropped Hanja, phonetic reading substitutions\nKEEP: correct text, structure, punctuation, whitespace\n\nInput format:\nT: (text elements) index|type|text\n Optional: index|ref|reference_text (PDF text layer for the above element)\nC: (table cells) tableIndex|row,col|text\n Optional: C_REF: (unused pdftotext blocks as table reference)\n\nFOOTNOTE (fn) SPECIAL INSTRUCTIONS:\n- Footnotes in archaeological reports contain institution names with Hanja that are severely garbled\n- Common pattern: (財)機關名硏究院 → (W)#X1CR003T or (W): 103 or similar ASCII noise\n- When OCR shows patterns like (W), (M), or random ASCII where an institution name should be, READ THE IMAGE directly\n- Institution names follow patterns like: (財)OO文化財硏究院, (株)OO, (社)OO學會\n\nTABLE CELL (C:) SPECIAL INSTRUCTIONS:\n- Table headers often contain Hanja that OCR cannot read: 發刊日, 時代, 調査緣由, 調査機關, 遺蹟名, 類型 및 基數\n- When OCR shows garbled characters like \"₩ A\", \"#쩯및표뽰\" in table cells, READ THE IMAGE directly\n- If C_REF is present, use it as additional context for correcting table cells\n\nWhen a |ref| line is present:\n- It shows text extracted directly from the PDF text layer for that element\n- If OCR text contains garbled characters but ref text looks correct, USE the ref text\n- For long paragraphs, align OCR and ref text segment by segment to identify and fix each garbled portion\n- IMPORTANT: If BOTH OCR and ref text are garbled (e.g. CJK font encoding issues), IGNORE the ref text and READ THE IMAGE directly\n\nWhen NO |ref| line is present:\n- The PDF text layer could not be matched to this element\n- READ THE IMAGE directly to determine the correct text\n\nOutput JSON with corrections:\ntc=[{i:index, s:[{f:\"garbled_substring\",r:\"corrected_text\"}, ...]}] for text\ncc=[{ti:tableIndex, r:row, c:col, t:corrected}] for table cells\n\nSubstitution rules for tc:\n- 'f': exact garbled/wrong substring from the input text (must match exactly)\n- 'r': the corrected replacement\n- Include ALL garbled portions for each element as separate s entries\n- Order substitutions left-to-right as they appear in the text\n- Do NOT include unchanged text — only the specific substrings that need fixing\n\nIf all correct: {\"tc\":[],\"cc\":[]}`;\n\n/** Options for VlmTextCorrector */\nexport interface VlmTextCorrectorOptions {\n /** Number of concurrent page processing (default: 1) */\n concurrency?: number;\n /** Maximum retries per VLM call (default: 3) */\n maxRetries?: number;\n /** Temperature for generation (default: 0) */\n temperature?: number;\n /** Abort signal for cancellation */\n abortSignal?: AbortSignal;\n /** Token usage aggregator for tracking */\n aggregator?: LLMTokenUsageAggregator;\n /** Callback fired after each page completes, with cumulative token usage */\n onTokenUsage?: (report: TokenUsageReport) => void;\n /** BCP 47 language tags detected during sampling (e.g., ['ko-KR', 'en-US']) */\n documentLanguages?: string[];\n /** Pre-extracted page texts from pdftotext (1-based pageNo → text) */\n pageTexts?: Map<number, string>;\n /** Pages containing Hanja detected from text layer (1-based). Only these pages get VLM correction. */\n koreanHanjaMixPages?: number[];\n}\n\n/** Result of VLM text correction */\nexport interface VlmTextCorrectionResult {\n /** Total number of text corrections applied */\n textCorrections: number;\n /** Total number of cell corrections applied */\n cellCorrections: number;\n /** Number of pages processed */\n pagesProcessed: number;\n /** Number of pages that failed VLM correction (OCR text kept as-is) */\n pagesFailed: number;\n}\n\n/**\n * VLM text corrector that fixes OCR errors by comparing page images\n * against OCR-extracted text.\n *\n * Reads the DoclingDocument from the OCR output directory, sends each page's\n * text elements and table cells to a VLM for correction, then merges\n * corrections back and saves the updated document.\n */\nexport class VlmTextCorrector {\n constructor(private readonly logger: LoggerMethods) {}\n\n /**\n * Read DoclingDocument from output directory, correct text via VLM,\n * and save the corrected document back.\n *\n * @param outputDir - Directory containing result.json and pages/\n * @param model - Vision language model for text correction\n * @param options - Processing options\n * @returns Correction statistics\n */\n async correctAndSave(\n outputDir: string,\n model: LanguageModel,\n options?: VlmTextCorrectorOptions,\n ): Promise<VlmTextCorrectionResult> {\n this.logger.info('[VlmTextCorrector] Starting text correction...');\n\n const resultPath = join(outputDir, 'result.json');\n const doc: DoclingDocument = JSON.parse(readFileSync(resultPath, 'utf-8'));\n\n let pageNumbers = this.getPageNumbers(doc);\n if (pageNumbers.length === 0) {\n this.logger.info('[VlmTextCorrector] No pages to process');\n return {\n textCorrections: 0,\n cellCorrections: 0,\n pagesProcessed: 0,\n pagesFailed: 0,\n };\n }\n\n if (\n options?.koreanHanjaMixPages &&\n options.koreanHanjaMixPages.length > 0\n ) {\n const totalPageCount = pageNumbers.length;\n const hanjaSet = new Set(options.koreanHanjaMixPages);\n pageNumbers = pageNumbers.filter((p) => hanjaSet.has(p));\n this.logger.info(\n `[VlmTextCorrector] Filtering to ${pageNumbers.length} Korean-Hanja mix pages out of ${totalPageCount} total`,\n );\n }\n\n const concurrency = options?.concurrency ?? DEFAULT_CONCURRENCY;\n this.logger.info(\n `[VlmTextCorrector] Processing ${pageNumbers.length} pages (concurrency: ${concurrency})...`,\n );\n\n const results = await ConcurrentPool.run(\n pageNumbers,\n concurrency,\n (pageNo) => this.correctPage(outputDir, doc, pageNo, model, options),\n () => {\n if (options?.onTokenUsage && options?.aggregator) {\n options.onTokenUsage(\n options.aggregator.getReport() as TokenUsageReport,\n );\n }\n },\n );\n\n // Aggregate results\n let totalTextCorrections = 0;\n let totalCellCorrections = 0;\n let pagesFailed = 0;\n\n for (const result of results) {\n if (result === null) {\n pagesFailed++;\n } else {\n totalTextCorrections += result.tc.length;\n totalCellCorrections += result.cc.length;\n }\n }\n\n // Apply corrections to document\n for (let i = 0; i < pageNumbers.length; i++) {\n const corrections = results[i];\n if (corrections === null) continue;\n this.applyCorrections(doc, pageNumbers[i], corrections);\n }\n\n // Save corrected document\n writeFileSync(resultPath, JSON.stringify(doc, null, 2));\n\n this.logger.info(\n `[VlmTextCorrector] Correction complete: ${totalTextCorrections} text, ${totalCellCorrections} cell corrections across ${pageNumbers.length} pages (${pagesFailed} failed)`,\n );\n\n return {\n textCorrections: totalTextCorrections,\n cellCorrections: totalCellCorrections,\n pagesProcessed: pageNumbers.length,\n pagesFailed,\n };\n }\n\n /**\n * Get sorted page numbers from the document.\n */\n private getPageNumbers(doc: DoclingDocument): number[] {\n return Object.values(doc.pages)\n .map((p) => p.page_no)\n .sort((a, b) => a - b);\n }\n\n /**\n * Correct text on a single page via VLM.\n * Returns null if VLM call fails (graceful degradation).\n */\n private async correctPage(\n outputDir: string,\n doc: DoclingDocument,\n pageNo: number,\n model: LanguageModel,\n options?: VlmTextCorrectorOptions,\n ): Promise<VlmTextCorrectionOutput | null> {\n try {\n const pageTexts = this.getPageTexts(doc, pageNo);\n const pageTables = this.getPageTables(doc, pageNo);\n\n // Skip pages with no text or table content\n if (pageTexts.length === 0 && pageTables.length === 0) {\n this.logger.debug(\n `[VlmTextCorrector] Page ${pageNo}: no text content, skipping`,\n );\n return { tc: [], cc: [] };\n }\n\n const imageBase64 = this.readPageImage(outputDir, pageNo);\n\n const pageText = options?.pageTexts?.get(pageNo);\n let references: Map<number, string> | undefined;\n let tableContext: string | undefined;\n\n if (pageText) {\n const { references: refs, unusedBlocks } =\n this.matchTextToReferenceWithUnused(pageTexts, pageText);\n references = refs;\n\n if (pageTables.length > 0 && unusedBlocks.length > 0) {\n tableContext = unusedBlocks.join('\\n');\n }\n }\n\n const userPrompt = this.buildUserPrompt(\n pageTexts,\n pageTables,\n references,\n tableContext,\n );\n\n const systemPrompt = this.buildLanguageAwareSystemPrompt(\n options?.documentLanguages,\n );\n const fullPrompt = systemPrompt + '\\n\\n' + userPrompt;\n\n const result = await LLMCaller.callVision({\n schema: vlmTextCorrectionSchema as any,\n messages: [\n {\n role: 'user' as const,\n content: [\n {\n type: 'text' as const,\n text: fullPrompt,\n },\n {\n type: 'image' as const,\n image: `data:image/png;base64,${imageBase64}`,\n },\n ],\n },\n ],\n primaryModel: model,\n maxRetries: options?.maxRetries ?? DEFAULT_MAX_RETRIES,\n temperature: options?.temperature ?? DEFAULT_TEMPERATURE,\n abortSignal: options?.abortSignal,\n component: 'VlmTextCorrector',\n phase: 'text-correction',\n });\n\n if (options?.aggregator) {\n options.aggregator.track(result.usage);\n }\n\n const output = result.output as VlmTextCorrectionOutput;\n\n if (output.tc.length > 0 || output.cc.length > 0) {\n this.logger.debug(\n `[VlmTextCorrector] Page ${pageNo}: ${output.tc.length} text, ${output.cc.length} cell corrections`,\n );\n }\n\n return output;\n } catch (error) {\n // Rethrow abort errors\n if (options?.abortSignal?.aborted) {\n throw error;\n }\n\n this.logger.warn(\n `[VlmTextCorrector] Page ${pageNo}: VLM correction failed, keeping OCR text`,\n error,\n );\n return null;\n }\n }\n\n /**\n * Get text items on a specific page, with their indices for prompt building.\n */\n private getPageTexts(\n doc: DoclingDocument,\n pageNo: number,\n ): Array<{ index: number; item: DoclingTextItem }> {\n const results: Array<{ index: number; item: DoclingTextItem }> = [];\n\n for (let i = 0; i < doc.texts.length; i++) {\n const item = doc.texts[i];\n if (!TEXT_LABELS.has(item.label)) continue;\n if (item.prov.some((p) => p.page_no === pageNo)) {\n results.push({ index: i, item });\n }\n }\n\n return results;\n }\n\n /**\n * Get table items on a specific page, with their indices.\n */\n private getPageTables(\n doc: DoclingDocument,\n pageNo: number,\n ): Array<{ index: number; item: DoclingTableItem }> {\n const results: Array<{ index: number; item: DoclingTableItem }> = [];\n\n for (let i = 0; i < doc.tables.length; i++) {\n const item = doc.tables[i];\n if (item.prov.some((p) => p.page_no === pageNo)) {\n results.push({ index: i, item });\n }\n }\n\n return results;\n }\n\n /**\n * Build compact user prompt for a page.\n *\n * Format:\n * T:\n * 0|sh|제1장 조사개요\n * 1|tx|본 보고서는 ...\n * C:\n * 0|0,0|유구명\n * 0|1,0|1호 住居址\n */\n buildUserPrompt(\n pageTexts: Array<{ index: number; item: DoclingTextItem }>,\n pageTables: Array<{ index: number; item: DoclingTableItem }>,\n references?: Map<number, string>,\n tableContext?: string,\n ): string {\n const parts: string[] = [];\n\n if (pageTexts.length > 0) {\n const textLines: string[] = [];\n pageTexts.forEach((entry, promptIndex) => {\n const typeCode = LABEL_TO_TYPE_CODE[entry.item.label] ?? 'tx';\n textLines.push(`${promptIndex}|${typeCode}|${entry.item.text}`);\n const ref = references?.get(promptIndex);\n if (ref) {\n textLines.push(`${promptIndex}|ref|${ref}`);\n }\n });\n parts.push('T:\\n' + textLines.join('\\n'));\n }\n\n if (pageTables.length > 0) {\n const cellLines: string[] = [];\n for (\n let tablePromptIndex = 0;\n tablePromptIndex < pageTables.length;\n tablePromptIndex++\n ) {\n const table = pageTables[tablePromptIndex].item;\n for (const cell of table.data.table_cells) {\n if (!cell.text || cell.text.trim().length === 0) continue;\n cellLines.push(\n `${tablePromptIndex}|${cell.start_row_offset_idx},${cell.start_col_offset_idx}|${cell.text}`,\n );\n }\n }\n if (cellLines.length > 0) {\n const cellSection = 'C:\\n' + cellLines.join('\\n');\n if (tableContext) {\n parts.push(cellSection + '\\nC_REF:\\n' + tableContext);\n } else {\n parts.push(cellSection);\n }\n }\n }\n\n return parts.join('\\n');\n }\n\n /**\n * Build a language-aware system prompt by prepending language context.\n */\n private buildLanguageAwareSystemPrompt(documentLanguages?: string[]): string {\n if (!documentLanguages?.length) {\n return TEXT_CORRECTION_SYSTEM_PROMPT;\n }\n const primaryBase = documentLanguages[0].split('-')[0];\n const primaryName =\n LANGUAGE_DISPLAY_NAMES[primaryBase] ?? documentLanguages[0];\n const otherNames = documentLanguages\n .slice(1)\n .map((code) => LANGUAGE_DISPLAY_NAMES[code.split('-')[0]] ?? code);\n const languageDesc =\n otherNames.length > 0\n ? `primarily written in ${primaryName}, with ${otherNames.join(', ')} also present`\n : `written in ${primaryName}`;\n const prefix =\n `LANGUAGE CONTEXT: This document is ${languageDesc}. ` +\n 'Focus on correcting characters that do not match this language.\\n\\n';\n return prefix + TEXT_CORRECTION_SYSTEM_PROMPT;\n }\n\n /**\n * Match pdftotext paragraph blocks to OCR elements using character multiset overlap.\n * Returns a map from prompt index to the best-matching reference block.\n */\n matchTextToReference(\n pageTexts: Array<{ index: number; item: DoclingTextItem }>,\n pageText: string,\n ): Map<number, string> {\n return this.matchTextToReferenceWithUnused(pageTexts, pageText).references;\n }\n\n /**\n * Match pdftotext paragraph blocks to OCR elements and also return unused blocks.\n * Unused blocks are those that were not consumed by any text element match.\n */\n private matchTextToReferenceWithUnused(\n pageTexts: Array<{ index: number; item: DoclingTextItem }>,\n pageText: string,\n ): { references: Map<number, string>; unusedBlocks: string[] } {\n const references = new Map<number, string>();\n\n const refBlocks = this.mergeIntoBlocks(pageText);\n\n if (refBlocks.length === 0) {\n return { references, unusedBlocks: [] };\n }\n\n const available = new Set(refBlocks.map((_, i) => i));\n\n for (let promptIndex = 0; promptIndex < pageTexts.length; promptIndex++) {\n const ocrText = pageTexts[promptIndex].item.text;\n\n let bestScore = 0;\n let bestBlockIndex = -1;\n\n for (const blockIndex of available) {\n const score = this.computeCharOverlap(ocrText, refBlocks[blockIndex]);\n if (score > bestScore) {\n bestScore = score;\n bestBlockIndex = blockIndex;\n }\n }\n\n if (bestBlockIndex >= 0 && bestScore >= REFERENCE_MATCH_THRESHOLD) {\n if (refBlocks[bestBlockIndex] !== ocrText) {\n references.set(promptIndex, refBlocks[bestBlockIndex]);\n }\n available.delete(bestBlockIndex);\n }\n }\n\n const unusedBlocks = [...available]\n .sort((a, b) => a - b)\n .map((i) => refBlocks[i]);\n\n return { references, unusedBlocks };\n }\n\n /**\n * Merge pdftotext output into paragraph blocks separated by blank lines.\n * Consecutive non-empty lines are joined with a space.\n */\n private mergeIntoBlocks(pageText: string): string[] {\n const blocks: string[] = [];\n let currentLines: string[] = [];\n\n for (const rawLine of pageText.split('\\n')) {\n const trimmed = rawLine.trim();\n if (trimmed.length === 0) {\n if (currentLines.length > 0) {\n blocks.push(currentLines.join(' '));\n currentLines = [];\n }\n } else {\n currentLines.push(trimmed);\n }\n }\n if (currentLines.length > 0) {\n blocks.push(currentLines.join(' '));\n }\n\n return blocks;\n }\n\n /**\n * Compute character multiset overlap ratio between two strings.\n * Returns a value between 0.0 and 1.0.\n */\n private computeCharOverlap(a: string, b: string): number {\n if (a.length === 0 || b.length === 0) return 0;\n\n const freqA = new Map<string, number>();\n for (const ch of a) {\n freqA.set(ch, (freqA.get(ch) ?? 0) + 1);\n }\n\n const freqB = new Map<string, number>();\n for (const ch of b) {\n freqB.set(ch, (freqB.get(ch) ?? 0) + 1);\n }\n\n let overlap = 0;\n for (const [ch, countA] of freqA) {\n const countB = freqB.get(ch) ?? 0;\n overlap += Math.min(countA, countB);\n }\n\n return overlap / Math.max(a.length, b.length);\n }\n\n /**\n * Read page image as base64.\n * Page images are 0-indexed: page_no N → pages/page_{N-1}.png\n */\n private readPageImage(outputDir: string, pageNo: number): string {\n const imagePath = join(outputDir, 'pages', `page_${pageNo - 1}.png`);\n return readFileSync(imagePath).toString('base64');\n }\n\n /**\n * Apply VLM corrections to the DoclingDocument.\n */\n private applyCorrections(\n doc: DoclingDocument,\n pageNo: number,\n corrections: VlmTextCorrectionOutput,\n ): void {\n // Apply text corrections (substitution-based)\n if (corrections.tc.length > 0) {\n const pageTexts = this.getPageTexts(doc, pageNo);\n for (const correction of corrections.tc) {\n if (correction.i >= 0 && correction.i < pageTexts.length) {\n const docIndex = pageTexts[correction.i].index;\n let text = doc.texts[docIndex].text;\n for (const sub of correction.s) {\n const idx = text.indexOf(sub.f);\n if (idx >= 0) {\n text =\n text.substring(0, idx) +\n sub.r +\n text.substring(idx + sub.f.length);\n } else {\n this.logger.warn(\n `[VlmTextCorrector] Page ${pageNo}, text ${correction.i}: ` +\n `find string not found, skipping substitution`,\n );\n }\n }\n if (text !== doc.texts[docIndex].text) {\n doc.texts[docIndex].text = text;\n doc.texts[docIndex].orig = text;\n }\n }\n }\n }\n\n // Apply cell corrections\n if (corrections.cc.length > 0) {\n const pageTables = this.getPageTables(doc, pageNo);\n for (const correction of corrections.cc) {\n if (correction.ti >= 0 && correction.ti < pageTables.length) {\n const table = pageTables[correction.ti].item;\n\n // Update table_cells\n for (const cell of table.data.table_cells) {\n if (\n cell.start_row_offset_idx === correction.r &&\n cell.start_col_offset_idx === correction.c\n ) {\n cell.text = correction.t;\n break;\n }\n }\n\n // Sync grid cell (grid stores separate objects from table_cells)\n const gridRow = table.data.grid[correction.r];\n if (gridRow) {\n const gridCell = gridRow[correction.c];\n if (gridCell) {\n gridCell.text = correction.t;\n }\n }\n }\n }\n }\n }\n}\n","import { z } from 'zod/v4';\n\n/**\n * Zod schema for VLM text correction response.\n *\n * The VLM returns only corrections (items that differ from OCR output).\n * If all text is correct, both arrays are empty.\n *\n * Short field names are used to minimize output tokens:\n * - tc = text corrections (substitution-based)\n * - cc = cell corrections (full replacement)\n * - i = text element index\n * - s = substitutions array [{f: find, r: replace}]\n * - ti = table index (within the page)\n * - r = row index\n * - c = column index\n * - t = corrected text (for cell corrections)\n */\nexport const vlmTextCorrectionSchema = z.object({\n /** Text element corrections (substitution-based) */\n tc: z.array(\n z.object({\n /** Text element index (from prompt) */\n i: z.number().int().nonnegative(),\n /** Substitutions: find/replace pairs applied left-to-right */\n s: z.array(\n z.object({\n /** Exact garbled substring to find */\n f: z.string(),\n /** Corrected replacement text */\n r: z.string(),\n }),\n ),\n }),\n ),\n /** Table cell corrections */\n cc: z.array(\n z.object({\n /** Table index (within the page) */\n ti: z.number().int().nonnegative(),\n /** Row index */\n r: z.number().int().nonnegative(),\n /** Column index */\n c: z.number().int().nonnegative(),\n /** Corrected cell text */\n t: z.string(),\n }),\n ),\n});\n\n/** Type inferred from the VLM text correction schema */\nexport type VlmTextCorrectionOutput = z.infer<typeof vlmTextCorrectionSchema>;\n","import type { LoggerMethods } from '@heripo/logger';\nimport type { Bcp47LanguageTag, OcrStrategy } from '@heripo/model';\nimport type { LLMTokenUsageAggregator } from '@heripo/shared';\nimport type { LanguageModel } from 'ai';\n\nimport type { PageRenderer } from '../processors/page-renderer';\n\nimport { normalizeToBcp47 } from '@heripo/model';\nimport { LLMCaller } from '@heripo/shared';\nimport { readFileSync } from 'node:fs';\nimport { z } from 'zod/v4';\n\nimport { PAGE_RENDERING } from '../config/constants.js';\nimport { PdfTextExtractor } from '../processors/pdf-text-extractor';\n\n/** Ratio of pages to trim from front and back (covers, TOC, appendices) */\nconst EDGE_TRIM_RATIO = 0.1;\n\n/** Default maximum number of pages to sample */\nconst DEFAULT_MAX_SAMPLE_PAGES = 15;\n\n/** Default max retries per VLM call */\nconst DEFAULT_MAX_RETRIES = 3;\n\n/** Regex to detect CJK Unified Ideographs (Hanja/Kanji/Hanzi) */\nconst CJK_REGEX = /[\\u4E00-\\u9FFF]/;\n\n/** Regex to detect Hangul syllables */\nconst HANGUL_REGEX = /[\\uAC00-\\uD7AF]/;\n\n/** Zod schema for VLM Korean-Hanja mix detection response */\nconst koreanHanjaMixSchema = z.object({\n hasKoreanHanjaMix: z\n .boolean()\n .describe(\n 'Whether the page contains any Hanja (漢字/Chinese characters) mixed with Korean text',\n ),\n detectedLanguages: z\n .array(z.string())\n .describe(\n 'BCP 47 language tags of languages found on this page, ordered by prevalence (e.g., [\"ko-KR\", \"en-US\"])',\n ),\n});\n\n/** System prompt for Korean-Hanja mix detection */\nconst KOREAN_HANJA_MIX_PROMPT = `Look at this page image carefully. Does it contain any Hanja (漢字/Chinese characters) mixed with Korean text?\n\nHanja examples: 遺蹟, 發掘, 調査, 報告書, 文化財\nNote: Hanja are Chinese characters used in Korean documents, different from modern Korean (한글).\n\nAnswer whether any Hanja characters are present on this page.\n\nAlso identify all languages present on this page. Return an array of BCP 47 language tags ordered by prevalence (primary language first).\nExamples: [\"ko-KR\", \"en-US\"], [\"ja-JP\"], [\"zh-TW\", \"en-US\"]`;\n\n/** Options for OcrStrategySampler */\nexport interface OcrStrategySamplerOptions {\n /** Maximum number of pages to sample (default: 15) */\n maxSamplePages?: number;\n /** Maximum retries per VLM call (default: 3) */\n maxRetries?: number;\n /** Temperature for VLM generation (default: 0) */\n temperature?: number;\n /** Abort signal for cancellation */\n abortSignal?: AbortSignal;\n /** Fallback model for retry after primary exhausts maxRetries */\n fallbackModel?: LanguageModel;\n /** Token usage aggregator for tracking */\n aggregator?: LLMTokenUsageAggregator;\n}\n\n/**\n * Samples pages from a PDF to determine whether to use ocrmac or VLM for processing.\n *\n * First attempts to detect Hangul-Hanja mix directly from the PDF text layer using\n * pdftotext (zero-cost, high accuracy for PDFs with embedded text). Only falls back\n * to VLM-based image analysis for image-only PDFs without a text layer.\n *\n * VLM fallback sampling strategy:\n * - Trim front/back 10% of pages (covers, TOC, appendices)\n * - Select up to 15 pages evenly distributed across the eligible range\n * - Early exit on first Korean-Hanja mix detection\n */\nexport class OcrStrategySampler {\n private readonly logger: LoggerMethods;\n private readonly pageRenderer: PageRenderer;\n private readonly textExtractor: PdfTextExtractor;\n\n constructor(\n logger: LoggerMethods,\n pageRenderer: PageRenderer,\n textExtractor?: PdfTextExtractor,\n ) {\n this.logger = logger;\n this.pageRenderer = pageRenderer;\n this.textExtractor = textExtractor ?? new PdfTextExtractor(logger);\n }\n\n /**\n * Sample pages from a PDF and determine the OCR strategy.\n *\n * @param pdfPath - Path to the PDF file\n * @param outputDir - Directory for temporary rendered pages\n * @param model - Vision language model for Korean-Hanja mix detection\n * @param options - Sampling options\n * @returns OcrStrategy with method ('ocrmac' or 'vlm') and metadata\n */\n async sample(\n pdfPath: string,\n outputDir: string,\n model: LanguageModel,\n options?: OcrStrategySamplerOptions,\n ): Promise<OcrStrategy> {\n const maxSamplePages = options?.maxSamplePages ?? DEFAULT_MAX_SAMPLE_PAGES;\n\n this.logger.info('[OcrStrategySampler] Starting OCR strategy sampling...');\n\n // Step 1: Try text layer pre-check (zero-cost Hangul-Hanja detection)\n const preCheckResult = await this.preCheckHanjaFromTextLayer(pdfPath);\n if (preCheckResult) {\n return preCheckResult;\n }\n\n // Step 2: Render pages at medium DPI for VLM analysis (image-only PDFs)\n const renderResult = await this.pageRenderer.renderPages(\n pdfPath,\n outputDir,\n { dpi: PAGE_RENDERING.SAMPLE_DPI },\n );\n\n if (renderResult.pageCount === 0) {\n this.logger.info('[OcrStrategySampler] No pages found in PDF');\n return {\n method: 'ocrmac',\n reason: 'No pages found in PDF',\n sampledPages: 0,\n totalPages: 0,\n };\n }\n\n // Step 3: Select sample page indices\n const sampleIndices = this.selectSamplePages(\n renderResult.pageCount,\n maxSamplePages,\n );\n\n this.logger.info(\n `[OcrStrategySampler] Sampling ${sampleIndices.length} of ${renderResult.pageCount} pages: [${sampleIndices.map((i) => i + 1).join(', ')}]`,\n );\n\n // Step 4: Check each sample page for Korean-Hanja mix (early exit on detection)\n let sampledCount = 0;\n const languageFrequency = new Map<Bcp47LanguageTag, number>();\n for (const idx of sampleIndices) {\n sampledCount++;\n const pageFile = renderResult.pageFiles[idx];\n const pageAnalysis = await this.analyzeSamplePage(\n pageFile,\n idx + 1,\n model,\n options,\n );\n\n for (const lang of pageAnalysis.detectedLanguages) {\n languageFrequency.set(lang, (languageFrequency.get(lang) ?? 0) + 1);\n }\n\n if (pageAnalysis.hasKoreanHanjaMix) {\n this.logger.info(\n `[OcrStrategySampler] Korean-Hanja mix detected on page ${idx + 1} → VLM strategy`,\n );\n const detectedLanguages = this.aggregateLanguages(languageFrequency);\n return {\n method: 'vlm',\n detectedLanguages,\n reason: `Korean-Hanja mix detected on page ${idx + 1}`,\n sampledPages: sampledCount,\n totalPages: renderResult.pageCount,\n };\n }\n }\n\n // Step 5: No Korean-Hanja mix found → ocrmac\n this.logger.info(\n '[OcrStrategySampler] No Korean-Hanja mix detected → ocrmac strategy',\n );\n const detectedLanguages = this.aggregateLanguages(languageFrequency);\n return {\n method: 'ocrmac',\n detectedLanguages,\n reason: `No Korean-Hanja mix detected in ${sampledCount} sampled pages`,\n sampledPages: sampledCount,\n totalPages: renderResult.pageCount,\n };\n }\n\n /**\n * Pre-check for Hangul-Hanja mix in PDF text layer using pdftotext.\n * Extracts full document text in a single process and checks at document level.\n * Only makes a definitive decision for Korean (Hangul) documents:\n * - Hangul + Hanja (anywhere in document) → VLM (confirmed Korean-Hanja mix)\n * - Hangul only → ocrmac with ko-KR (confirmed Korean)\n * - No Hangul (English, Japanese, etc.) → null (delegates to VLM for language detection)\n */\n private async preCheckHanjaFromTextLayer(\n pdfPath: string,\n ): Promise<OcrStrategy | null> {\n try {\n const totalPages = await this.textExtractor.getPageCount(pdfPath);\n if (totalPages === 0) return null;\n\n const fullText = await this.textExtractor.extractFullText(pdfPath);\n if (fullText.trim().length === 0) {\n this.logger.debug(\n '[OcrStrategySampler] No Hangul in text layer, falling back to VLM sampling',\n );\n return null;\n }\n\n const hasHangul = HANGUL_REGEX.test(fullText);\n const hasHanja = CJK_REGEX.test(fullText);\n\n if (!hasHangul) {\n this.logger.debug(\n '[OcrStrategySampler] No Hangul in text layer, falling back to VLM sampling',\n );\n return null;\n }\n\n if (hasHanja) {\n const pageTextArray = fullText.split('\\f');\n const koreanHanjaMixPages: number[] = [];\n for (let i = 0; i < pageTextArray.length; i++) {\n if (CJK_REGEX.test(pageTextArray[i])) {\n koreanHanjaMixPages.push(i + 1); // 1-based\n }\n }\n\n this.logger.info(\n `[OcrStrategySampler] Hangul-Hanja mix detected in text layer → VLM strategy (${koreanHanjaMixPages.length} pages with Hanja)`,\n );\n return {\n method: 'vlm',\n detectedLanguages: ['ko-KR'],\n reason: 'Hangul-Hanja mix found in PDF text layer',\n koreanHanjaMixPages,\n sampledPages: totalPages,\n totalPages,\n };\n }\n\n this.logger.info(\n '[OcrStrategySampler] No Hangul-Hanja mix in text layer → ocrmac strategy',\n );\n return {\n method: 'ocrmac',\n detectedLanguages: ['ko-KR'],\n reason: `No Hangul-Hanja mix in PDF text layer (${totalPages} pages checked)`,\n sampledPages: totalPages,\n totalPages,\n };\n } catch {\n this.logger.debug(\n '[OcrStrategySampler] Text layer pre-check failed, falling back to VLM sampling',\n );\n return null;\n }\n }\n\n /**\n * Select page indices for sampling.\n * Trims front/back edges and distributes samples evenly.\n *\n * @param totalPages - Total number of pages\n * @param maxSamples - Maximum number of samples\n * @returns Array of 0-based page indices\n */\n selectSamplePages(totalPages: number, maxSamples: number): number[] {\n if (totalPages === 0) return [];\n\n // For very small documents, sample all pages\n if (totalPages <= maxSamples) {\n return Array.from({ length: totalPages }, (_, i) => i);\n }\n\n // Trim front/back edges\n const trimCount = Math.max(1, Math.ceil(totalPages * EDGE_TRIM_RATIO));\n const start = trimCount;\n const end = totalPages - trimCount;\n const eligibleCount = end - start;\n\n // If trimming leaves no eligible pages, use middle page\n if (eligibleCount <= 0) {\n return [Math.floor(totalPages / 2)];\n }\n\n // If eligible pages fit within maxSamples, use all\n if (eligibleCount <= maxSamples) {\n return Array.from({ length: eligibleCount }, (_, i) => start + i);\n }\n\n // Distribute samples evenly across eligible range\n const indices: number[] = [];\n const step = eligibleCount / maxSamples;\n for (let i = 0; i < maxSamples; i++) {\n indices.push(start + Math.floor(i * step));\n }\n return indices;\n }\n\n /**\n * Analyze a single sample page for Korean-Hanja mixed script and primary language.\n * Normalizes raw VLM language responses to valid BCP 47 tags, filtering out invalid ones.\n *\n * @returns Object with Korean-Hanja detection result and normalized detected languages\n */\n private async analyzeSamplePage(\n pageFile: string,\n pageNo: number,\n model: LanguageModel,\n options?: OcrStrategySamplerOptions,\n ): Promise<{\n hasKoreanHanjaMix: boolean;\n detectedLanguages: Bcp47LanguageTag[];\n }> {\n this.logger.debug(\n `[OcrStrategySampler] Analyzing page ${pageNo} for Korean-Hanja mix and language...`,\n );\n\n const base64Image = readFileSync(pageFile).toString('base64');\n\n const messages = [\n {\n role: 'user' as const,\n content: [\n { type: 'text' as const, text: KOREAN_HANJA_MIX_PROMPT },\n {\n type: 'image' as const,\n image: `data:image/png;base64,${base64Image}`,\n },\n ],\n },\n ];\n\n const result = await LLMCaller.callVision({\n schema: koreanHanjaMixSchema as any,\n messages,\n primaryModel: model,\n fallbackModel: options?.fallbackModel,\n maxRetries: options?.maxRetries ?? DEFAULT_MAX_RETRIES,\n temperature: options?.temperature ?? 0,\n abortSignal: options?.abortSignal,\n component: 'OcrStrategySampler',\n phase: 'korean-hanja-mix-detection',\n });\n\n if (options?.aggregator) {\n options.aggregator.track(result.usage);\n }\n\n const output = result.output as {\n hasKoreanHanjaMix: boolean;\n detectedLanguages: string[];\n };\n\n const normalizedLanguages = output.detectedLanguages\n .map(normalizeToBcp47)\n .filter((tag): tag is Bcp47LanguageTag => tag !== null);\n\n this.logger.debug(\n `[OcrStrategySampler] Page ${pageNo}: hasKoreanHanjaMix=${output.hasKoreanHanjaMix}, detectedLanguages=${normalizedLanguages.join(',')}`,\n );\n\n return {\n hasKoreanHanjaMix: output.hasKoreanHanjaMix,\n detectedLanguages: normalizedLanguages,\n };\n }\n\n /**\n * Aggregate language frequency map into a sorted array.\n * Returns languages sorted by frequency (descending), or undefined if empty.\n */\n private aggregateLanguages(\n frequencyMap: Map<Bcp47LanguageTag, number>,\n ): Bcp47LanguageTag[] | undefined {\n if (frequencyMap.size === 0) return undefined;\n\n return [...frequencyMap.entries()]\n .sort((a, b) => b[1] - a[1])\n .map(([lang]) => lang);\n }\n}\n","import type { Server } from 'node:http';\n\nimport { createReadStream, statSync } from 'node:fs';\nimport { createServer } from 'node:http';\nimport { basename } from 'node:path';\n\n/**\n * Simple local HTTP server for serving a single file\n *\n * Used to serve local PDF files to docling-serve which requires HTTP URLs.\n */\nexport class LocalFileServer {\n private server: Server | null = null;\n private port: number = 0;\n\n /**\n * Start serving a file and return the URL\n *\n * @param filePath Absolute path to the file to serve\n * @returns URL to access the file\n */\n async start(filePath: string): Promise<string> {\n const filename = basename(filePath);\n const stat = statSync(filePath);\n\n return new Promise((resolve, reject) => {\n this.server = createServer((req, res) => {\n if (req.url === `/${filename}`) {\n res.writeHead(200, {\n 'Content-Type': 'application/pdf',\n 'Content-Length': stat.size,\n });\n createReadStream(filePath).pipe(res);\n } else {\n res.writeHead(404);\n res.end('Not Found');\n }\n });\n\n this.server.on('error', reject);\n\n // Listen on random available port\n this.server.listen(0, '127.0.0.1', () => {\n const address = this.server!.address();\n if (typeof address === 'object' && address !== null) {\n this.port = address.port;\n resolve(`http://127.0.0.1:${this.port}/${filename}`);\n } else {\n reject(new Error('Failed to get server address'));\n }\n });\n });\n }\n\n /**\n * Stop the server\n */\n stop(): Promise<void> {\n return new Promise((resolve) => {\n if (this.server) {\n this.server.close(() => {\n this.server = null;\n this.port = 0;\n resolve();\n });\n } else {\n resolve();\n }\n });\n }\n}\n","import type { LoggerMethods } from '@heripo/logger';\n\nimport { spawnAsync } from '@heripo/shared';\nimport { existsSync, rmSync } from 'node:fs';\nimport { tmpdir } from 'node:os';\nimport { join } from 'node:path';\n\nimport { IMAGE_PDF_CONVERTER } from '../config/constants';\n\n/**\n * Utility class for converting PDF to image-based PDF using ImageMagick.\n * Used as a fallback when regular PDF conversion fails due to encoding issues.\n *\n * ## System Requirements\n * - ImageMagick (`brew install imagemagick`)\n * - Ghostscript (`brew install ghostscript`)\n */\nexport class ImagePdfConverter {\n constructor(private readonly logger: LoggerMethods) {}\n\n /**\n * Convert a PDF file to an image-based PDF.\n * Downloads the PDF from URL, converts it using ImageMagick, and returns the path.\n *\n * @param pdfUrl - URL of the source PDF\n * @param reportId - Report identifier for temp file naming\n * @returns Path to the converted image PDF in temp directory\n */\n async convert(pdfUrl: string, reportId: string): Promise<string> {\n const timestamp = Date.now();\n const tempDir = tmpdir();\n const inputPath = join(tempDir, `${reportId}-${timestamp}-input.pdf`);\n const outputPath = join(tempDir, `${reportId}-${timestamp}-image.pdf`);\n\n try {\n this.logger.info('[ImagePdfConverter] Downloading PDF from URL...');\n await this.downloadPdf(pdfUrl, inputPath);\n\n this.logger.info('[ImagePdfConverter] Converting to image PDF...');\n await this.convertToImagePdf(inputPath, outputPath);\n\n this.logger.info('[ImagePdfConverter] Image PDF created:', outputPath);\n return outputPath;\n } finally {\n // Always cleanup input file\n if (existsSync(inputPath)) {\n rmSync(inputPath, { force: true });\n }\n }\n }\n\n /**\n * Download PDF from URL to local path using curl\n */\n private async downloadPdf(url: string, outputPath: string): Promise<void> {\n const result = await spawnAsync('curl', [\n '-L', // Follow redirects\n '-o',\n outputPath,\n '-s', // Silent mode\n '--fail', // Fail on HTTP errors\n url,\n ]);\n\n if (result.code !== 0) {\n throw new Error(\n `Failed to download PDF: ${result.stderr || 'Unknown error'}`,\n );\n }\n }\n\n /**\n * Convert PDF to image-based PDF using ImageMagick\n */\n private async convertToImagePdf(\n inputPath: string,\n outputPath: string,\n ): Promise<void> {\n const result = await spawnAsync('magick', [\n '-density',\n IMAGE_PDF_CONVERTER.DENSITY.toString(),\n inputPath,\n '-quality',\n IMAGE_PDF_CONVERTER.QUALITY.toString(),\n outputPath,\n ]);\n\n if (result.code !== 0) {\n throw new Error(\n `Failed to convert PDF to image PDF: ${result.stderr || 'Unknown error'}`,\n );\n }\n }\n\n /**\n * Cleanup the temporary image PDF file\n */\n cleanup(imagePdfPath: string): void {\n if (existsSync(imagePdfPath)) {\n this.logger.info(\n '[ImagePdfConverter] Cleaning up temp file:',\n imagePdfPath,\n );\n rmSync(imagePdfPath, { force: true });\n }\n }\n}\n","import type {\n VlmPageElement,\n VlmQualityIssueType,\n} from '../types/vlm-page-result';\n\n/** A single quality issue found during validation */\nexport interface VlmQualityIssue {\n /** Type of issue detected */\n type: VlmQualityIssueType;\n /** Human-readable description of the issue */\n message: string;\n /** Element reading-order indices that triggered the issue */\n affectedElements: number[];\n}\n\n/** Result of VLM response quality validation */\nexport interface VlmValidationResult {\n /** Whether the response passes quality validation */\n isValid: boolean;\n /** List of quality issues found (empty if valid) */\n issues: VlmQualityIssue[];\n}\n\n/** Minimum non-whitespace characters required for script anomaly check */\nconst MIN_CONTENT_LENGTH = 20;\n\n/**\n * Minimum ratio of Hangul/CJK characters when primary language starts with 'ko'.\n * 10% is very permissive — catches pure-Latin hallucinations while\n * allowing mixed content with numbers, coordinates, and English terms.\n */\nconst KOREAN_SCRIPT_RATIO_THRESHOLD = 0.1;\n\n/** Known placeholder text patterns (case-insensitive) */\nconst PLACEHOLDER_PATTERNS: RegExp[] = [\n /lorem\\s+ipsum/i,\n /dolor\\s+sit\\s+amet/i,\n /consectetur\\s+adipiscing/i,\n /sed\\s+do\\s+eiusmod/i,\n /ut\\s+labore\\s+et\\s+dolore/i,\n];\n\n/** Patterns indicating VLM described the image instead of transcribing text (Korean) */\nconst META_DESCRIPTION_PATTERNS_KO: RegExp[] = [\n /이미지\\s*해상도/,\n /판독하기?\\s*어렵/,\n /해상도가?\\s*(매우\\s*)?(낮|부족)/,\n /텍스트를?\\s*판독/,\n /글자를?\\s*읽기?\\s*어렵/,\n /정확한?\\s*판독이?\\s*(불가|어렵)/,\n];\n\n/** Patterns indicating VLM described the image instead of transcribing text (English) */\nconst META_DESCRIPTION_PATTERNS_EN: RegExp[] = [\n /the image contains/i,\n /unable to (read|transcribe)/i,\n /resolution.*(too low|insufficient)/i,\n /cannot (read|make out|decipher)/i,\n /text is (not |un)?(legible|readable)/i,\n /exact transcription is not possible/i,\n];\n\n/**\n * Minimum ratio of repetitive pattern characters to total content\n * for flagging as repetitive.\n */\nconst REPETITIVE_PATTERN_RATIO_THRESHOLD = 0.3;\n\n/** Minimum number of repetitions to consider a pattern repetitive */\nconst REPETITIVE_PATTERN_MIN_REPEATS = 5;\n\n/** Matches Hangul Syllables and Hangul Jamo */\nconst HANGUL_REGEX = /[\\uAC00-\\uD7AF\\u1100-\\u11FF]/g;\n\n/** Matches CJK Unified Ideographs (Hanja / Kanji / Hanzi) */\nconst CJK_REGEX = /[\\u4E00-\\u9FFF]/g;\n\n/**\n * Lightweight, stateless validator for VLM page extraction responses.\n *\n * Detects four categories of hallucination without any additional VLM calls:\n * 1. Placeholder text (Lorem ipsum and variants)\n * 2. Script anomaly (expected Korean but got Latin-only text)\n * 3. Meta description (VLM described the image instead of transcribing text)\n * 4. Repetitive pattern (repeated character patterns like `: : : : :`)\n */\nexport class VlmResponseValidator {\n /**\n * Validate VLM page result quality.\n *\n * @param elements - Extracted page elements to validate\n * @param documentLanguages - BCP 47 language tags (e.g., ['ko-KR', 'en-US'])\n * @returns Validation result with issues list\n */\n static validate(\n elements: VlmPageElement[],\n documentLanguages?: string[],\n ): VlmValidationResult {\n const issues: VlmQualityIssue[] = [];\n\n const textElements = elements.filter(\n (el) => el.type !== 'picture' && el.content.length > 0,\n );\n\n if (textElements.length === 0) {\n return { isValid: true, issues: [] };\n }\n\n const placeholderIssue = this.detectPlaceholderText(textElements);\n if (placeholderIssue) {\n issues.push(placeholderIssue);\n }\n\n if (documentLanguages?.[0]?.startsWith('ko')) {\n const scriptIssue = this.detectScriptAnomaly(textElements);\n if (scriptIssue) {\n issues.push(scriptIssue);\n }\n }\n\n const metaIssue = this.detectMetaDescription(textElements);\n if (metaIssue) {\n issues.push(metaIssue);\n }\n\n const repetitiveIssue = this.detectRepetitivePattern(textElements);\n if (repetitiveIssue) {\n issues.push(repetitiveIssue);\n }\n\n return { isValid: issues.length === 0, issues };\n }\n\n /**\n * Detect known placeholder / filler text in elements.\n */\n private static detectPlaceholderText(\n elements: VlmPageElement[],\n ): VlmQualityIssue | null {\n const affectedElements: number[] = [];\n\n for (const el of elements) {\n for (const pattern of PLACEHOLDER_PATTERNS) {\n if (pattern.test(el.content)) {\n affectedElements.push(el.order);\n break;\n }\n }\n }\n\n if (affectedElements.length === 0) return null;\n\n return {\n type: 'placeholder_text',\n message: `Detected placeholder text (e.g., Lorem ipsum) in ${affectedElements.length} element(s)`,\n affectedElements,\n };\n }\n\n /**\n * Detect script anomaly: expected Korean content but found mostly Latin text.\n * Counts Hangul + CJK characters and flags if the ratio is below threshold.\n */\n private static detectScriptAnomaly(\n elements: VlmPageElement[],\n ): VlmQualityIssue | null {\n const allContent = elements.map((el) => el.content).join('');\n const nonWhitespace = allContent.replace(/\\s/g, '');\n\n if (nonWhitespace.length < MIN_CONTENT_LENGTH) {\n return null;\n }\n\n const hangulCount = allContent.match(HANGUL_REGEX)?.length ?? 0;\n const cjkCount = allContent.match(CJK_REGEX)?.length ?? 0;\n const koreanCjkCount = hangulCount + cjkCount;\n const ratio = koreanCjkCount / nonWhitespace.length;\n\n if (ratio < KOREAN_SCRIPT_RATIO_THRESHOLD) {\n return {\n type: 'script_anomaly',\n message: `Expected Korean text but found ${(ratio * 100).toFixed(1)}% Korean/CJK characters (threshold: ${KOREAN_SCRIPT_RATIO_THRESHOLD * 100}%)`,\n affectedElements: elements.map((el) => el.order),\n };\n }\n\n return null;\n }\n\n /**\n * Detect meta description: VLM described the image/resolution instead\n * of transcribing actual text content.\n */\n private static detectMetaDescription(\n elements: VlmPageElement[],\n ): VlmQualityIssue | null {\n const affectedElements: number[] = [];\n const allPatterns = [\n ...META_DESCRIPTION_PATTERNS_KO,\n ...META_DESCRIPTION_PATTERNS_EN,\n ];\n\n for (const el of elements) {\n for (const pattern of allPatterns) {\n if (pattern.test(el.content)) {\n affectedElements.push(el.order);\n break;\n }\n }\n }\n\n if (affectedElements.length === 0) return null;\n\n return {\n type: 'meta_description',\n message: `Detected meta-description of image instead of text transcription in ${affectedElements.length} element(s)`,\n affectedElements,\n };\n }\n\n /**\n * Detect repetitive character patterns (e.g., `: : : : :` or `= = = = =`).\n * Flags when the same character repeats with spaces 5+ times and the\n * repetitive portion exceeds 30% of total content.\n */\n private static detectRepetitivePattern(\n elements: VlmPageElement[],\n ): VlmQualityIssue | null {\n const allContent = elements.map((el) => el.content).join('\\n');\n\n if (allContent.trim().length === 0) return null;\n\n // Match patterns like \"x x x x x\" where x is a non-whitespace character\n const repetitiveRegex = /(\\S)(\\s+\\1){4,}/g;\n let totalRepetitiveLength = 0;\n\n let match: RegExpExecArray | null;\n while ((match = repetitiveRegex.exec(allContent)) !== null) {\n const repeatedChar = match[1];\n // Count actual repetitions: split by the repeated character\n const segment = match[0];\n const parts = segment.split(/\\s+/).filter((p) => p === repeatedChar);\n /* v8 ignore start -- regex {4,} guarantees ≥5 parts; defensive guard only */\n if (parts.length >= REPETITIVE_PATTERN_MIN_REPEATS) {\n /* v8 ignore stop */\n totalRepetitiveLength += segment.length;\n }\n }\n\n if (totalRepetitiveLength === 0) return null;\n\n const ratio = totalRepetitiveLength / allContent.length;\n\n if (ratio < REPETITIVE_PATTERN_RATIO_THRESHOLD) return null;\n\n return {\n type: 'repetitive_pattern',\n message: `Detected repetitive character patterns (${(ratio * 100).toFixed(0)}% of content)`,\n affectedElements: elements.map((el) => el.order),\n };\n }\n}\n"],"mappings":"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","names":["import_node_child_process","import_node_os","import_node_path","Error","import_node_fs","import_promises","import_node_path","import_node_fs","import_node_path","import_promises","import_node_child_process","import_promises","err","import_node_fs","import_node_path","import_node_fs","import_node_path","import_node_fs","import_v4","DEFAULT_MAX_RETRIES","detectedLanguages","import_node_fs","import_node_path","import_node_fs","import_node_path","resultPath","HANGUL_REGEX","CJK_REGEX"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/core/pdf-parser.ts","../src/config/constants.ts","../../shared/src/utils/batch-processor.ts","../../shared/src/utils/concurrent-pool.ts","../../shared/src/utils/spawn-utils.ts","../../shared/src/utils/llm-caller.ts","../../shared/src/utils/provider-detector.ts","../../shared/src/utils/llm-token-usage-aggregator.ts","../src/environment/docling-environment.ts","../src/utils/python-version.ts","../src/core/pdf-converter.ts","../src/errors/image-pdf-fallback-error.ts","../src/processors/image-extractor.ts","../src/utils/jq.ts","../src/processors/page-renderer.ts","../src/processors/pdf-text-extractor.ts","../src/processors/vlm-text-corrector.ts","../src/types/vlm-text-correction-schema.ts","../src/samplers/ocr-strategy-sampler.ts","../src/utils/local-file-server.ts","../src/core/chunked-pdf-converter.ts","../src/processors/docling-document-merger.ts","../src/core/image-pdf-converter.ts","../src/validators/vlm-response-validator.ts"],"sourcesContent":["export { PDFParser } from './core/pdf-parser';\nexport type {\n ConversionCompleteCallback,\n ConvertWithStrategyResult,\n PDFConvertOptions,\n} from './core/pdf-converter';\nexport { ImagePdfFallbackError } from './errors/image-pdf-fallback-error';\nexport { VlmResponseValidator } from './validators/vlm-response-validator';\nexport type {\n VlmValidationResult,\n VlmQualityIssue,\n} from './validators/vlm-response-validator';\nexport type {\n VlmPageQuality,\n VlmQualityIssueType,\n} from './types/vlm-page-result';\n","import type { LoggerMethods } from '@heripo/logger';\nimport type { TokenUsageReport } from '@heripo/model';\nimport type { DoclingAPIClient } from 'docling-sdk';\n\nimport { Docling } from 'docling-sdk';\nimport { execSync } from 'node:child_process';\nimport { platform } from 'node:os';\nimport { join } from 'node:path';\n\nimport { PDF_PARSER } from '../config/constants';\nimport { DoclingEnvironment } from '../environment/docling-environment';\nimport {\n type ConversionCompleteCallback,\n type PDFConvertOptions,\n PDFConverter,\n} from './pdf-converter';\n\ntype Options = {\n logger: LoggerMethods;\n timeout?: number;\n venvPath?: string;\n killExistingProcess?: boolean;\n /**\n * Enable fallback to image-based PDF when conversion fails.\n * Only works with local server mode (port option).\n * Requires ImageMagick and Ghostscript to be installed.\n */\n enableImagePdfFallback?: boolean;\n} & ({ port?: number } | { baseUrl: string });\n\n/**\n * PDFParser - A PDF document parser using docling-serve\n *\n * ## System Requirements\n * Before using PDFParser, ensure your system meets these requirements:\n *\n * ### Operating System\n * - macOS 10.15 (Catalina) or later\n *\n * ### Required Software\n * - `python3` (version 3.9 - 3.12)\n * - Python 3.13+ is NOT compatible with docling-serve\n * - Recommended: Python 3.11 or 3.12\n * - Install specific version: `pyenv install 3.12.0 && pyenv global 3.12.0`\n * - `jq` - JSON processor\n * - Install: `brew install jq`\n * - `poppler` - PDF text extraction tools (pdftotext, pdfinfo)\n * - Install: `brew install poppler`\n * - `lsof` - List open files (usually pre-installed on macOS)\n *\n * ## Initialization Process\n * When `init()` is called, the following setup occurs automatically:\n *\n * ### If using external server (baseUrl provided):\n * 1. Connects to the provided baseUrl\n * 2. Waits for server health check (up to 120 seconds)\n *\n * ### If using local server (default):\n * 1. **Python Environment Setup**\n * - Verifies Python version compatibility (3.9-3.12)\n * - Creates Python virtual environment at `venvPath` (default: `.venv`)\n * - Verifies virtual environment Python version\n *\n * 2. **Dependency Installation**\n * - Upgrades pip to latest version\n * - Installs setuptools and wheel\n * - Installs pyarrow (binary-only to avoid compilation)\n * - Installs docling-serve package\n *\n * 3. **Server Management**\n * - Checks if specified port is already in use\n * - If `killExistingProcess` is true, kills any process using the port\n * - If port is in use and `killExistingProcess` is false, reuses existing server\n * - Otherwise, starts new docling-serve instance on specified port\n * - Waits for server to become ready (health check, up to 120 seconds)\n *\n * ## Notes\n * - First initialization may take several minutes due to Python package downloads\n * - Subsequent initializations are faster if virtual environment already exists\n * - The virtual environment and packages are reused across sessions\n * - Server process runs in background and needs to be managed separately if needed\n */\nexport class PDFParser {\n private readonly logger: LoggerMethods;\n private readonly port?: number;\n private readonly baseUrl?: string;\n private readonly timeout: number;\n private readonly venvPath: string;\n private readonly killExistingProcess: boolean;\n private readonly enableImagePdfFallback: boolean;\n private client: DoclingAPIClient | null = null;\n private environment?: DoclingEnvironment;\n\n constructor(options: Options) {\n const {\n logger,\n timeout = PDF_PARSER.DEFAULT_TIMEOUT_MS,\n venvPath,\n killExistingProcess = false,\n enableImagePdfFallback = false,\n } = options;\n\n this.logger = logger;\n\n if ('baseUrl' in options) {\n this.baseUrl = options.baseUrl;\n this.port = undefined;\n } else {\n this.port = options.port;\n this.baseUrl = undefined;\n }\n\n this.timeout = timeout;\n this.venvPath = venvPath || join(process.cwd(), '.venv');\n this.killExistingProcess = killExistingProcess;\n this.enableImagePdfFallback = enableImagePdfFallback;\n }\n\n async init(): Promise<void> {\n this.logger.info('[PDFParser] Initializing...');\n\n this.checkOperatingSystem();\n this.checkJqInstalled();\n this.checkPopplerInstalled();\n this.checkMacOSVersion();\n\n // Check ImageMagick/Ghostscript only for local server mode with fallback enabled\n if (this.enableImagePdfFallback && !this.baseUrl) {\n this.checkImageMagickInstalled();\n this.checkGhostscriptInstalled();\n } else if (this.enableImagePdfFallback && this.baseUrl) {\n this.logger.warn(\n '[PDFParser] enableImagePdfFallback is ignored when using external server (baseUrl)',\n );\n }\n\n if (this.baseUrl) {\n this.logger.info('[PDFParser] Using external server:', this.baseUrl);\n this.client = new Docling({\n api: { baseUrl: this.baseUrl, timeout: this.timeout },\n });\n await this.waitForServerReady();\n return;\n }\n\n this.logger.info('[PDFParser] Setting up local server...');\n try {\n this.environment = new DoclingEnvironment({\n logger: this.logger,\n venvPath: this.venvPath,\n port: this.port as number,\n killExistingProcess: this.killExistingProcess,\n });\n\n await this.environment.setup();\n\n const clientUrl = `http://localhost:${this.port}`;\n this.client = new Docling({\n api: {\n baseUrl: clientUrl,\n timeout: this.timeout,\n },\n });\n\n await this.waitForServerReady();\n this.logger.info('[PDFParser] Ready');\n } catch (error) {\n this.logger.error('[PDFParser] Initialization failed:', error);\n throw new Error(`Failed to initialize PDFParser: ${error}`);\n }\n }\n\n private checkOperatingSystem(): void {\n if (platform() !== 'darwin') {\n throw new Error(\n 'PDFParser is only supported on macOS. Current platform: ' + platform(),\n );\n }\n }\n\n private checkJqInstalled(): void {\n try {\n execSync('which jq', { stdio: 'ignore' });\n } catch {\n throw new Error(\n 'jq is not installed. Please install jq using: brew install jq',\n );\n }\n }\n\n private checkPopplerInstalled(): void {\n try {\n execSync('which pdftotext', { stdio: 'ignore' });\n } catch {\n throw new Error(\n 'poppler is not installed. Please install poppler using: brew install poppler',\n );\n }\n }\n\n private checkMacOSVersion(): void {\n try {\n const versionOutput = execSync('sw_vers -productVersion', {\n encoding: 'utf-8',\n }).trim();\n const versionMatch = versionOutput.match(/^(\\d+)\\.(\\d+)/);\n if (versionMatch) {\n const major = parseInt(versionMatch[1]);\n const minor = parseInt(versionMatch[2]);\n if (major < 10 || (major === 10 && minor < 15)) {\n throw new Error(\n `macOS 10.15 or later is required. Current version: ${versionOutput}`,\n );\n }\n }\n } catch (error) {\n if (error instanceof Error && error.message.includes('macOS 10.15')) {\n throw error;\n }\n throw new Error('Failed to check macOS version');\n }\n }\n\n private checkImageMagickInstalled(): void {\n try {\n execSync('which magick', { stdio: 'ignore' });\n } catch {\n throw new Error(\n 'ImageMagick is not installed but enableImagePdfFallback is enabled. ' +\n 'Please install ImageMagick using: brew install imagemagick',\n );\n }\n }\n\n private checkGhostscriptInstalled(): void {\n try {\n execSync('which gs', { stdio: 'ignore' });\n } catch {\n throw new Error(\n 'Ghostscript is not installed but enableImagePdfFallback is enabled. ' +\n 'Please install Ghostscript using: brew install ghostscript',\n );\n }\n }\n\n /**\n * Check if an error is a connection refused error (ECONNREFUSED).\n * This typically indicates the Docling server has crashed.\n */\n private isConnectionRefusedError(error: unknown): boolean {\n if (error instanceof Error) {\n // Check message and cause chain for ECONNREFUSED\n if (error.message.includes('ECONNREFUSED')) {\n return true;\n }\n if (\n error.cause instanceof Error &&\n error.cause.message.includes('ECONNREFUSED')\n ) {\n return true;\n }\n }\n return false;\n }\n\n /**\n * Restart the Docling server after it has crashed.\n * This kills any existing process on the port, starts a new server,\n * and waits for it to become ready.\n *\n * Note: This method is only called when canRecover is true,\n * which guarantees this.port is defined.\n */\n private async restartServer(): Promise<void> {\n this.logger.info('[PDFParser] Restarting server...');\n\n // Kill existing process on port\n // Note: this.port is guaranteed to be defined by the caller (canRecover check)\n await DoclingEnvironment.killProcessOnPort(this.logger, this.port!);\n\n // Start new server\n const environment = new DoclingEnvironment({\n logger: this.logger,\n venvPath: this.venvPath,\n port: this.port!,\n killExistingProcess: false, // Already killed above\n });\n\n await environment.startServer();\n\n // Recreate client\n this.client?.destroy();\n this.client = new Docling({\n api: {\n baseUrl: `http://localhost:${this.port}`,\n timeout: this.timeout,\n },\n });\n\n await this.waitForServerReady();\n this.logger.info('[PDFParser] Server restarted successfully');\n }\n\n private async waitForServerReady(): Promise<void> {\n const maxAttempts = PDF_PARSER.MAX_HEALTH_CHECK_ATTEMPTS;\n const checkInterval = PDF_PARSER.HEALTH_CHECK_INTERVAL_MS;\n const logInterval = PDF_PARSER.HEALTH_CHECK_LOG_INTERVAL_MS;\n let lastLogTime = 0;\n\n for (let attempt = 1; attempt <= maxAttempts; attempt++) {\n try {\n await this.client!.health();\n this.logger.info('[PDFParser] Server is ready');\n return;\n } catch {\n const now = Date.now();\n if (now - lastLogTime >= logInterval) {\n this.logger.info(\n '[PDFParser] Waiting for server... (attempt',\n attempt,\n '/',\n maxAttempts,\n ')',\n );\n lastLogTime = now;\n }\n\n if (attempt < maxAttempts) {\n await new Promise((resolve) => setTimeout(resolve, checkInterval));\n }\n }\n }\n\n throw new Error('Server failed to become ready after maximum attempts');\n }\n\n public async parse(\n url: string,\n reportId: string,\n onComplete: ConversionCompleteCallback,\n cleanupAfterCallback: boolean,\n options: PDFConvertOptions,\n abortSignal?: AbortSignal,\n ): Promise<TokenUsageReport | null> {\n if (!this.client) {\n throw new Error(\n 'PDFParser is not initialized. Call init() before using parse()',\n );\n }\n\n // Check ImageMagick/Ghostscript for forceImagePdf (lazy check at parse time)\n if (options.forceImagePdf && !this.baseUrl) {\n this.checkImageMagickInstalled();\n this.checkGhostscriptInstalled();\n }\n\n // Use strategy-based flow when new options are provided\n const useStrategyFlow =\n options.strategySamplerModel !== undefined ||\n options.forcedMethod !== undefined;\n\n if (useStrategyFlow) {\n return this.parseWithStrategy(\n url,\n reportId,\n onComplete,\n cleanupAfterCallback,\n options,\n abortSignal,\n );\n }\n\n // Enable recovery only for local server mode\n const canRecover = !this.baseUrl && this.port !== undefined;\n const maxAttempts = PDF_PARSER.MAX_SERVER_RECOVERY_ATTEMPTS;\n let attempt = 0;\n\n while (attempt <= maxAttempts) {\n try {\n // Enable fallback only for local server mode\n const effectiveFallbackEnabled =\n this.enableImagePdfFallback && !this.baseUrl;\n const converter = new PDFConverter(\n this.logger,\n this.client,\n effectiveFallbackEnabled,\n this.timeout,\n );\n return await converter.convert(\n url,\n reportId,\n onComplete,\n cleanupAfterCallback,\n options,\n abortSignal,\n );\n } catch (error) {\n // If aborted, don't retry - re-throw immediately\n if (abortSignal?.aborted) {\n throw error;\n }\n\n // Attempt server recovery on ECONNREFUSED (server crashed)\n if (\n canRecover &&\n this.isConnectionRefusedError(error) &&\n attempt < maxAttempts\n ) {\n this.logger.warn(\n '[PDFParser] Connection refused, attempting server recovery...',\n );\n await this.restartServer();\n attempt++;\n continue;\n }\n throw error;\n }\n }\n\n /* v8 ignore start */\n return null;\n /* v8 ignore stop */\n }\n\n /**\n * Parse a PDF using OCR strategy sampling to decide between ocrmac and VLM.\n * Delegates to PDFConverter.convertWithStrategy() and returns the token usage report.\n *\n * Server recovery (restart on ECONNREFUSED) is preserved because\n * the ocrmac path still uses the Docling server.\n */\n private async parseWithStrategy(\n url: string,\n reportId: string,\n onComplete: ConversionCompleteCallback,\n cleanupAfterCallback: boolean,\n options: PDFConvertOptions,\n abortSignal?: AbortSignal,\n ): Promise<TokenUsageReport | null> {\n // Enable recovery only for local server mode\n const canRecover = !this.baseUrl && this.port !== undefined;\n const maxAttempts = PDF_PARSER.MAX_SERVER_RECOVERY_ATTEMPTS;\n let attempt = 0;\n\n while (attempt <= maxAttempts) {\n try {\n const effectiveFallbackEnabled =\n this.enableImagePdfFallback && !this.baseUrl;\n const converter = new PDFConverter(\n this.logger,\n this.client!,\n effectiveFallbackEnabled,\n this.timeout,\n );\n const result = await converter.convertWithStrategy(\n url,\n reportId,\n onComplete,\n cleanupAfterCallback,\n options,\n abortSignal,\n );\n return result.tokenUsageReport;\n } catch (error) {\n // If aborted, don't retry - re-throw immediately\n if (abortSignal?.aborted) {\n throw error;\n }\n\n // Attempt server recovery on ECONNREFUSED (server crashed)\n if (\n canRecover &&\n this.isConnectionRefusedError(error) &&\n attempt < maxAttempts\n ) {\n this.logger.warn(\n '[PDFParser] Connection refused, attempting server recovery...',\n );\n await this.restartServer();\n attempt++;\n continue;\n }\n throw error;\n }\n }\n\n /* v8 ignore start */\n return null;\n /* v8 ignore stop */\n }\n\n /**\n * Dispose the parser instance.\n * - Sets the internal client to null\n * - If a local docling server was started (no baseUrl), kills the process on the configured port\n */\n public async dispose(): Promise<void> {\n this.logger.info('[PDFParser] Disposing...');\n\n try {\n // Only manage local server lifecycle when we started it (i.e., no external baseUrl)\n if (!this.baseUrl && this.port) {\n await DoclingEnvironment.killProcessOnPort(this.logger, this.port);\n }\n } catch (error) {\n this.logger.error('[PDFParser] Error while disposing:', error);\n } finally {\n // Always clear the client reference\n this.client?.destroy();\n this.client = null;\n this.logger.info('[PDFParser] Disposed');\n }\n }\n}\n","/**\n * Configuration constants for PDFParser\n */\nexport const PDF_PARSER = {\n /**\n * Default timeout for API calls in milliseconds\n */\n DEFAULT_TIMEOUT_MS: 100000,\n\n /**\n * Maximum number of health check attempts before giving up\n */\n MAX_HEALTH_CHECK_ATTEMPTS: 60,\n\n /**\n * Interval between health check attempts in milliseconds\n */\n HEALTH_CHECK_INTERVAL_MS: 2000,\n\n /**\n * Interval between log messages during health check in milliseconds\n */\n HEALTH_CHECK_LOG_INTERVAL_MS: 5000,\n\n /**\n * Maximum retry attempts for server recovery on ECONNREFUSED\n */\n MAX_SERVER_RECOVERY_ATTEMPTS: 1,\n} as const;\n\n/**\n * Configuration constants for PDFConverter\n */\nexport const PDF_CONVERTER = {\n /**\n * Interval for progress polling in milliseconds\n */\n POLL_INTERVAL_MS: 1000,\n\n /**\n * Default timeout for task completion in milliseconds (30 minutes)\n */\n DEFAULT_TIMEOUT_MS: 1_800_000,\n} as const;\n\n/**\n * Configuration constants for DoclingEnvironment\n */\nexport const DOCLING_ENVIRONMENT = {\n /**\n * Delay after starting docling-serve to allow startup\n */\n STARTUP_DELAY_MS: 2000,\n} as const;\n\n/**\n * Configuration constants for page rendering DPI\n */\nexport const PAGE_RENDERING = {\n /** Default rendering DPI for VLM text recognition quality */\n DEFAULT_DPI: 200,\n /** Low-resolution DPI for OCR strategy sampling */\n SAMPLE_DPI: 150,\n} as const;\n\n/**\n * Configuration constants for ImagePdfConverter\n */\n/**\n * Configuration constants for chunked PDF conversion\n */\nexport const CHUNKED_CONVERSION = {\n /** Number of pages per chunk */\n DEFAULT_CHUNK_SIZE: 10,\n /** Maximum retry attempts per failed chunk */\n DEFAULT_MAX_RETRIES: 2,\n} as const;\n\n/**\n * Configuration constants for ImagePdfConverter\n */\nexport const IMAGE_PDF_CONVERTER = {\n /**\n * ImageMagick density option (DPI) for PDF to image conversion\n */\n DENSITY: PAGE_RENDERING.DEFAULT_DPI,\n\n /**\n * ImageMagick quality option (1-100)\n */\n QUALITY: 100,\n} as const;\n","/**\n * BatchProcessor - Batch processing utility\n *\n * Provides functionality to split large arrays into batches for parallel processing.\n */\nexport class BatchProcessor {\n /**\n * Splits an array into batches of specified size.\n *\n * @param items - Array to split\n * @param batchSize - Size of each batch\n * @returns Array of batches\n *\n * @example\n * ```typescript\n * const items = [1, 2, 3, 4, 5];\n * const batches = BatchProcessor.createBatches(items, 2);\n * // [[1, 2], [3, 4], [5]]\n * ```\n */\n static createBatches<T>(items: T[], batchSize: number): T[][] {\n const batches: T[][] = [];\n for (let i = 0; i < items.length; i += batchSize) {\n batches.push(items.slice(i, i + batchSize));\n }\n return batches;\n }\n\n /**\n * Splits an array into batches and executes async function in parallel.\n *\n * @param items - Array to process\n * @param batchSize - Size of each batch\n * @param processFn - Async function to process each batch\n * @returns Flattened array of processed results\n *\n * @example\n * ```typescript\n * const texts = ['a', 'b', 'c', 'd', 'e'];\n * const results = await BatchProcessor.processBatch(\n * texts,\n * 2,\n * async (batch) => {\n * return batch.map(t => t.toUpperCase());\n * }\n * );\n * // ['A', 'B', 'C', 'D', 'E']\n * ```\n */\n static async processBatch<T, R>(\n items: T[],\n batchSize: number,\n processFn: (batch: T[]) => Promise<R[]>,\n ): Promise<R[]> {\n const batches = this.createBatches(items, batchSize);\n const results = await Promise.all(batches.map((batch) => processFn(batch)));\n return results.flat();\n }\n\n /**\n * Splits an array into batches and executes sync function in parallel.\n *\n * @param items - Array to process\n * @param batchSize - Size of each batch\n * @param processFn - Sync function to process each batch\n * @returns Flattened array of processed results\n *\n * @example\n * ```typescript\n * const numbers = [1, 2, 3, 4, 5];\n * const results = BatchProcessor.processBatchSync(\n * numbers,\n * 2,\n * (batch) => batch.map(n => n * 2)\n * );\n * // [2, 4, 6, 8, 10]\n * ```\n */\n static processBatchSync<T, R>(\n items: T[],\n batchSize: number,\n processFn: (batch: T[]) => R[],\n ): R[] {\n const batches = this.createBatches(items, batchSize);\n const results = batches.map((batch) => processFn(batch));\n return results.flat();\n }\n}\n","/**\n * ConcurrentPool - Worker pool utility for concurrent task execution.\n *\n * Unlike batch processing where all items in a batch must complete before\n * the next batch starts, the pool keeps N workers active at all times.\n * When a worker finishes, it immediately picks up the next available item.\n */\nexport class ConcurrentPool {\n /**\n * Process items concurrently using a worker pool pattern.\n *\n * Spawns up to `concurrency` workers that pull items from a shared queue.\n * Each worker processes one item at a time; when it finishes, it immediately\n * takes the next available item. Results maintain the original item order.\n *\n * @param items - Array of items to process\n * @param concurrency - Maximum number of concurrent workers\n * @param processFn - Async function to process each item\n * @param onItemComplete - Optional callback fired after each item completes\n * @returns Array of results in the same order as the input items\n */\n static async run<T, R>(\n items: T[],\n concurrency: number,\n processFn: (item: T, index: number) => Promise<R>,\n onItemComplete?: (result: R, index: number) => void,\n ): Promise<R[]> {\n const results: R[] = new Array(items.length);\n let nextIndex = 0;\n\n async function worker(): Promise<void> {\n while (nextIndex < items.length) {\n const index = nextIndex++;\n results[index] = await processFn(items[index], index);\n onItemComplete?.(results[index], index);\n }\n }\n\n const workers = Array.from(\n { length: Math.min(concurrency, items.length) },\n () => worker(),\n );\n await Promise.all(workers);\n return results;\n }\n}\n","import type { SpawnOptions } from 'node:child_process';\n\nimport { spawn } from 'node:child_process';\n\n/**\n * Result of a spawn operation\n */\nexport interface SpawnResult {\n stdout: string;\n stderr: string;\n code: number;\n}\n\n/**\n * Extended spawn options with output capture control\n */\nexport interface SpawnAsyncOptions extends SpawnOptions {\n /**\n * Whether to capture stdout (default: true)\n */\n captureStdout?: boolean;\n\n /**\n * Whether to capture stderr (default: true)\n */\n captureStderr?: boolean;\n}\n\n/**\n * Execute a command asynchronously and return the result\n *\n * Eliminates the repetitive Promise wrapper pattern used throughout\n * DoclingEnvironment for spawn operations.\n *\n * @param command - The command to execute\n * @param args - Arguments to pass to the command\n * @param options - Spawn options with optional output capture control\n * @returns Promise resolving to stdout, stderr, and exit code\n *\n * @example\n * ```typescript\n * // Simple usage\n * const result = await spawnAsync('python3', ['--version']);\n * console.log(result.stdout); // \"Python 3.12.0\"\n *\n * // With options\n * const result = await spawnAsync('pip', ['install', 'package'], {\n * cwd: '/path/to/venv',\n * captureStderr: true,\n * });\n * ```\n */\nexport function spawnAsync(\n command: string,\n args: string[],\n options: SpawnAsyncOptions = {},\n): Promise<SpawnResult> {\n const {\n captureStdout = true,\n captureStderr = true,\n ...spawnOptions\n } = options;\n\n return new Promise((resolve, reject) => {\n const proc = spawn(command, args, spawnOptions);\n\n let stdout = '';\n let stderr = '';\n\n if (captureStdout && proc.stdout) {\n proc.stdout.on('data', (data) => {\n stdout += data.toString();\n });\n }\n\n if (captureStderr && proc.stderr) {\n proc.stderr.on('data', (data) => {\n stderr += data.toString();\n });\n }\n\n proc.on('close', (code) => {\n resolve({ stdout, stderr, code: code ?? 0 });\n });\n\n proc.on('error', reject);\n });\n}\n","import type { z } from 'zod';\n\nimport {\n type LanguageModel,\n NoObjectGeneratedError,\n Output,\n generateText,\n hasToolCall,\n tool,\n} from 'ai';\n\nimport { detectProvider } from './provider-detector';\n\n/**\n * Configuration for LLM API call with retry and fallback support\n */\nexport interface LLMCallConfig<TSchema extends z.ZodType> {\n /**\n * Zod schema for response validation\n */\n schema: TSchema;\n\n /**\n * System prompt for LLM\n */\n systemPrompt: string;\n\n /**\n * User prompt for LLM\n */\n userPrompt: string;\n\n /**\n * Primary model for the call (required)\n */\n primaryModel: LanguageModel;\n\n /**\n * Fallback model for retry after primary model exhausts maxRetries (optional)\n */\n fallbackModel?: LanguageModel;\n\n /**\n * Maximum retry count per model (default: 3)\n */\n maxRetries: number;\n\n /**\n * Temperature for generation (optional, 0-1)\n */\n temperature?: number;\n\n /**\n * Abort signal for cancellation support\n */\n abortSignal?: AbortSignal;\n\n /**\n * Component name for tracking (e.g., 'TocExtractor', 'PageRangeParser')\n */\n component: string;\n\n /**\n * Phase name for tracking (e.g., 'extraction', 'validation', 'sampling')\n */\n phase: string;\n}\n\n/**\n * Configuration for LLM vision call with message format\n */\nexport interface LLMVisionCallConfig<TSchema extends z.ZodType> {\n /**\n * Zod schema for response validation\n */\n schema: TSchema;\n\n /**\n * Messages array for vision LLM (instead of systemPrompt/userPrompt)\n */\n messages: Array<{ role: 'user' | 'assistant'; content: any[] | string }>;\n\n /**\n * Primary model for the call (required)\n */\n primaryModel: LanguageModel;\n\n /**\n * Fallback model for retry after primary model exhausts maxRetries (optional)\n */\n fallbackModel?: LanguageModel;\n\n /**\n * Maximum retry count per model (default: 3)\n */\n maxRetries: number;\n\n /**\n * Temperature for generation (optional, 0-1)\n */\n temperature?: number;\n\n /**\n * Abort signal for cancellation support\n */\n abortSignal?: AbortSignal;\n\n /**\n * Component name for tracking (e.g., 'TocExtractor', 'PageRangeParser')\n */\n component: string;\n\n /**\n * Phase name for tracking (e.g., 'extraction', 'validation', 'sampling')\n */\n phase: string;\n}\n\n/**\n * Token usage information with model tracking\n */\nexport interface ExtendedTokenUsage {\n component: string;\n phase: string;\n model: 'primary' | 'fallback';\n modelName: string;\n inputTokens: number;\n outputTokens: number;\n totalTokens: number;\n}\n\n/**\n * Result of LLM call including usage information\n */\nexport interface LLMCallResult<T> {\n output: T;\n usage: ExtendedTokenUsage;\n usedFallback: boolean;\n}\n\n/**\n * Base execution configuration for LLM calls\n */\ninterface ExecutionConfig {\n primaryModel: LanguageModel;\n fallbackModel?: LanguageModel;\n abortSignal?: AbortSignal;\n component: string;\n phase: string;\n}\n\n/**\n * LLMCaller - Centralized LLM API caller with retry and fallback support\n *\n * Wraps AI SDK's generateText with enhanced retry strategy:\n * 1. Try primary model with maxRetries\n * 2. If all attempts fail and fallbackModel provided, try fallback with maxRetries\n * 3. Return usage data with model type indicator\n *\n * @example\n * ```typescript\n * const result = await LLMCaller.call({\n * schema: MyZodSchema,\n * systemPrompt: 'You are a helpful assistant',\n * userPrompt: 'Extract the TOC from this markdown',\n * primaryModel: openai('gpt-5'),\n * fallbackModel: anthropic('claude-opus-4-5'),\n * maxRetries: 3,\n * component: 'TocExtractor',\n * phase: 'extraction',\n * });\n *\n * console.log(result.output); // Parsed result\n * console.log(result.usage); // Token usage with model info\n * console.log(result.usedFallback); // Whether fallback was used\n * ```\n */\nexport class LLMCaller {\n /**\n * Extract model name from LanguageModel object\n *\n * Attempts to get model ID from various possible fields in the LanguageModel object.\n */\n private static extractModelName(model: LanguageModel): string {\n const modelObj = model as Record<string, unknown>;\n\n // Try common field names\n if (typeof modelObj.modelId === 'string') return modelObj.modelId;\n if (typeof modelObj.id === 'string') return modelObj.id;\n if (typeof modelObj.model === 'string') return modelObj.model;\n if (typeof modelObj.name === 'string') return modelObj.name;\n\n // Fallback: return object representation\n return String(model);\n }\n\n /**\n * Build usage information from response\n */\n private static buildUsage(\n config: ExecutionConfig,\n modelName: string,\n response: {\n usage?: {\n inputTokens?: number;\n outputTokens?: number;\n totalTokens?: number;\n };\n },\n usedFallback: boolean,\n ): ExtendedTokenUsage {\n return {\n component: config.component,\n phase: config.phase,\n model: usedFallback ? 'fallback' : 'primary',\n modelName,\n inputTokens: response.usage?.inputTokens ?? 0,\n outputTokens: response.usage?.outputTokens ?? 0,\n totalTokens: response.usage?.totalTokens ?? 0,\n };\n }\n\n /**\n * Maximum number of retries when structured output generation fails.\n * Total attempts = MAX_STRUCTURED_OUTPUT_RETRIES + 1.\n *\n * Applied to both:\n * - `Output.object()` path: retries on NoObjectGeneratedError (schema mismatch)\n * - Tool call path: retries when model does not produce a tool call\n */\n private static readonly MAX_STRUCTURED_OUTPUT_RETRIES = 10;\n\n /**\n * Generate structured output via forced tool call.\n *\n * Used for providers (Together AI, unknown) that do not reliably support\n * `Output.object()`. Forces the model to call a tool whose inputSchema\n * is the target Zod schema, then extracts the parsed input.\n *\n * Retries up to MAX_STRUCTURED_OUTPUT_RETRIES times when the model does not\n * produce a tool call, for a total of MAX_STRUCTURED_OUTPUT_RETRIES + 1 attempts.\n *\n * @throws NoObjectGeneratedError when all attempts fail to produce a tool call\n */\n private static async generateViaToolCall<TOutput>(\n model: LanguageModel,\n schema: z.ZodType<TOutput>,\n promptParams: Record<string, unknown>,\n ): Promise<{\n output: TOutput;\n usage?: {\n inputTokens?: number;\n outputTokens?: number;\n totalTokens?: number;\n };\n }> {\n const submitTool = tool({\n description: 'Submit the structured result',\n inputSchema: schema,\n });\n\n let lastResult: any;\n\n for (\n let attempt = 0;\n attempt <= this.MAX_STRUCTURED_OUTPUT_RETRIES;\n attempt++\n ) {\n lastResult = await (generateText as any)({\n ...promptParams,\n model,\n tools: { submitResult: submitTool },\n toolChoice: { type: 'tool', toolName: 'submitResult' },\n stopWhen: hasToolCall('submitResult'),\n });\n\n const toolCall = lastResult.toolCalls?.[0] as\n | { input: unknown }\n | undefined;\n if (toolCall) {\n return {\n output: toolCall.input as TOutput,\n usage: lastResult.usage,\n };\n }\n }\n\n throw new NoObjectGeneratedError({\n message: 'Model did not produce a tool call for structured output',\n text: lastResult.text ?? '',\n response: lastResult.response,\n usage: lastResult.usage,\n finishReason: lastResult.finishReason,\n });\n }\n\n /**\n * Generate structured output with provider-aware strategy.\n *\n * Strategy per provider:\n * - OpenAI / Anthropic / Google Gemini: `Output.object()` with schema retry\n * - Together AI / unknown: forced tool call pattern\n *\n * Retries up to MAX_STRUCTURED_OUTPUT_RETRIES times on NoObjectGeneratedError\n * (schema mismatch), re-throwing the last error if all attempts fail.\n */\n private static async generateStructuredOutput<TOutput>(\n model: LanguageModel,\n schema: z.ZodType<TOutput>,\n promptParams: Record<string, unknown>,\n ): Promise<{\n output: TOutput;\n usage?: {\n inputTokens?: number;\n outputTokens?: number;\n totalTokens?: number;\n };\n }> {\n const providerType = detectProvider(model);\n\n if (providerType === 'togetherai' || providerType === 'unknown') {\n return this.generateViaToolCall(model, schema, promptParams);\n }\n\n let lastError: unknown;\n\n for (\n let attempt = 0;\n attempt <= this.MAX_STRUCTURED_OUTPUT_RETRIES;\n attempt++\n ) {\n try {\n return await (generateText as any)({\n model,\n output: Output.object({ schema }),\n ...promptParams,\n });\n } catch (error) {\n if (NoObjectGeneratedError.isInstance(error)) {\n lastError = error;\n continue;\n }\n throw error;\n }\n }\n\n throw lastError;\n }\n\n /**\n * Execute LLM call with fallback support\n *\n * Common execution logic for both text and vision calls.\n * Logs additional details when NoObjectGeneratedError occurs.\n */\n private static async executeWithFallback<TOutput>(\n config: ExecutionConfig,\n generateFn: (model: LanguageModel) => Promise<{\n output: TOutput;\n usage?: {\n inputTokens?: number;\n outputTokens?: number;\n totalTokens?: number;\n };\n }>,\n ): Promise<LLMCallResult<TOutput>> {\n const primaryModelName = this.extractModelName(config.primaryModel);\n\n // Attempt 1: Try primary model\n try {\n const response = await generateFn(config.primaryModel);\n\n return {\n output: response.output,\n usage: this.buildUsage(config, primaryModelName, response, false),\n usedFallback: false,\n };\n } catch (primaryError) {\n // If aborted, don't try fallback - re-throw immediately\n if (config.abortSignal?.aborted) {\n throw primaryError;\n }\n\n // If no fallback model, throw immediately\n if (!config.fallbackModel) {\n throw primaryError;\n }\n\n // Attempt 2: Try fallback model\n const fallbackModelName = this.extractModelName(config.fallbackModel);\n const response = await generateFn(config.fallbackModel);\n\n return {\n output: response.output,\n usage: this.buildUsage(config, fallbackModelName, response, true),\n usedFallback: true,\n };\n }\n }\n\n /**\n * Call LLM with retry and fallback support\n *\n * Retry Strategy:\n * 1. Try primary model up to maxRetries times\n * 2. If all fail and fallbackModel provided, try fallback up to maxRetries times\n * 3. Throw error if all attempts exhausted\n *\n * Provider-aware strategy is automatically applied based on the model's provider field.\n *\n * @template TOutput - Output type from schema validation\n * @param config - LLM call configuration\n * @returns Result with parsed object and usage information\n * @throws Error if all retry attempts fail\n */\n static async call<TOutput = unknown>(\n config: LLMCallConfig<z.ZodType<TOutput>>,\n ): Promise<LLMCallResult<TOutput>> {\n return this.executeWithFallback(config, (model) =>\n this.generateStructuredOutput(model, config.schema, {\n system: config.systemPrompt,\n prompt: config.userPrompt,\n temperature: config.temperature,\n maxRetries: config.maxRetries,\n abortSignal: config.abortSignal,\n }),\n );\n }\n\n /**\n * Call LLM for vision tasks with message format support\n *\n * Same retry and fallback logic as call(), but using message format instead of system/user prompts.\n * Provider-aware strategy is automatically applied based on the model's provider field.\n *\n * @template TOutput - Output type from schema validation\n * @param config - LLM vision call configuration\n * @returns Result with parsed object and usage information\n * @throws Error if all retry attempts fail\n */\n static async callVision<TOutput = unknown>(\n config: LLMVisionCallConfig<z.ZodType<TOutput>>,\n ): Promise<LLMCallResult<TOutput>> {\n return this.executeWithFallback(config, (model) =>\n this.generateStructuredOutput(model, config.schema, {\n messages: config.messages,\n temperature: config.temperature,\n maxRetries: config.maxRetries,\n abortSignal: config.abortSignal,\n }),\n );\n }\n}\n","import type { LanguageModel } from 'ai';\n\nexport type ProviderType =\n | 'openai'\n | 'google'\n | 'anthropic'\n | 'togetherai'\n | 'unknown';\n\n/**\n * Detect the provider type from a LanguageModel instance.\n *\n * Reads the `provider` field of the model object and matches it against\n * known provider identifiers. Falls back to 'unknown' if unrecognized.\n */\nexport function detectProvider(model: LanguageModel): ProviderType {\n const providerId = (model as { provider?: string }).provider;\n if (!providerId || typeof providerId !== 'string') return 'unknown';\n\n if (providerId.includes('openai')) return 'openai';\n if (providerId.includes('google')) return 'google';\n if (providerId.includes('anthropic')) return 'anthropic';\n if (providerId.includes('together')) return 'togetherai';\n\n return 'unknown';\n}\n","import type { LoggerMethods } from '@heripo/logger';\n\nimport type { ExtendedTokenUsage } from './llm-caller';\n\n/**\n * Token usage totals\n */\nexport interface TokenUsage {\n inputTokens: number;\n outputTokens: number;\n totalTokens: number;\n}\n\n/**\n * Format token usage as a human-readable string\n *\n * @param usage - Token usage object with input, output, and total counts\n * @returns Formatted string like \"1500 input, 300 output, 1800 total\"\n */\nfunction formatTokens(usage: TokenUsage): string {\n return `${usage.inputTokens} input, ${usage.outputTokens} output, ${usage.totalTokens} total`;\n}\n\n/**\n * Aggregated token usage for a specific component\n */\ninterface ComponentAggregate {\n component: string;\n phases: Record<\n string,\n {\n primary?: {\n modelName: string;\n inputTokens: number;\n outputTokens: number;\n totalTokens: number;\n };\n fallback?: {\n modelName: string;\n inputTokens: number;\n outputTokens: number;\n totalTokens: number;\n };\n total: {\n inputTokens: number;\n outputTokens: number;\n totalTokens: number;\n };\n }\n >;\n total: {\n inputTokens: number;\n outputTokens: number;\n totalTokens: number;\n };\n}\n\n/**\n * LLMTokenUsageAggregator - Aggregates token usage across all LLM calls\n *\n * Unlike LLMTokenUsageTracker which logs immediately after each component,\n * this aggregator collects usage data from all components and logs a comprehensive\n * summary at the end of document processing.\n *\n * Tracks usage by:\n * - Component (TocExtractor, PageRangeParser, etc.)\n * - Phase (extraction, validation, sampling, etc.)\n * - Model (primary vs fallback)\n *\n * @example\n * ```typescript\n * const aggregator = new LLMTokenUsageAggregator();\n *\n * // Track usage from each LLM call\n * aggregator.track({\n * component: 'TocExtractor',\n * phase: 'extraction',\n * model: 'primary',\n * modelName: 'gpt-5',\n * inputTokens: 1500,\n * outputTokens: 300,\n * totalTokens: 1800,\n * });\n *\n * aggregator.track({\n * component: 'PageRangeParser',\n * phase: 'sampling',\n * model: 'fallback',\n * modelName: 'claude-opus-4-5',\n * inputTokens: 2000,\n * outputTokens: 100,\n * totalTokens: 2100,\n * });\n *\n * // Log comprehensive summary\n * aggregator.logSummary(logger);\n * // Outputs:\n * // [DocumentProcessor] Token usage summary:\n * // TocExtractor:\n * // - extraction (primary: gpt-5): 1500 input, 300 output, 1800 total\n * // TocExtractor total: 1500 input, 300 output, 1800 total\n * // PageRangeParser:\n * // - sampling (fallback: claude-opus-4-5): 2000 input, 100 output, 2100 total\n * // PageRangeParser total: 2000 input, 100 output, 2100 total\n * // Grand total: 3500 input, 400 output, 3900 total\n * ```\n */\nexport class LLMTokenUsageAggregator {\n private usage: Record<string, ComponentAggregate> = {};\n\n /**\n * Track token usage from an LLM call\n *\n * @param usage - Extended token usage with component/phase/model information\n */\n track(usage: ExtendedTokenUsage): void {\n // Initialize component if not seen before\n if (!this.usage[usage.component]) {\n this.usage[usage.component] = {\n component: usage.component,\n phases: {},\n total: {\n inputTokens: 0,\n outputTokens: 0,\n totalTokens: 0,\n },\n };\n }\n\n const component = this.usage[usage.component];\n\n // Initialize phase if not seen before\n if (!component.phases[usage.phase]) {\n component.phases[usage.phase] = {\n total: {\n inputTokens: 0,\n outputTokens: 0,\n totalTokens: 0,\n },\n };\n }\n\n const phase = component.phases[usage.phase];\n\n // Track by model type\n if (usage.model === 'primary') {\n if (!phase.primary) {\n phase.primary = {\n modelName: usage.modelName,\n inputTokens: 0,\n outputTokens: 0,\n totalTokens: 0,\n };\n }\n\n phase.primary.inputTokens += usage.inputTokens;\n phase.primary.outputTokens += usage.outputTokens;\n phase.primary.totalTokens += usage.totalTokens;\n } else if (usage.model === 'fallback') {\n if (!phase.fallback) {\n phase.fallback = {\n modelName: usage.modelName,\n inputTokens: 0,\n outputTokens: 0,\n totalTokens: 0,\n };\n }\n\n phase.fallback.inputTokens += usage.inputTokens;\n phase.fallback.outputTokens += usage.outputTokens;\n phase.fallback.totalTokens += usage.totalTokens;\n }\n\n // Update phase total\n phase.total.inputTokens += usage.inputTokens;\n phase.total.outputTokens += usage.outputTokens;\n phase.total.totalTokens += usage.totalTokens;\n\n // Update component total\n component.total.inputTokens += usage.inputTokens;\n component.total.outputTokens += usage.outputTokens;\n component.total.totalTokens += usage.totalTokens;\n }\n\n /**\n * Get aggregated usage grouped by component\n *\n * @returns Array of component aggregates with phase breakdown\n */\n getByComponent(): ComponentAggregate[] {\n return Object.values(this.usage);\n }\n\n /**\n * Get token usage report in structured JSON format\n *\n * Converts internal usage data to external TokenUsageReport format suitable\n * for serialization and reporting. The report includes component breakdown,\n * phase-level details, and both primary and fallback model usage.\n *\n * @returns Structured token usage report with components and total\n */\n getReport(): {\n components: Array<{\n component: string;\n phases: Array<{\n phase: string;\n primary?: {\n modelName: string;\n inputTokens: number;\n outputTokens: number;\n totalTokens: number;\n };\n fallback?: {\n modelName: string;\n inputTokens: number;\n outputTokens: number;\n totalTokens: number;\n };\n total: {\n inputTokens: number;\n outputTokens: number;\n totalTokens: number;\n };\n }>;\n total: {\n inputTokens: number;\n outputTokens: number;\n totalTokens: number;\n };\n }>;\n total: TokenUsage;\n } {\n const components: Array<{\n component: string;\n phases: Array<{\n phase: string;\n primary?: {\n modelName: string;\n inputTokens: number;\n outputTokens: number;\n totalTokens: number;\n };\n fallback?: {\n modelName: string;\n inputTokens: number;\n outputTokens: number;\n totalTokens: number;\n };\n total: {\n inputTokens: number;\n outputTokens: number;\n totalTokens: number;\n };\n }>;\n total: {\n inputTokens: number;\n outputTokens: number;\n totalTokens: number;\n };\n }> = [];\n\n for (const component of Object.values(this.usage)) {\n const phases: Array<{\n phase: string;\n primary?: {\n modelName: string;\n inputTokens: number;\n outputTokens: number;\n totalTokens: number;\n };\n fallback?: {\n modelName: string;\n inputTokens: number;\n outputTokens: number;\n totalTokens: number;\n };\n total: {\n inputTokens: number;\n outputTokens: number;\n totalTokens: number;\n };\n }> = [];\n\n for (const [phaseName, phaseData] of Object.entries(component.phases)) {\n const phaseReport: {\n phase: string;\n primary?: {\n modelName: string;\n inputTokens: number;\n outputTokens: number;\n totalTokens: number;\n };\n fallback?: {\n modelName: string;\n inputTokens: number;\n outputTokens: number;\n totalTokens: number;\n };\n total: {\n inputTokens: number;\n outputTokens: number;\n totalTokens: number;\n };\n } = {\n phase: phaseName,\n total: {\n inputTokens: phaseData.total.inputTokens,\n outputTokens: phaseData.total.outputTokens,\n totalTokens: phaseData.total.totalTokens,\n },\n };\n\n if (phaseData.primary) {\n phaseReport.primary = {\n modelName: phaseData.primary.modelName,\n inputTokens: phaseData.primary.inputTokens,\n outputTokens: phaseData.primary.outputTokens,\n totalTokens: phaseData.primary.totalTokens,\n };\n }\n\n if (phaseData.fallback) {\n phaseReport.fallback = {\n modelName: phaseData.fallback.modelName,\n inputTokens: phaseData.fallback.inputTokens,\n outputTokens: phaseData.fallback.outputTokens,\n totalTokens: phaseData.fallback.totalTokens,\n };\n }\n\n phases.push(phaseReport);\n }\n\n components.push({\n component: component.component,\n phases,\n total: {\n inputTokens: component.total.inputTokens,\n outputTokens: component.total.outputTokens,\n totalTokens: component.total.totalTokens,\n },\n });\n }\n\n const totalUsage = this.getTotalUsage();\n\n return {\n components,\n total: {\n inputTokens: totalUsage.inputTokens,\n outputTokens: totalUsage.outputTokens,\n totalTokens: totalUsage.totalTokens,\n },\n };\n }\n\n /**\n * Get total usage across all components and phases\n *\n * @returns Aggregated token usage totals\n */\n getTotalUsage(): TokenUsage {\n let totalInput = 0;\n let totalOutput = 0;\n let totalTokens = 0;\n\n for (const component of Object.values(this.usage)) {\n totalInput += component.total.inputTokens;\n totalOutput += component.total.outputTokens;\n totalTokens += component.total.totalTokens;\n }\n\n return {\n inputTokens: totalInput,\n outputTokens: totalOutput,\n totalTokens: totalTokens,\n };\n }\n\n /**\n * Log comprehensive token usage summary\n *\n * Outputs usage grouped by component, with phase and model breakdown.\n * Shows primary and fallback token usage separately for each phase.\n * Call this once at the end of document processing.\n *\n * @param logger - Logger instance for output\n */\n logSummary(logger: LoggerMethods): void {\n const components = this.getByComponent();\n\n if (components.length === 0) {\n logger.info('[DocumentProcessor] No token usage to report');\n return;\n }\n\n logger.info('[DocumentProcessor] Token usage summary:');\n logger.info('');\n\n let grandInputTokens = 0;\n let grandOutputTokens = 0;\n let grandTotalTokens = 0;\n let grandPrimaryInputTokens = 0;\n let grandPrimaryOutputTokens = 0;\n let grandPrimaryTotalTokens = 0;\n let grandFallbackInputTokens = 0;\n let grandFallbackOutputTokens = 0;\n let grandFallbackTotalTokens = 0;\n\n for (const component of components) {\n logger.info(`${component.component}:`);\n\n for (const [phase, phaseData] of Object.entries(component.phases)) {\n logger.info(` - ${phase}:`);\n\n // Show primary model usage\n if (phaseData.primary) {\n logger.info(\n ` primary (${phaseData.primary.modelName}): ${formatTokens(phaseData.primary)}`,\n );\n grandPrimaryInputTokens += phaseData.primary.inputTokens;\n grandPrimaryOutputTokens += phaseData.primary.outputTokens;\n grandPrimaryTotalTokens += phaseData.primary.totalTokens;\n }\n\n // Show fallback model usage\n if (phaseData.fallback) {\n logger.info(\n ` fallback (${phaseData.fallback.modelName}): ${formatTokens(phaseData.fallback)}`,\n );\n grandFallbackInputTokens += phaseData.fallback.inputTokens;\n grandFallbackOutputTokens += phaseData.fallback.outputTokens;\n grandFallbackTotalTokens += phaseData.fallback.totalTokens;\n }\n\n // Show phase subtotal\n logger.info(` subtotal: ${formatTokens(phaseData.total)}`);\n }\n\n logger.info(\n ` ${component.component} total: ${formatTokens(component.total)}`,\n );\n logger.info('');\n\n grandInputTokens += component.total.inputTokens;\n grandOutputTokens += component.total.outputTokens;\n grandTotalTokens += component.total.totalTokens;\n }\n\n // Show grand total with primary/fallback breakdown\n logger.info('--- Summary ---');\n if (grandPrimaryTotalTokens > 0) {\n logger.info(\n `Primary total: ${formatTokens({\n inputTokens: grandPrimaryInputTokens,\n outputTokens: grandPrimaryOutputTokens,\n totalTokens: grandPrimaryTotalTokens,\n })}`,\n );\n }\n if (grandFallbackTotalTokens > 0) {\n logger.info(\n `Fallback total: ${formatTokens({\n inputTokens: grandFallbackInputTokens,\n outputTokens: grandFallbackOutputTokens,\n totalTokens: grandFallbackTotalTokens,\n })}`,\n );\n }\n logger.info(\n `Grand total: ${formatTokens({\n inputTokens: grandInputTokens,\n outputTokens: grandOutputTokens,\n totalTokens: grandTotalTokens,\n })}`,\n );\n }\n\n /**\n * Reset all tracked usage\n *\n * Call this at the start of a new document processing run.\n */\n reset(): void {\n this.usage = {};\n }\n}\n","import type { LoggerMethods } from '@heripo/logger';\n\nimport { spawnAsync } from '@heripo/shared';\nimport { spawn } from 'node:child_process';\nimport { join } from 'node:path';\n\nimport { DOCLING_ENVIRONMENT } from '../config/constants';\nimport {\n PythonVersionError,\n type PythonVersionInfo,\n parsePythonVersion,\n validatePythonVersion,\n} from '../utils/python-version';\n\nexport class DoclingEnvironment {\n private readonly logger: LoggerMethods;\n private readonly venvPath: string;\n private readonly port: number;\n private readonly killExistingProcess: boolean;\n\n constructor(options: {\n logger: LoggerMethods;\n venvPath: string;\n port: number;\n killExistingProcess: boolean;\n }) {\n this.logger = options.logger;\n this.venvPath = options.venvPath;\n this.port = options.port;\n this.killExistingProcess = options.killExistingProcess;\n }\n\n async setup(): Promise<void> {\n this.logger.info('[DoclingEnvironment] Setting up Python environment...');\n\n await this.checkPythonVersion();\n await this.setupPythonEnvironment();\n await this.upgradePip();\n await this.installSetuptools();\n await this.installPyArrow();\n await this.installDoclingServe();\n\n // Check if server is already running\n const portInUse = await this.isPortInUse(this.port);\n\n if (portInUse && !this.killExistingProcess) {\n this.logger.info(\n '[DoclingEnvironment] Reusing existing server on port',\n this.port,\n );\n } else {\n await this.startDoclingServe();\n }\n\n this.logger.info('[DoclingEnvironment] Setup completed');\n }\n\n private async checkPythonVersion(): Promise<PythonVersionInfo> {\n const result = await spawnAsync('python3', ['--version']);\n\n if (result.code !== 0) {\n throw new Error('Failed to check Python version');\n }\n\n const output = result.stdout + result.stderr;\n const version = parsePythonVersion(output);\n\n if (!version) {\n throw new Error('Could not parse Python version');\n }\n\n this.logger.info(\n '[DoclingEnvironment] Python version:',\n version.versionString,\n );\n\n try {\n validatePythonVersion(version, 'system');\n } catch (error) {\n if (error instanceof PythonVersionError && version.minor >= 13) {\n this.logger.error(\n '[DoclingEnvironment] Python 3.13+ is not compatible. Install 3.11 or 3.12 with: pyenv install 3.12.0 && pyenv global 3.12.0',\n );\n }\n throw error;\n }\n\n return version;\n }\n\n private async setupPythonEnvironment(): Promise<void> {\n const result = await spawnAsync('python3', ['-m', 'venv', this.venvPath]);\n\n if (result.code !== 0) {\n throw new Error('Failed to create Python virtual environment');\n }\n\n await this.verifyVenvPythonVersion();\n }\n\n private async verifyVenvPythonVersion(): Promise<void> {\n const pythonPath = join(this.venvPath, 'bin', 'python');\n const result = await spawnAsync(pythonPath, ['--version']);\n\n if (result.code !== 0) {\n throw new Error('Failed to verify venv Python version');\n }\n\n const output = result.stdout + result.stderr;\n const version = parsePythonVersion(output);\n\n if (!version) {\n throw new Error('Could not parse venv Python version');\n }\n\n validatePythonVersion(version, 'venv');\n }\n\n private async upgradePip(): Promise<void> {\n const pipPath = join(this.venvPath, 'bin', 'pip');\n const result = await spawnAsync(pipPath, ['install', '--upgrade', 'pip']);\n\n if (result.code !== 0) {\n this.logger.error(\n '[DoclingEnvironment] Failed to upgrade pip:',\n result.stderr,\n );\n throw new Error(`Failed to upgrade pip. Exit code: ${result.code}`);\n }\n }\n\n private async installSetuptools(): Promise<void> {\n const pipPath = join(this.venvPath, 'bin', 'pip');\n const result = await spawnAsync(pipPath, [\n 'install',\n '--upgrade',\n 'setuptools',\n 'wheel',\n ]);\n\n if (result.code !== 0) {\n this.logger.error(\n '[DoclingEnvironment] Failed to install setuptools:',\n result.stderr,\n );\n throw new Error(\n `Failed to install setuptools. Exit code: ${result.code}`,\n );\n }\n }\n\n private async installPyArrow(): Promise<void> {\n const pipPath = join(this.venvPath, 'bin', 'pip');\n const result = await spawnAsync(pipPath, [\n 'install',\n '--only-binary',\n ':all:',\n 'pyarrow',\n ]);\n\n if (result.code !== 0) {\n this.logger.error(\n '[DoclingEnvironment] Failed to install pyarrow:',\n result.stderr,\n );\n throw new Error(`Failed to install pyarrow. Exit code: ${result.code}`);\n }\n }\n\n private async installDoclingServe(): Promise<void> {\n const pipPath = join(this.venvPath, 'bin', 'pip');\n const result = await spawnAsync(pipPath, [\n 'install',\n '--upgrade',\n 'docling-serve',\n ]);\n\n if (result.code !== 0) {\n this.logger.error(\n '[DoclingEnvironment] Failed to install docling-serve:',\n result.stderr,\n );\n throw new Error(\n `Failed to install docling-serve. Exit code: ${result.code}`,\n );\n }\n }\n\n private async isPortInUse(port: number): Promise<boolean> {\n try {\n const result = await spawnAsync('lsof', ['-ti', `:${port}`]);\n return result.code === 0 && !!result.stdout.trim();\n } catch {\n return false;\n }\n }\n\n /**\n * Start the docling-serve server without running full setup.\n * Useful for restarting the server after it has crashed.\n */\n public async startServer(): Promise<void> {\n await this.startDoclingServe();\n }\n\n // Process-killing logic is provided as a static method to allow reuse without instantiation\n public static async killProcessOnPort(\n logger: LoggerMethods,\n port: number,\n ): Promise<void> {\n return new Promise((resolve) => {\n const lsof = spawn('lsof', ['-ti', `:${port}`]);\n const pids: string[] = [];\n\n lsof.stdout?.on('data', (data) => {\n const txt: string = data.toString();\n pids.push(\n ...txt\n .split(/\\s+/)\n .map((s) => s.trim())\n .filter(Boolean),\n );\n });\n\n lsof.on('close', () => {\n if (pids.length === 0) return resolve();\n\n let remaining = pids.length;\n const done = () => {\n if (--remaining <= 0) resolve();\n };\n\n logger.info(\n '[DoclingEnvironment] Killing process',\n pids.join(', '),\n 'on port',\n port,\n );\n for (const pid of pids) {\n const killProc = spawn('kill', ['-9', pid]);\n\n killProc.on('close', (killCode) => {\n if (killCode !== 0) {\n logger.info('[DoclingEnvironment] Failed to kill process', pid);\n }\n done();\n });\n killProc.on('error', (Error) => {\n logger.info('[DoclingEnvironment] Failed to kill process', Error);\n done();\n });\n }\n });\n\n lsof.on('error', () => resolve());\n });\n }\n\n private async startDoclingServe(): Promise<void> {\n return new Promise(async (resolve, reject) => {\n // Kill any existing process on the port if option is enabled\n if (this.killExistingProcess) {\n await DoclingEnvironment.killProcessOnPort(this.logger, this.port);\n }\n\n const venvPath = this.venvPath;\n const doclingServePath = join(venvPath, 'bin', 'docling-serve');\n const args = ['run', '--port', this.port.toString()];\n\n this.logger.info(\n '[DoclingEnvironment] Starting docling-serve on port',\n this.port,\n );\n const doclingProcess = spawn(doclingServePath, args, {\n detached: true, // Detached from parent process\n stdio: 'ignore', // Remove stdio pipes to prevent event loop from hanging\n env: {\n ...process.env,\n // Enable remote API calls for API VLM models\n DOCLING_SERVE_ENABLE_REMOTE_SERVICES: 'true',\n },\n });\n\n doclingProcess.unref(); // Parent doesn't wait for child process to exit\n\n doclingProcess.on('error', (error) => {\n this.logger.error('[DoclingEnvironment] docling-serve error:', error);\n reject(error);\n });\n\n // Give docling-serve time to start\n setTimeout(() => {\n resolve();\n }, DOCLING_ENVIRONMENT.STARTUP_DELAY_MS);\n });\n }\n}\n","/**\n * Python version information\n */\nexport interface PythonVersionInfo {\n major: number;\n minor: number;\n versionString: string;\n}\n\n/**\n * Regex pattern to extract Python version from command output\n */\nexport const PYTHON_VERSION_REGEX = /Python (\\d+)\\.(\\d+)/;\n\n/**\n * Minimum supported Python version\n */\nexport const MIN_PYTHON_VERSION = { major: 3, minor: 9 };\n\n/**\n * Maximum supported Python version (exclusive upper bound)\n * Python 3.13+ is NOT compatible with docling-serve\n */\nexport const MAX_PYTHON_MINOR = 12;\n\n/**\n * Error thrown when Python version is invalid\n */\nexport class PythonVersionError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'PythonVersionError';\n }\n}\n\n/**\n * Parse Python version from command output\n *\n * @param output - Output from `python3 --version` command\n * @returns Parsed version info or null if parsing fails\n *\n * @example\n * ```typescript\n * const version = parsePythonVersion('Python 3.12.0');\n * // { major: 3, minor: 12, versionString: '3.12' }\n * ```\n */\nexport function parsePythonVersion(output: string): PythonVersionInfo | null {\n const match = output.match(PYTHON_VERSION_REGEX);\n if (!match) return null;\n\n const major = parseInt(match[1]);\n const minor = parseInt(match[2]);\n\n return {\n major,\n minor,\n versionString: `${major}.${minor}`,\n };\n}\n\n/**\n * Validate that Python version is within supported range\n *\n * @param version - Parsed Python version info\n * @param context - Context for error messages ('system' or 'venv')\n * @throws PythonVersionError if version is outside supported range\n *\n * @example\n * ```typescript\n * const version = parsePythonVersion('Python 3.12.0');\n * validatePythonVersion(version, 'system'); // OK\n *\n * const tooNew = parsePythonVersion('Python 3.13.0');\n * validatePythonVersion(tooNew, 'venv'); // throws PythonVersionError\n * ```\n */\nexport function validatePythonVersion(\n version: PythonVersionInfo,\n context: 'system' | 'venv' = 'system',\n): void {\n const { major, minor } = version;\n const prefix = context === 'venv' ? 'Venv Python' : 'Python';\n\n // Check if too new (3.13+)\n if (major === 3 && minor >= 13) {\n throw new PythonVersionError(\n `${prefix} ${major}.${minor} is too new. docling-serve requires Python 3.11 or 3.12.`,\n );\n }\n\n // Check if too old (< 3.9)\n if (major !== 3 || minor < MIN_PYTHON_VERSION.minor) {\n throw new PythonVersionError('Python 3.9 or higher is required');\n }\n}\n","import type { LoggerMethods } from '@heripo/logger';\nimport type { OcrStrategy, TokenUsageReport } from '@heripo/model';\nimport type { LanguageModel } from 'ai';\nimport type {\n AsyncConversionTask,\n ConversionOptions,\n DoclingAPIClient,\n} from 'docling-sdk';\n\nimport { LLMTokenUsageAggregator } from '@heripo/shared';\nimport { omit } from 'es-toolkit';\nimport { copyFileSync, createWriteStream, existsSync, rmSync } from 'node:fs';\nimport { rename, writeFile } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { pipeline } from 'node:stream/promises';\n\nimport {\n CHUNKED_CONVERSION,\n PAGE_RENDERING,\n PDF_CONVERTER,\n} from '../config/constants';\nimport { ImagePdfFallbackError } from '../errors/image-pdf-fallback-error';\nimport { ImageExtractor } from '../processors/image-extractor';\nimport { PageRenderer } from '../processors/page-renderer';\nimport { PdfTextExtractor } from '../processors/pdf-text-extractor';\nimport { VlmTextCorrector } from '../processors/vlm-text-corrector';\nimport { OcrStrategySampler } from '../samplers/ocr-strategy-sampler';\nimport { runJqFileJson, runJqFileToFile } from '../utils/jq';\nimport { LocalFileServer } from '../utils/local-file-server';\nimport { ChunkedPDFConverter } from './chunked-pdf-converter';\nimport { ImagePdfConverter } from './image-pdf-converter';\n\n/**\n * Callback function invoked after PDF conversion completes\n * @param outputPath Absolute path to the output directory containing result files\n */\nexport type ConversionCompleteCallback = (\n outputPath: string,\n) => Promise<void> | void;\n\n/**\n * Extended options for PDF conversion.\n */\nexport type PDFConvertOptions = Omit<\n ConversionOptions,\n | 'to_formats'\n | 'image_export_mode'\n | 'ocr_engine'\n | 'accelerator_options'\n | 'ocr_options'\n | 'generate_picture_images'\n | 'generate_page_images'\n | 'images_scale'\n | 'force_ocr'\n | 'pipeline'\n | 'vlm_pipeline_model_local'\n | 'vlm_pipeline_model_api'\n> & {\n num_threads?: number;\n /**\n * Force pre-conversion to image-based PDF before processing.\n * Requires ImageMagick and Ghostscript.\n */\n forceImagePdf?: boolean;\n /** Vision model for OCR strategy sampling (enables new strategy-based flow) */\n strategySamplerModel?: LanguageModel;\n /** Vision model for VLM page processing (required when strategy selects VLM) */\n vlmProcessorModel?: LanguageModel;\n /** Concurrency for VLM page processing (default: 1) */\n vlmConcurrency?: number;\n /** Skip sampling and default to ocrmac */\n skipSampling?: boolean;\n /** Force a specific OCR method, bypassing sampling */\n forcedMethod?: 'ocrmac' | 'vlm';\n /** Token usage aggregator for tracking across sampling and VLM processing */\n aggregator?: LLMTokenUsageAggregator;\n /** Callback fired after each batch of VLM pages completes, with cumulative token usage */\n onTokenUsage?: (report: TokenUsageReport) => void;\n /** Enable chunked conversion for large PDFs (local files only) */\n chunkedConversion?: boolean;\n /** Pages per chunk (default: CHUNKED_CONVERSION.DEFAULT_CHUNK_SIZE) */\n chunkSize?: number;\n /** Max retry attempts per failed chunk (default: CHUNKED_CONVERSION.DEFAULT_MAX_RETRIES) */\n chunkMaxRetries?: number;\n};\n\n/** Result of strategy-based conversion */\nexport interface ConvertWithStrategyResult {\n /** The OCR strategy that was determined */\n strategy: OcrStrategy;\n /** Token usage report from sampling and/or VLM processing (null when no LLM usage occurs) */\n tokenUsageReport: TokenUsageReport | null;\n}\n\nexport class PDFConverter {\n constructor(\n private readonly logger: LoggerMethods,\n private readonly client: DoclingAPIClient,\n private readonly enableImagePdfFallback: boolean = false,\n private readonly timeout: number = PDF_CONVERTER.DEFAULT_TIMEOUT_MS,\n ) {}\n\n async convert(\n url: string,\n reportId: string,\n onComplete: ConversionCompleteCallback,\n cleanupAfterCallback: boolean,\n options: PDFConvertOptions,\n abortSignal?: AbortSignal,\n ): Promise<TokenUsageReport | null> {\n this.logger.info('[PDFConverter] Converting:', url);\n\n // Chunked conversion for large local PDFs\n if (options.chunkedConversion && url.startsWith('file://')) {\n const chunked = new ChunkedPDFConverter(\n this.logger,\n this.client,\n {\n chunkSize: options.chunkSize ?? CHUNKED_CONVERSION.DEFAULT_CHUNK_SIZE,\n maxRetries:\n options.chunkMaxRetries ?? CHUNKED_CONVERSION.DEFAULT_MAX_RETRIES,\n },\n this.timeout,\n );\n return chunked.convertChunked(\n url,\n reportId,\n onComplete,\n cleanupAfterCallback,\n options,\n (opts) => this.buildConversionOptions(opts),\n abortSignal,\n );\n }\n\n // Force image PDF pre-conversion when explicitly requested\n if (options.forceImagePdf) {\n return this.convertViaImagePdf(\n url,\n reportId,\n onComplete,\n cleanupAfterCallback,\n options,\n abortSignal,\n );\n }\n\n // Standard pipeline: direct conversion with optional image PDF fallback\n return this.convertWithFallback(\n url,\n reportId,\n onComplete,\n cleanupAfterCallback,\n options,\n abortSignal,\n );\n }\n\n /**\n * Convert a PDF using OCR strategy sampling to decide between ocrmac and VLM.\n *\n * Flow:\n * 1. Determine strategy (forced, skipped, or sampled via VLM)\n * 2. If VLM → OCR pipeline + VlmTextCorrector (text correction)\n * 3. If ocrmac → existing Docling conversion\n *\n * @returns ConvertWithStrategyResult with the chosen strategy and token report\n */\n async convertWithStrategy(\n url: string,\n reportId: string,\n onComplete: ConversionCompleteCallback,\n cleanupAfterCallback: boolean,\n options: PDFConvertOptions,\n abortSignal?: AbortSignal,\n ): Promise<ConvertWithStrategyResult> {\n this.logger.info('[PDFConverter] Starting strategy-based conversion:', url);\n\n // Create an internal aggregator if none was provided so that\n // sampling + VLM processing token usage is always captured.\n const aggregator = options.aggregator ?? new LLMTokenUsageAggregator();\n const trackedOptions: PDFConvertOptions = { ...options, aggregator };\n\n const pdfPath = url.startsWith('file://') ? url.slice(7) : null;\n\n // Step 1: Determine OCR strategy\n const strategy = await this.determineStrategy(\n pdfPath,\n reportId,\n trackedOptions,\n abortSignal,\n );\n this.logger.info(\n `[PDFConverter] OCR strategy: ${strategy.method} (${strategy.reason})`,\n );\n\n // Emit token usage after sampling phase (so frontend sees sampling cost immediately)\n if (trackedOptions.onTokenUsage) {\n const samplingReport = this.buildTokenReport(aggregator);\n if (samplingReport) {\n trackedOptions.onTokenUsage(samplingReport);\n }\n }\n\n // Step 2: Execute conversion based on strategy\n if (strategy.method === 'vlm') {\n await this.convertWithVlm(\n pdfPath,\n reportId,\n onComplete,\n cleanupAfterCallback,\n trackedOptions,\n abortSignal,\n strategy.detectedLanguages,\n strategy.koreanHanjaMixPages,\n );\n return {\n strategy,\n tokenUsageReport: this.buildTokenReport(aggregator),\n };\n }\n\n // ocrmac path: delegate to existing Docling conversion\n const ocrmacOptions: PDFConvertOptions = strategy.detectedLanguages\n ? { ...trackedOptions, ocr_lang: strategy.detectedLanguages }\n : trackedOptions;\n await this.convert(\n url,\n reportId,\n onComplete,\n cleanupAfterCallback,\n ocrmacOptions,\n abortSignal,\n );\n return {\n strategy,\n tokenUsageReport: this.buildTokenReport(aggregator),\n };\n }\n\n /**\n * Build a token usage report from the aggregator.\n * Returns null when no LLM calls were tracked (e.g. forced ocrmac without sampling).\n */\n private buildTokenReport(\n aggregator: LLMTokenUsageAggregator,\n ): TokenUsageReport | null {\n const report = aggregator.getReport();\n if (report.components.length === 0) {\n return null;\n }\n return report;\n }\n\n /**\n * Determine the OCR strategy based on options and page sampling.\n *\n * When sampling is possible (strategySamplerModel + local file), it always\n * runs — even with forcedMethod — so that detectedLanguages are available\n * for OCR engine configuration. The forced method simply overrides the\n * sampled method choice.\n */\n private async determineStrategy(\n pdfPath: string | null,\n reportId: string,\n options: PDFConvertOptions,\n abortSignal?: AbortSignal,\n ): Promise<OcrStrategy> {\n // Cannot sample: skip, no sampler model, or non-local URL\n if (options.skipSampling || !options.strategySamplerModel || !pdfPath) {\n const method = options.forcedMethod ?? 'ocrmac';\n const reason = options.forcedMethod\n ? `Forced: ${options.forcedMethod}`\n : !pdfPath\n ? 'Non-local URL, sampling skipped'\n : 'Sampling skipped';\n return { method, reason, sampledPages: 0, totalPages: 0 };\n }\n\n // Sample pages to determine strategy (also detects languages)\n const samplingDir = join(process.cwd(), 'output', reportId, '_sampling');\n const sampler = new OcrStrategySampler(\n this.logger,\n new PageRenderer(this.logger),\n new PdfTextExtractor(this.logger),\n );\n\n try {\n const strategy = await sampler.sample(\n pdfPath,\n samplingDir,\n options.strategySamplerModel,\n {\n aggregator: options.aggregator,\n abortSignal,\n },\n );\n\n // Override method when forced, preserving detected languages from sampling\n if (options.forcedMethod) {\n return {\n ...strategy,\n method: options.forcedMethod,\n reason: `Forced: ${options.forcedMethod} (${strategy.reason})`,\n };\n }\n\n return strategy;\n } finally {\n // Always clean up sampling temp directory\n if (existsSync(samplingDir)) {\n rmSync(samplingDir, { recursive: true, force: true });\n }\n }\n }\n\n /**\n * Execute VLM-enhanced PDF conversion.\n *\n * Runs the standard OCR pipeline (Docling) first, then applies VLM text\n * correction to fix garbled Chinese characters (漢字/Hanja) in OCR output.\n */\n private async convertWithVlm(\n pdfPath: string | null,\n reportId: string,\n onComplete: ConversionCompleteCallback,\n cleanupAfterCallback: boolean,\n options: PDFConvertOptions,\n abortSignal?: AbortSignal,\n detectedLanguages?: string[],\n koreanHanjaMixPages?: number[],\n ): Promise<void> {\n if (!options.vlmProcessorModel) {\n throw new Error('vlmProcessorModel is required when OCR strategy is VLM');\n }\n if (!pdfPath) {\n throw new Error('VLM conversion requires a local file (file:// URL)');\n }\n\n const url = `file://${pdfPath}`;\n\n // Wrap the original callback with VLM text correction\n const wrappedCallback: ConversionCompleteCallback = async (outputDir) => {\n // Pre-extract text from PDF text layer for VLM reference\n let pageTexts: Map<number, string> | undefined;\n try {\n const resultPath = join(outputDir, 'result.json');\n // Use jq to extract only page count — avoids loading full JSON into memory\n const totalPages = await runJqFileJson<number>(\n '.pages | length',\n resultPath,\n );\n const textExtractor = new PdfTextExtractor(this.logger);\n pageTexts = await textExtractor.extractText(pdfPath, totalPages);\n } catch {\n this.logger.warn(\n '[PDFConverter] pdftotext extraction failed, proceeding without text reference',\n );\n }\n\n // Save OCR-only result before VLM correction for debugging\n const resultPath = join(outputDir, 'result.json');\n const ocrOriginPath = join(outputDir, 'result_ocr_origin.json');\n copyFileSync(resultPath, ocrOriginPath);\n\n const corrector = new VlmTextCorrector(this.logger);\n await corrector.correctAndSave(outputDir, options.vlmProcessorModel!, {\n concurrency: options.vlmConcurrency,\n aggregator: options.aggregator,\n abortSignal,\n onTokenUsage: options.onTokenUsage,\n documentLanguages: detectedLanguages,\n pageTexts,\n koreanHanjaMixPages,\n });\n await onComplete(outputDir);\n };\n\n // Run the standard OCR pipeline, then apply VLM correction via the wrapped callback\n const vlmOptions: PDFConvertOptions = detectedLanguages\n ? { ...options, ocr_lang: detectedLanguages }\n : options;\n await this.convert(\n url,\n reportId,\n wrappedCallback,\n cleanupAfterCallback,\n vlmOptions,\n abortSignal,\n );\n\n this.logger.info('[PDFConverter] VLM conversion completed successfully');\n }\n\n /**\n * Convert by first creating an image PDF, then running the conversion.\n * Used when forceImagePdf option is enabled.\n */\n private async convertViaImagePdf(\n url: string,\n reportId: string,\n onComplete: ConversionCompleteCallback,\n cleanupAfterCallback: boolean,\n options: PDFConvertOptions,\n abortSignal?: AbortSignal,\n ): Promise<TokenUsageReport | null> {\n this.logger.info(\n '[PDFConverter] Force image PDF mode: converting to image PDF first...',\n );\n const imagePdfConverter = new ImagePdfConverter(this.logger);\n let imagePdfPath: string | null = null;\n\n try {\n imagePdfPath = await imagePdfConverter.convert(url, reportId);\n const localUrl = `file://${imagePdfPath}`;\n this.logger.info(\n '[PDFConverter] Image PDF ready, starting conversion:',\n localUrl,\n );\n\n return await this.performConversion(\n localUrl,\n reportId,\n onComplete,\n cleanupAfterCallback,\n options,\n abortSignal,\n );\n } finally {\n if (imagePdfPath) {\n imagePdfConverter.cleanup(imagePdfPath);\n }\n }\n }\n\n /**\n * Convert directly with optional image PDF fallback on failure.\n * Used by standard (OCR) pipeline.\n */\n private async convertWithFallback(\n url: string,\n reportId: string,\n onComplete: ConversionCompleteCallback,\n cleanupAfterCallback: boolean,\n options: PDFConvertOptions,\n abortSignal?: AbortSignal,\n ): Promise<TokenUsageReport | null> {\n let originalError: Error | null = null;\n\n try {\n return await this.performConversion(\n url,\n reportId,\n onComplete,\n cleanupAfterCallback,\n options,\n abortSignal,\n );\n } catch (error) {\n // If aborted, don't try fallback - re-throw immediately\n if (abortSignal?.aborted) {\n throw error;\n }\n\n originalError = error as Error;\n this.logger.error('[PDFConverter] Conversion failed:', error);\n\n if (!this.enableImagePdfFallback) {\n throw error;\n }\n }\n\n // Fallback: Convert to image PDF and retry\n this.logger.info('[PDFConverter] Attempting image PDF fallback...');\n const imagePdfConverter = new ImagePdfConverter(this.logger);\n let imagePdfPath: string | null = null;\n\n try {\n imagePdfPath = await imagePdfConverter.convert(url, reportId);\n\n // Use file:// URL for local file\n const localUrl = `file://${imagePdfPath}`;\n this.logger.info('[PDFConverter] Retrying with image PDF:', localUrl);\n\n const report = await this.performConversion(\n localUrl,\n reportId,\n onComplete,\n cleanupAfterCallback,\n options,\n abortSignal,\n );\n\n this.logger.info('[PDFConverter] Fallback conversion succeeded');\n return report;\n } catch (fallbackError) {\n this.logger.error(\n '[PDFConverter] Fallback conversion also failed:',\n fallbackError,\n );\n throw new ImagePdfFallbackError(originalError!, fallbackError as Error);\n } finally {\n // Cleanup temp image PDF\n if (imagePdfPath) {\n imagePdfConverter.cleanup(imagePdfPath);\n }\n }\n }\n\n private async performConversion(\n url: string,\n reportId: string,\n onComplete: ConversionCompleteCallback,\n cleanupAfterCallback: boolean,\n options: PDFConvertOptions,\n abortSignal?: AbortSignal,\n ): Promise<TokenUsageReport | null> {\n const startTime = Date.now();\n const conversionOptions = this.buildConversionOptions(options);\n\n this.logger.info(\n `[PDFConverter] OCR languages: ${JSON.stringify(conversionOptions.ocr_options?.lang)}`,\n );\n this.logger.info(\n '[PDFConverter] Converting document with Async Source API...',\n );\n this.logger.info('[PDFConverter] Server will download from URL directly');\n this.logger.info(\n '[PDFConverter] Results will be returned as ZIP to avoid memory limits',\n );\n\n // Resolve URL (start local server for file:// URLs)\n const { httpUrl, server } = await this.resolveUrl(url);\n\n try {\n const task = await this.startConversionTask(httpUrl, conversionOptions);\n await this.trackTaskProgress(task);\n\n // Check abort after docling task completes\n if (abortSignal?.aborted) {\n this.logger.info(\n '[PDFConverter] Conversion aborted after docling completion',\n );\n const error = new Error('PDF conversion was aborted');\n error.name = 'AbortError';\n throw error;\n }\n\n await this.downloadResult(task.taskId);\n } finally {\n // Stop local file server if started\n if (server) {\n this.logger.info('[PDFConverter] Stopping local file server...');\n await server.stop();\n }\n }\n\n const cwd = process.cwd();\n const zipPath = join(cwd, 'result.zip');\n const extractDir = join(cwd, 'result_extracted');\n const outputDir = join(cwd, 'output', reportId);\n\n try {\n await this.processConvertedFiles(zipPath, extractDir, outputDir);\n\n // Render page images using ImageMagick (replaces Docling's page image generation)\n await this.renderPageImages(url, outputDir);\n\n // Check abort before callback\n if (abortSignal?.aborted) {\n this.logger.info('[PDFConverter] Conversion aborted before callback');\n const error = new Error('PDF conversion was aborted');\n error.name = 'AbortError';\n throw error;\n }\n\n // Execute callback with absolute output path\n this.logger.info('[PDFConverter] Executing completion callback...');\n await onComplete(outputDir);\n\n const duration = Date.now() - startTime;\n this.logger.info('[PDFConverter] Conversion completed successfully!');\n this.logger.info('[PDFConverter] Total time:', duration, 'ms');\n } finally {\n // Clean up temporary files (always cleanup temp files)\n this.logger.info('[PDFConverter] Cleaning up temporary files...');\n if (existsSync(zipPath)) {\n rmSync(zipPath, { force: true });\n }\n if (existsSync(extractDir)) {\n rmSync(extractDir, { recursive: true, force: true });\n }\n\n // Cleanup output directory only if requested\n if (cleanupAfterCallback) {\n this.logger.info(\n '[PDFConverter] Cleaning up output directory:',\n outputDir,\n );\n if (existsSync(outputDir)) {\n rmSync(outputDir, { recursive: true, force: true });\n }\n } else {\n this.logger.info('[PDFConverter] Output preserved at:', outputDir);\n }\n }\n\n return null;\n }\n\n private buildConversionOptions(\n options: PDFConvertOptions,\n ): ConversionOptions {\n return {\n ...omit(options, [\n 'num_threads',\n 'forceImagePdf',\n 'strategySamplerModel',\n 'vlmProcessorModel',\n 'skipSampling',\n 'forcedMethod',\n 'aggregator',\n 'onTokenUsage',\n 'chunkedConversion',\n 'chunkSize',\n 'chunkMaxRetries',\n ]),\n to_formats: ['json', 'html'],\n image_export_mode: 'embedded',\n ocr_engine: 'ocrmac',\n ocr_options: {\n kind: 'ocrmac',\n lang: options.ocr_lang ?? ['ko-KR', 'en-US'],\n recognition: 'accurate',\n framework: 'livetext',\n },\n generate_picture_images: true,\n generate_page_images: false, // Page images are rendered by PageRenderer (ImageMagick) after conversion\n images_scale: 2.0,\n /**\n * While disabling this option yields the most accurate text extraction for readable PDFs,\n * text layers overlaid on images or drawings can introduce noise when not merged properly.\n * In practice, archaeological report PDFs almost always contain such overlapping cases.\n * Enabling force_ocr mitigates this risk. Although OCR may introduce minor errors compared\n * to direct text extraction, the accuracy remains high since the source is digital, not scanned paper.\n */\n force_ocr: true,\n accelerator_options: {\n device: 'mps',\n num_threads: options.num_threads,\n },\n };\n }\n\n private async startConversionTask(\n url: string,\n conversionOptions: ConversionOptions,\n ): Promise<AsyncConversionTask> {\n const task = await this.client.convertSourceAsync({\n sources: [\n {\n kind: 'http',\n url,\n },\n ],\n options: conversionOptions,\n target: {\n kind: 'zip',\n },\n });\n\n this.logger.info(`[PDFConverter] Task created: ${task.taskId}`);\n this.logger.info('[PDFConverter] Polling for progress...');\n\n return task;\n }\n\n /**\n * Start a local file server for file:// URLs\n *\n * @param url URL to check (file:// or http://)\n * @returns Object with httpUrl and optional server to stop later\n */\n private async resolveUrl(\n url: string,\n ): Promise<{ httpUrl: string; server?: LocalFileServer }> {\n if (url.startsWith('file://')) {\n const filePath = url.slice(7); // Remove 'file://' prefix\n const server = new LocalFileServer();\n const httpUrl = await server.start(filePath);\n\n this.logger.info('[PDFConverter] Started local file server:', httpUrl);\n\n return { httpUrl, server };\n }\n\n return { httpUrl: url };\n }\n\n private async trackTaskProgress(task: AsyncConversionTask): Promise<void> {\n const conversionStartTime = Date.now();\n let lastProgressLine = '';\n\n const logProgress = (status: {\n task_status: string;\n task_position?: number;\n task_meta?: { total_documents?: number; processed_documents?: number };\n }) => {\n const parts: string[] = [`Status: ${status.task_status}`];\n\n if (status.task_position !== undefined) {\n parts.push(`position: ${status.task_position}`);\n }\n\n const meta = status.task_meta;\n if (meta) {\n if (\n meta.processed_documents !== undefined &&\n meta.total_documents !== undefined\n ) {\n parts.push(\n `progress: ${meta.processed_documents}/${meta.total_documents}`,\n );\n }\n }\n\n const progressLine = `\\r[PDFConverter] ${parts.join(' | ')}`;\n if (progressLine !== lastProgressLine) {\n lastProgressLine = progressLine;\n process.stdout.write(progressLine);\n }\n };\n\n while (true) {\n if (Date.now() - conversionStartTime > this.timeout) {\n throw new Error('Task timeout');\n }\n\n const status = await task.poll();\n\n logProgress(status);\n\n if (status.task_status === 'success') {\n this.logger.info('\\n[PDFConverter] Conversion completed!');\n return;\n }\n\n if (status.task_status === 'failure') {\n // Try to get detailed error info from the task result\n const errorDetails = await this.getTaskFailureDetails(task);\n const elapsed = Math.round((Date.now() - conversionStartTime) / 1000);\n this.logger.error(\n `\\n[PDFConverter] Task failed after ${elapsed}s: ${errorDetails}`,\n );\n throw new Error(`Task failed: ${errorDetails}`);\n }\n\n await new Promise((resolve) =>\n setTimeout(resolve, PDF_CONVERTER.POLL_INTERVAL_MS),\n );\n }\n }\n\n /**\n * Fetch detailed error information from a failed task result.\n */\n private async getTaskFailureDetails(\n task: AsyncConversionTask,\n ): Promise<string> {\n try {\n const result = await task.getResult();\n if (result.errors?.length) {\n return result.errors\n .map((e: { message: string }) => e.message)\n .join('; ');\n }\n /* v8 ignore start -- status is always present in ConvertDocumentResponse */\n return `status: ${result.status ?? 'unknown'}`;\n /* v8 ignore stop */\n } catch (err) {\n this.logger.error('[PDFConverter] Failed to retrieve task result:', err);\n return 'unable to retrieve error details';\n }\n }\n\n private async downloadResult(taskId: string): Promise<void> {\n this.logger.info(\n '\\n[PDFConverter] Task completed, downloading ZIP file...',\n );\n\n const zipResult = await this.client.getTaskResultFile(taskId);\n\n const zipPath = join(process.cwd(), 'result.zip');\n this.logger.info('[PDFConverter] Saving ZIP file to:', zipPath);\n\n if (zipResult.fileStream) {\n const writeStream = createWriteStream(zipPath);\n await pipeline(zipResult.fileStream, writeStream);\n return;\n }\n\n if (zipResult.data) {\n await writeFile(zipPath, zipResult.data);\n return;\n }\n\n // Fallback: direct HTTP download when SDK stream/data unavailable\n this.logger.warn(\n '[PDFConverter] SDK file result unavailable, falling back to direct download...',\n );\n const baseUrl = this.client.getConfig().baseUrl;\n const response = await fetch(`${baseUrl}/v1/result/${taskId}`, {\n headers: { Accept: 'application/zip' },\n });\n\n if (!response.ok) {\n throw new Error(\n `Failed to download ZIP file: ${response.status} ${response.statusText}`,\n );\n }\n\n const buffer = new Uint8Array(await response.arrayBuffer());\n await writeFile(zipPath, buffer);\n }\n\n private async processConvertedFiles(\n zipPath: string,\n extractDir: string,\n outputDir: string,\n ): Promise<void> {\n // Extract and save documents with images\n await ImageExtractor.extractAndSaveDocumentsFromZip(\n this.logger,\n zipPath,\n extractDir,\n outputDir,\n );\n }\n\n /**\n * Render page images from the source PDF using ImageMagick and update result.json.\n * Uses jq to update the JSON file without loading it into Node.js memory.\n * Replaces Docling's generate_page_images which fails on large PDFs\n * due to memory limits when embedding all page images as base64.\n */\n private async renderPageImages(\n url: string,\n outputDir: string,\n ): Promise<void> {\n if (!url.startsWith('file://')) {\n this.logger.warn(\n '[PDFConverter] Page image rendering skipped: only supported for local files (file:// URLs)',\n );\n return;\n }\n\n const pdfPath = url.slice(7);\n this.logger.info(\n '[PDFConverter] Rendering page images with ImageMagick...',\n );\n\n const renderer = new PageRenderer(this.logger);\n const renderResult = await renderer.renderPages(pdfPath, outputDir);\n\n // Update result.json with page image URIs using jq to avoid loading large JSON\n const resultPath = join(outputDir, 'result.json');\n const tmpPath = resultPath + '.tmp';\n const jqProgram = `\n .pages |= with_entries(\n if (.value.page_no - 1) >= 0 and (.value.page_no - 1) < ${renderResult.pageCount} then\n .value.image.uri = \"pages/page_\\\\(.value.page_no - 1).png\" |\n .value.image.mimetype = \"image/png\" |\n .value.image.dpi = ${PAGE_RENDERING.DEFAULT_DPI}\n else . end\n )\n `;\n await runJqFileToFile(jqProgram, resultPath, tmpPath);\n await rename(tmpPath, resultPath);\n\n this.logger.info(\n `[PDFConverter] Rendered ${renderResult.pageCount} page images`,\n );\n }\n}\n","/**\n * Error thrown when both original PDF conversion and image PDF fallback fail.\n * Contains both errors for debugging purposes.\n */\nexport class ImagePdfFallbackError extends Error {\n public readonly name = 'ImagePdfFallbackError';\n\n constructor(\n public readonly originalError: Error,\n public readonly fallbackError: Error,\n ) {\n super(\n `PDF conversion failed with fallback. ` +\n `Original: ${originalError.message}. ` +\n `Fallback: ${fallbackError.message}`,\n );\n }\n}\n","import type { LoggerMethods } from '@heripo/logger';\n\nimport {\n createReadStream,\n createWriteStream,\n existsSync,\n mkdirSync,\n readdirSync,\n rmSync,\n writeFileSync,\n} from 'node:fs';\nimport { extname, join } from 'node:path';\nimport { Transform } from 'node:stream';\nimport { pipeline } from 'node:stream/promises';\nimport * as yauzl from 'yauzl';\n\nimport {\n jqExtractBase64PngStringsStreaming,\n jqReplaceBase64WithPathsToFile,\n} from '../utils/jq';\n\n/**\n * ImageExtractor handles extraction and processing of images from PDF conversion results\n *\n * This class provides functionality to:\n * - Extract ZIP files containing converted PDF documents\n * - Extract base64-encoded images from JSON and HTML content\n * - Save images as separate PNG files\n * - Replace base64 data with relative file paths\n */\nexport class ImageExtractor {\n /**\n * Extract a ZIP file to a target directory\n */\n private static async extractZip(\n zipPath: string,\n targetDir: string,\n ): Promise<void> {\n return new Promise((resolve, reject) => {\n yauzl.open(zipPath, { lazyEntries: true }, (err, zipfile) => {\n if (err || !zipfile) {\n reject(err || new Error('Failed to open zip file'));\n return;\n }\n\n zipfile.readEntry();\n\n zipfile.on('entry', (entry) => {\n const entryPath = join(targetDir, entry.fileName);\n\n if (/\\/$/.test(entry.fileName)) {\n // Directory entry\n mkdirSync(entryPath, { recursive: true });\n zipfile.readEntry();\n } else {\n // File entry\n zipfile.openReadStream(entry, (err, readStream) => {\n if (err || !readStream) {\n reject(err || new Error('Failed to open read stream'));\n return;\n }\n\n mkdirSync(join(entryPath, '..'), { recursive: true });\n const writeStream = createWriteStream(entryPath);\n\n readStream.pipe(writeStream);\n writeStream.on('finish', () => {\n zipfile.readEntry();\n });\n writeStream.on('error', reject);\n });\n }\n });\n\n zipfile.on('end', () => {\n resolve();\n });\n\n zipfile.on('error', reject);\n });\n });\n }\n\n /**\n * Extract a base64-encoded image to a file and return the relative path\n */\n private static extractBase64ImageToFile(\n base64Data: string,\n imagesDir: string,\n index: number,\n prefix: string,\n dirName: string,\n ): string {\n const PREFIX = 'data:image/png;base64,';\n const base64Content = base64Data.startsWith(PREFIX)\n ? base64Data.slice(PREFIX.length)\n : base64Data;\n\n const filename = `${prefix}_${index}.png`;\n const filepath = join(imagesDir, filename);\n\n // Convert base64 to buffer and write to file\n const buffer = Buffer.from(base64Content, 'base64');\n writeFileSync(filepath, buffer);\n\n return `${dirName}/${filename}`;\n }\n\n /**\n * Extract base64 images from HTML using streaming.\n * Reads HTML file as a stream, extracts base64 images from src attributes,\n * saves them as PNG files, and replaces with file paths in the output HTML.\n * Returns the number of images extracted.\n */\n static async extractImagesFromHtmlStream(\n htmlInputPath: string,\n htmlOutputPath: string,\n imagesDir: string,\n ): Promise<number> {\n let imageIndex = 0;\n let pending = '';\n const MARKER = 'src=\"data:image/png;base64,';\n\n const transform = new Transform({\n decodeStrings: false,\n encoding: 'utf-8',\n transform(chunk: string, _encoding, callback) {\n pending += chunk;\n let result = '';\n\n while (true) {\n const markerIdx = pending.indexOf(MARKER);\n\n if (markerIdx === -1) {\n // Keep a tail that could be a partial marker match\n const safeEnd = Math.max(0, pending.length - MARKER.length);\n result += pending.slice(0, safeEnd);\n pending = pending.slice(safeEnd);\n break;\n }\n\n // Flush everything before the marker\n result += pending.slice(0, markerIdx);\n\n // Find the closing quote after base64 data\n const dataStart = markerIdx + MARKER.length;\n const quoteIdx = pending.indexOf('\"', dataStart);\n\n if (quoteIdx === -1) {\n // Closing quote not in buffer yet — keep everything from marker onward\n pending = pending.slice(markerIdx);\n break;\n }\n\n // Extract base64 content and save as image file\n const base64Content = pending.slice(dataStart, quoteIdx);\n const filename = `image_${imageIndex}.png`;\n const filepath = join(imagesDir, filename);\n const buf = Buffer.from(base64Content, 'base64');\n writeFileSync(filepath, buf);\n\n const relativePath = `images/${filename}`;\n result += `src=\"${relativePath}\"`;\n imageIndex++;\n\n pending = pending.slice(quoteIdx + 1);\n }\n\n if (result.length > 0) {\n this.push(result);\n }\n callback();\n },\n flush(callback) {\n if (pending.length > 0) {\n this.push(pending);\n }\n callback();\n },\n });\n\n const rs = createReadStream(htmlInputPath, { encoding: 'utf-8' });\n const ws = createWriteStream(htmlOutputPath, { encoding: 'utf-8' });\n\n await pipeline(rs, transform, ws);\n\n return imageIndex;\n }\n\n /**\n * Save JSON and HTML documents with base64 images extracted to separate files.\n * Uses jq for JSON processing and streaming for HTML to handle large files.\n *\n * This method:\n * 1. Extracts base64-encoded images from JSON and HTML content\n * 2. Saves images as separate PNG files\n * 3. Replaces base64 data with relative file paths\n * 4. Saves the transformed documents to the output directory\n */\n private static async saveDocumentsWithExtractedImages(\n logger: LoggerMethods,\n outputDir: string,\n filename: string,\n jsonSourcePath: string,\n htmlSourcePath: string,\n ): Promise<void> {\n // Clear output directory completely at the start, then recreate it\n try {\n if (existsSync(outputDir)) {\n rmSync(outputDir, { recursive: true, force: true });\n }\n } catch (e) {\n logger.warn('[PDFConverter] Failed to clear output directory:', e);\n }\n mkdirSync(outputDir, { recursive: true });\n\n // Get filename without extension\n const baseName = filename.replace(extname(filename), '');\n\n // Save JSON with extracted images (using jq streaming for large files)\n const jsonPath = join(outputDir, `${baseName}.json`);\n try {\n // Create images directory for picture images from JSON\n const imagesDir = join(outputDir, 'images');\n if (!existsSync(imagesDir)) {\n mkdirSync(imagesDir, { recursive: true });\n }\n\n // Step 1: Extract base64 images using streaming jq (one at a time, no accumulation)\n const imageCount = await jqExtractBase64PngStringsStreaming(\n jsonSourcePath,\n (base64Data, index) => {\n ImageExtractor.extractBase64ImageToFile(\n base64Data,\n imagesDir,\n index,\n 'pic',\n 'images',\n );\n },\n );\n\n logger.info(\n `[PDFConverter] Extracted ${imageCount} picture images from JSON to ${imagesDir}`,\n );\n\n // Step 2: Replace base64 images with file paths using jq, pipe directly to file\n await jqReplaceBase64WithPathsToFile(\n jsonSourcePath,\n jsonPath,\n 'images',\n 'pic',\n );\n\n logger.info(\n `[PDFConverter] Replaced ${imageCount} base64 images with file paths`,\n );\n } catch (e) {\n logger.warn(\n '[PDFConverter] Failed to extract images from JSON using jq. Error:',\n e,\n );\n throw e;\n }\n logger.info('[PDFConverter] Saved JSON:', jsonPath);\n\n // Save HTML with extracted images using streaming\n const htmlPath = join(outputDir, `${baseName}.html`);\n try {\n // Create images directory for HTML images\n const imagesDir = join(outputDir, 'images');\n if (!existsSync(imagesDir)) {\n mkdirSync(imagesDir, { recursive: true });\n }\n\n const htmlImageCount = await ImageExtractor.extractImagesFromHtmlStream(\n htmlSourcePath,\n htmlPath,\n imagesDir,\n );\n\n logger.info(\n `[PDFConverter] Extracted ${htmlImageCount} images from HTML to ${imagesDir}`,\n );\n } catch (e) {\n logger.warn(\n '[PDFConverter] Failed to extract images from HTML, copying original. Error:',\n e,\n );\n // Fallback: copy original HTML using streaming\n const rs = createReadStream(htmlSourcePath);\n const ws = createWriteStream(htmlPath);\n await pipeline(rs, ws);\n }\n logger.info('[PDFConverter] Saved HTML:', htmlPath);\n }\n\n /**\n * Extract documents from ZIP and save with extracted images\n * Uses jq for JSON processing and streaming for HTML to handle large files\n * without loading into Node.js memory\n *\n * Complete workflow:\n * 1. Extract ZIP file to temporary directory\n * 2. Find JSON and HTML files from extracted files\n * 3. Use jq to stream-extract base64 images from JSON and save as separate files\n * 4. Use jq to replace base64 with file paths in JSON (piped to file)\n * 5. Process HTML with streaming Transform to extract and replace images\n * 6. Save transformed documents to output directory (as result.json and result.html)\n */\n static async extractAndSaveDocumentsFromZip(\n logger: LoggerMethods,\n zipPath: string,\n extractDir: string,\n outputDir: string,\n ): Promise<void> {\n // Extract zip file\n logger.info('[PDFConverter] Extracting ZIP file...');\n await ImageExtractor.extractZip(zipPath, extractDir);\n\n // Find JSON and HTML files dynamically\n const files = readdirSync(extractDir);\n const jsonFile = files.find((f) => extname(f).toLowerCase() === '.json');\n const htmlFile = files.find((f) => extname(f).toLowerCase() === '.html');\n\n if (!jsonFile || !htmlFile) {\n throw new Error(\n `Expected one JSON and one HTML file in extracted directory. Found: ${files.join(', ')}`,\n );\n }\n\n // Get file paths\n const jsonPath = join(extractDir, jsonFile);\n const htmlPath = join(extractDir, htmlFile);\n\n // Save converted files to output directory with extracted images\n // Both JSON and HTML are processed with streaming to avoid loading large files into memory\n logger.info('[PDFConverter] Saving converted files to output...');\n await ImageExtractor.saveDocumentsWithExtractedImages(\n logger,\n outputDir,\n 'result',\n jsonPath,\n htmlPath,\n );\n\n logger.info('[PDFConverter] Files saved to:', outputDir);\n }\n}\n","import { spawn } from 'node:child_process';\nimport { createWriteStream } from 'node:fs';\nimport { rename } from 'node:fs/promises';\nimport { pipeline } from 'node:stream/promises';\n\n/**\n * Resolve jq executable path from environment or default to 'jq' in PATH.\n */\nfunction getJqPath(): string {\n const p = process.env.JQ_PATH?.trim();\n return p && p.length > 0 ? p : 'jq';\n}\n\n/**\n * Run a jq program against a JSON file and parse the JSON result.\n * - program: jq filter/program string\n * - filePath: path to the input JSON file\n * Returns parsed JSON as type T.\n */\nexport function runJqFileJson<T = unknown>(\n program: string,\n filePath: string,\n): Promise<T> {\n return new Promise<T>((resolve, reject) => {\n const jqPath = getJqPath();\n const args = [\n '-c', // compact output (single line when possible)\n program,\n filePath,\n ];\n\n const child = spawn(jqPath, args, {\n stdio: ['ignore', 'pipe', 'pipe'],\n env: process.env,\n });\n\n let stdout = '';\n let stderr = '';\n\n child.stdout.setEncoding('utf-8');\n child.stderr.setEncoding('utf-8');\n\n child.stdout.on('data', (chunk: string) => {\n stdout += chunk;\n });\n child.stderr.on('data', (chunk: string) => {\n stderr += chunk;\n });\n\n child.on('error', (err) => {\n reject(err);\n });\n\n child.on('close', (code) => {\n if (code !== 0) {\n const error = new Error(\n `jq exited with code ${code}. ${stderr ? 'Stderr: ' + stderr : ''}`,\n );\n return reject(error);\n }\n try {\n // jq may output trailing newlines; trim is safe for JSON\n const text = stdout.trim();\n const parsed = JSON.parse(text) as T;\n resolve(parsed);\n } catch (e) {\n reject(\n new Error(\n `Failed to parse jq output as JSON. Output length=${stdout.length}. Error: ${(e as Error).message}`,\n ),\n );\n }\n });\n });\n}\n\n/**\n * Run a jq program against a JSON file and pipe output directly to a file.\n * Avoids loading jq output into Node.js memory.\n */\nexport function runJqFileToFile(\n program: string,\n inputPath: string,\n outputPath: string,\n): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n const jqPath = getJqPath();\n const args = [program, inputPath];\n\n const child = spawn(jqPath, args, {\n stdio: ['ignore', 'pipe', 'pipe'],\n env: process.env,\n });\n\n let stderr = '';\n let exitCode: number | null = null;\n let pipelineDone = false;\n let settled = false;\n\n child.stderr.setEncoding('utf-8');\n child.stderr.on('data', (chunk: string) => {\n stderr += chunk;\n });\n\n const ws = createWriteStream(outputPath);\n\n function trySettle() {\n if (settled) return;\n if (!pipelineDone || exitCode === null) return;\n settled = true;\n if (exitCode !== 0) {\n reject(\n new Error(\n `jq exited with code ${exitCode}. ${stderr ? 'Stderr: ' + stderr : ''}`,\n ),\n );\n } else {\n resolve();\n }\n }\n\n child.on('error', (err) => {\n if (settled) return;\n settled = true;\n ws.destroy();\n reject(err);\n });\n\n pipeline(child.stdout, ws)\n .then(() => {\n pipelineDone = true;\n trySettle();\n })\n .catch((err) => {\n if (settled) return;\n settled = true;\n reject(err);\n });\n\n child.on('close', (code) => {\n exitCode = code ?? 1;\n trySettle();\n });\n });\n}\n\n/**\n * Run a jq program with -r (raw output) and process stdout line by line.\n * Each line is passed to the onLine callback immediately, avoiding memory accumulation.\n */\nexport function runJqFileLines(\n program: string,\n filePath: string,\n onLine: (line: string) => void,\n): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n const jqPath = getJqPath();\n const args = ['-r', program, filePath];\n\n const child = spawn(jqPath, args, {\n stdio: ['ignore', 'pipe', 'pipe'],\n env: process.env,\n });\n\n let stderr = '';\n let buffer = '';\n let callbackError = false;\n\n child.stdout.setEncoding('utf-8');\n child.stderr.setEncoding('utf-8');\n\n function safeOnLine(line: string): void {\n if (callbackError) return;\n try {\n onLine(line);\n } catch (err) {\n callbackError = true;\n child.kill();\n reject(err);\n }\n }\n\n child.stdout.on('data', (chunk: string) => {\n buffer += chunk;\n let newlineIdx: number;\n while ((newlineIdx = buffer.indexOf('\\n')) !== -1) {\n const line = buffer.slice(0, newlineIdx);\n buffer = buffer.slice(newlineIdx + 1);\n if (line.length > 0) {\n safeOnLine(line);\n }\n }\n });\n\n child.stderr.on('data', (chunk: string) => {\n stderr += chunk;\n });\n\n child.on('error', (err) => {\n if (!callbackError) reject(err);\n });\n\n child.on('close', (code) => {\n if (callbackError) return;\n // Process any remaining data in buffer\n if (buffer.length > 0) {\n safeOnLine(buffer);\n }\n if (callbackError) return;\n\n if (code !== 0) {\n reject(\n new Error(\n `jq exited with code ${code}. ${stderr ? 'Stderr: ' + stderr : ''}`,\n ),\n );\n } else {\n resolve();\n }\n });\n });\n}\n\n/**\n * Convenience: extract all base64 PNG data-URI strings from a JSON file.\n */\nexport function jqExtractBase64PngStrings(filePath: string): Promise<string[]> {\n const program = `\n [\n .. |\n select(type == \"string\" and startswith(\"data:image/png;base64\"))\n ]\n `;\n return runJqFileJson<string[]>(program, filePath);\n}\n\n/**\n * Streaming extraction of base64 PNG data-URI strings from a JSON file.\n * Each image is passed to the onImage callback immediately instead of accumulating all in memory.\n * Returns the total count of images found.\n */\nexport async function jqExtractBase64PngStringsStreaming(\n filePath: string,\n onImage: (base64Data: string, index: number) => void,\n): Promise<number> {\n let index = 0;\n await runJqFileLines(\n '.. | select(type == \"string\" and startswith(\"data:image/png;base64\"))',\n filePath,\n (line) => {\n onImage(line, index);\n index++;\n },\n );\n return index;\n}\n\n/**\n * Convenience: replace base64 PNG data-URIs with file paths like `${dirName}/${prefix}_<idx>.png`.\n * Returns an object: { data, count }\n */\nexport function jqReplaceBase64WithPaths(\n filePath: string,\n dirName: string,\n prefix: string,\n): Promise<{ data: unknown; count: number }> {\n const program = `\n reduce paths(type == \"string\" and startswith(\"data:image/png;base64\")) as $p (\n {data: ., counter: 0};\n .counter as $idx |\n .data |= setpath($p; \"${dirName}/${prefix}_\\\\($idx).png\") |\n .counter += 1\n ) | {data: .data, count: .counter}\n `;\n return runJqFileJson<{ data: unknown; count: number }>(program, filePath);\n}\n\n/**\n * Replace base64 PNG data-URIs with file paths and pipe result directly to an output file.\n * Avoids loading the entire transformed JSON into Node.js memory.\n * Uses a temporary file and atomic rename to avoid corrupting the output.\n */\nexport async function jqReplaceBase64WithPathsToFile(\n inputPath: string,\n outputPath: string,\n dirName: string,\n prefix: string,\n): Promise<void> {\n const program = `\n reduce paths(type == \"string\" and startswith(\"data:image/png;base64\")) as $p (\n {data: ., counter: 0};\n .counter as $idx |\n .data |= setpath($p; \"${dirName}/${prefix}_\\\\($idx).png\") |\n .counter += 1\n ) | .data\n `;\n const tmpPath = outputPath + '.tmp';\n await runJqFileToFile(program, inputPath, tmpPath);\n await rename(tmpPath, outputPath);\n}\n","import type { LoggerMethods } from '@heripo/logger';\n\nimport { spawnAsync } from '@heripo/shared';\nimport { existsSync, mkdirSync, readdirSync } from 'node:fs';\nimport { join } from 'node:path';\n\nimport { PAGE_RENDERING } from '../config/constants.js';\n\n/** Minimum percentage increment between progress log messages */\nconst PROGRESS_LOG_PERCENT_STEP = 10;\n\n/** Result of page rendering */\nexport interface PageRenderResult {\n /** Total number of pages rendered */\n pageCount: number;\n /** Absolute path to the pages directory */\n pagesDir: string;\n /** Sorted list of rendered page file paths (absolute) */\n pageFiles: string[];\n}\n\n/** Options for page rendering */\nexport interface PageRendererOptions {\n /** DPI for rendered images (default: 200) */\n dpi?: number;\n}\n\n/**\n * Renders PDF pages to individual PNG images using ImageMagick.\n *\n * ## System Requirements\n * - ImageMagick (`brew install imagemagick`)\n * - Ghostscript (`brew install ghostscript`)\n */\nexport class PageRenderer {\n private lastLoggedPercent = 0;\n\n constructor(private readonly logger: LoggerMethods) {}\n\n /**\n * Render all pages of a PDF to individual PNG files.\n *\n * Uses per-page rendering (`magick 'input.pdf[N]'`) when page count is known,\n * limiting peak memory to ~15MB/page instead of loading all pages at once.\n *\n * @param pdfPath - Absolute path to the source PDF file\n * @param outputDir - Directory where pages/ subdirectory will be created\n * @param options - Rendering options\n * @returns Render result with page count and file paths\n */\n async renderPages(\n pdfPath: string,\n outputDir: string,\n options?: PageRendererOptions,\n ): Promise<PageRenderResult> {\n const dpi = options?.dpi ?? PAGE_RENDERING.DEFAULT_DPI;\n const pagesDir = join(outputDir, 'pages');\n\n if (!existsSync(pagesDir)) {\n mkdirSync(pagesDir, { recursive: true });\n }\n\n const totalPages = await this.getPageCount(pdfPath);\n\n if (totalPages > 0) {\n this.logger.info(\n `[PageRenderer] Rendering ${totalPages} pages at ${dpi} DPI...`,\n );\n this.lastLoggedPercent = 0;\n\n for (let i = 0; i < totalPages; i++) {\n const result = await spawnAsync(\n 'magick',\n [\n '-density',\n dpi.toString(),\n `${pdfPath}[${i}]`,\n '-background',\n 'white',\n '-alpha',\n 'remove',\n '-alpha',\n 'off',\n join(pagesDir, `page_${i}.png`),\n ],\n { captureStdout: false },\n );\n\n if (result.code !== 0) {\n throw new Error(\n `[PageRenderer] Failed to render page ${i + 1}/${totalPages}: ${result.stderr || 'Unknown error'}`,\n );\n }\n\n this.logProgress(i + 1, totalPages);\n }\n } else {\n // Fallback: render all pages at once when pdfinfo is unavailable\n this.logger.info(`[PageRenderer] Rendering PDF at ${dpi} DPI...`);\n\n const result = await spawnAsync(\n 'magick',\n [\n '-density',\n dpi.toString(),\n pdfPath,\n '-background',\n 'white',\n '-alpha',\n 'remove',\n '-alpha',\n 'off',\n join(pagesDir, 'page_%d.png'),\n ],\n { captureStdout: false },\n );\n\n if (result.code !== 0) {\n throw new Error(\n `[PageRenderer] Failed to render PDF pages: ${result.stderr || 'Unknown error'}`,\n );\n }\n }\n\n const pageFiles = readdirSync(pagesDir)\n .filter((f) => f.startsWith('page_') && f.endsWith('.png'))\n .sort((a, b) => {\n const numA = parseInt(a.replace('page_', '').replace('.png', ''), 10);\n const numB = parseInt(b.replace('page_', '').replace('.png', ''), 10);\n return numA - numB;\n })\n .map((f) => join(pagesDir, f));\n\n this.logger.info(\n `[PageRenderer] Rendered ${pageFiles.length} pages to ${pagesDir}`,\n );\n\n return {\n pageCount: pageFiles.length,\n pagesDir,\n pageFiles,\n };\n }\n\n /**\n * Log rendering progress at appropriate intervals (every 10%).\n */\n private logProgress(current: number, total: number): void {\n const percent = Math.floor((current / total) * 100);\n if (\n percent >= this.lastLoggedPercent + PROGRESS_LOG_PERCENT_STEP ||\n current === total\n ) {\n this.lastLoggedPercent = percent;\n this.logger.info(\n `[PageRenderer] Rendering pages: ${current}/${total} (${percent}%)`,\n );\n }\n }\n\n /**\n * Get total page count using pdfinfo.\n * Returns 0 on failure (progress logging will be skipped).\n */\n private async getPageCount(pdfPath: string): Promise<number> {\n try {\n const result = await spawnAsync('pdfinfo', [pdfPath]);\n if (result.code !== 0) return 0;\n const match = result.stdout.match(/^Pages:\\s+(\\d+)/m);\n return match ? parseInt(match[1], 10) : 0;\n } catch {\n return 0;\n }\n }\n}\n","import type { LoggerMethods } from '@heripo/logger';\n\nimport { spawnAsync } from '@heripo/shared';\n\n/**\n * Extracts text from PDF pages using the pdftotext command-line tool.\n *\n * Uses the `-layout` flag to preserve the original page layout.\n * Failures are logged as warnings and produce empty strings,\n * allowing the pipeline to gracefully fall back to VLM-only OCR.\n *\n * ## System Requirements\n * - Poppler utils (`brew install poppler`)\n */\nexport class PdfTextExtractor {\n constructor(private readonly logger: LoggerMethods) {}\n\n /**\n * Extract text from all pages of a PDF.\n *\n * @param pdfPath - Absolute path to the source PDF file\n * @param totalPages - Total number of pages in the PDF\n * @returns Map of 1-based page numbers to extracted text strings\n */\n async extractText(\n pdfPath: string,\n totalPages: number,\n ): Promise<Map<number, string>> {\n this.logger.info(\n `[PdfTextExtractor] Extracting text from ${totalPages} pages...`,\n );\n\n const pageTexts = new Map<number, string>();\n\n for (let page = 1; page <= totalPages; page++) {\n const text = await this.extractPageText(pdfPath, page);\n pageTexts.set(page, text);\n }\n\n const nonEmptyCount = [...pageTexts.values()].filter(\n (t) => t.trim().length > 0,\n ).length;\n this.logger.info(\n `[PdfTextExtractor] Extracted text from ${nonEmptyCount}/${totalPages} pages`,\n );\n\n return pageTexts;\n }\n\n /**\n * Get total page count of a PDF using pdfinfo.\n * Returns 0 on failure.\n */\n async getPageCount(pdfPath: string): Promise<number> {\n const result = await spawnAsync('pdfinfo', [pdfPath]);\n if (result.code !== 0) {\n this.logger.warn(\n `[PdfTextExtractor] pdfinfo failed: ${result.stderr || 'Unknown error'}`,\n );\n return 0;\n }\n const match = result.stdout.match(/^Pages:\\s+(\\d+)/m);\n return match ? parseInt(match[1], 10) : 0;\n }\n\n /**\n * Extract text from the entire PDF in a single pdftotext invocation.\n * Returns empty string on failure (logged as warning).\n */\n async extractFullText(pdfPath: string): Promise<string> {\n const result = await spawnAsync('pdftotext', ['-layout', pdfPath, '-']);\n\n if (result.code !== 0) {\n this.logger.warn(\n `[PdfTextExtractor] pdftotext (full) failed: ${result.stderr || 'Unknown error'}`,\n );\n return '';\n }\n\n return result.stdout;\n }\n\n /**\n * Extract text from a single PDF page using pdftotext.\n * Returns empty string on failure (logged as warning).\n */\n async extractPageText(pdfPath: string, page: number): Promise<string> {\n const result = await spawnAsync('pdftotext', [\n '-f',\n page.toString(),\n '-l',\n page.toString(),\n '-layout',\n pdfPath,\n '-',\n ]);\n\n if (result.code !== 0) {\n this.logger.warn(\n `[PdfTextExtractor] pdftotext failed for page ${page}: ${result.stderr || 'Unknown error'}`,\n );\n return '';\n }\n\n return result.stdout;\n }\n}\n","import type { LoggerMethods } from '@heripo/logger';\nimport type {\n DoclingDocument,\n DoclingTableItem,\n DoclingTextItem,\n TokenUsageReport,\n} from '@heripo/model';\nimport type { LLMTokenUsageAggregator } from '@heripo/shared';\nimport type { LanguageModel } from 'ai';\n\nimport { ConcurrentPool, LLMCaller } from '@heripo/shared';\nimport { readFileSync, writeFileSync } from 'node:fs';\nimport { join } from 'node:path';\n\nimport {\n type VlmTextCorrectionOutput,\n vlmTextCorrectionSchema,\n} from '../types/vlm-text-correction-schema';\n\n/** Language display names for prompt context (keyed by ISO 639-1 base language code) */\nconst LANGUAGE_DISPLAY_NAMES: Record<string, string> = {\n ko: 'Korean (한국어)',\n ja: 'Japanese (日本語)',\n zh: 'Chinese (中文)',\n en: 'English',\n fr: 'French (Français)',\n de: 'German (Deutsch)',\n es: 'Spanish (Español)',\n pt: 'Portuguese (Português)',\n ru: 'Russian (Русский)',\n uk: 'Ukrainian (Українська)',\n it: 'Italian (Italiano)',\n};\n\n/** Minimum character overlap ratio to consider a pdftotext line as matching an OCR element */\nconst REFERENCE_MATCH_THRESHOLD = 0.4;\n\n/** Default concurrency for parallel page processing */\nconst DEFAULT_CONCURRENCY = 1;\n\n/** Default max retries per VLM call */\nconst DEFAULT_MAX_RETRIES = 3;\n\n/** Default temperature for VLM generation */\nconst DEFAULT_TEMPERATURE = 0;\n\n/** Type abbreviation codes for text element labels */\nconst LABEL_TO_TYPE_CODE: Record<string, string> = {\n section_header: 'sh',\n text: 'tx',\n caption: 'ca',\n footnote: 'fn',\n list_item: 'li',\n page_header: 'ph',\n page_footer: 'pf',\n};\n\n/** Text labels that should be included in VLM correction */\nconst TEXT_LABELS = new Set(Object.keys(LABEL_TO_TYPE_CODE));\n\n/**\n * System prompt for VLM text correction.\n * Instructs the VLM to compare OCR text against the page image and fix errors.\n */\nconst TEXT_CORRECTION_SYSTEM_PROMPT = `You are a text correction engine for OCR output from Korean archaeological (考古學) report PDFs. Compare OCR text against the page image and reference text to fix errors.\n\nThe OCR engine cannot read Chinese characters (漢字/Hanja) correctly. These errors appear as:\n- Random ASCII letters/symbols: 熊津 → \"M\", 小京制 → \"5☆\", 故址 → \"Bbt\"\n- Meaningless Korean syllables: 東明 → \"햇배\", 金憲昌 → \"숲\", 總管 → \"3씁\"\n- Number/symbol noise: 熊川州 → \"IEJIM\", 湯井郡 → \"3#\"\n- Hanja dropped entirely: (株)韓國纖維 → (주), (財)忠淸文化財硏究院 → (재)충남문화재연구원\n- Phonetic reading substitution (音讀): 漢字 replaced by Korean pronunciation, e.g. 忠淸文化財硏究院 → 충남문화재연구원, 實玉洞遺蹟 → 실옥동유적\n\nFIX: garbled/wrong Chinese characters, mojibake, encoding artifacts, random ASCII/Korean replacing Hanja, dropped Hanja, phonetic reading substitutions\nKEEP: correct text, structure, punctuation, whitespace\n\nInput format:\nT: (text elements) index|type|text\n Optional: index|ref|reference_text (PDF text layer for the above element)\nC: (table cells) tableIndex|row,col|text\n Optional: C_REF: (unused pdftotext blocks as table reference)\n\nFOOTNOTE (fn) SPECIAL INSTRUCTIONS:\n- Footnotes in archaeological reports contain institution names with Hanja that are severely garbled\n- Common pattern: (財)機關名硏究院 → (W)#X1CR003T or (W): 103 or similar ASCII noise\n- When OCR shows patterns like (W), (M), or random ASCII where an institution name should be, READ THE IMAGE directly\n- Institution names follow patterns like: (財)OO文化財硏究院, (株)OO, (社)OO學會\n\nTABLE CELL (C:) SPECIAL INSTRUCTIONS:\n- Table headers often contain Hanja that OCR cannot read: 發刊日, 時代, 調査緣由, 調査機關, 遺蹟名, 類型 및 基數\n- When OCR shows garbled characters like \"₩ A\", \"#쩯및표뽰\" in table cells, READ THE IMAGE directly\n- If C_REF is present, use it as additional context for correcting table cells\n\nWhen a |ref| line is present:\n- It shows text extracted directly from the PDF text layer for that element\n- If OCR text contains garbled characters but ref text looks correct, USE the ref text\n- For long paragraphs, align OCR and ref text segment by segment to identify and fix each garbled portion\n- IMPORTANT: If BOTH OCR and ref text are garbled (e.g. CJK font encoding issues), IGNORE the ref text and READ THE IMAGE directly\n\nWhen NO |ref| line is present:\n- The PDF text layer could not be matched to this element\n- READ THE IMAGE directly to determine the correct text\n\nOutput JSON with corrections:\ntc=[{i:index, s:[{f:\"garbled_substring\",r:\"corrected_text\"}, ...]}] for text\ncc=[{ti:tableIndex, r:row, c:col, t:corrected}] for table cells\n\nSubstitution rules for tc:\n- 'f': exact garbled/wrong substring from the input text (must match exactly)\n- 'r': the corrected replacement\n- Include ALL garbled portions for each element as separate s entries\n- Order substitutions left-to-right as they appear in the text\n- Do NOT include unchanged text — only the specific substrings that need fixing\n\nIf all correct: {\"tc\":[],\"cc\":[]}`;\n\n/** Options for VlmTextCorrector */\nexport interface VlmTextCorrectorOptions {\n /** Number of concurrent page processing (default: 1) */\n concurrency?: number;\n /** Maximum retries per VLM call (default: 3) */\n maxRetries?: number;\n /** Temperature for generation (default: 0) */\n temperature?: number;\n /** Abort signal for cancellation */\n abortSignal?: AbortSignal;\n /** Token usage aggregator for tracking */\n aggregator?: LLMTokenUsageAggregator;\n /** Callback fired after each page completes, with cumulative token usage */\n onTokenUsage?: (report: TokenUsageReport) => void;\n /** BCP 47 language tags detected during sampling (e.g., ['ko-KR', 'en-US']) */\n documentLanguages?: string[];\n /** Pre-extracted page texts from pdftotext (1-based pageNo → text) */\n pageTexts?: Map<number, string>;\n /** Pages containing Hanja detected from text layer (1-based). Only these pages get VLM correction. */\n koreanHanjaMixPages?: number[];\n}\n\n/** Result of VLM text correction */\nexport interface VlmTextCorrectionResult {\n /** Total number of text corrections applied */\n textCorrections: number;\n /** Total number of cell corrections applied */\n cellCorrections: number;\n /** Number of pages processed */\n pagesProcessed: number;\n /** Number of pages that failed VLM correction (OCR text kept as-is) */\n pagesFailed: number;\n}\n\n/**\n * VLM text corrector that fixes OCR errors by comparing page images\n * against OCR-extracted text.\n *\n * Reads the DoclingDocument from the OCR output directory, sends each page's\n * text elements and table cells to a VLM for correction, then merges\n * corrections back and saves the updated document.\n */\nexport class VlmTextCorrector {\n constructor(private readonly logger: LoggerMethods) {}\n\n /**\n * Read DoclingDocument from output directory, correct text via VLM,\n * and save the corrected document back.\n *\n * @param outputDir - Directory containing result.json and pages/\n * @param model - Vision language model for text correction\n * @param options - Processing options\n * @returns Correction statistics\n */\n async correctAndSave(\n outputDir: string,\n model: LanguageModel,\n options?: VlmTextCorrectorOptions,\n ): Promise<VlmTextCorrectionResult> {\n this.logger.info('[VlmTextCorrector] Starting text correction...');\n\n const resultPath = join(outputDir, 'result.json');\n const doc: DoclingDocument = JSON.parse(readFileSync(resultPath, 'utf-8'));\n\n let pageNumbers = this.getPageNumbers(doc);\n if (pageNumbers.length === 0) {\n this.logger.info('[VlmTextCorrector] No pages to process');\n return {\n textCorrections: 0,\n cellCorrections: 0,\n pagesProcessed: 0,\n pagesFailed: 0,\n };\n }\n\n if (\n options?.koreanHanjaMixPages &&\n options.koreanHanjaMixPages.length > 0\n ) {\n const totalPageCount = pageNumbers.length;\n const hanjaSet = new Set(options.koreanHanjaMixPages);\n pageNumbers = pageNumbers.filter((p) => hanjaSet.has(p));\n this.logger.info(\n `[VlmTextCorrector] Filtering to ${pageNumbers.length} Korean-Hanja mix pages out of ${totalPageCount} total`,\n );\n }\n\n const concurrency = options?.concurrency ?? DEFAULT_CONCURRENCY;\n this.logger.info(\n `[VlmTextCorrector] Processing ${pageNumbers.length} pages (concurrency: ${concurrency})...`,\n );\n\n const results = await ConcurrentPool.run(\n pageNumbers,\n concurrency,\n (pageNo) => this.correctPage(outputDir, doc, pageNo, model, options),\n () => {\n if (options?.onTokenUsage && options?.aggregator) {\n options.onTokenUsage(\n options.aggregator.getReport() as TokenUsageReport,\n );\n }\n },\n );\n\n // Aggregate results\n let totalTextCorrections = 0;\n let totalCellCorrections = 0;\n let pagesFailed = 0;\n\n for (const result of results) {\n if (result === null) {\n pagesFailed++;\n } else {\n totalTextCorrections += result.tc.length;\n totalCellCorrections += result.cc.length;\n }\n }\n\n // Apply corrections to document\n for (let i = 0; i < pageNumbers.length; i++) {\n const corrections = results[i];\n if (corrections === null) continue;\n this.applyCorrections(doc, pageNumbers[i], corrections);\n }\n\n // Save corrected document\n writeFileSync(resultPath, JSON.stringify(doc, null, 2));\n\n this.logger.info(\n `[VlmTextCorrector] Correction complete: ${totalTextCorrections} text, ${totalCellCorrections} cell corrections across ${pageNumbers.length} pages (${pagesFailed} failed)`,\n );\n\n return {\n textCorrections: totalTextCorrections,\n cellCorrections: totalCellCorrections,\n pagesProcessed: pageNumbers.length,\n pagesFailed,\n };\n }\n\n /**\n * Get sorted page numbers from the document.\n */\n private getPageNumbers(doc: DoclingDocument): number[] {\n return Object.values(doc.pages)\n .map((p) => p.page_no)\n .sort((a, b) => a - b);\n }\n\n /**\n * Correct text on a single page via VLM.\n * Returns null if VLM call fails (graceful degradation).\n */\n private async correctPage(\n outputDir: string,\n doc: DoclingDocument,\n pageNo: number,\n model: LanguageModel,\n options?: VlmTextCorrectorOptions,\n ): Promise<VlmTextCorrectionOutput | null> {\n try {\n const pageTexts = this.getPageTexts(doc, pageNo);\n const pageTables = this.getPageTables(doc, pageNo);\n\n // Skip pages with no text or table content\n if (pageTexts.length === 0 && pageTables.length === 0) {\n this.logger.debug(\n `[VlmTextCorrector] Page ${pageNo}: no text content, skipping`,\n );\n return { tc: [], cc: [] };\n }\n\n const imageBase64 = this.readPageImage(outputDir, pageNo);\n\n const pageText = options?.pageTexts?.get(pageNo);\n let references: Map<number, string> | undefined;\n let tableContext: string | undefined;\n\n if (pageText) {\n const { references: refs, unusedBlocks } =\n this.matchTextToReferenceWithUnused(pageTexts, pageText);\n references = refs;\n\n if (pageTables.length > 0 && unusedBlocks.length > 0) {\n tableContext = unusedBlocks.join('\\n');\n }\n }\n\n const userPrompt = this.buildUserPrompt(\n pageTexts,\n pageTables,\n references,\n tableContext,\n );\n\n const systemPrompt = this.buildLanguageAwareSystemPrompt(\n options?.documentLanguages,\n );\n const fullPrompt = systemPrompt + '\\n\\n' + userPrompt;\n\n const result = await LLMCaller.callVision({\n schema: vlmTextCorrectionSchema as any,\n messages: [\n {\n role: 'user' as const,\n content: [\n {\n type: 'text' as const,\n text: fullPrompt,\n },\n {\n type: 'image' as const,\n image: `data:image/png;base64,${imageBase64}`,\n },\n ],\n },\n ],\n primaryModel: model,\n maxRetries: options?.maxRetries ?? DEFAULT_MAX_RETRIES,\n temperature: options?.temperature ?? DEFAULT_TEMPERATURE,\n abortSignal: options?.abortSignal,\n component: 'VlmTextCorrector',\n phase: 'text-correction',\n });\n\n if (options?.aggregator) {\n options.aggregator.track(result.usage);\n }\n\n const output = result.output as VlmTextCorrectionOutput;\n\n if (output.tc.length > 0 || output.cc.length > 0) {\n this.logger.debug(\n `[VlmTextCorrector] Page ${pageNo}: ${output.tc.length} text, ${output.cc.length} cell corrections`,\n );\n }\n\n return output;\n } catch (error) {\n // Rethrow abort errors\n if (options?.abortSignal?.aborted) {\n throw error;\n }\n\n this.logger.warn(\n `[VlmTextCorrector] Page ${pageNo}: VLM correction failed, keeping OCR text`,\n error,\n );\n return null;\n }\n }\n\n /**\n * Get text items on a specific page, with their indices for prompt building.\n */\n private getPageTexts(\n doc: DoclingDocument,\n pageNo: number,\n ): Array<{ index: number; item: DoclingTextItem }> {\n const results: Array<{ index: number; item: DoclingTextItem }> = [];\n\n for (let i = 0; i < doc.texts.length; i++) {\n const item = doc.texts[i];\n if (!TEXT_LABELS.has(item.label)) continue;\n if (item.prov.some((p) => p.page_no === pageNo)) {\n results.push({ index: i, item });\n }\n }\n\n return results;\n }\n\n /**\n * Get table items on a specific page, with their indices.\n */\n private getPageTables(\n doc: DoclingDocument,\n pageNo: number,\n ): Array<{ index: number; item: DoclingTableItem }> {\n const results: Array<{ index: number; item: DoclingTableItem }> = [];\n\n for (let i = 0; i < doc.tables.length; i++) {\n const item = doc.tables[i];\n if (item.prov.some((p) => p.page_no === pageNo)) {\n results.push({ index: i, item });\n }\n }\n\n return results;\n }\n\n /**\n * Build compact user prompt for a page.\n *\n * Format:\n * T:\n * 0|sh|제1장 조사개요\n * 1|tx|본 보고서는 ...\n * C:\n * 0|0,0|유구명\n * 0|1,0|1호 住居址\n */\n buildUserPrompt(\n pageTexts: Array<{ index: number; item: DoclingTextItem }>,\n pageTables: Array<{ index: number; item: DoclingTableItem }>,\n references?: Map<number, string>,\n tableContext?: string,\n ): string {\n const parts: string[] = [];\n\n if (pageTexts.length > 0) {\n const textLines: string[] = [];\n pageTexts.forEach((entry, promptIndex) => {\n const typeCode = LABEL_TO_TYPE_CODE[entry.item.label] ?? 'tx';\n textLines.push(`${promptIndex}|${typeCode}|${entry.item.text}`);\n const ref = references?.get(promptIndex);\n if (ref) {\n textLines.push(`${promptIndex}|ref|${ref}`);\n }\n });\n parts.push('T:\\n' + textLines.join('\\n'));\n }\n\n if (pageTables.length > 0) {\n const cellLines: string[] = [];\n for (\n let tablePromptIndex = 0;\n tablePromptIndex < pageTables.length;\n tablePromptIndex++\n ) {\n const table = pageTables[tablePromptIndex].item;\n for (const cell of table.data.table_cells) {\n if (!cell.text || cell.text.trim().length === 0) continue;\n cellLines.push(\n `${tablePromptIndex}|${cell.start_row_offset_idx},${cell.start_col_offset_idx}|${cell.text}`,\n );\n }\n }\n if (cellLines.length > 0) {\n const cellSection = 'C:\\n' + cellLines.join('\\n');\n if (tableContext) {\n parts.push(cellSection + '\\nC_REF:\\n' + tableContext);\n } else {\n parts.push(cellSection);\n }\n }\n }\n\n return parts.join('\\n');\n }\n\n /**\n * Build a language-aware system prompt by prepending language context.\n */\n private buildLanguageAwareSystemPrompt(documentLanguages?: string[]): string {\n if (!documentLanguages?.length) {\n return TEXT_CORRECTION_SYSTEM_PROMPT;\n }\n const primaryBase = documentLanguages[0].split('-')[0];\n const primaryName =\n LANGUAGE_DISPLAY_NAMES[primaryBase] ?? documentLanguages[0];\n const otherNames = documentLanguages\n .slice(1)\n .map((code) => LANGUAGE_DISPLAY_NAMES[code.split('-')[0]] ?? code);\n const languageDesc =\n otherNames.length > 0\n ? `primarily written in ${primaryName}, with ${otherNames.join(', ')} also present`\n : `written in ${primaryName}`;\n const prefix =\n `LANGUAGE CONTEXT: This document is ${languageDesc}. ` +\n 'Focus on correcting characters that do not match this language.\\n\\n';\n return prefix + TEXT_CORRECTION_SYSTEM_PROMPT;\n }\n\n /**\n * Match pdftotext paragraph blocks to OCR elements using character multiset overlap.\n * Returns a map from prompt index to the best-matching reference block.\n */\n matchTextToReference(\n pageTexts: Array<{ index: number; item: DoclingTextItem }>,\n pageText: string,\n ): Map<number, string> {\n return this.matchTextToReferenceWithUnused(pageTexts, pageText).references;\n }\n\n /**\n * Match pdftotext paragraph blocks to OCR elements and also return unused blocks.\n * Unused blocks are those that were not consumed by any text element match.\n */\n private matchTextToReferenceWithUnused(\n pageTexts: Array<{ index: number; item: DoclingTextItem }>,\n pageText: string,\n ): { references: Map<number, string>; unusedBlocks: string[] } {\n const references = new Map<number, string>();\n\n const refBlocks = this.mergeIntoBlocks(pageText);\n\n if (refBlocks.length === 0) {\n return { references, unusedBlocks: [] };\n }\n\n const available = new Set(refBlocks.map((_, i) => i));\n\n for (let promptIndex = 0; promptIndex < pageTexts.length; promptIndex++) {\n const ocrText = pageTexts[promptIndex].item.text;\n\n let bestScore = 0;\n let bestBlockIndex = -1;\n\n for (const blockIndex of available) {\n const score = this.computeCharOverlap(ocrText, refBlocks[blockIndex]);\n if (score > bestScore) {\n bestScore = score;\n bestBlockIndex = blockIndex;\n }\n }\n\n if (bestBlockIndex >= 0 && bestScore >= REFERENCE_MATCH_THRESHOLD) {\n if (refBlocks[bestBlockIndex] !== ocrText) {\n references.set(promptIndex, refBlocks[bestBlockIndex]);\n }\n available.delete(bestBlockIndex);\n }\n }\n\n const unusedBlocks = [...available]\n .sort((a, b) => a - b)\n .map((i) => refBlocks[i]);\n\n return { references, unusedBlocks };\n }\n\n /**\n * Merge pdftotext output into paragraph blocks separated by blank lines.\n * Consecutive non-empty lines are joined with a space.\n */\n private mergeIntoBlocks(pageText: string): string[] {\n const blocks: string[] = [];\n let currentLines: string[] = [];\n\n for (const rawLine of pageText.split('\\n')) {\n const trimmed = rawLine.trim();\n if (trimmed.length === 0) {\n if (currentLines.length > 0) {\n blocks.push(currentLines.join(' '));\n currentLines = [];\n }\n } else {\n currentLines.push(trimmed);\n }\n }\n if (currentLines.length > 0) {\n blocks.push(currentLines.join(' '));\n }\n\n return blocks;\n }\n\n /**\n * Compute character multiset overlap ratio between two strings.\n * Returns a value between 0.0 and 1.0.\n */\n private computeCharOverlap(a: string, b: string): number {\n if (a.length === 0 || b.length === 0) return 0;\n\n const freqA = new Map<string, number>();\n for (const ch of a) {\n freqA.set(ch, (freqA.get(ch) ?? 0) + 1);\n }\n\n const freqB = new Map<string, number>();\n for (const ch of b) {\n freqB.set(ch, (freqB.get(ch) ?? 0) + 1);\n }\n\n let overlap = 0;\n for (const [ch, countA] of freqA) {\n const countB = freqB.get(ch) ?? 0;\n overlap += Math.min(countA, countB);\n }\n\n return overlap / Math.max(a.length, b.length);\n }\n\n /**\n * Read page image as base64.\n * Page images are 0-indexed: page_no N → pages/page_{N-1}.png\n */\n private readPageImage(outputDir: string, pageNo: number): string {\n const imagePath = join(outputDir, 'pages', `page_${pageNo - 1}.png`);\n return readFileSync(imagePath).toString('base64');\n }\n\n /**\n * Apply VLM corrections to the DoclingDocument.\n */\n private applyCorrections(\n doc: DoclingDocument,\n pageNo: number,\n corrections: VlmTextCorrectionOutput,\n ): void {\n // Apply text corrections (substitution-based)\n if (corrections.tc.length > 0) {\n const pageTexts = this.getPageTexts(doc, pageNo);\n for (const correction of corrections.tc) {\n if (correction.i >= 0 && correction.i < pageTexts.length) {\n const docIndex = pageTexts[correction.i].index;\n let text = doc.texts[docIndex].text;\n for (const sub of correction.s) {\n const idx = text.indexOf(sub.f);\n if (idx >= 0) {\n text =\n text.substring(0, idx) +\n sub.r +\n text.substring(idx + sub.f.length);\n } else {\n this.logger.warn(\n `[VlmTextCorrector] Page ${pageNo}, text ${correction.i}: ` +\n `find string not found, skipping substitution`,\n );\n }\n }\n if (text !== doc.texts[docIndex].text) {\n doc.texts[docIndex].text = text;\n doc.texts[docIndex].orig = text;\n }\n }\n }\n }\n\n // Apply cell corrections\n if (corrections.cc.length > 0) {\n const pageTables = this.getPageTables(doc, pageNo);\n for (const correction of corrections.cc) {\n if (correction.ti >= 0 && correction.ti < pageTables.length) {\n const table = pageTables[correction.ti].item;\n\n // Update table_cells\n for (const cell of table.data.table_cells) {\n if (\n cell.start_row_offset_idx === correction.r &&\n cell.start_col_offset_idx === correction.c\n ) {\n cell.text = correction.t;\n break;\n }\n }\n\n // Sync grid cell (grid stores separate objects from table_cells)\n const gridRow = table.data.grid[correction.r];\n if (gridRow) {\n const gridCell = gridRow[correction.c];\n if (gridCell) {\n gridCell.text = correction.t;\n }\n }\n }\n }\n }\n }\n}\n","import { z } from 'zod/v4';\n\n/**\n * Zod schema for VLM text correction response.\n *\n * The VLM returns only corrections (items that differ from OCR output).\n * If all text is correct, both arrays are empty.\n *\n * Short field names are used to minimize output tokens:\n * - tc = text corrections (substitution-based)\n * - cc = cell corrections (full replacement)\n * - i = text element index\n * - s = substitutions array [{f: find, r: replace}]\n * - ti = table index (within the page)\n * - r = row index\n * - c = column index\n * - t = corrected text (for cell corrections)\n */\nexport const vlmTextCorrectionSchema = z.object({\n /** Text element corrections (substitution-based) */\n tc: z.array(\n z.object({\n /** Text element index (from prompt) */\n i: z.number().int().nonnegative(),\n /** Substitutions: find/replace pairs applied left-to-right */\n s: z.array(\n z.object({\n /** Exact garbled substring to find */\n f: z.string(),\n /** Corrected replacement text */\n r: z.string(),\n }),\n ),\n }),\n ),\n /** Table cell corrections */\n cc: z.array(\n z.object({\n /** Table index (within the page) */\n ti: z.number().int().nonnegative(),\n /** Row index */\n r: z.number().int().nonnegative(),\n /** Column index */\n c: z.number().int().nonnegative(),\n /** Corrected cell text */\n t: z.string(),\n }),\n ),\n});\n\n/** Type inferred from the VLM text correction schema */\nexport type VlmTextCorrectionOutput = z.infer<typeof vlmTextCorrectionSchema>;\n","import type { LoggerMethods } from '@heripo/logger';\nimport type { Bcp47LanguageTag, OcrStrategy } from '@heripo/model';\nimport type { LLMTokenUsageAggregator } from '@heripo/shared';\nimport type { LanguageModel } from 'ai';\n\nimport type { PageRenderer } from '../processors/page-renderer';\n\nimport { normalizeToBcp47 } from '@heripo/model';\nimport { LLMCaller } from '@heripo/shared';\nimport { readFileSync } from 'node:fs';\nimport { z } from 'zod/v4';\n\nimport { PAGE_RENDERING } from '../config/constants.js';\nimport { PdfTextExtractor } from '../processors/pdf-text-extractor';\n\n/** Ratio of pages to trim from front and back (covers, TOC, appendices) */\nconst EDGE_TRIM_RATIO = 0.1;\n\n/** Default maximum number of pages to sample */\nconst DEFAULT_MAX_SAMPLE_PAGES = 15;\n\n/** Default max retries per VLM call */\nconst DEFAULT_MAX_RETRIES = 3;\n\n/** Regex to detect CJK Unified Ideographs (Hanja/Kanji/Hanzi) */\nconst CJK_REGEX = /[\\u4E00-\\u9FFF]/;\n\n/** Regex to detect Hangul syllables */\nconst HANGUL_REGEX = /[\\uAC00-\\uD7AF]/;\n\n/** Zod schema for VLM Korean-Hanja mix detection response */\nconst koreanHanjaMixSchema = z.object({\n hasKoreanHanjaMix: z\n .boolean()\n .describe(\n 'Whether the page contains any Hanja (漢字/Chinese characters) mixed with Korean text',\n ),\n detectedLanguages: z\n .array(z.string())\n .describe(\n 'BCP 47 language tags of languages found on this page, ordered by prevalence (e.g., [\"ko-KR\", \"en-US\"])',\n ),\n});\n\n/** System prompt for Korean-Hanja mix detection */\nconst KOREAN_HANJA_MIX_PROMPT = `Look at this page image carefully. Does it contain any Hanja (漢字/Chinese characters) mixed with Korean text?\n\nHanja examples: 遺蹟, 發掘, 調査, 報告書, 文化財\nNote: Hanja are Chinese characters used in Korean documents, different from modern Korean (한글).\n\nAnswer whether any Hanja characters are present on this page.\n\nAlso identify all languages present on this page. Return an array of BCP 47 language tags ordered by prevalence (primary language first).\nExamples: [\"ko-KR\", \"en-US\"], [\"ja-JP\"], [\"zh-TW\", \"en-US\"]`;\n\n/** Options for OcrStrategySampler */\nexport interface OcrStrategySamplerOptions {\n /** Maximum number of pages to sample (default: 15) */\n maxSamplePages?: number;\n /** Maximum retries per VLM call (default: 3) */\n maxRetries?: number;\n /** Temperature for VLM generation (default: 0) */\n temperature?: number;\n /** Abort signal for cancellation */\n abortSignal?: AbortSignal;\n /** Fallback model for retry after primary exhausts maxRetries */\n fallbackModel?: LanguageModel;\n /** Token usage aggregator for tracking */\n aggregator?: LLMTokenUsageAggregator;\n}\n\n/**\n * Samples pages from a PDF to determine whether to use ocrmac or VLM for processing.\n *\n * First attempts to detect Hangul-Hanja mix directly from the PDF text layer using\n * pdftotext (zero-cost, high accuracy for PDFs with embedded text). Only falls back\n * to VLM-based image analysis for image-only PDFs without a text layer.\n *\n * VLM fallback sampling strategy:\n * - Trim front/back 10% of pages (covers, TOC, appendices)\n * - Select up to 15 pages evenly distributed across the eligible range\n * - Early exit on first Korean-Hanja mix detection\n */\nexport class OcrStrategySampler {\n private readonly logger: LoggerMethods;\n private readonly pageRenderer: PageRenderer;\n private readonly textExtractor: PdfTextExtractor;\n\n constructor(\n logger: LoggerMethods,\n pageRenderer: PageRenderer,\n textExtractor?: PdfTextExtractor,\n ) {\n this.logger = logger;\n this.pageRenderer = pageRenderer;\n this.textExtractor = textExtractor ?? new PdfTextExtractor(logger);\n }\n\n /**\n * Sample pages from a PDF and determine the OCR strategy.\n *\n * @param pdfPath - Path to the PDF file\n * @param outputDir - Directory for temporary rendered pages\n * @param model - Vision language model for Korean-Hanja mix detection\n * @param options - Sampling options\n * @returns OcrStrategy with method ('ocrmac' or 'vlm') and metadata\n */\n async sample(\n pdfPath: string,\n outputDir: string,\n model: LanguageModel,\n options?: OcrStrategySamplerOptions,\n ): Promise<OcrStrategy> {\n const maxSamplePages = options?.maxSamplePages ?? DEFAULT_MAX_SAMPLE_PAGES;\n\n this.logger.info('[OcrStrategySampler] Starting OCR strategy sampling...');\n\n // Step 1: Try text layer pre-check (zero-cost Hangul-Hanja detection)\n const preCheckResult = await this.preCheckHanjaFromTextLayer(pdfPath);\n if (preCheckResult) {\n return preCheckResult;\n }\n\n // Step 2: Render pages at medium DPI for VLM analysis (image-only PDFs)\n const renderResult = await this.pageRenderer.renderPages(\n pdfPath,\n outputDir,\n { dpi: PAGE_RENDERING.SAMPLE_DPI },\n );\n\n if (renderResult.pageCount === 0) {\n this.logger.info('[OcrStrategySampler] No pages found in PDF');\n return {\n method: 'ocrmac',\n reason: 'No pages found in PDF',\n sampledPages: 0,\n totalPages: 0,\n };\n }\n\n // Step 3: Select sample page indices\n const sampleIndices = this.selectSamplePages(\n renderResult.pageCount,\n maxSamplePages,\n );\n\n this.logger.info(\n `[OcrStrategySampler] Sampling ${sampleIndices.length} of ${renderResult.pageCount} pages: [${sampleIndices.map((i) => i + 1).join(', ')}]`,\n );\n\n // Step 4: Check each sample page for Korean-Hanja mix (early exit on detection)\n let sampledCount = 0;\n const languageFrequency = new Map<Bcp47LanguageTag, number>();\n for (const idx of sampleIndices) {\n sampledCount++;\n const pageFile = renderResult.pageFiles[idx];\n const pageAnalysis = await this.analyzeSamplePage(\n pageFile,\n idx + 1,\n model,\n options,\n );\n\n for (const lang of pageAnalysis.detectedLanguages) {\n languageFrequency.set(lang, (languageFrequency.get(lang) ?? 0) + 1);\n }\n\n if (pageAnalysis.hasKoreanHanjaMix) {\n this.logger.info(\n `[OcrStrategySampler] Korean-Hanja mix detected on page ${idx + 1} → VLM strategy`,\n );\n const detectedLanguages = this.aggregateLanguages(languageFrequency);\n return {\n method: 'vlm',\n detectedLanguages,\n reason: `Korean-Hanja mix detected on page ${idx + 1}`,\n sampledPages: sampledCount,\n totalPages: renderResult.pageCount,\n };\n }\n }\n\n // Step 5: No Korean-Hanja mix found → ocrmac\n this.logger.info(\n '[OcrStrategySampler] No Korean-Hanja mix detected → ocrmac strategy',\n );\n const detectedLanguages = this.aggregateLanguages(languageFrequency);\n return {\n method: 'ocrmac',\n detectedLanguages,\n reason: `No Korean-Hanja mix detected in ${sampledCount} sampled pages`,\n sampledPages: sampledCount,\n totalPages: renderResult.pageCount,\n };\n }\n\n /**\n * Pre-check for Hangul-Hanja mix in PDF text layer using pdftotext.\n * Extracts full document text in a single process and checks at document level.\n * Only makes a definitive decision for Korean (Hangul) documents:\n * - Hangul + Hanja (anywhere in document) → VLM (confirmed Korean-Hanja mix)\n * - Hangul only → ocrmac with ko-KR (confirmed Korean)\n * - No Hangul (English, Japanese, etc.) → null (delegates to VLM for language detection)\n */\n private async preCheckHanjaFromTextLayer(\n pdfPath: string,\n ): Promise<OcrStrategy | null> {\n try {\n const totalPages = await this.textExtractor.getPageCount(pdfPath);\n if (totalPages === 0) return null;\n\n const fullText = await this.textExtractor.extractFullText(pdfPath);\n if (fullText.trim().length === 0) {\n this.logger.debug(\n '[OcrStrategySampler] No Hangul in text layer, falling back to VLM sampling',\n );\n return null;\n }\n\n const hasHangul = HANGUL_REGEX.test(fullText);\n const hasHanja = CJK_REGEX.test(fullText);\n\n if (!hasHangul) {\n this.logger.debug(\n '[OcrStrategySampler] No Hangul in text layer, falling back to VLM sampling',\n );\n return null;\n }\n\n if (hasHanja) {\n const pageTextArray = fullText.split('\\f');\n const koreanHanjaMixPages: number[] = [];\n for (let i = 0; i < pageTextArray.length; i++) {\n if (CJK_REGEX.test(pageTextArray[i])) {\n koreanHanjaMixPages.push(i + 1); // 1-based\n }\n }\n\n this.logger.info(\n `[OcrStrategySampler] Hangul-Hanja mix detected in text layer → VLM strategy (${koreanHanjaMixPages.length} pages with Hanja)`,\n );\n return {\n method: 'vlm',\n detectedLanguages: ['ko-KR'],\n reason: 'Hangul-Hanja mix found in PDF text layer',\n koreanHanjaMixPages,\n sampledPages: totalPages,\n totalPages,\n };\n }\n\n this.logger.info(\n '[OcrStrategySampler] No Hangul-Hanja mix in text layer → ocrmac strategy',\n );\n return {\n method: 'ocrmac',\n detectedLanguages: ['ko-KR'],\n reason: `No Hangul-Hanja mix in PDF text layer (${totalPages} pages checked)`,\n sampledPages: totalPages,\n totalPages,\n };\n } catch {\n this.logger.debug(\n '[OcrStrategySampler] Text layer pre-check failed, falling back to VLM sampling',\n );\n return null;\n }\n }\n\n /**\n * Select page indices for sampling.\n * Trims front/back edges and distributes samples evenly.\n *\n * @param totalPages - Total number of pages\n * @param maxSamples - Maximum number of samples\n * @returns Array of 0-based page indices\n */\n selectSamplePages(totalPages: number, maxSamples: number): number[] {\n if (totalPages === 0) return [];\n\n // For very small documents, sample all pages\n if (totalPages <= maxSamples) {\n return Array.from({ length: totalPages }, (_, i) => i);\n }\n\n // Trim front/back edges\n const trimCount = Math.max(1, Math.ceil(totalPages * EDGE_TRIM_RATIO));\n const start = trimCount;\n const end = totalPages - trimCount;\n const eligibleCount = end - start;\n\n // If trimming leaves no eligible pages, use middle page\n if (eligibleCount <= 0) {\n return [Math.floor(totalPages / 2)];\n }\n\n // If eligible pages fit within maxSamples, use all\n if (eligibleCount <= maxSamples) {\n return Array.from({ length: eligibleCount }, (_, i) => start + i);\n }\n\n // Distribute samples evenly across eligible range\n const indices: number[] = [];\n const step = eligibleCount / maxSamples;\n for (let i = 0; i < maxSamples; i++) {\n indices.push(start + Math.floor(i * step));\n }\n return indices;\n }\n\n /**\n * Analyze a single sample page for Korean-Hanja mixed script and primary language.\n * Normalizes raw VLM language responses to valid BCP 47 tags, filtering out invalid ones.\n *\n * @returns Object with Korean-Hanja detection result and normalized detected languages\n */\n private async analyzeSamplePage(\n pageFile: string,\n pageNo: number,\n model: LanguageModel,\n options?: OcrStrategySamplerOptions,\n ): Promise<{\n hasKoreanHanjaMix: boolean;\n detectedLanguages: Bcp47LanguageTag[];\n }> {\n this.logger.debug(\n `[OcrStrategySampler] Analyzing page ${pageNo} for Korean-Hanja mix and language...`,\n );\n\n const base64Image = readFileSync(pageFile).toString('base64');\n\n const messages = [\n {\n role: 'user' as const,\n content: [\n { type: 'text' as const, text: KOREAN_HANJA_MIX_PROMPT },\n {\n type: 'image' as const,\n image: `data:image/png;base64,${base64Image}`,\n },\n ],\n },\n ];\n\n const result = await LLMCaller.callVision({\n schema: koreanHanjaMixSchema as any,\n messages,\n primaryModel: model,\n fallbackModel: options?.fallbackModel,\n maxRetries: options?.maxRetries ?? DEFAULT_MAX_RETRIES,\n temperature: options?.temperature ?? 0,\n abortSignal: options?.abortSignal,\n component: 'OcrStrategySampler',\n phase: 'korean-hanja-mix-detection',\n });\n\n if (options?.aggregator) {\n options.aggregator.track(result.usage);\n }\n\n const output = result.output as {\n hasKoreanHanjaMix: boolean;\n detectedLanguages: string[];\n };\n\n const normalizedLanguages = output.detectedLanguages\n .map(normalizeToBcp47)\n .filter((tag): tag is Bcp47LanguageTag => tag !== null);\n\n this.logger.debug(\n `[OcrStrategySampler] Page ${pageNo}: hasKoreanHanjaMix=${output.hasKoreanHanjaMix}, detectedLanguages=${normalizedLanguages.join(',')}`,\n );\n\n return {\n hasKoreanHanjaMix: output.hasKoreanHanjaMix,\n detectedLanguages: normalizedLanguages,\n };\n }\n\n /**\n * Aggregate language frequency map into a sorted array.\n * Returns languages sorted by frequency (descending), or undefined if empty.\n */\n private aggregateLanguages(\n frequencyMap: Map<Bcp47LanguageTag, number>,\n ): Bcp47LanguageTag[] | undefined {\n if (frequencyMap.size === 0) return undefined;\n\n return [...frequencyMap.entries()]\n .sort((a, b) => b[1] - a[1])\n .map(([lang]) => lang);\n }\n}\n","import type { Server } from 'node:http';\n\nimport { createReadStream, statSync } from 'node:fs';\nimport { createServer } from 'node:http';\nimport { basename } from 'node:path';\n\n/**\n * Simple local HTTP server for serving a single file\n *\n * Used to serve local PDF files to docling-serve which requires HTTP URLs.\n */\nexport class LocalFileServer {\n private server: Server | null = null;\n private port: number = 0;\n\n /**\n * Start serving a file and return the URL\n *\n * @param filePath Absolute path to the file to serve\n * @returns URL to access the file\n */\n async start(filePath: string): Promise<string> {\n const filename = basename(filePath);\n const stat = statSync(filePath);\n\n return new Promise((resolve, reject) => {\n this.server = createServer((req, res) => {\n if (req.url === `/${filename}`) {\n res.writeHead(200, {\n 'Content-Type': 'application/pdf',\n 'Content-Length': stat.size,\n });\n createReadStream(filePath).pipe(res);\n } else {\n res.writeHead(404);\n res.end('Not Found');\n }\n });\n\n this.server.on('error', reject);\n\n // Listen on random available port\n this.server.listen(0, '127.0.0.1', () => {\n const address = this.server!.address();\n if (typeof address === 'object' && address !== null) {\n this.port = address.port;\n resolve(`http://127.0.0.1:${this.port}/${filename}`);\n } else {\n reject(new Error('Failed to get server address'));\n }\n });\n });\n }\n\n /**\n * Stop the server\n */\n stop(): Promise<void> {\n return new Promise((resolve) => {\n if (this.server) {\n this.server.close(() => {\n this.server = null;\n this.port = 0;\n resolve();\n });\n } else {\n resolve();\n }\n });\n }\n}\n","import type { LoggerMethods } from '@heripo/logger';\nimport type { DoclingDocument, TokenUsageReport } from '@heripo/model';\nimport type { ConversionOptions, DoclingAPIClient } from 'docling-sdk';\n\nimport type {\n ConversionCompleteCallback,\n PDFConvertOptions,\n} from './pdf-converter';\n\nimport { spawnAsync } from '@heripo/shared';\nimport {\n copyFileSync,\n createWriteStream,\n existsSync,\n mkdirSync,\n readFileSync,\n readdirSync,\n rmSync,\n writeFileSync,\n} from 'node:fs';\nimport { rename, writeFile } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { pipeline } from 'node:stream/promises';\n\nimport { PAGE_RENDERING, PDF_CONVERTER } from '../config/constants';\nimport { DoclingDocumentMerger } from '../processors/docling-document-merger';\nimport { ImageExtractor } from '../processors/image-extractor';\nimport { PageRenderer } from '../processors/page-renderer';\nimport { runJqFileJson, runJqFileToFile } from '../utils/jq';\nimport { LocalFileServer } from '../utils/local-file-server';\n\n/** Configuration for chunked conversion */\nexport interface ChunkedConversionConfig {\n chunkSize: number;\n maxRetries: number;\n}\n\n/** Page range for a single chunk [start, end] (1-based, inclusive) */\ntype PageRange = [number, number];\n\n/**\n * Converts large PDFs in chunks using Docling's page_range option.\n *\n * Splits the PDF into fixed-size page ranges, converts each chunk sequentially,\n * then merges the resulting DoclingDocuments into a single output that is\n * indistinguishable from a single-pass conversion.\n */\nexport class ChunkedPDFConverter {\n constructor(\n private readonly logger: LoggerMethods,\n private readonly client: DoclingAPIClient,\n private readonly config: ChunkedConversionConfig,\n private readonly timeout: number = PDF_CONVERTER.DEFAULT_TIMEOUT_MS,\n ) {}\n\n /**\n * Convert a local PDF in chunks.\n *\n * @param url - file:// URL to the source PDF\n * @param reportId - Unique report identifier for output directory naming\n * @param onComplete - Callback invoked with the final output directory\n * @param cleanupAfterCallback - Whether to clean up the output directory after callback\n * @param options - PDF conversion options (chunked-specific fields are stripped internally)\n * @param buildConversionOptions - Function to build Docling ConversionOptions from PDFConvertOptions\n * @param abortSignal - Optional abort signal for cancellation\n */\n async convertChunked(\n url: string,\n reportId: string,\n onComplete: ConversionCompleteCallback,\n cleanupAfterCallback: boolean,\n options: PDFConvertOptions,\n buildConversionOptions: (options: PDFConvertOptions) => ConversionOptions,\n abortSignal?: AbortSignal,\n ): Promise<TokenUsageReport | null> {\n const pdfPath = url.slice(7); // Remove 'file://' prefix\n const cwd = process.cwd();\n const outputDir = join(cwd, 'output', reportId);\n const chunksBaseDir = join(cwd, 'output', reportId, '_chunks');\n\n // Step 1: Get total page count\n const totalPages = await this.getPageCount(pdfPath);\n if (totalPages === 0) {\n throw new Error(\n '[ChunkedPDFConverter] Failed to detect page count from PDF',\n );\n }\n\n // Step 2: Calculate chunk ranges\n const chunks = this.calculateChunks(totalPages);\n this.logger.info(\n `[ChunkedPDFConverter] Starting: ${totalPages} pages → ${chunks.length} chunks of ${this.config.chunkSize}`,\n );\n\n // Step 3: Start local file server (once for all chunks)\n const server = new LocalFileServer();\n const httpUrl = await server.start(pdfPath);\n this.logger.info(\n '[ChunkedPDFConverter] Started local file server:',\n httpUrl,\n );\n\n const chunkDocuments: DoclingDocument[] = [];\n\n try {\n // Step 4: Process each chunk sequentially\n for (let i = 0; i < chunks.length; i++) {\n this.checkAbort(abortSignal);\n\n const [start, end] = chunks[i];\n const chunkDir = join(chunksBaseDir, `_chunk_${i}`);\n mkdirSync(chunkDir, { recursive: true });\n\n const doc = await this.convertChunk(\n i,\n chunks.length,\n start,\n end,\n httpUrl,\n chunkDir,\n options,\n buildConversionOptions,\n );\n\n chunkDocuments.push(doc);\n }\n } finally {\n // Always stop the local file server\n this.logger.info('[ChunkedPDFConverter] Stopping local file server...');\n await server.stop();\n }\n\n this.checkAbort(abortSignal);\n\n // Step 5: Merge all chunk documents\n this.logger.info(\n `[ChunkedPDFConverter] All ${chunks.length} chunks completed, merging...`,\n );\n const merger = new DoclingDocumentMerger();\n const picFileOffsets = this.buildPicFileOffsets(\n chunksBaseDir,\n chunks.length,\n );\n const merged = merger.merge(chunkDocuments, picFileOffsets);\n\n this.logger.info(\n `[ChunkedPDFConverter] Merged: ${merged.texts.length} texts, ${merged.pictures.length} pictures, ${merged.tables.length} tables, ${Object.keys(merged.pages).length} pages`,\n );\n\n // Step 6: Build final output directory\n mkdirSync(outputDir, { recursive: true });\n const imagesDir = join(outputDir, 'images');\n mkdirSync(imagesDir, { recursive: true });\n\n // Relocate images from chunk directories with global indexing\n this.relocateImages(chunksBaseDir, chunks.length, imagesDir);\n\n // Save merged result.json\n const resultPath = join(outputDir, 'result.json');\n writeFileSync(resultPath, JSON.stringify(merged));\n\n try {\n // Step 7: Render page images (ImageMagick, same as non-chunked)\n await this.renderPageImages(pdfPath, outputDir);\n\n // Step 7.5: Clean up orphaned pic_ files\n this.cleanupOrphanedPicFiles(resultPath, imagesDir);\n\n this.checkAbort(abortSignal);\n\n // Step 8: Execute completion callback\n this.logger.info(\n '[ChunkedPDFConverter] Executing completion callback...',\n );\n await onComplete(outputDir);\n } finally {\n // Step 9: Cleanup (always runs)\n if (existsSync(chunksBaseDir)) {\n rmSync(chunksBaseDir, { recursive: true, force: true });\n }\n\n if (cleanupAfterCallback) {\n this.logger.info(\n '[ChunkedPDFConverter] Cleaning up output directory:',\n outputDir,\n );\n if (existsSync(outputDir)) {\n rmSync(outputDir, { recursive: true, force: true });\n }\n } else {\n this.logger.info(\n '[ChunkedPDFConverter] Output preserved at:',\n outputDir,\n );\n }\n }\n\n return null;\n }\n\n /**\n * Convert a single chunk with retry logic.\n */\n private async convertChunk(\n chunkIndex: number,\n totalChunks: number,\n startPage: number,\n endPage: number,\n httpUrl: string,\n chunkDir: string,\n options: PDFConvertOptions,\n buildConversionOptions: (options: PDFConvertOptions) => ConversionOptions,\n ): Promise<DoclingDocument> {\n const chunkLabel = `Chunk ${chunkIndex + 1}/${totalChunks} (pages ${startPage}-${endPage})`;\n\n for (let attempt = 0; attempt <= this.config.maxRetries; attempt++) {\n try {\n if (attempt > 0) {\n this.logger.info(\n `[ChunkedPDFConverter] ${chunkLabel}: retrying (${attempt}/${this.config.maxRetries})...`,\n );\n } else {\n this.logger.info(\n `[ChunkedPDFConverter] ${chunkLabel}: converting...`,\n );\n }\n\n const startTime = Date.now();\n\n // Build conversion options with page_range\n const conversionOptions = buildConversionOptions({\n ...options,\n page_range: [startPage, endPage],\n });\n\n // Start conversion task\n const task = await this.client.convertSourceAsync({\n sources: [{ kind: 'http', url: httpUrl }],\n options: conversionOptions,\n target: { kind: 'zip' },\n });\n\n // Poll until completion\n await this.trackTaskProgress(task);\n\n // Download ZIP result\n const zipPath = join(chunkDir, 'result.zip');\n await this.downloadResult(task.taskId, zipPath);\n\n // Extract ZIP and process images\n const extractDir = join(chunkDir, 'extracted');\n const chunkOutputDir = join(chunkDir, 'output');\n await ImageExtractor.extractAndSaveDocumentsFromZip(\n this.logger,\n zipPath,\n extractDir,\n chunkOutputDir,\n );\n\n // Parse result.json into a DoclingDocument object\n const resultJsonPath = join(chunkOutputDir, 'result.json');\n const doc = await runJqFileJson<DoclingDocument>('.', resultJsonPath);\n\n // Cleanup chunk temp files (ZIP + extracted)\n if (existsSync(zipPath)) rmSync(zipPath, { force: true });\n if (existsSync(extractDir)) {\n rmSync(extractDir, { recursive: true, force: true });\n }\n\n const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);\n if (attempt > 0) {\n this.logger.info(\n `[ChunkedPDFConverter] ${chunkLabel}: completed on retry ${attempt} (${elapsed}s)`,\n );\n } else {\n this.logger.info(\n `[ChunkedPDFConverter] ${chunkLabel}: completed (${elapsed}s)`,\n );\n }\n\n return doc;\n } catch (error) {\n if (attempt >= this.config.maxRetries) {\n this.logger.error(\n `[ChunkedPDFConverter] ${chunkLabel}: failed after ${this.config.maxRetries} retries`,\n );\n throw error;\n }\n this.logger.warn(\n `[ChunkedPDFConverter] ${chunkLabel}: failed, retrying (${attempt + 1}/${this.config.maxRetries})...`,\n );\n }\n }\n\n /* v8 ignore start -- unreachable: loop always returns or throws */\n throw new Error('Unreachable');\n /* v8 ignore stop */\n }\n\n /** Calculate page ranges for chunks */\n calculateChunks(totalPages: number): PageRange[] {\n if (this.config.chunkSize <= 0) {\n throw new Error('[ChunkedPDFConverter] chunkSize must be positive');\n }\n\n const ranges: PageRange[] = [];\n for (let start = 1; start <= totalPages; start += this.config.chunkSize) {\n const end = Math.min(start + this.config.chunkSize - 1, totalPages);\n ranges.push([start, end]);\n }\n return ranges;\n }\n\n /** Get total page count using pdfinfo */\n private async getPageCount(pdfPath: string): Promise<number> {\n const result = await spawnAsync('pdfinfo', [pdfPath]);\n if (result.code !== 0) {\n return 0;\n }\n const match = result.stdout.match(/^Pages:\\s+(\\d+)/m);\n return match ? parseInt(match[1], 10) : 0;\n }\n\n /** Poll task progress until completion */\n private async trackTaskProgress(task: {\n taskId: string;\n poll: () => Promise<{ task_status: string }>;\n getResult: () => Promise<{\n errors?: { message: string }[];\n status?: string;\n }>;\n }): Promise<void> {\n const startTime = Date.now();\n\n while (true) {\n if (Date.now() - startTime > this.timeout) {\n throw new Error('[ChunkedPDFConverter] Chunk task timeout');\n }\n\n const status = await task.poll();\n\n if (status.task_status === 'success') return;\n\n if (status.task_status === 'failure') {\n let details = 'unknown';\n try {\n const result = await task.getResult();\n if (result.errors?.length) {\n details = result.errors.map((e) => e.message).join('; ');\n }\n } catch {\n // ignore\n }\n throw new Error(`[ChunkedPDFConverter] Chunk task failed: ${details}`);\n }\n\n await new Promise((resolve) =>\n setTimeout(resolve, PDF_CONVERTER.POLL_INTERVAL_MS),\n );\n }\n }\n\n /** Download ZIP result for a task */\n private async downloadResult(taskId: string, zipPath: string): Promise<void> {\n const zipResult = await this.client.getTaskResultFile(taskId);\n\n if (zipResult.fileStream) {\n const writeStream = createWriteStream(zipPath);\n await pipeline(zipResult.fileStream, writeStream);\n return;\n }\n\n if (zipResult.data) {\n await writeFile(zipPath, zipResult.data);\n return;\n }\n\n // Fallback: direct HTTP download\n const baseUrl = this.client.getConfig().baseUrl;\n const response = await fetch(`${baseUrl}/v1/result/${taskId}`, {\n headers: { Accept: 'application/zip' },\n });\n\n if (!response.ok) {\n throw new Error(\n `Failed to download chunk ZIP: ${response.status} ${response.statusText}`,\n );\n }\n\n const buffer = new Uint8Array(await response.arrayBuffer());\n await writeFile(zipPath, buffer);\n }\n\n /**\n * Relocate images from chunk output directories to the final images directory\n * with global indexing.\n */\n private relocateImages(\n chunksBaseDir: string,\n totalChunks: number,\n imagesDir: string,\n ): void {\n // 1. Relocate pic_ files (JSON base64 images)\n let picGlobalIndex = 0;\n for (let i = 0; i < totalChunks; i++) {\n const chunkImagesDir = join(\n chunksBaseDir,\n `_chunk_${i}`,\n 'output',\n 'images',\n );\n if (!existsSync(chunkImagesDir)) continue;\n\n const picFiles = readdirSync(chunkImagesDir)\n .filter((f) => f.startsWith('pic_') && f.endsWith('.png'))\n .sort((a, b) => {\n const numA = parseInt(a.replace('pic_', '').replace('.png', ''), 10);\n const numB = parseInt(b.replace('pic_', '').replace('.png', ''), 10);\n return numA - numB;\n });\n\n for (const file of picFiles) {\n const src = join(chunkImagesDir, file);\n const dest = join(imagesDir, `pic_${picGlobalIndex}.png`);\n copyFileSync(src, dest);\n picGlobalIndex++;\n }\n }\n\n // 2. Relocate image_ files (HTML content images)\n let imageGlobalIndex = 0;\n for (let i = 0; i < totalChunks; i++) {\n const chunkImagesDir = join(\n chunksBaseDir,\n `_chunk_${i}`,\n 'output',\n 'images',\n );\n if (!existsSync(chunkImagesDir)) continue;\n\n const imageFiles = readdirSync(chunkImagesDir)\n .filter((f) => f.startsWith('image_') && f.endsWith('.png'))\n .sort((a, b) => {\n const numA = parseInt(\n a.replace('image_', '').replace('.png', ''),\n 10,\n );\n const numB = parseInt(\n b.replace('image_', '').replace('.png', ''),\n 10,\n );\n return numA - numB;\n });\n\n for (const file of imageFiles) {\n const src = join(chunkImagesDir, file);\n const dest = join(imagesDir, `image_${imageGlobalIndex}.png`);\n copyFileSync(src, dest);\n imageGlobalIndex++;\n }\n }\n\n this.logger.info(\n `[ChunkedPDFConverter] Relocated ${picGlobalIndex} pic + ${imageGlobalIndex} image files to ${imagesDir}`,\n );\n }\n\n /** Render page images from PDF using ImageMagick and update result.json */\n private async renderPageImages(\n pdfPath: string,\n outputDir: string,\n ): Promise<void> {\n this.logger.info(\n '[ChunkedPDFConverter] Rendering page images with ImageMagick...',\n );\n\n const renderer = new PageRenderer(this.logger);\n const renderResult = await renderer.renderPages(pdfPath, outputDir);\n\n const resultPath = join(outputDir, 'result.json');\n const tmpPath = resultPath + '.tmp';\n const jqProgram = `\n .pages |= with_entries(\n if (.value.page_no - 1) >= 0 and (.value.page_no - 1) < ${renderResult.pageCount} then\n .value.image.uri = \"pages/page_\\\\(.value.page_no - 1).png\" |\n .value.image.mimetype = \"image/png\" |\n .value.image.dpi = ${PAGE_RENDERING.DEFAULT_DPI}\n else . end\n )\n `;\n await runJqFileToFile(jqProgram, resultPath, tmpPath);\n await rename(tmpPath, resultPath);\n\n this.logger.info(\n `[ChunkedPDFConverter] Rendered ${renderResult.pageCount} page images`,\n );\n }\n\n /**\n * Remove pic_ files from images directory that are not referenced in result.json.\n * Chunked Docling conversion embeds page images as base64 in JSON, which get\n * extracted as pic_ files. After renderPageImages replaces page URIs with\n * pages/page_N.png, these pic_ files become orphaned.\n */\n private cleanupOrphanedPicFiles(resultPath: string, imagesDir: string): void {\n const content = readFileSync(resultPath, 'utf-8');\n const referencedPics = new Set<string>();\n const picPattern = /images\\/pic_\\d+\\.png/g;\n let match;\n while ((match = picPattern.exec(content)) !== null) {\n referencedPics.add(match[0].replace('images/', ''));\n }\n\n const picFiles = readdirSync(imagesDir).filter(\n (f) => f.startsWith('pic_') && f.endsWith('.png'),\n );\n\n let removedCount = 0;\n for (const file of picFiles) {\n if (!referencedPics.has(file)) {\n rmSync(join(imagesDir, file), { force: true });\n removedCount++;\n }\n }\n\n if (removedCount > 0) {\n this.logger.info(\n `[ChunkedPDFConverter] Cleaned up ${removedCount} orphaned pic_ files (${referencedPics.size} referenced, kept)`,\n );\n }\n }\n\n /**\n * Build cumulative pic_ file offsets per chunk for correct URI remapping.\n * Each offset[i] is the total number of pic_ files in chunks 0..i-1.\n */\n private buildPicFileOffsets(\n chunksBaseDir: string,\n totalChunks: number,\n ): number[] {\n const offsets: number[] = [];\n let cumulative = 0;\n for (let i = 0; i < totalChunks; i++) {\n offsets.push(cumulative);\n const dir = join(chunksBaseDir, `_chunk_${i}`, 'output', 'images');\n const count = existsSync(dir)\n ? readdirSync(dir).filter(\n (f) => f.startsWith('pic_') && f.endsWith('.png'),\n ).length\n : 0;\n cumulative += count;\n }\n return offsets;\n }\n\n /** Check if abort has been signalled and throw if so */\n private checkAbort(signal?: AbortSignal): void {\n if (signal?.aborted) {\n const error = new Error('Chunked PDF conversion was aborted');\n error.name = 'AbortError';\n throw error;\n }\n }\n}\n","import type { DoclingDocument, DoclingPictureItem } from '@heripo/model';\n\n/** Offsets for remapping $ref paths across chunks */\ninterface RefOffsets {\n texts: number;\n pictures: number;\n tables: number;\n groups: number;\n}\n\n/** Regex matching $ref paths that need offset remapping */\nconst REF_PATTERN = /^#\\/(texts|pictures|tables|groups)\\/(\\d+)$/;\n\n/** Regex matching image URIs like \"images/pic_N.png\" */\nconst IMAGE_URI_PATTERN = /^images\\/pic_(\\d+)\\.png$/;\n\n/**\n * Merges multiple DoclingDocuments into a single document.\n *\n * Handles $ref remapping, image path remapping, and pages merging\n * so that the merged result is indistinguishable from a single-pass conversion.\n */\nexport class DoclingDocumentMerger {\n /**\n * Merge an array of DoclingDocuments into one.\n * The first chunk's metadata (schema_name, version, name, origin) is used as the base.\n *\n * @param chunks - Array of DoclingDocument objects to merge (must have at least 1)\n * @param picFileOffsets - Optional cumulative pic_ file counts per chunk.\n * When provided, picFileOffsets[i] is used for pic_ URI remapping instead of\n * the pictures array length, aligning URIs with relocated file indices.\n * @returns Merged DoclingDocument\n */\n merge(chunks: DoclingDocument[], picFileOffsets?: number[]): DoclingDocument {\n if (chunks.length === 0) {\n throw new Error('Cannot merge zero chunks');\n }\n\n if (chunks.length === 1) {\n return chunks[0];\n }\n\n // Start with a deep clone of the first chunk as the base\n const base = structuredClone(chunks[0]);\n\n for (let i = 1; i < chunks.length; i++) {\n const chunk = chunks[i];\n const offsets: RefOffsets = {\n texts: base.texts.length,\n pictures: base.pictures.length,\n tables: base.tables.length,\n groups: base.groups.length,\n };\n const picFileOffset = picFileOffsets\n ? picFileOffsets[i]\n : offsets.pictures;\n\n // Merge texts\n for (const text of chunk.texts) {\n const remapped = structuredClone(text);\n remapped.self_ref = this.remapRef(remapped.self_ref, offsets);\n if (remapped.parent) {\n remapped.parent.$ref = this.remapRef(remapped.parent.$ref, offsets);\n }\n remapped.children = remapped.children.map((c) => ({\n $ref: this.remapRef(c.$ref, offsets),\n }));\n base.texts.push(remapped);\n }\n\n // Merge pictures\n for (const picture of chunk.pictures) {\n const remapped = structuredClone(picture);\n remapped.self_ref = this.remapRef(remapped.self_ref, offsets);\n if (remapped.parent) {\n remapped.parent.$ref = this.remapRef(remapped.parent.$ref, offsets);\n }\n remapped.children = remapped.children.map((c) => ({\n $ref: this.remapRef(c.$ref, offsets),\n }));\n remapped.captions = remapped.captions.map((c) => ({\n $ref: this.remapRef(c.$ref, offsets),\n }));\n // Remap image URI using file-based offset for correct alignment\n this.remapPictureImageUri(remapped, picFileOffset);\n base.pictures.push(remapped);\n }\n\n // Merge tables\n for (const table of chunk.tables) {\n const remapped = structuredClone(table);\n remapped.self_ref = this.remapRef(remapped.self_ref, offsets);\n if (remapped.parent) {\n remapped.parent.$ref = this.remapRef(remapped.parent.$ref, offsets);\n }\n remapped.children = remapped.children.map((c) => ({\n $ref: this.remapRef(c.$ref, offsets),\n }));\n remapped.captions = remapped.captions.map((c) => ({\n $ref: this.remapRef(c.$ref, offsets),\n }));\n remapped.footnotes = remapped.footnotes.map((f) => ({\n $ref: this.remapRef(f.$ref, offsets),\n }));\n base.tables.push(remapped);\n }\n\n // Merge groups\n for (const group of chunk.groups) {\n const remapped = structuredClone(group);\n remapped.self_ref = this.remapRef(remapped.self_ref, offsets);\n if (remapped.parent) {\n remapped.parent.$ref = this.remapRef(remapped.parent.$ref, offsets);\n }\n remapped.children = remapped.children.map((c) => ({\n $ref: this.remapRef(c.$ref, offsets),\n }));\n base.groups.push(remapped);\n }\n\n // Merge body children\n for (const child of chunk.body.children) {\n base.body.children.push({\n $ref: this.remapRef(child.$ref, offsets),\n });\n }\n\n // Merge furniture children\n for (const child of chunk.furniture.children) {\n base.furniture.children.push({\n $ref: this.remapRef(child.$ref, offsets),\n });\n }\n\n // Merge pages (keys are global page number strings, so no collision)\n Object.assign(base.pages, chunk.pages);\n }\n\n return base;\n }\n\n /**\n * Remap a $ref string by applying offsets.\n * Only refs matching \"#/{texts|pictures|tables|groups}/{N}\" are remapped.\n * Refs like \"#/body\" or \"#/furniture\" pass through unchanged.\n */\n remapRef(ref: string, offsets: RefOffsets): string {\n const match = REF_PATTERN.exec(ref);\n if (!match) {\n return ref;\n }\n\n const kind = match[1] as keyof RefOffsets;\n const index = parseInt(match[2], 10);\n return `#/${kind}/${index + offsets[kind]}`;\n }\n\n /**\n * Remap image URI in a picture item by applying the pic file offset.\n * Transforms \"images/pic_N.png\" → \"images/pic_{N+offset}.png\"\n */\n private remapPictureImageUri(\n picture: DoclingPictureItem,\n picFileOffset: number,\n ): void {\n // DoclingPictureItem may have an `image` field at runtime from Docling output\n const rec = picture as unknown as { image?: { uri?: string } };\n const image = rec.image;\n if (!image?.uri) return;\n\n const match = IMAGE_URI_PATTERN.exec(image.uri);\n if (match) {\n const index = parseInt(match[1], 10);\n image.uri = `images/pic_${index + picFileOffset}.png`;\n }\n }\n}\n","import type { LoggerMethods } from '@heripo/logger';\n\nimport { spawnAsync } from '@heripo/shared';\nimport { existsSync, rmSync } from 'node:fs';\nimport { tmpdir } from 'node:os';\nimport { join } from 'node:path';\n\nimport { IMAGE_PDF_CONVERTER } from '../config/constants';\n\n/**\n * Utility class for converting PDF to image-based PDF using ImageMagick.\n * Used as a fallback when regular PDF conversion fails due to encoding issues.\n *\n * ## System Requirements\n * - ImageMagick (`brew install imagemagick`)\n * - Ghostscript (`brew install ghostscript`)\n */\nexport class ImagePdfConverter {\n constructor(private readonly logger: LoggerMethods) {}\n\n /**\n * Convert a PDF file to an image-based PDF.\n * Downloads the PDF from URL, converts it using ImageMagick, and returns the path.\n *\n * @param pdfUrl - URL of the source PDF\n * @param reportId - Report identifier for temp file naming\n * @returns Path to the converted image PDF in temp directory\n */\n async convert(pdfUrl: string, reportId: string): Promise<string> {\n const timestamp = Date.now();\n const tempDir = tmpdir();\n const inputPath = join(tempDir, `${reportId}-${timestamp}-input.pdf`);\n const outputPath = join(tempDir, `${reportId}-${timestamp}-image.pdf`);\n\n try {\n this.logger.info('[ImagePdfConverter] Downloading PDF from URL...');\n await this.downloadPdf(pdfUrl, inputPath);\n\n this.logger.info('[ImagePdfConverter] Converting to image PDF...');\n await this.convertToImagePdf(inputPath, outputPath);\n\n this.logger.info('[ImagePdfConverter] Image PDF created:', outputPath);\n return outputPath;\n } finally {\n // Always cleanup input file\n if (existsSync(inputPath)) {\n rmSync(inputPath, { force: true });\n }\n }\n }\n\n /**\n * Download PDF from URL to local path using curl\n */\n private async downloadPdf(url: string, outputPath: string): Promise<void> {\n const result = await spawnAsync('curl', [\n '-L', // Follow redirects\n '-o',\n outputPath,\n '-s', // Silent mode\n '--fail', // Fail on HTTP errors\n url,\n ]);\n\n if (result.code !== 0) {\n throw new Error(\n `Failed to download PDF: ${result.stderr || 'Unknown error'}`,\n );\n }\n }\n\n /**\n * Convert PDF to image-based PDF using ImageMagick\n */\n private async convertToImagePdf(\n inputPath: string,\n outputPath: string,\n ): Promise<void> {\n const result = await spawnAsync('magick', [\n '-density',\n IMAGE_PDF_CONVERTER.DENSITY.toString(),\n inputPath,\n '-quality',\n IMAGE_PDF_CONVERTER.QUALITY.toString(),\n outputPath,\n ]);\n\n if (result.code !== 0) {\n throw new Error(\n `Failed to convert PDF to image PDF: ${result.stderr || 'Unknown error'}`,\n );\n }\n }\n\n /**\n * Cleanup the temporary image PDF file\n */\n cleanup(imagePdfPath: string): void {\n if (existsSync(imagePdfPath)) {\n this.logger.info(\n '[ImagePdfConverter] Cleaning up temp file:',\n imagePdfPath,\n );\n rmSync(imagePdfPath, { force: true });\n }\n }\n}\n","import type {\n VlmPageElement,\n VlmQualityIssueType,\n} from '../types/vlm-page-result';\n\n/** A single quality issue found during validation */\nexport interface VlmQualityIssue {\n /** Type of issue detected */\n type: VlmQualityIssueType;\n /** Human-readable description of the issue */\n message: string;\n /** Element reading-order indices that triggered the issue */\n affectedElements: number[];\n}\n\n/** Result of VLM response quality validation */\nexport interface VlmValidationResult {\n /** Whether the response passes quality validation */\n isValid: boolean;\n /** List of quality issues found (empty if valid) */\n issues: VlmQualityIssue[];\n}\n\n/** Minimum non-whitespace characters required for script anomaly check */\nconst MIN_CONTENT_LENGTH = 20;\n\n/**\n * Minimum ratio of Hangul/CJK characters when primary language starts with 'ko'.\n * 10% is very permissive — catches pure-Latin hallucinations while\n * allowing mixed content with numbers, coordinates, and English terms.\n */\nconst KOREAN_SCRIPT_RATIO_THRESHOLD = 0.1;\n\n/** Known placeholder text patterns (case-insensitive) */\nconst PLACEHOLDER_PATTERNS: RegExp[] = [\n /lorem\\s+ipsum/i,\n /dolor\\s+sit\\s+amet/i,\n /consectetur\\s+adipiscing/i,\n /sed\\s+do\\s+eiusmod/i,\n /ut\\s+labore\\s+et\\s+dolore/i,\n];\n\n/** Patterns indicating VLM described the image instead of transcribing text (Korean) */\nconst META_DESCRIPTION_PATTERNS_KO: RegExp[] = [\n /이미지\\s*해상도/,\n /판독하기?\\s*어렵/,\n /해상도가?\\s*(매우\\s*)?(낮|부족)/,\n /텍스트를?\\s*판독/,\n /글자를?\\s*읽기?\\s*어렵/,\n /정확한?\\s*판독이?\\s*(불가|어렵)/,\n];\n\n/** Patterns indicating VLM described the image instead of transcribing text (English) */\nconst META_DESCRIPTION_PATTERNS_EN: RegExp[] = [\n /the image contains/i,\n /unable to (read|transcribe)/i,\n /resolution.*(too low|insufficient)/i,\n /cannot (read|make out|decipher)/i,\n /text is (not |un)?(legible|readable)/i,\n /exact transcription is not possible/i,\n];\n\n/**\n * Minimum ratio of repetitive pattern characters to total content\n * for flagging as repetitive.\n */\nconst REPETITIVE_PATTERN_RATIO_THRESHOLD = 0.3;\n\n/** Minimum number of repetitions to consider a pattern repetitive */\nconst REPETITIVE_PATTERN_MIN_REPEATS = 5;\n\n/** Matches Hangul Syllables and Hangul Jamo */\nconst HANGUL_REGEX = /[\\uAC00-\\uD7AF\\u1100-\\u11FF]/g;\n\n/** Matches CJK Unified Ideographs (Hanja / Kanji / Hanzi) */\nconst CJK_REGEX = /[\\u4E00-\\u9FFF]/g;\n\n/**\n * Lightweight, stateless validator for VLM page extraction responses.\n *\n * Detects four categories of hallucination without any additional VLM calls:\n * 1. Placeholder text (Lorem ipsum and variants)\n * 2. Script anomaly (expected Korean but got Latin-only text)\n * 3. Meta description (VLM described the image instead of transcribing text)\n * 4. Repetitive pattern (repeated character patterns like `: : : : :`)\n */\nexport class VlmResponseValidator {\n /**\n * Validate VLM page result quality.\n *\n * @param elements - Extracted page elements to validate\n * @param documentLanguages - BCP 47 language tags (e.g., ['ko-KR', 'en-US'])\n * @returns Validation result with issues list\n */\n static validate(\n elements: VlmPageElement[],\n documentLanguages?: string[],\n ): VlmValidationResult {\n const issues: VlmQualityIssue[] = [];\n\n const textElements = elements.filter(\n (el) => el.type !== 'picture' && el.content.length > 0,\n );\n\n if (textElements.length === 0) {\n return { isValid: true, issues: [] };\n }\n\n const placeholderIssue = this.detectPlaceholderText(textElements);\n if (placeholderIssue) {\n issues.push(placeholderIssue);\n }\n\n if (documentLanguages?.[0]?.startsWith('ko')) {\n const scriptIssue = this.detectScriptAnomaly(textElements);\n if (scriptIssue) {\n issues.push(scriptIssue);\n }\n }\n\n const metaIssue = this.detectMetaDescription(textElements);\n if (metaIssue) {\n issues.push(metaIssue);\n }\n\n const repetitiveIssue = this.detectRepetitivePattern(textElements);\n if (repetitiveIssue) {\n issues.push(repetitiveIssue);\n }\n\n return { isValid: issues.length === 0, issues };\n }\n\n /**\n * Detect known placeholder / filler text in elements.\n */\n private static detectPlaceholderText(\n elements: VlmPageElement[],\n ): VlmQualityIssue | null {\n const affectedElements: number[] = [];\n\n for (const el of elements) {\n for (const pattern of PLACEHOLDER_PATTERNS) {\n if (pattern.test(el.content)) {\n affectedElements.push(el.order);\n break;\n }\n }\n }\n\n if (affectedElements.length === 0) return null;\n\n return {\n type: 'placeholder_text',\n message: `Detected placeholder text (e.g., Lorem ipsum) in ${affectedElements.length} element(s)`,\n affectedElements,\n };\n }\n\n /**\n * Detect script anomaly: expected Korean content but found mostly Latin text.\n * Counts Hangul + CJK characters and flags if the ratio is below threshold.\n */\n private static detectScriptAnomaly(\n elements: VlmPageElement[],\n ): VlmQualityIssue | null {\n const allContent = elements.map((el) => el.content).join('');\n const nonWhitespace = allContent.replace(/\\s/g, '');\n\n if (nonWhitespace.length < MIN_CONTENT_LENGTH) {\n return null;\n }\n\n const hangulCount = allContent.match(HANGUL_REGEX)?.length ?? 0;\n const cjkCount = allContent.match(CJK_REGEX)?.length ?? 0;\n const koreanCjkCount = hangulCount + cjkCount;\n const ratio = koreanCjkCount / nonWhitespace.length;\n\n if (ratio < KOREAN_SCRIPT_RATIO_THRESHOLD) {\n return {\n type: 'script_anomaly',\n message: `Expected Korean text but found ${(ratio * 100).toFixed(1)}% Korean/CJK characters (threshold: ${KOREAN_SCRIPT_RATIO_THRESHOLD * 100}%)`,\n affectedElements: elements.map((el) => el.order),\n };\n }\n\n return null;\n }\n\n /**\n * Detect meta description: VLM described the image/resolution instead\n * of transcribing actual text content.\n */\n private static detectMetaDescription(\n elements: VlmPageElement[],\n ): VlmQualityIssue | null {\n const affectedElements: number[] = [];\n const allPatterns = [\n ...META_DESCRIPTION_PATTERNS_KO,\n ...META_DESCRIPTION_PATTERNS_EN,\n ];\n\n for (const el of elements) {\n for (const pattern of allPatterns) {\n if (pattern.test(el.content)) {\n affectedElements.push(el.order);\n break;\n }\n }\n }\n\n if (affectedElements.length === 0) return null;\n\n return {\n type: 'meta_description',\n message: `Detected meta-description of image instead of text transcription in ${affectedElements.length} element(s)`,\n affectedElements,\n };\n }\n\n /**\n * Detect repetitive character patterns (e.g., `: : : : :` or `= = = = =`).\n * Flags when the same character repeats with spaces 5+ times and the\n * repetitive portion exceeds 30% of total content.\n */\n private static detectRepetitivePattern(\n elements: VlmPageElement[],\n ): VlmQualityIssue | null {\n const allContent = elements.map((el) => el.content).join('\\n');\n\n if (allContent.trim().length === 0) return null;\n\n // Match patterns like \"x x x x x\" where x is a non-whitespace character\n const repetitiveRegex = /(\\S)(\\s+\\1){4,}/g;\n let totalRepetitiveLength = 0;\n\n let match: RegExpExecArray | null;\n while ((match = repetitiveRegex.exec(allContent)) !== null) {\n const repeatedChar = match[1];\n // Count actual repetitions: split by the repeated character\n const segment = match[0];\n const parts = segment.split(/\\s+/).filter((p) => p === repeatedChar);\n /* v8 ignore start -- regex {4,} guarantees ≥5 parts; defensive guard only */\n if (parts.length >= REPETITIVE_PATTERN_MIN_REPEATS) {\n /* v8 ignore stop */\n totalRepetitiveLength += segment.length;\n }\n }\n\n if (totalRepetitiveLength === 0) return null;\n\n const ratio = totalRepetitiveLength / allContent.length;\n\n if (ratio < REPETITIVE_PATTERN_RATIO_THRESHOLD) return null;\n\n return {\n type: 'repetitive_pattern',\n message: `Detected repetitive character patterns (${(ratio * 100).toFixed(0)}% of content)`,\n affectedElements: elements.map((el) => el.order),\n };\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACIA,yBAAwB;AACxB,IAAAA,6BAAyB;AACzB,IAAAC,kBAAyB;AACzB,IAAAC,oBAAqB;;;ACJd,IAAM,aAAa;AAAA;AAAA;AAAA;AAAA,EAIxB,oBAAoB;AAAA;AAAA;AAAA;AAAA,EAKpB,2BAA2B;AAAA;AAAA;AAAA;AAAA,EAK3B,0BAA0B;AAAA;AAAA;AAAA;AAAA,EAK1B,8BAA8B;AAAA;AAAA;AAAA;AAAA,EAK9B,8BAA8B;AAChC;AAKO,IAAM,gBAAgB;AAAA;AAAA;AAAA;AAAA,EAI3B,kBAAkB;AAAA;AAAA;AAAA;AAAA,EAKlB,oBAAoB;AACtB;AAKO,IAAM,sBAAsB;AAAA;AAAA;AAAA;AAAA,EAIjC,kBAAkB;AACpB;AAKO,IAAM,iBAAiB;AAAA;AAAA,EAE5B,aAAa;AAAA;AAAA,EAEb,YAAY;AACd;AAQO,IAAM,qBAAqB;AAAA;AAAA,EAEhC,oBAAoB;AAAA;AAAA,EAEpB,qBAAqB;AACvB;AAKO,IAAM,sBAAsB;AAAA;AAAA;AAAA;AAAA,EAIjC,SAAS,eAAe;AAAA;AAAA;AAAA;AAAA,EAKxB,SAAS;AACX;;;AGzFA,2BAAsB;ACAtB,gBAOO;AFFA,IAAM,iBAAN,MAAqB;;;;;;;;;;;;;;EAc1B,aAAa,IACX,OACA,aACA,WACA,gBACc;AACd,UAAM,UAAe,IAAI,MAAM,MAAM,MAAM;AAC3C,QAAI,YAAY;AAEhB,mBAAe,SAAwB;AACrC,aAAO,YAAY,MAAM,QAAQ;AAC/B,cAAM,QAAQ;AACd,gBAAQ,KAAK,IAAI,MAAM,UAAU,MAAM,KAAK,GAAG,KAAK;AACpD,yBAAiB,QAAQ,KAAK,GAAG,KAAK;MACxC;IACF;AAEA,UAAM,UAAU,MAAM;MACpB,EAAE,QAAQ,KAAK,IAAI,aAAa,MAAM,MAAM,EAAE;MAC9C,MAAM,OAAO;IACf;AACA,UAAM,QAAQ,IAAI,OAAO;AACzB,WAAO;EACT;AACF;ACOO,SAAS,WACd,SACA,MACA,UAA6B,CAAC,GACR;AACtB,QAAM;IACJ,gBAAgB;IAChB,gBAAgB;IAChB,GAAG;EACL,IAAI;AAEJ,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,UAAM,WAAO,4BAAM,SAAS,MAAM,YAAY;AAE9C,QAAI,SAAS;AACb,QAAI,SAAS;AAEb,QAAI,iBAAiB,KAAK,QAAQ;AAChC,WAAK,OAAO,GAAG,QAAQ,CAAC,SAAS;AAC/B,kBAAU,KAAK,SAAS;MAC1B,CAAC;IACH;AAEA,QAAI,iBAAiB,KAAK,QAAQ;AAChC,WAAK,OAAO,GAAG,QAAQ,CAAC,SAAS;AAC/B,kBAAU,KAAK,SAAS;MAC1B,CAAC;IACH;AAEA,SAAK,GAAG,SAAS,CAAC,SAAS;AACzB,cAAQ,EAAE,QAAQ,QAAQ,MAAM,QAAQ,EAAE,CAAC;IAC7C,CAAC;AAED,SAAK,GAAG,SAAS,MAAM;EACzB,CAAC;AACH;AExEO,SAAS,eAAe,OAAoC;AACjE,QAAM,aAAc,MAAgC;AACpD,MAAI,CAAC,cAAc,OAAO,eAAe,SAAU,QAAO;AAE1D,MAAI,WAAW,SAAS,QAAQ,EAAG,QAAO;AAC1C,MAAI,WAAW,SAAS,QAAQ,EAAG,QAAO;AAC1C,MAAI,WAAW,SAAS,WAAW,EAAG,QAAO;AAC7C,MAAI,WAAW,SAAS,UAAU,EAAG,QAAO;AAE5C,SAAO;AACT;ADwJO,IAAM,YAAN,MAAgB;;;;;;EAMrB,OAAe,iBAAiB,OAA8B;AAC5D,UAAM,WAAW;AAGjB,QAAI,OAAO,SAAS,YAAY,SAAU,QAAO,SAAS;AAC1D,QAAI,OAAO,SAAS,OAAO,SAAU,QAAO,SAAS;AACrD,QAAI,OAAO,SAAS,UAAU,SAAU,QAAO,SAAS;AACxD,QAAI,OAAO,SAAS,SAAS,SAAU,QAAO,SAAS;AAGvD,WAAO,OAAO,KAAK;EACrB;;;;EAKA,OAAe,WACb,QACA,WACA,UAOA,cACoB;AACpB,WAAO;MACL,WAAW,OAAO;MAClB,OAAO,OAAO;MACd,OAAO,eAAe,aAAa;MACnC;MACA,aAAa,SAAS,OAAO,eAAe;MAC5C,cAAc,SAAS,OAAO,gBAAgB;MAC9C,aAAa,SAAS,OAAO,eAAe;IAC9C;EACF;;;;;;;;;EAUA,OAAwB,gCAAgC;;;;;;;;;;;;;EAcxD,aAAqB,oBACnB,OACA,QACA,cAQC;AACD,UAAM,iBAAa,gBAAK;MACtB,aAAa;MACb,aAAa;IACf,CAAC;AAED,QAAI;AAEJ,aACM,UAAU,GACd,WAAW,KAAK,+BAChB,WACA;AACA,mBAAa,UAAO,wBAAqB;QACvC,GAAG;QACH;QACA,OAAO,EAAE,cAAc,WAAW;QAClC,YAAY,EAAE,MAAM,QAAQ,UAAU,eAAe;QACrD,cAAU,uBAAY,cAAc;MACtC,CAAC;AAED,YAAM,WAAW,WAAW,YAAY,CAAC;AAGzC,UAAI,UAAU;AACZ,eAAO;UACL,QAAQ,SAAS;UACjB,OAAO,WAAW;QACpB;MACF;IACF;AAEA,UAAM,IAAI,iCAAuB;MAC/B,SAAS;MACT,MAAM,WAAW,QAAQ;MACzB,UAAU,WAAW;MACrB,OAAO,WAAW;MAClB,cAAc,WAAW;IAC3B,CAAC;EACH;;;;;;;;;;;EAYA,aAAqB,yBACnB,OACA,QACA,cAQC;AACD,UAAM,eAAe,eAAe,KAAK;AAEzC,QAAI,iBAAiB,gBAAgB,iBAAiB,WAAW;AAC/D,aAAO,KAAK,oBAAoB,OAAO,QAAQ,YAAY;IAC7D;AAEA,QAAI;AAEJ,aACM,UAAU,GACd,WAAW,KAAK,+BAChB,WACA;AACA,UAAI;AACF,eAAO,UAAO,wBAAqB;UACjC;UACA,QAAQ,iBAAO,OAAO,EAAE,OAAO,CAAC;UAChC,GAAG;QACL,CAAC;MACH,SAAS,OAAO;AACd,YAAI,iCAAuB,WAAW,KAAK,GAAG;AAC5C,sBAAY;AACZ;QACF;AACA,cAAM;MACR;IACF;AAEA,UAAM;EACR;;;;;;;EAQA,aAAqB,oBACnB,QACA,YAQiC;AACjC,UAAM,mBAAmB,KAAK,iBAAiB,OAAO,YAAY;AAGlE,QAAI;AACF,YAAM,WAAW,MAAM,WAAW,OAAO,YAAY;AAErD,aAAO;QACL,QAAQ,SAAS;QACjB,OAAO,KAAK,WAAW,QAAQ,kBAAkB,UAAU,KAAK;QAChE,cAAc;MAChB;IACF,SAAS,cAAc;AAErB,UAAI,OAAO,aAAa,SAAS;AAC/B,cAAM;MACR;AAGA,UAAI,CAAC,OAAO,eAAe;AACzB,cAAM;MACR;AAGA,YAAM,oBAAoB,KAAK,iBAAiB,OAAO,aAAa;AACpE,YAAM,WAAW,MAAM,WAAW,OAAO,aAAa;AAEtD,aAAO;QACL,QAAQ,SAAS;QACjB,OAAO,KAAK,WAAW,QAAQ,mBAAmB,UAAU,IAAI;QAChE,cAAc;MAChB;IACF;EACF;;;;;;;;;;;;;;;;EAiBA,aAAa,KACX,QACiC;AACjC,WAAO,KAAK;MAAoB;MAAQ,CAAC,UACvC,KAAK,yBAAyB,OAAO,OAAO,QAAQ;QAClD,QAAQ,OAAO;QACf,QAAQ,OAAO;QACf,aAAa,OAAO;QACpB,YAAY,OAAO;QACnB,aAAa,OAAO;MACtB,CAAC;IACH;EACF;;;;;;;;;;;;EAaA,aAAa,WACX,QACiC;AACjC,WAAO,KAAK;MAAoB;MAAQ,CAAC,UACvC,KAAK,yBAAyB,OAAO,OAAO,QAAQ;QAClD,UAAU,OAAO;QACjB,aAAa,OAAO;QACpB,YAAY,OAAO;QACnB,aAAa,OAAO;MACtB,CAAC;IACH;EACF;AACF;AEjbA,SAAS,aAAa,OAA2B;AAC/C,SAAO,GAAG,MAAM,WAAW,WAAW,MAAM,YAAY,YAAY,MAAM,WAAW;AACvF;AAsFO,IAAM,0BAAN,MAA8B;EAC3B,QAA4C,CAAC;;;;;;EAOrD,MAAM,OAAiC;AAErC,QAAI,CAAC,KAAK,MAAM,MAAM,SAAS,GAAG;AAChC,WAAK,MAAM,MAAM,SAAS,IAAI;QAC5B,WAAW,MAAM;QACjB,QAAQ,CAAC;QACT,OAAO;UACL,aAAa;UACb,cAAc;UACd,aAAa;QACf;MACF;IACF;AAEA,UAAM,YAAY,KAAK,MAAM,MAAM,SAAS;AAG5C,QAAI,CAAC,UAAU,OAAO,MAAM,KAAK,GAAG;AAClC,gBAAU,OAAO,MAAM,KAAK,IAAI;QAC9B,OAAO;UACL,aAAa;UACb,cAAc;UACd,aAAa;QACf;MACF;IACF;AAEA,UAAM,QAAQ,UAAU,OAAO,MAAM,KAAK;AAG1C,QAAI,MAAM,UAAU,WAAW;AAC7B,UAAI,CAAC,MAAM,SAAS;AAClB,cAAM,UAAU;UACd,WAAW,MAAM;UACjB,aAAa;UACb,cAAc;UACd,aAAa;QACf;MACF;AAEA,YAAM,QAAQ,eAAe,MAAM;AACnC,YAAM,QAAQ,gBAAgB,MAAM;AACpC,YAAM,QAAQ,eAAe,MAAM;IACrC,WAAW,MAAM,UAAU,YAAY;AACrC,UAAI,CAAC,MAAM,UAAU;AACnB,cAAM,WAAW;UACf,WAAW,MAAM;UACjB,aAAa;UACb,cAAc;UACd,aAAa;QACf;MACF;AAEA,YAAM,SAAS,eAAe,MAAM;AACpC,YAAM,SAAS,gBAAgB,MAAM;AACrC,YAAM,SAAS,eAAe,MAAM;IACtC;AAGA,UAAM,MAAM,eAAe,MAAM;AACjC,UAAM,MAAM,gBAAgB,MAAM;AAClC,UAAM,MAAM,eAAe,MAAM;AAGjC,cAAU,MAAM,eAAe,MAAM;AACrC,cAAU,MAAM,gBAAgB,MAAM;AACtC,cAAU,MAAM,eAAe,MAAM;EACvC;;;;;;EAOA,iBAAuC;AACrC,WAAO,OAAO,OAAO,KAAK,KAAK;EACjC;;;;;;;;;;EAWA,YA8BE;AACA,UAAM,aA2BD,CAAC;AAEN,eAAW,aAAa,OAAO,OAAO,KAAK,KAAK,GAAG;AACjD,YAAM,SAmBD,CAAC;AAEN,iBAAW,CAAC,WAAW,SAAS,KAAK,OAAO,QAAQ,UAAU,MAAM,GAAG;AACrE,cAAM,cAmBF;UACF,OAAO;UACP,OAAO;YACL,aAAa,UAAU,MAAM;YAC7B,cAAc,UAAU,MAAM;YAC9B,aAAa,UAAU,MAAM;UAC/B;QACF;AAEA,YAAI,UAAU,SAAS;AACrB,sBAAY,UAAU;YACpB,WAAW,UAAU,QAAQ;YAC7B,aAAa,UAAU,QAAQ;YAC/B,cAAc,UAAU,QAAQ;YAChC,aAAa,UAAU,QAAQ;UACjC;QACF;AAEA,YAAI,UAAU,UAAU;AACtB,sBAAY,WAAW;YACrB,WAAW,UAAU,SAAS;YAC9B,aAAa,UAAU,SAAS;YAChC,cAAc,UAAU,SAAS;YACjC,aAAa,UAAU,SAAS;UAClC;QACF;AAEA,eAAO,KAAK,WAAW;MACzB;AAEA,iBAAW,KAAK;QACd,WAAW,UAAU;QACrB;QACA,OAAO;UACL,aAAa,UAAU,MAAM;UAC7B,cAAc,UAAU,MAAM;UAC9B,aAAa,UAAU,MAAM;QAC/B;MACF,CAAC;IACH;AAEA,UAAM,aAAa,KAAK,cAAc;AAEtC,WAAO;MACL;MACA,OAAO;QACL,aAAa,WAAW;QACxB,cAAc,WAAW;QACzB,aAAa,WAAW;MAC1B;IACF;EACF;;;;;;EAOA,gBAA4B;AAC1B,QAAI,aAAa;AACjB,QAAI,cAAc;AAClB,QAAI,cAAc;AAElB,eAAW,aAAa,OAAO,OAAO,KAAK,KAAK,GAAG;AACjD,oBAAc,UAAU,MAAM;AAC9B,qBAAe,UAAU,MAAM;AAC/B,qBAAe,UAAU,MAAM;IACjC;AAEA,WAAO;MACL,aAAa;MACb,cAAc;MACd;IACF;EACF;;;;;;;;;;EAWA,WAAW,QAA6B;AACtC,UAAM,aAAa,KAAK,eAAe;AAEvC,QAAI,WAAW,WAAW,GAAG;AAC3B,aAAO,KAAK,8CAA8C;AAC1D;IACF;AAEA,WAAO,KAAK,0CAA0C;AACtD,WAAO,KAAK,EAAE;AAEd,QAAI,mBAAmB;AACvB,QAAI,oBAAoB;AACxB,QAAI,mBAAmB;AACvB,QAAI,0BAA0B;AAC9B,QAAI,2BAA2B;AAC/B,QAAI,0BAA0B;AAC9B,QAAI,2BAA2B;AAC/B,QAAI,4BAA4B;AAChC,QAAI,2BAA2B;AAE/B,eAAW,aAAa,YAAY;AAClC,aAAO,KAAK,GAAG,UAAU,SAAS,GAAG;AAErC,iBAAW,CAAC,OAAO,SAAS,KAAK,OAAO,QAAQ,UAAU,MAAM,GAAG;AACjE,eAAO,KAAK,OAAO,KAAK,GAAG;AAG3B,YAAI,UAAU,SAAS;AACrB,iBAAO;YACL,kBAAkB,UAAU,QAAQ,SAAS,MAAM,aAAa,UAAU,OAAO,CAAC;UACpF;AACA,qCAA2B,UAAU,QAAQ;AAC7C,sCAA4B,UAAU,QAAQ;AAC9C,qCAA2B,UAAU,QAAQ;QAC/C;AAGA,YAAI,UAAU,UAAU;AACtB,iBAAO;YACL,mBAAmB,UAAU,SAAS,SAAS,MAAM,aAAa,UAAU,QAAQ,CAAC;UACvF;AACA,sCAA4B,UAAU,SAAS;AAC/C,uCAA6B,UAAU,SAAS;AAChD,sCAA4B,UAAU,SAAS;QACjD;AAGA,eAAO,KAAK,mBAAmB,aAAa,UAAU,KAAK,CAAC,EAAE;MAChE;AAEA,aAAO;QACL,KAAK,UAAU,SAAS,WAAW,aAAa,UAAU,KAAK,CAAC;MAClE;AACA,aAAO,KAAK,EAAE;AAEd,0BAAoB,UAAU,MAAM;AACpC,2BAAqB,UAAU,MAAM;AACrC,0BAAoB,UAAU,MAAM;IACtC;AAGA,WAAO,KAAK,iBAAiB;AAC7B,QAAI,0BAA0B,GAAG;AAC/B,aAAO;QACL,kBAAkB,aAAa;UAC7B,aAAa;UACb,cAAc;UACd,aAAa;QACf,CAAC,CAAC;MACJ;IACF;AACA,QAAI,2BAA2B,GAAG;AAChC,aAAO;QACL,mBAAmB,aAAa;UAC9B,aAAa;UACb,cAAc;UACd,aAAa;QACf,CAAC,CAAC;MACJ;IACF;AACA,WAAO;MACL,gBAAgB,aAAa;QAC3B,aAAa;QACb,cAAc;QACd,aAAa;MACf,CAAC,CAAC;IACJ;EACF;;;;;;EAOA,QAAc;AACZ,SAAK,QAAQ,CAAC;EAChB;AACF;;;ACpeA,gCAAsB;AACtB,uBAAqB;;;ACQd,IAAM,uBAAuB;AAK7B,IAAM,qBAAqB,EAAE,OAAO,GAAG,OAAO,EAAE;AAWhD,IAAM,qBAAN,cAAiC,MAAM;AAAA,EAC5C,YAAY,SAAiB;AAC3B,UAAM,OAAO;AACb,SAAK,OAAO;AAAA,EACd;AACF;AAcO,SAAS,mBAAmB,QAA0C;AAC3E,QAAM,QAAQ,OAAO,MAAM,oBAAoB;AAC/C,MAAI,CAAC,MAAO,QAAO;AAEnB,QAAM,QAAQ,SAAS,MAAM,CAAC,CAAC;AAC/B,QAAM,QAAQ,SAAS,MAAM,CAAC,CAAC;AAE/B,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,eAAe,GAAG,KAAK,IAAI,KAAK;AAAA,EAClC;AACF;AAkBO,SAAS,sBACd,SACA,UAA6B,UACvB;AACN,QAAM,EAAE,OAAO,MAAM,IAAI;AACzB,QAAM,SAAS,YAAY,SAAS,gBAAgB;AAGpD,MAAI,UAAU,KAAK,SAAS,IAAI;AAC9B,UAAM,IAAI;AAAA,MACR,GAAG,MAAM,IAAI,KAAK,IAAI,KAAK;AAAA,IAC7B;AAAA,EACF;AAGA,MAAI,UAAU,KAAK,QAAQ,mBAAmB,OAAO;AACnD,UAAM,IAAI,mBAAmB,kCAAkC;AAAA,EACjE;AACF;;;ADjFO,IAAM,qBAAN,MAAM,oBAAmB;AAAA,EACb;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAEjB,YAAY,SAKT;AACD,SAAK,SAAS,QAAQ;AACtB,SAAK,WAAW,QAAQ;AACxB,SAAK,OAAO,QAAQ;AACpB,SAAK,sBAAsB,QAAQ;AAAA,EACrC;AAAA,EAEA,MAAM,QAAuB;AAC3B,SAAK,OAAO,KAAK,uDAAuD;AAExE,UAAM,KAAK,mBAAmB;AAC9B,UAAM,KAAK,uBAAuB;AAClC,UAAM,KAAK,WAAW;AACtB,UAAM,KAAK,kBAAkB;AAC7B,UAAM,KAAK,eAAe;AAC1B,UAAM,KAAK,oBAAoB;AAG/B,UAAM,YAAY,MAAM,KAAK,YAAY,KAAK,IAAI;AAElD,QAAI,aAAa,CAAC,KAAK,qBAAqB;AAC1C,WAAK,OAAO;AAAA,QACV;AAAA,QACA,KAAK;AAAA,MACP;AAAA,IACF,OAAO;AACL,YAAM,KAAK,kBAAkB;AAAA,IAC/B;AAEA,SAAK,OAAO,KAAK,sCAAsC;AAAA,EACzD;AAAA,EAEA,MAAc,qBAAiD;AAC7D,UAAM,SAAS,MAAM,WAAW,WAAW,CAAC,WAAW,CAAC;AAExD,QAAI,OAAO,SAAS,GAAG;AACrB,YAAM,IAAI,MAAM,gCAAgC;AAAA,IAClD;AAEA,UAAM,SAAS,OAAO,SAAS,OAAO;AACtC,UAAM,UAAU,mBAAmB,MAAM;AAEzC,QAAI,CAAC,SAAS;AACZ,YAAM,IAAI,MAAM,gCAAgC;AAAA,IAClD;AAEA,SAAK,OAAO;AAAA,MACV;AAAA,MACA,QAAQ;AAAA,IACV;AAEA,QAAI;AACF,4BAAsB,SAAS,QAAQ;AAAA,IACzC,SAAS,OAAO;AACd,UAAI,iBAAiB,sBAAsB,QAAQ,SAAS,IAAI;AAC9D,aAAK,OAAO;AAAA,UACV;AAAA,QACF;AAAA,MACF;AACA,YAAM;AAAA,IACR;AAEA,WAAO;AAAA,EACT;AAAA,EAEA,MAAc,yBAAwC;AACpD,UAAM,SAAS,MAAM,WAAW,WAAW,CAAC,MAAM,QAAQ,KAAK,QAAQ,CAAC;AAExE,QAAI,OAAO,SAAS,GAAG;AACrB,YAAM,IAAI,MAAM,6CAA6C;AAAA,IAC/D;AAEA,UAAM,KAAK,wBAAwB;AAAA,EACrC;AAAA,EAEA,MAAc,0BAAyC;AACrD,UAAM,iBAAa,uBAAK,KAAK,UAAU,OAAO,QAAQ;AACtD,UAAM,SAAS,MAAM,WAAW,YAAY,CAAC,WAAW,CAAC;AAEzD,QAAI,OAAO,SAAS,GAAG;AACrB,YAAM,IAAI,MAAM,sCAAsC;AAAA,IACxD;AAEA,UAAM,SAAS,OAAO,SAAS,OAAO;AACtC,UAAM,UAAU,mBAAmB,MAAM;AAEzC,QAAI,CAAC,SAAS;AACZ,YAAM,IAAI,MAAM,qCAAqC;AAAA,IACvD;AAEA,0BAAsB,SAAS,MAAM;AAAA,EACvC;AAAA,EAEA,MAAc,aAA4B;AACxC,UAAM,cAAU,uBAAK,KAAK,UAAU,OAAO,KAAK;AAChD,UAAM,SAAS,MAAM,WAAW,SAAS,CAAC,WAAW,aAAa,KAAK,CAAC;AAExE,QAAI,OAAO,SAAS,GAAG;AACrB,WAAK,OAAO;AAAA,QACV;AAAA,QACA,OAAO;AAAA,MACT;AACA,YAAM,IAAI,MAAM,qCAAqC,OAAO,IAAI,EAAE;AAAA,IACpE;AAAA,EACF;AAAA,EAEA,MAAc,oBAAmC;AAC/C,UAAM,cAAU,uBAAK,KAAK,UAAU,OAAO,KAAK;AAChD,UAAM,SAAS,MAAM,WAAW,SAAS;AAAA,MACvC;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF,CAAC;AAED,QAAI,OAAO,SAAS,GAAG;AACrB,WAAK,OAAO;AAAA,QACV;AAAA,QACA,OAAO;AAAA,MACT;AACA,YAAM,IAAI;AAAA,QACR,4CAA4C,OAAO,IAAI;AAAA,MACzD;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAc,iBAAgC;AAC5C,UAAM,cAAU,uBAAK,KAAK,UAAU,OAAO,KAAK;AAChD,UAAM,SAAS,MAAM,WAAW,SAAS;AAAA,MACvC;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF,CAAC;AAED,QAAI,OAAO,SAAS,GAAG;AACrB,WAAK,OAAO;AAAA,QACV;AAAA,QACA,OAAO;AAAA,MACT;AACA,YAAM,IAAI,MAAM,yCAAyC,OAAO,IAAI,EAAE;AAAA,IACxE;AAAA,EACF;AAAA,EAEA,MAAc,sBAAqC;AACjD,UAAM,cAAU,uBAAK,KAAK,UAAU,OAAO,KAAK;AAChD,UAAM,SAAS,MAAM,WAAW,SAAS;AAAA,MACvC;AAAA,MACA;AAAA,MACA;AAAA,IACF,CAAC;AAED,QAAI,OAAO,SAAS,GAAG;AACrB,WAAK,OAAO;AAAA,QACV;AAAA,QACA,OAAO;AAAA,MACT;AACA,YAAM,IAAI;AAAA,QACR,+CAA+C,OAAO,IAAI;AAAA,MAC5D;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAc,YAAY,MAAgC;AACxD,QAAI;AACF,YAAM,SAAS,MAAM,WAAW,QAAQ,CAAC,OAAO,IAAI,IAAI,EAAE,CAAC;AAC3D,aAAO,OAAO,SAAS,KAAK,CAAC,CAAC,OAAO,OAAO,KAAK;AAAA,IACnD,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAa,cAA6B;AACxC,UAAM,KAAK,kBAAkB;AAAA,EAC/B;AAAA;AAAA,EAGA,aAAoB,kBAClB,QACA,MACe;AACf,WAAO,IAAI,QAAQ,CAAC,YAAY;AAC9B,YAAM,WAAO,iCAAM,QAAQ,CAAC,OAAO,IAAI,IAAI,EAAE,CAAC;AAC9C,YAAM,OAAiB,CAAC;AAExB,WAAK,QAAQ,GAAG,QAAQ,CAAC,SAAS;AAChC,cAAM,MAAc,KAAK,SAAS;AAClC,aAAK;AAAA,UACH,GAAG,IACA,MAAM,KAAK,EACX,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,EACnB,OAAO,OAAO;AAAA,QACnB;AAAA,MACF,CAAC;AAED,WAAK,GAAG,SAAS,MAAM;AACrB,YAAI,KAAK,WAAW,EAAG,QAAO,QAAQ;AAEtC,YAAI,YAAY,KAAK;AACrB,cAAM,OAAO,MAAM;AACjB,cAAI,EAAE,aAAa,EAAG,SAAQ;AAAA,QAChC;AAEA,eAAO;AAAA,UACL;AAAA,UACA,KAAK,KAAK,IAAI;AAAA,UACd;AAAA,UACA;AAAA,QACF;AACA,mBAAW,OAAO,MAAM;AACtB,gBAAM,eAAW,iCAAM,QAAQ,CAAC,MAAM,GAAG,CAAC;AAE1C,mBAAS,GAAG,SAAS,CAAC,aAAa;AACjC,gBAAI,aAAa,GAAG;AAClB,qBAAO,KAAK,+CAA+C,GAAG;AAAA,YAChE;AACA,iBAAK;AAAA,UACP,CAAC;AACD,mBAAS,GAAG,SAAS,CAACC,WAAU;AAC9B,mBAAO,KAAK,+CAA+CA,MAAK;AAChE,iBAAK;AAAA,UACP,CAAC;AAAA,QACH;AAAA,MACF,CAAC;AAED,WAAK,GAAG,SAAS,MAAM,QAAQ,CAAC;AAAA,IAClC,CAAC;AAAA,EACH;AAAA,EAEA,MAAc,oBAAmC;AAC/C,WAAO,IAAI,QAAQ,OAAO,SAAS,WAAW;AAE5C,UAAI,KAAK,qBAAqB;AAC5B,cAAM,oBAAmB,kBAAkB,KAAK,QAAQ,KAAK,IAAI;AAAA,MACnE;AAEA,YAAM,WAAW,KAAK;AACtB,YAAM,uBAAmB,uBAAK,UAAU,OAAO,eAAe;AAC9D,YAAM,OAAO,CAAC,OAAO,UAAU,KAAK,KAAK,SAAS,CAAC;AAEnD,WAAK,OAAO;AAAA,QACV;AAAA,QACA,KAAK;AAAA,MACP;AACA,YAAM,qBAAiB,iCAAM,kBAAkB,MAAM;AAAA,QACnD,UAAU;AAAA;AAAA,QACV,OAAO;AAAA;AAAA,QACP,KAAK;AAAA,UACH,GAAG,QAAQ;AAAA;AAAA,UAEX,sCAAsC;AAAA,QACxC;AAAA,MACF,CAAC;AAED,qBAAe,MAAM;AAErB,qBAAe,GAAG,SAAS,CAAC,UAAU;AACpC,aAAK,OAAO,MAAM,6CAA6C,KAAK;AACpE,eAAO,KAAK;AAAA,MACd,CAAC;AAGD,iBAAW,MAAM;AACf,gBAAQ;AAAA,MACV,GAAG,oBAAoB,gBAAgB;AAAA,IACzC,CAAC;AAAA,EACH;AACF;;;AE9RA,wBAAqB;AACrB,IAAAC,kBAAoE;AACpE,IAAAC,mBAAkC;AAClC,IAAAC,oBAAqB;AACrB,IAAAD,mBAAyB;;;ACVlB,IAAM,wBAAN,cAAoC,MAAM;AAAA,EAG/C,YACkB,eACA,eAChB;AACA;AAAA,MACE,kDACe,cAAc,OAAO,eACrB,cAAc,OAAO;AAAA,IACtC;AAPgB;AACA;AAAA,EAOlB;AAAA,EAXgB,OAAO;AAYzB;;;ACfA,IAAAE,kBAQO;AACP,IAAAC,oBAA8B;AAC9B,yBAA0B;AAC1B,IAAAC,mBAAyB;AACzB,YAAuB;;;ACdvB,IAAAC,6BAAsB;AACtB,qBAAkC;AAClC,sBAAuB;AACvB,IAAAC,mBAAyB;AAKzB,SAAS,YAAoB;AAC3B,QAAM,IAAI,QAAQ,IAAI,SAAS,KAAK;AACpC,SAAO,KAAK,EAAE,SAAS,IAAI,IAAI;AACjC;AAQO,SAAS,cACd,SACA,UACY;AACZ,SAAO,IAAI,QAAW,CAAC,SAAS,WAAW;AACzC,UAAM,SAAS,UAAU;AACzB,UAAM,OAAO;AAAA,MACX;AAAA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAEA,UAAM,YAAQ,kCAAM,QAAQ,MAAM;AAAA,MAChC,OAAO,CAAC,UAAU,QAAQ,MAAM;AAAA,MAChC,KAAK,QAAQ;AAAA,IACf,CAAC;AAED,QAAI,SAAS;AACb,QAAI,SAAS;AAEb,UAAM,OAAO,YAAY,OAAO;AAChC,UAAM,OAAO,YAAY,OAAO;AAEhC,UAAM,OAAO,GAAG,QAAQ,CAAC,UAAkB;AACzC,gBAAU;AAAA,IACZ,CAAC;AACD,UAAM,OAAO,GAAG,QAAQ,CAAC,UAAkB;AACzC,gBAAU;AAAA,IACZ,CAAC;AAED,UAAM,GAAG,SAAS,CAAC,QAAQ;AACzB,aAAO,GAAG;AAAA,IACZ,CAAC;AAED,UAAM,GAAG,SAAS,CAAC,SAAS;AAC1B,UAAI,SAAS,GAAG;AACd,cAAM,QAAQ,IAAI;AAAA,UAChB,uBAAuB,IAAI,KAAK,SAAS,aAAa,SAAS,EAAE;AAAA,QACnE;AACA,eAAO,OAAO,KAAK;AAAA,MACrB;AACA,UAAI;AAEF,cAAM,OAAO,OAAO,KAAK;AACzB,cAAM,SAAS,KAAK,MAAM,IAAI;AAC9B,gBAAQ,MAAM;AAAA,MAChB,SAAS,GAAG;AACV;AAAA,UACE,IAAI;AAAA,YACF,oDAAoD,OAAO,MAAM,YAAa,EAAY,OAAO;AAAA,UACnG;AAAA,QACF;AAAA,MACF;AAAA,IACF,CAAC;AAAA,EACH,CAAC;AACH;AAMO,SAAS,gBACd,SACA,WACA,YACe;AACf,SAAO,IAAI,QAAc,CAAC,SAAS,WAAW;AAC5C,UAAM,SAAS,UAAU;AACzB,UAAM,OAAO,CAAC,SAAS,SAAS;AAEhC,UAAM,YAAQ,kCAAM,QAAQ,MAAM;AAAA,MAChC,OAAO,CAAC,UAAU,QAAQ,MAAM;AAAA,MAChC,KAAK,QAAQ;AAAA,IACf,CAAC;AAED,QAAI,SAAS;AACb,QAAI,WAA0B;AAC9B,QAAI,eAAe;AACnB,QAAI,UAAU;AAEd,UAAM,OAAO,YAAY,OAAO;AAChC,UAAM,OAAO,GAAG,QAAQ,CAAC,UAAkB;AACzC,gBAAU;AAAA,IACZ,CAAC;AAED,UAAM,SAAK,kCAAkB,UAAU;AAEvC,aAAS,YAAY;AACnB,UAAI,QAAS;AACb,UAAI,CAAC,gBAAgB,aAAa,KAAM;AACxC,gBAAU;AACV,UAAI,aAAa,GAAG;AAClB;AAAA,UACE,IAAI;AAAA,YACF,uBAAuB,QAAQ,KAAK,SAAS,aAAa,SAAS,EAAE;AAAA,UACvE;AAAA,QACF;AAAA,MACF,OAAO;AACL,gBAAQ;AAAA,MACV;AAAA,IACF;AAEA,UAAM,GAAG,SAAS,CAAC,QAAQ;AACzB,UAAI,QAAS;AACb,gBAAU;AACV,SAAG,QAAQ;AACX,aAAO,GAAG;AAAA,IACZ,CAAC;AAED,mCAAS,MAAM,QAAQ,EAAE,EACtB,KAAK,MAAM;AACV,qBAAe;AACf,gBAAU;AAAA,IACZ,CAAC,EACA,MAAM,CAAC,QAAQ;AACd,UAAI,QAAS;AACb,gBAAU;AACV,aAAO,GAAG;AAAA,IACZ,CAAC;AAEH,UAAM,GAAG,SAAS,CAAC,SAAS;AAC1B,iBAAW,QAAQ;AACnB,gBAAU;AAAA,IACZ,CAAC;AAAA,EACH,CAAC;AACH;AAMO,SAAS,eACd,SACA,UACA,QACe;AACf,SAAO,IAAI,QAAc,CAAC,SAAS,WAAW;AAC5C,UAAM,SAAS,UAAU;AACzB,UAAM,OAAO,CAAC,MAAM,SAAS,QAAQ;AAErC,UAAM,YAAQ,kCAAM,QAAQ,MAAM;AAAA,MAChC,OAAO,CAAC,UAAU,QAAQ,MAAM;AAAA,MAChC,KAAK,QAAQ;AAAA,IACf,CAAC;AAED,QAAI,SAAS;AACb,QAAI,SAAS;AACb,QAAI,gBAAgB;AAEpB,UAAM,OAAO,YAAY,OAAO;AAChC,UAAM,OAAO,YAAY,OAAO;AAEhC,aAAS,WAAW,MAAoB;AACtC,UAAI,cAAe;AACnB,UAAI;AACF,eAAO,IAAI;AAAA,MACb,SAAS,KAAK;AACZ,wBAAgB;AAChB,cAAM,KAAK;AACX,eAAO,GAAG;AAAA,MACZ;AAAA,IACF;AAEA,UAAM,OAAO,GAAG,QAAQ,CAAC,UAAkB;AACzC,gBAAU;AACV,UAAI;AACJ,cAAQ,aAAa,OAAO,QAAQ,IAAI,OAAO,IAAI;AACjD,cAAM,OAAO,OAAO,MAAM,GAAG,UAAU;AACvC,iBAAS,OAAO,MAAM,aAAa,CAAC;AACpC,YAAI,KAAK,SAAS,GAAG;AACnB,qBAAW,IAAI;AAAA,QACjB;AAAA,MACF;AAAA,IACF,CAAC;AAED,UAAM,OAAO,GAAG,QAAQ,CAAC,UAAkB;AACzC,gBAAU;AAAA,IACZ,CAAC;AAED,UAAM,GAAG,SAAS,CAAC,QAAQ;AACzB,UAAI,CAAC,cAAe,QAAO,GAAG;AAAA,IAChC,CAAC;AAED,UAAM,GAAG,SAAS,CAAC,SAAS;AAC1B,UAAI,cAAe;AAEnB,UAAI,OAAO,SAAS,GAAG;AACrB,mBAAW,MAAM;AAAA,MACnB;AACA,UAAI,cAAe;AAEnB,UAAI,SAAS,GAAG;AACd;AAAA,UACE,IAAI;AAAA,YACF,uBAAuB,IAAI,KAAK,SAAS,aAAa,SAAS,EAAE;AAAA,UACnE;AAAA,QACF;AAAA,MACF,OAAO;AACL,gBAAQ;AAAA,MACV;AAAA,IACF,CAAC;AAAA,EACH,CAAC;AACH;AAoBA,eAAsB,mCACpB,UACA,SACiB;AACjB,MAAI,QAAQ;AACZ,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA,CAAC,SAAS;AACR,cAAQ,MAAM,KAAK;AACnB;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;AA2BA,eAAsB,+BACpB,WACA,YACA,SACA,QACe;AACf,QAAM,UAAU;AAAA;AAAA;AAAA;AAAA,gCAIc,OAAO,IAAI,MAAM;AAAA;AAAA;AAAA;AAI/C,QAAM,UAAU,aAAa;AAC7B,QAAM,gBAAgB,SAAS,WAAW,OAAO;AACjD,YAAM,wBAAO,SAAS,UAAU;AAClC;;;AD7QO,IAAM,iBAAN,MAAM,gBAAe;AAAA;AAAA;AAAA;AAAA,EAI1B,aAAqB,WACnB,SACA,WACe;AACf,WAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,MAAM,WAAK,SAAS,EAAE,aAAa,KAAK,GAAG,CAAC,KAAK,YAAY;AAC3D,YAAI,OAAO,CAAC,SAAS;AACnB,iBAAO,OAAO,IAAI,MAAM,yBAAyB,CAAC;AAClD;AAAA,QACF;AAEA,gBAAQ,UAAU;AAElB,gBAAQ,GAAG,SAAS,CAAC,UAAU;AAC7B,gBAAM,gBAAY,wBAAK,WAAW,MAAM,QAAQ;AAEhD,cAAI,MAAM,KAAK,MAAM,QAAQ,GAAG;AAE9B,2CAAU,WAAW,EAAE,WAAW,KAAK,CAAC;AACxC,oBAAQ,UAAU;AAAA,UACpB,OAAO;AAEL,oBAAQ,eAAe,OAAO,CAACC,MAAK,eAAe;AACjD,kBAAIA,QAAO,CAAC,YAAY;AACtB,uBAAOA,QAAO,IAAI,MAAM,4BAA4B,CAAC;AACrD;AAAA,cACF;AAEA,iDAAU,wBAAK,WAAW,IAAI,GAAG,EAAE,WAAW,KAAK,CAAC;AACpD,oBAAM,kBAAc,mCAAkB,SAAS;AAE/C,yBAAW,KAAK,WAAW;AAC3B,0BAAY,GAAG,UAAU,MAAM;AAC7B,wBAAQ,UAAU;AAAA,cACpB,CAAC;AACD,0BAAY,GAAG,SAAS,MAAM;AAAA,YAChC,CAAC;AAAA,UACH;AAAA,QACF,CAAC;AAED,gBAAQ,GAAG,OAAO,MAAM;AACtB,kBAAQ;AAAA,QACV,CAAC;AAED,gBAAQ,GAAG,SAAS,MAAM;AAAA,MAC5B,CAAC;AAAA,IACH,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA,EAKA,OAAe,yBACb,YACA,WACA,OACA,QACA,SACQ;AACR,UAAM,SAAS;AACf,UAAM,gBAAgB,WAAW,WAAW,MAAM,IAC9C,WAAW,MAAM,OAAO,MAAM,IAC9B;AAEJ,UAAM,WAAW,GAAG,MAAM,IAAI,KAAK;AACnC,UAAM,eAAW,wBAAK,WAAW,QAAQ;AAGzC,UAAM,SAAS,OAAO,KAAK,eAAe,QAAQ;AAClD,uCAAc,UAAU,MAAM;AAE9B,WAAO,GAAG,OAAO,IAAI,QAAQ;AAAA,EAC/B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,aAAa,4BACX,eACA,gBACA,WACiB;AACjB,QAAI,aAAa;AACjB,QAAI,UAAU;AACd,UAAM,SAAS;AAEf,UAAM,YAAY,IAAI,6BAAU;AAAA,MAC9B,eAAe;AAAA,MACf,UAAU;AAAA,MACV,UAAU,OAAe,WAAW,UAAU;AAC5C,mBAAW;AACX,YAAI,SAAS;AAEb,eAAO,MAAM;AACX,gBAAM,YAAY,QAAQ,QAAQ,MAAM;AAExC,cAAI,cAAc,IAAI;AAEpB,kBAAM,UAAU,KAAK,IAAI,GAAG,QAAQ,SAAS,OAAO,MAAM;AAC1D,sBAAU,QAAQ,MAAM,GAAG,OAAO;AAClC,sBAAU,QAAQ,MAAM,OAAO;AAC/B;AAAA,UACF;AAGA,oBAAU,QAAQ,MAAM,GAAG,SAAS;AAGpC,gBAAM,YAAY,YAAY,OAAO;AACrC,gBAAM,WAAW,QAAQ,QAAQ,KAAK,SAAS;AAE/C,cAAI,aAAa,IAAI;AAEnB,sBAAU,QAAQ,MAAM,SAAS;AACjC;AAAA,UACF;AAGA,gBAAM,gBAAgB,QAAQ,MAAM,WAAW,QAAQ;AACvD,gBAAM,WAAW,SAAS,UAAU;AACpC,gBAAM,eAAW,wBAAK,WAAW,QAAQ;AACzC,gBAAM,MAAM,OAAO,KAAK,eAAe,QAAQ;AAC/C,6CAAc,UAAU,GAAG;AAE3B,gBAAM,eAAe,UAAU,QAAQ;AACvC,oBAAU,QAAQ,YAAY;AAC9B;AAEA,oBAAU,QAAQ,MAAM,WAAW,CAAC;AAAA,QACtC;AAEA,YAAI,OAAO,SAAS,GAAG;AACrB,eAAK,KAAK,MAAM;AAAA,QAClB;AACA,iBAAS;AAAA,MACX;AAAA,MACA,MAAM,UAAU;AACd,YAAI,QAAQ,SAAS,GAAG;AACtB,eAAK,KAAK,OAAO;AAAA,QACnB;AACA,iBAAS;AAAA,MACX;AAAA,IACF,CAAC;AAED,UAAM,SAAK,kCAAiB,eAAe,EAAE,UAAU,QAAQ,CAAC;AAChE,UAAM,SAAK,mCAAkB,gBAAgB,EAAE,UAAU,QAAQ,CAAC;AAElE,cAAM,2BAAS,IAAI,WAAW,EAAE;AAEhC,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYA,aAAqB,iCACnB,QACA,WACA,UACA,gBACA,gBACe;AAEf,QAAI;AACF,cAAI,4BAAW,SAAS,GAAG;AACzB,oCAAO,WAAW,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AAAA,MACpD;AAAA,IACF,SAAS,GAAG;AACV,aAAO,KAAK,oDAAoD,CAAC;AAAA,IACnE;AACA,mCAAU,WAAW,EAAE,WAAW,KAAK,CAAC;AAGxC,UAAM,WAAW,SAAS,YAAQ,2BAAQ,QAAQ,GAAG,EAAE;AAGvD,UAAM,eAAW,wBAAK,WAAW,GAAG,QAAQ,OAAO;AACnD,QAAI;AAEF,YAAM,gBAAY,wBAAK,WAAW,QAAQ;AAC1C,UAAI,KAAC,4BAAW,SAAS,GAAG;AAC1B,uCAAU,WAAW,EAAE,WAAW,KAAK,CAAC;AAAA,MAC1C;AAGA,YAAM,aAAa,MAAM;AAAA,QACvB;AAAA,QACA,CAAC,YAAY,UAAU;AACrB,0BAAe;AAAA,YACb;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAEA,aAAO;AAAA,QACL,4BAA4B,UAAU,gCAAgC,SAAS;AAAA,MACjF;AAGA,YAAM;AAAA,QACJ;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAEA,aAAO;AAAA,QACL,2BAA2B,UAAU;AAAA,MACvC;AAAA,IACF,SAAS,GAAG;AACV,aAAO;AAAA,QACL;AAAA,QACA;AAAA,MACF;AACA,YAAM;AAAA,IACR;AACA,WAAO,KAAK,8BAA8B,QAAQ;AAGlD,UAAM,eAAW,wBAAK,WAAW,GAAG,QAAQ,OAAO;AACnD,QAAI;AAEF,YAAM,gBAAY,wBAAK,WAAW,QAAQ;AAC1C,UAAI,KAAC,4BAAW,SAAS,GAAG;AAC1B,uCAAU,WAAW,EAAE,WAAW,KAAK,CAAC;AAAA,MAC1C;AAEA,YAAM,iBAAiB,MAAM,gBAAe;AAAA,QAC1C;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAEA,aAAO;AAAA,QACL,4BAA4B,cAAc,wBAAwB,SAAS;AAAA,MAC7E;AAAA,IACF,SAAS,GAAG;AACV,aAAO;AAAA,QACL;AAAA,QACA;AAAA,MACF;AAEA,YAAM,SAAK,kCAAiB,cAAc;AAC1C,YAAM,SAAK,mCAAkB,QAAQ;AACrC,gBAAM,2BAAS,IAAI,EAAE;AAAA,IACvB;AACA,WAAO,KAAK,8BAA8B,QAAQ;AAAA,EACpD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAeA,aAAa,+BACX,QACA,SACA,YACA,WACe;AAEf,WAAO,KAAK,uCAAuC;AACnD,UAAM,gBAAe,WAAW,SAAS,UAAU;AAGnD,UAAM,YAAQ,6BAAY,UAAU;AACpC,UAAM,WAAW,MAAM,KAAK,CAAC,UAAM,2BAAQ,CAAC,EAAE,YAAY,MAAM,OAAO;AACvE,UAAM,WAAW,MAAM,KAAK,CAAC,UAAM,2BAAQ,CAAC,EAAE,YAAY,MAAM,OAAO;AAEvE,QAAI,CAAC,YAAY,CAAC,UAAU;AAC1B,YAAM,IAAI;AAAA,QACR,sEAAsE,MAAM,KAAK,IAAI,CAAC;AAAA,MACxF;AAAA,IACF;AAGA,UAAM,eAAW,wBAAK,YAAY,QAAQ;AAC1C,UAAM,eAAW,wBAAK,YAAY,QAAQ;AAI1C,WAAO,KAAK,oDAAoD;AAChE,UAAM,gBAAe;AAAA,MACnB;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAEA,WAAO,KAAK,kCAAkC,SAAS;AAAA,EACzD;AACF;;;AEzVA,IAAAC,kBAAmD;AACnD,IAAAC,oBAAqB;AAKrB,IAAM,4BAA4B;AAyB3B,IAAM,eAAN,MAAmB;AAAA,EAGxB,YAA6B,QAAuB;AAAvB;AAAA,EAAwB;AAAA,EAF7C,oBAAoB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAe5B,MAAM,YACJ,SACA,WACA,SAC2B;AAC3B,UAAM,MAAM,SAAS,OAAO,eAAe;AAC3C,UAAM,eAAW,wBAAK,WAAW,OAAO;AAExC,QAAI,KAAC,4BAAW,QAAQ,GAAG;AACzB,qCAAU,UAAU,EAAE,WAAW,KAAK,CAAC;AAAA,IACzC;AAEA,UAAM,aAAa,MAAM,KAAK,aAAa,OAAO;AAElD,QAAI,aAAa,GAAG;AAClB,WAAK,OAAO;AAAA,QACV,4BAA4B,UAAU,aAAa,GAAG;AAAA,MACxD;AACA,WAAK,oBAAoB;AAEzB,eAAS,IAAI,GAAG,IAAI,YAAY,KAAK;AACnC,cAAM,SAAS,MAAM;AAAA,UACnB;AAAA,UACA;AAAA,YACE;AAAA,YACA,IAAI,SAAS;AAAA,YACb,GAAG,OAAO,IAAI,CAAC;AAAA,YACf;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,gBACA,wBAAK,UAAU,QAAQ,CAAC,MAAM;AAAA,UAChC;AAAA,UACA,EAAE,eAAe,MAAM;AAAA,QACzB;AAEA,YAAI,OAAO,SAAS,GAAG;AACrB,gBAAM,IAAI;AAAA,YACR,wCAAwC,IAAI,CAAC,IAAI,UAAU,KAAK,OAAO,UAAU,eAAe;AAAA,UAClG;AAAA,QACF;AAEA,aAAK,YAAY,IAAI,GAAG,UAAU;AAAA,MACpC;AAAA,IACF,OAAO;AAEL,WAAK,OAAO,KAAK,mCAAmC,GAAG,SAAS;AAEhE,YAAM,SAAS,MAAM;AAAA,QACnB;AAAA,QACA;AAAA,UACE;AAAA,UACA,IAAI,SAAS;AAAA,UACb;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,cACA,wBAAK,UAAU,aAAa;AAAA,QAC9B;AAAA,QACA,EAAE,eAAe,MAAM;AAAA,MACzB;AAEA,UAAI,OAAO,SAAS,GAAG;AACrB,cAAM,IAAI;AAAA,UACR,8CAA8C,OAAO,UAAU,eAAe;AAAA,QAChF;AAAA,MACF;AAAA,IACF;AAEA,UAAM,gBAAY,6BAAY,QAAQ,EACnC,OAAO,CAAC,MAAM,EAAE,WAAW,OAAO,KAAK,EAAE,SAAS,MAAM,CAAC,EACzD,KAAK,CAAC,GAAG,MAAM;AACd,YAAM,OAAO,SAAS,EAAE,QAAQ,SAAS,EAAE,EAAE,QAAQ,QAAQ,EAAE,GAAG,EAAE;AACpE,YAAM,OAAO,SAAS,EAAE,QAAQ,SAAS,EAAE,EAAE,QAAQ,QAAQ,EAAE,GAAG,EAAE;AACpE,aAAO,OAAO;AAAA,IAChB,CAAC,EACA,IAAI,CAAC,UAAM,wBAAK,UAAU,CAAC,CAAC;AAE/B,SAAK,OAAO;AAAA,MACV,2BAA2B,UAAU,MAAM,aAAa,QAAQ;AAAA,IAClE;AAEA,WAAO;AAAA,MACL,WAAW,UAAU;AAAA,MACrB;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKQ,YAAY,SAAiB,OAAqB;AACxD,UAAM,UAAU,KAAK,MAAO,UAAU,QAAS,GAAG;AAClD,QACE,WAAW,KAAK,oBAAoB,6BACpC,YAAY,OACZ;AACA,WAAK,oBAAoB;AACzB,WAAK,OAAO;AAAA,QACV,mCAAmC,OAAO,IAAI,KAAK,KAAK,OAAO;AAAA,MACjE;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAc,aAAa,SAAkC;AAC3D,QAAI;AACF,YAAM,SAAS,MAAM,WAAW,WAAW,CAAC,OAAO,CAAC;AACpD,UAAI,OAAO,SAAS,EAAG,QAAO;AAC9B,YAAM,QAAQ,OAAO,OAAO,MAAM,kBAAkB;AACpD,aAAO,QAAQ,SAAS,MAAM,CAAC,GAAG,EAAE,IAAI;AAAA,IAC1C,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AACF;;;AChKO,IAAM,mBAAN,MAAuB;AAAA,EAC5B,YAA6B,QAAuB;AAAvB;AAAA,EAAwB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASrD,MAAM,YACJ,SACA,YAC8B;AAC9B,SAAK,OAAO;AAAA,MACV,2CAA2C,UAAU;AAAA,IACvD;AAEA,UAAM,YAAY,oBAAI,IAAoB;AAE1C,aAAS,OAAO,GAAG,QAAQ,YAAY,QAAQ;AAC7C,YAAM,OAAO,MAAM,KAAK,gBAAgB,SAAS,IAAI;AACrD,gBAAU,IAAI,MAAM,IAAI;AAAA,IAC1B;AAEA,UAAM,gBAAgB,CAAC,GAAG,UAAU,OAAO,CAAC,EAAE;AAAA,MAC5C,CAAC,MAAM,EAAE,KAAK,EAAE,SAAS;AAAA,IAC3B,EAAE;AACF,SAAK,OAAO;AAAA,MACV,0CAA0C,aAAa,IAAI,UAAU;AAAA,IACvE;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,aAAa,SAAkC;AACnD,UAAM,SAAS,MAAM,WAAW,WAAW,CAAC,OAAO,CAAC;AACpD,QAAI,OAAO,SAAS,GAAG;AACrB,WAAK,OAAO;AAAA,QACV,sCAAsC,OAAO,UAAU,eAAe;AAAA,MACxE;AACA,aAAO;AAAA,IACT;AACA,UAAM,QAAQ,OAAO,OAAO,MAAM,kBAAkB;AACpD,WAAO,QAAQ,SAAS,MAAM,CAAC,GAAG,EAAE,IAAI;AAAA,EAC1C;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,gBAAgB,SAAkC;AACtD,UAAM,SAAS,MAAM,WAAW,aAAa,CAAC,WAAW,SAAS,GAAG,CAAC;AAEtE,QAAI,OAAO,SAAS,GAAG;AACrB,WAAK,OAAO;AAAA,QACV,+CAA+C,OAAO,UAAU,eAAe;AAAA,MACjF;AACA,aAAO;AAAA,IACT;AAEA,WAAO,OAAO;AAAA,EAChB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,gBAAgB,SAAiB,MAA+B;AACpE,UAAM,SAAS,MAAM,WAAW,aAAa;AAAA,MAC3C;AAAA,MACA,KAAK,SAAS;AAAA,MACd;AAAA,MACA,KAAK,SAAS;AAAA,MACd;AAAA,MACA;AAAA,MACA;AAAA,IACF,CAAC;AAED,QAAI,OAAO,SAAS,GAAG;AACrB,WAAK,OAAO;AAAA,QACV,gDAAgD,IAAI,KAAK,OAAO,UAAU,eAAe;AAAA,MAC3F;AACA,aAAO;AAAA,IACT;AAEA,WAAO,OAAO;AAAA,EAChB;AACF;;;AC/FA,IAAAC,kBAA4C;AAC5C,IAAAC,oBAAqB;;;ACZrB,gBAAkB;AAkBX,IAAM,0BAA0B,YAAE,OAAO;AAAA;AAAA,EAE9C,IAAI,YAAE;AAAA,IACJ,YAAE,OAAO;AAAA;AAAA,MAEP,GAAG,YAAE,OAAO,EAAE,IAAI,EAAE,YAAY;AAAA;AAAA,MAEhC,GAAG,YAAE;AAAA,QACH,YAAE,OAAO;AAAA;AAAA,UAEP,GAAG,YAAE,OAAO;AAAA;AAAA,UAEZ,GAAG,YAAE,OAAO;AAAA,QACd,CAAC;AAAA,MACH;AAAA,IACF,CAAC;AAAA,EACH;AAAA;AAAA,EAEA,IAAI,YAAE;AAAA,IACJ,YAAE,OAAO;AAAA;AAAA,MAEP,IAAI,YAAE,OAAO,EAAE,IAAI,EAAE,YAAY;AAAA;AAAA,MAEjC,GAAG,YAAE,OAAO,EAAE,IAAI,EAAE,YAAY;AAAA;AAAA,MAEhC,GAAG,YAAE,OAAO,EAAE,IAAI,EAAE,YAAY;AAAA;AAAA,MAEhC,GAAG,YAAE,OAAO;AAAA,IACd,CAAC;AAAA,EACH;AACF,CAAC;;;AD5BD,IAAM,yBAAiD;AAAA,EACrD,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AAGA,IAAM,4BAA4B;AAGlC,IAAM,sBAAsB;AAG5B,IAAM,sBAAsB;AAG5B,IAAM,sBAAsB;AAG5B,IAAM,qBAA6C;AAAA,EACjD,gBAAgB;AAAA,EAChB,MAAM;AAAA,EACN,SAAS;AAAA,EACT,UAAU;AAAA,EACV,WAAW;AAAA,EACX,aAAa;AAAA,EACb,aAAa;AACf;AAGA,IAAM,cAAc,IAAI,IAAI,OAAO,KAAK,kBAAkB,CAAC;AAM3D,IAAM,gCAAgC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA8F/B,IAAM,mBAAN,MAAuB;AAAA,EAC5B,YAA6B,QAAuB;AAAvB;AAAA,EAAwB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWrD,MAAM,eACJ,WACA,OACA,SACkC;AAClC,SAAK,OAAO,KAAK,gDAAgD;AAEjE,UAAM,iBAAa,wBAAK,WAAW,aAAa;AAChD,UAAM,MAAuB,KAAK,UAAM,8BAAa,YAAY,OAAO,CAAC;AAEzE,QAAI,cAAc,KAAK,eAAe,GAAG;AACzC,QAAI,YAAY,WAAW,GAAG;AAC5B,WAAK,OAAO,KAAK,wCAAwC;AACzD,aAAO;AAAA,QACL,iBAAiB;AAAA,QACjB,iBAAiB;AAAA,QACjB,gBAAgB;AAAA,QAChB,aAAa;AAAA,MACf;AAAA,IACF;AAEA,QACE,SAAS,uBACT,QAAQ,oBAAoB,SAAS,GACrC;AACA,YAAM,iBAAiB,YAAY;AACnC,YAAM,WAAW,IAAI,IAAI,QAAQ,mBAAmB;AACpD,oBAAc,YAAY,OAAO,CAAC,MAAM,SAAS,IAAI,CAAC,CAAC;AACvD,WAAK,OAAO;AAAA,QACV,mCAAmC,YAAY,MAAM,kCAAkC,cAAc;AAAA,MACvG;AAAA,IACF;AAEA,UAAM,cAAc,SAAS,eAAe;AAC5C,SAAK,OAAO;AAAA,MACV,iCAAiC,YAAY,MAAM,wBAAwB,WAAW;AAAA,IACxF;AAEA,UAAM,UAAU,MAAM,eAAe;AAAA,MACnC;AAAA,MACA;AAAA,MACA,CAAC,WAAW,KAAK,YAAY,WAAW,KAAK,QAAQ,OAAO,OAAO;AAAA,MACnE,MAAM;AACJ,YAAI,SAAS,gBAAgB,SAAS,YAAY;AAChD,kBAAQ;AAAA,YACN,QAAQ,WAAW,UAAU;AAAA,UAC/B;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAGA,QAAI,uBAAuB;AAC3B,QAAI,uBAAuB;AAC3B,QAAI,cAAc;AAElB,eAAW,UAAU,SAAS;AAC5B,UAAI,WAAW,MAAM;AACnB;AAAA,MACF,OAAO;AACL,gCAAwB,OAAO,GAAG;AAClC,gCAAwB,OAAO,GAAG;AAAA,MACpC;AAAA,IACF;AAGA,aAAS,IAAI,GAAG,IAAI,YAAY,QAAQ,KAAK;AAC3C,YAAM,cAAc,QAAQ,CAAC;AAC7B,UAAI,gBAAgB,KAAM;AAC1B,WAAK,iBAAiB,KAAK,YAAY,CAAC,GAAG,WAAW;AAAA,IACxD;AAGA,uCAAc,YAAY,KAAK,UAAU,KAAK,MAAM,CAAC,CAAC;AAEtD,SAAK,OAAO;AAAA,MACV,2CAA2C,oBAAoB,UAAU,oBAAoB,4BAA4B,YAAY,MAAM,WAAW,WAAW;AAAA,IACnK;AAEA,WAAO;AAAA,MACL,iBAAiB;AAAA,MACjB,iBAAiB;AAAA,MACjB,gBAAgB,YAAY;AAAA,MAC5B;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKQ,eAAe,KAAgC;AACrD,WAAO,OAAO,OAAO,IAAI,KAAK,EAC3B,IAAI,CAAC,MAAM,EAAE,OAAO,EACpB,KAAK,CAAC,GAAG,MAAM,IAAI,CAAC;AAAA,EACzB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAc,YACZ,WACA,KACA,QACA,OACA,SACyC;AACzC,QAAI;AACF,YAAM,YAAY,KAAK,aAAa,KAAK,MAAM;AAC/C,YAAM,aAAa,KAAK,cAAc,KAAK,MAAM;AAGjD,UAAI,UAAU,WAAW,KAAK,WAAW,WAAW,GAAG;AACrD,aAAK,OAAO;AAAA,UACV,2BAA2B,MAAM;AAAA,QACnC;AACA,eAAO,EAAE,IAAI,CAAC,GAAG,IAAI,CAAC,EAAE;AAAA,MAC1B;AAEA,YAAM,cAAc,KAAK,cAAc,WAAW,MAAM;AAExD,YAAM,WAAW,SAAS,WAAW,IAAI,MAAM;AAC/C,UAAI;AACJ,UAAI;AAEJ,UAAI,UAAU;AACZ,cAAM,EAAE,YAAY,MAAM,aAAa,IACrC,KAAK,+BAA+B,WAAW,QAAQ;AACzD,qBAAa;AAEb,YAAI,WAAW,SAAS,KAAK,aAAa,SAAS,GAAG;AACpD,yBAAe,aAAa,KAAK,IAAI;AAAA,QACvC;AAAA,MACF;AAEA,YAAM,aAAa,KAAK;AAAA,QACtB;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAEA,YAAM,eAAe,KAAK;AAAA,QACxB,SAAS;AAAA,MACX;AACA,YAAM,aAAa,eAAe,SAAS;AAE3C,YAAM,SAAS,MAAM,UAAU,WAAW;AAAA,QACxC,QAAQ;AAAA,QACR,UAAU;AAAA,UACR;AAAA,YACE,MAAM;AAAA,YACN,SAAS;AAAA,cACP;AAAA,gBACE,MAAM;AAAA,gBACN,MAAM;AAAA,cACR;AAAA,cACA;AAAA,gBACE,MAAM;AAAA,gBACN,OAAO,yBAAyB,WAAW;AAAA,cAC7C;AAAA,YACF;AAAA,UACF;AAAA,QACF;AAAA,QACA,cAAc;AAAA,QACd,YAAY,SAAS,cAAc;AAAA,QACnC,aAAa,SAAS,eAAe;AAAA,QACrC,aAAa,SAAS;AAAA,QACtB,WAAW;AAAA,QACX,OAAO;AAAA,MACT,CAAC;AAED,UAAI,SAAS,YAAY;AACvB,gBAAQ,WAAW,MAAM,OAAO,KAAK;AAAA,MACvC;AAEA,YAAM,SAAS,OAAO;AAEtB,UAAI,OAAO,GAAG,SAAS,KAAK,OAAO,GAAG,SAAS,GAAG;AAChD,aAAK,OAAO;AAAA,UACV,2BAA2B,MAAM,KAAK,OAAO,GAAG,MAAM,UAAU,OAAO,GAAG,MAAM;AAAA,QAClF;AAAA,MACF;AAEA,aAAO;AAAA,IACT,SAAS,OAAO;AAEd,UAAI,SAAS,aAAa,SAAS;AACjC,cAAM;AAAA,MACR;AAEA,WAAK,OAAO;AAAA,QACV,2BAA2B,MAAM;AAAA,QACjC;AAAA,MACF;AACA,aAAO;AAAA,IACT;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKQ,aACN,KACA,QACiD;AACjD,UAAM,UAA2D,CAAC;AAElE,aAAS,IAAI,GAAG,IAAI,IAAI,MAAM,QAAQ,KAAK;AACzC,YAAM,OAAO,IAAI,MAAM,CAAC;AACxB,UAAI,CAAC,YAAY,IAAI,KAAK,KAAK,EAAG;AAClC,UAAI,KAAK,KAAK,KAAK,CAAC,MAAM,EAAE,YAAY,MAAM,GAAG;AAC/C,gBAAQ,KAAK,EAAE,OAAO,GAAG,KAAK,CAAC;AAAA,MACjC;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKQ,cACN,KACA,QACkD;AAClD,UAAM,UAA4D,CAAC;AAEnE,aAAS,IAAI,GAAG,IAAI,IAAI,OAAO,QAAQ,KAAK;AAC1C,YAAM,OAAO,IAAI,OAAO,CAAC;AACzB,UAAI,KAAK,KAAK,KAAK,CAAC,MAAM,EAAE,YAAY,MAAM,GAAG;AAC/C,gBAAQ,KAAK,EAAE,OAAO,GAAG,KAAK,CAAC;AAAA,MACjC;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAaA,gBACE,WACA,YACA,YACA,cACQ;AACR,UAAM,QAAkB,CAAC;AAEzB,QAAI,UAAU,SAAS,GAAG;AACxB,YAAM,YAAsB,CAAC;AAC7B,gBAAU,QAAQ,CAAC,OAAO,gBAAgB;AACxC,cAAM,WAAW,mBAAmB,MAAM,KAAK,KAAK,KAAK;AACzD,kBAAU,KAAK,GAAG,WAAW,IAAI,QAAQ,IAAI,MAAM,KAAK,IAAI,EAAE;AAC9D,cAAM,MAAM,YAAY,IAAI,WAAW;AACvC,YAAI,KAAK;AACP,oBAAU,KAAK,GAAG,WAAW,QAAQ,GAAG,EAAE;AAAA,QAC5C;AAAA,MACF,CAAC;AACD,YAAM,KAAK,SAAS,UAAU,KAAK,IAAI,CAAC;AAAA,IAC1C;AAEA,QAAI,WAAW,SAAS,GAAG;AACzB,YAAM,YAAsB,CAAC;AAC7B,eACM,mBAAmB,GACvB,mBAAmB,WAAW,QAC9B,oBACA;AACA,cAAM,QAAQ,WAAW,gBAAgB,EAAE;AAC3C,mBAAW,QAAQ,MAAM,KAAK,aAAa;AACzC,cAAI,CAAC,KAAK,QAAQ,KAAK,KAAK,KAAK,EAAE,WAAW,EAAG;AACjD,oBAAU;AAAA,YACR,GAAG,gBAAgB,IAAI,KAAK,oBAAoB,IAAI,KAAK,oBAAoB,IAAI,KAAK,IAAI;AAAA,UAC5F;AAAA,QACF;AAAA,MACF;AACA,UAAI,UAAU,SAAS,GAAG;AACxB,cAAM,cAAc,SAAS,UAAU,KAAK,IAAI;AAChD,YAAI,cAAc;AAChB,gBAAM,KAAK,cAAc,eAAe,YAAY;AAAA,QACtD,OAAO;AACL,gBAAM,KAAK,WAAW;AAAA,QACxB;AAAA,MACF;AAAA,IACF;AAEA,WAAO,MAAM,KAAK,IAAI;AAAA,EACxB;AAAA;AAAA;AAAA;AAAA,EAKQ,+BAA+B,mBAAsC;AAC3E,QAAI,CAAC,mBAAmB,QAAQ;AAC9B,aAAO;AAAA,IACT;AACA,UAAM,cAAc,kBAAkB,CAAC,EAAE,MAAM,GAAG,EAAE,CAAC;AACrD,UAAM,cACJ,uBAAuB,WAAW,KAAK,kBAAkB,CAAC;AAC5D,UAAM,aAAa,kBAChB,MAAM,CAAC,EACP,IAAI,CAAC,SAAS,uBAAuB,KAAK,MAAM,GAAG,EAAE,CAAC,CAAC,KAAK,IAAI;AACnE,UAAM,eACJ,WAAW,SAAS,IAChB,wBAAwB,WAAW,UAAU,WAAW,KAAK,IAAI,CAAC,kBAClE,cAAc,WAAW;AAC/B,UAAM,SACJ,sCAAsC,YAAY;AAAA;AAAA;AAEpD,WAAO,SAAS;AAAA,EAClB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,qBACE,WACA,UACqB;AACrB,WAAO,KAAK,+BAA+B,WAAW,QAAQ,EAAE;AAAA,EAClE;AAAA;AAAA;AAAA;AAAA;AAAA,EAMQ,+BACN,WACA,UAC6D;AAC7D,UAAM,aAAa,oBAAI,IAAoB;AAE3C,UAAM,YAAY,KAAK,gBAAgB,QAAQ;AAE/C,QAAI,UAAU,WAAW,GAAG;AAC1B,aAAO,EAAE,YAAY,cAAc,CAAC,EAAE;AAAA,IACxC;AAEA,UAAM,YAAY,IAAI,IAAI,UAAU,IAAI,CAAC,GAAG,MAAM,CAAC,CAAC;AAEpD,aAAS,cAAc,GAAG,cAAc,UAAU,QAAQ,eAAe;AACvE,YAAM,UAAU,UAAU,WAAW,EAAE,KAAK;AAE5C,UAAI,YAAY;AAChB,UAAI,iBAAiB;AAErB,iBAAW,cAAc,WAAW;AAClC,cAAM,QAAQ,KAAK,mBAAmB,SAAS,UAAU,UAAU,CAAC;AACpE,YAAI,QAAQ,WAAW;AACrB,sBAAY;AACZ,2BAAiB;AAAA,QACnB;AAAA,MACF;AAEA,UAAI,kBAAkB,KAAK,aAAa,2BAA2B;AACjE,YAAI,UAAU,cAAc,MAAM,SAAS;AACzC,qBAAW,IAAI,aAAa,UAAU,cAAc,CAAC;AAAA,QACvD;AACA,kBAAU,OAAO,cAAc;AAAA,MACjC;AAAA,IACF;AAEA,UAAM,eAAe,CAAC,GAAG,SAAS,EAC/B,KAAK,CAAC,GAAG,MAAM,IAAI,CAAC,EACpB,IAAI,CAAC,MAAM,UAAU,CAAC,CAAC;AAE1B,WAAO,EAAE,YAAY,aAAa;AAAA,EACpC;AAAA;AAAA;AAAA;AAAA;AAAA,EAMQ,gBAAgB,UAA4B;AAClD,UAAM,SAAmB,CAAC;AAC1B,QAAI,eAAyB,CAAC;AAE9B,eAAW,WAAW,SAAS,MAAM,IAAI,GAAG;AAC1C,YAAM,UAAU,QAAQ,KAAK;AAC7B,UAAI,QAAQ,WAAW,GAAG;AACxB,YAAI,aAAa,SAAS,GAAG;AAC3B,iBAAO,KAAK,aAAa,KAAK,GAAG,CAAC;AAClC,yBAAe,CAAC;AAAA,QAClB;AAAA,MACF,OAAO;AACL,qBAAa,KAAK,OAAO;AAAA,MAC3B;AAAA,IACF;AACA,QAAI,aAAa,SAAS,GAAG;AAC3B,aAAO,KAAK,aAAa,KAAK,GAAG,CAAC;AAAA,IACpC;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA,EAMQ,mBAAmB,GAAW,GAAmB;AACvD,QAAI,EAAE,WAAW,KAAK,EAAE,WAAW,EAAG,QAAO;AAE7C,UAAM,QAAQ,oBAAI,IAAoB;AACtC,eAAW,MAAM,GAAG;AAClB,YAAM,IAAI,KAAK,MAAM,IAAI,EAAE,KAAK,KAAK,CAAC;AAAA,IACxC;AAEA,UAAM,QAAQ,oBAAI,IAAoB;AACtC,eAAW,MAAM,GAAG;AAClB,YAAM,IAAI,KAAK,MAAM,IAAI,EAAE,KAAK,KAAK,CAAC;AAAA,IACxC;AAEA,QAAI,UAAU;AACd,eAAW,CAAC,IAAI,MAAM,KAAK,OAAO;AAChC,YAAM,SAAS,MAAM,IAAI,EAAE,KAAK;AAChC,iBAAW,KAAK,IAAI,QAAQ,MAAM;AAAA,IACpC;AAEA,WAAO,UAAU,KAAK,IAAI,EAAE,QAAQ,EAAE,MAAM;AAAA,EAC9C;AAAA;AAAA;AAAA;AAAA;AAAA,EAMQ,cAAc,WAAmB,QAAwB;AAC/D,UAAM,gBAAY,wBAAK,WAAW,SAAS,QAAQ,SAAS,CAAC,MAAM;AACnE,eAAO,8BAAa,SAAS,EAAE,SAAS,QAAQ;AAAA,EAClD;AAAA;AAAA;AAAA;AAAA,EAKQ,iBACN,KACA,QACA,aACM;AAEN,QAAI,YAAY,GAAG,SAAS,GAAG;AAC7B,YAAM,YAAY,KAAK,aAAa,KAAK,MAAM;AAC/C,iBAAW,cAAc,YAAY,IAAI;AACvC,YAAI,WAAW,KAAK,KAAK,WAAW,IAAI,UAAU,QAAQ;AACxD,gBAAM,WAAW,UAAU,WAAW,CAAC,EAAE;AACzC,cAAI,OAAO,IAAI,MAAM,QAAQ,EAAE;AAC/B,qBAAW,OAAO,WAAW,GAAG;AAC9B,kBAAM,MAAM,KAAK,QAAQ,IAAI,CAAC;AAC9B,gBAAI,OAAO,GAAG;AACZ,qBACE,KAAK,UAAU,GAAG,GAAG,IACrB,IAAI,IACJ,KAAK,UAAU,MAAM,IAAI,EAAE,MAAM;AAAA,YACrC,OAAO;AACL,mBAAK,OAAO;AAAA,gBACV,2BAA2B,MAAM,UAAU,WAAW,CAAC;AAAA,cAEzD;AAAA,YACF;AAAA,UACF;AACA,cAAI,SAAS,IAAI,MAAM,QAAQ,EAAE,MAAM;AACrC,gBAAI,MAAM,QAAQ,EAAE,OAAO;AAC3B,gBAAI,MAAM,QAAQ,EAAE,OAAO;AAAA,UAC7B;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAGA,QAAI,YAAY,GAAG,SAAS,GAAG;AAC7B,YAAM,aAAa,KAAK,cAAc,KAAK,MAAM;AACjD,iBAAW,cAAc,YAAY,IAAI;AACvC,YAAI,WAAW,MAAM,KAAK,WAAW,KAAK,WAAW,QAAQ;AAC3D,gBAAM,QAAQ,WAAW,WAAW,EAAE,EAAE;AAGxC,qBAAW,QAAQ,MAAM,KAAK,aAAa;AACzC,gBACE,KAAK,yBAAyB,WAAW,KACzC,KAAK,yBAAyB,WAAW,GACzC;AACA,mBAAK,OAAO,WAAW;AACvB;AAAA,YACF;AAAA,UACF;AAGA,gBAAM,UAAU,MAAM,KAAK,KAAK,WAAW,CAAC;AAC5C,cAAI,SAAS;AACX,kBAAM,WAAW,QAAQ,WAAW,CAAC;AACrC,gBAAI,UAAU;AACZ,uBAAS,OAAO,WAAW;AAAA,YAC7B;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;;;AE9pBA,mBAAiC;AAEjC,IAAAC,kBAA6B;AAC7B,IAAAC,aAAkB;AAMlB,IAAM,kBAAkB;AAGxB,IAAM,2BAA2B;AAGjC,IAAMC,uBAAsB;AAG5B,IAAM,YAAY;AAGlB,IAAM,eAAe;AAGrB,IAAM,uBAAuB,aAAE,OAAO;AAAA,EACpC,mBAAmB,aAChB,QAAQ,EACR;AAAA,IACC;AAAA,EACF;AAAA,EACF,mBAAmB,aAChB,MAAM,aAAE,OAAO,CAAC,EAChB;AAAA,IACC;AAAA,EACF;AACJ,CAAC;AAGD,IAAM,0BAA0B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAsCzB,IAAM,qBAAN,MAAyB;AAAA,EACb;AAAA,EACA;AAAA,EACA;AAAA,EAEjB,YACE,QACA,cACA,eACA;AACA,SAAK,SAAS;AACd,SAAK,eAAe;AACpB,SAAK,gBAAgB,iBAAiB,IAAI,iBAAiB,MAAM;AAAA,EACnE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA,MAAM,OACJ,SACA,WACA,OACA,SACsB;AACtB,UAAM,iBAAiB,SAAS,kBAAkB;AAElD,SAAK,OAAO,KAAK,wDAAwD;AAGzE,UAAM,iBAAiB,MAAM,KAAK,2BAA2B,OAAO;AACpE,QAAI,gBAAgB;AAClB,aAAO;AAAA,IACT;AAGA,UAAM,eAAe,MAAM,KAAK,aAAa;AAAA,MAC3C;AAAA,MACA;AAAA,MACA,EAAE,KAAK,eAAe,WAAW;AAAA,IACnC;AAEA,QAAI,aAAa,cAAc,GAAG;AAChC,WAAK,OAAO,KAAK,4CAA4C;AAC7D,aAAO;AAAA,QACL,QAAQ;AAAA,QACR,QAAQ;AAAA,QACR,cAAc;AAAA,QACd,YAAY;AAAA,MACd;AAAA,IACF;AAGA,UAAM,gBAAgB,KAAK;AAAA,MACzB,aAAa;AAAA,MACb;AAAA,IACF;AAEA,SAAK,OAAO;AAAA,MACV,iCAAiC,cAAc,MAAM,OAAO,aAAa,SAAS,YAAY,cAAc,IAAI,CAAC,MAAM,IAAI,CAAC,EAAE,KAAK,IAAI,CAAC;AAAA,IAC1I;AAGA,QAAI,eAAe;AACnB,UAAM,oBAAoB,oBAAI,IAA8B;AAC5D,eAAW,OAAO,eAAe;AAC/B;AACA,YAAM,WAAW,aAAa,UAAU,GAAG;AAC3C,YAAM,eAAe,MAAM,KAAK;AAAA,QAC9B;AAAA,QACA,MAAM;AAAA,QACN;AAAA,QACA;AAAA,MACF;AAEA,iBAAW,QAAQ,aAAa,mBAAmB;AACjD,0BAAkB,IAAI,OAAO,kBAAkB,IAAI,IAAI,KAAK,KAAK,CAAC;AAAA,MACpE;AAEA,UAAI,aAAa,mBAAmB;AAClC,aAAK,OAAO;AAAA,UACV,0DAA0D,MAAM,CAAC;AAAA,QACnE;AACA,cAAMC,qBAAoB,KAAK,mBAAmB,iBAAiB;AACnE,eAAO;AAAA,UACL,QAAQ;AAAA,UACR,mBAAAA;AAAA,UACA,QAAQ,qCAAqC,MAAM,CAAC;AAAA,UACpD,cAAc;AAAA,UACd,YAAY,aAAa;AAAA,QAC3B;AAAA,MACF;AAAA,IACF;AAGA,SAAK,OAAO;AAAA,MACV;AAAA,IACF;AACA,UAAM,oBAAoB,KAAK,mBAAmB,iBAAiB;AACnE,WAAO;AAAA,MACL,QAAQ;AAAA,MACR;AAAA,MACA,QAAQ,mCAAmC,YAAY;AAAA,MACvD,cAAc;AAAA,MACd,YAAY,aAAa;AAAA,IAC3B;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,MAAc,2BACZ,SAC6B;AAC7B,QAAI;AACF,YAAM,aAAa,MAAM,KAAK,cAAc,aAAa,OAAO;AAChE,UAAI,eAAe,EAAG,QAAO;AAE7B,YAAM,WAAW,MAAM,KAAK,cAAc,gBAAgB,OAAO;AACjE,UAAI,SAAS,KAAK,EAAE,WAAW,GAAG;AAChC,aAAK,OAAO;AAAA,UACV;AAAA,QACF;AACA,eAAO;AAAA,MACT;AAEA,YAAM,YAAY,aAAa,KAAK,QAAQ;AAC5C,YAAM,WAAW,UAAU,KAAK,QAAQ;AAExC,UAAI,CAAC,WAAW;AACd,aAAK,OAAO;AAAA,UACV;AAAA,QACF;AACA,eAAO;AAAA,MACT;AAEA,UAAI,UAAU;AACZ,cAAM,gBAAgB,SAAS,MAAM,IAAI;AACzC,cAAM,sBAAgC,CAAC;AACvC,iBAAS,IAAI,GAAG,IAAI,cAAc,QAAQ,KAAK;AAC7C,cAAI,UAAU,KAAK,cAAc,CAAC,CAAC,GAAG;AACpC,gCAAoB,KAAK,IAAI,CAAC;AAAA,UAChC;AAAA,QACF;AAEA,aAAK,OAAO;AAAA,UACV,qFAAgF,oBAAoB,MAAM;AAAA,QAC5G;AACA,eAAO;AAAA,UACL,QAAQ;AAAA,UACR,mBAAmB,CAAC,OAAO;AAAA,UAC3B,QAAQ;AAAA,UACR;AAAA,UACA,cAAc;AAAA,UACd;AAAA,QACF;AAAA,MACF;AAEA,WAAK,OAAO;AAAA,QACV;AAAA,MACF;AACA,aAAO;AAAA,QACL,QAAQ;AAAA,QACR,mBAAmB,CAAC,OAAO;AAAA,QAC3B,QAAQ,0CAA0C,UAAU;AAAA,QAC5D,cAAc;AAAA,QACd;AAAA,MACF;AAAA,IACF,QAAQ;AACN,WAAK,OAAO;AAAA,QACV;AAAA,MACF;AACA,aAAO;AAAA,IACT;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,kBAAkB,YAAoB,YAA8B;AAClE,QAAI,eAAe,EAAG,QAAO,CAAC;AAG9B,QAAI,cAAc,YAAY;AAC5B,aAAO,MAAM,KAAK,EAAE,QAAQ,WAAW,GAAG,CAAC,GAAG,MAAM,CAAC;AAAA,IACvD;AAGA,UAAM,YAAY,KAAK,IAAI,GAAG,KAAK,KAAK,aAAa,eAAe,CAAC;AACrE,UAAM,QAAQ;AACd,UAAM,MAAM,aAAa;AACzB,UAAM,gBAAgB,MAAM;AAG5B,QAAI,iBAAiB,GAAG;AACtB,aAAO,CAAC,KAAK,MAAM,aAAa,CAAC,CAAC;AAAA,IACpC;AAGA,QAAI,iBAAiB,YAAY;AAC/B,aAAO,MAAM,KAAK,EAAE,QAAQ,cAAc,GAAG,CAAC,GAAG,MAAM,QAAQ,CAAC;AAAA,IAClE;AAGA,UAAM,UAAoB,CAAC;AAC3B,UAAM,OAAO,gBAAgB;AAC7B,aAAS,IAAI,GAAG,IAAI,YAAY,KAAK;AACnC,cAAQ,KAAK,QAAQ,KAAK,MAAM,IAAI,IAAI,CAAC;AAAA,IAC3C;AACA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAc,kBACZ,UACA,QACA,OACA,SAIC;AACD,SAAK,OAAO;AAAA,MACV,uCAAuC,MAAM;AAAA,IAC/C;AAEA,UAAM,kBAAc,8BAAa,QAAQ,EAAE,SAAS,QAAQ;AAE5D,UAAM,WAAW;AAAA,MACf;AAAA,QACE,MAAM;AAAA,QACN,SAAS;AAAA,UACP,EAAE,MAAM,QAAiB,MAAM,wBAAwB;AAAA,UACvD;AAAA,YACE,MAAM;AAAA,YACN,OAAO,yBAAyB,WAAW;AAAA,UAC7C;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAEA,UAAM,SAAS,MAAM,UAAU,WAAW;AAAA,MACxC,QAAQ;AAAA,MACR;AAAA,MACA,cAAc;AAAA,MACd,eAAe,SAAS;AAAA,MACxB,YAAY,SAAS,cAAcD;AAAA,MACnC,aAAa,SAAS,eAAe;AAAA,MACrC,aAAa,SAAS;AAAA,MACtB,WAAW;AAAA,MACX,OAAO;AAAA,IACT,CAAC;AAED,QAAI,SAAS,YAAY;AACvB,cAAQ,WAAW,MAAM,OAAO,KAAK;AAAA,IACvC;AAEA,UAAM,SAAS,OAAO;AAKtB,UAAM,sBAAsB,OAAO,kBAChC,IAAI,6BAAgB,EACpB,OAAO,CAAC,QAAiC,QAAQ,IAAI;AAExD,SAAK,OAAO;AAAA,MACV,6BAA6B,MAAM,uBAAuB,OAAO,iBAAiB,uBAAuB,oBAAoB,KAAK,GAAG,CAAC;AAAA,IACxI;AAEA,WAAO;AAAA,MACL,mBAAmB,OAAO;AAAA,MAC1B,mBAAmB;AAAA,IACrB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMQ,mBACN,cACgC;AAChC,QAAI,aAAa,SAAS,EAAG,QAAO;AAEpC,WAAO,CAAC,GAAG,aAAa,QAAQ,CAAC,EAC9B,KAAK,CAAC,GAAG,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC,EAC1B,IAAI,CAAC,CAAC,IAAI,MAAM,IAAI;AAAA,EACzB;AACF;;;ACtYA,IAAAE,kBAA2C;AAC3C,uBAA6B;AAC7B,IAAAC,oBAAyB;AAOlB,IAAM,kBAAN,MAAsB;AAAA,EACnB,SAAwB;AAAA,EACxB,OAAe;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQvB,MAAM,MAAM,UAAmC;AAC7C,UAAM,eAAW,4BAAS,QAAQ;AAClC,UAAM,WAAO,0BAAS,QAAQ;AAE9B,WAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,WAAK,aAAS,+BAAa,CAAC,KAAK,QAAQ;AACvC,YAAI,IAAI,QAAQ,IAAI,QAAQ,IAAI;AAC9B,cAAI,UAAU,KAAK;AAAA,YACjB,gBAAgB;AAAA,YAChB,kBAAkB,KAAK;AAAA,UACzB,CAAC;AACD,gDAAiB,QAAQ,EAAE,KAAK,GAAG;AAAA,QACrC,OAAO;AACL,cAAI,UAAU,GAAG;AACjB,cAAI,IAAI,WAAW;AAAA,QACrB;AAAA,MACF,CAAC;AAED,WAAK,OAAO,GAAG,SAAS,MAAM;AAG9B,WAAK,OAAO,OAAO,GAAG,aAAa,MAAM;AACvC,cAAM,UAAU,KAAK,OAAQ,QAAQ;AACrC,YAAI,OAAO,YAAY,YAAY,YAAY,MAAM;AACnD,eAAK,OAAO,QAAQ;AACpB,kBAAQ,oBAAoB,KAAK,IAAI,IAAI,QAAQ,EAAE;AAAA,QACrD,OAAO;AACL,iBAAO,IAAI,MAAM,8BAA8B,CAAC;AAAA,QAClD;AAAA,MACF,CAAC;AAAA,IACH,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA,EAKA,OAAsB;AACpB,WAAO,IAAI,QAAQ,CAAC,YAAY;AAC9B,UAAI,KAAK,QAAQ;AACf,aAAK,OAAO,MAAM,MAAM;AACtB,eAAK,SAAS;AACd,eAAK,OAAO;AACZ,kBAAQ;AAAA,QACV,CAAC;AAAA,MACH,OAAO;AACL,gBAAQ;AAAA,MACV;AAAA,IACF,CAAC;AAAA,EACH;AACF;;;AC5DA,IAAAC,kBASO;AACP,IAAAC,mBAAkC;AAClC,IAAAC,oBAAqB;AACrB,IAAAD,mBAAyB;;;ACXzB,IAAM,cAAc;AAGpB,IAAM,oBAAoB;AAQnB,IAAM,wBAAN,MAA4B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWjC,MAAM,QAA2B,gBAA4C;AAC3E,QAAI,OAAO,WAAW,GAAG;AACvB,YAAM,IAAI,MAAM,0BAA0B;AAAA,IAC5C;AAEA,QAAI,OAAO,WAAW,GAAG;AACvB,aAAO,OAAO,CAAC;AAAA,IACjB;AAGA,UAAM,OAAO,gBAAgB,OAAO,CAAC,CAAC;AAEtC,aAAS,IAAI,GAAG,IAAI,OAAO,QAAQ,KAAK;AACtC,YAAM,QAAQ,OAAO,CAAC;AACtB,YAAM,UAAsB;AAAA,QAC1B,OAAO,KAAK,MAAM;AAAA,QAClB,UAAU,KAAK,SAAS;AAAA,QACxB,QAAQ,KAAK,OAAO;AAAA,QACpB,QAAQ,KAAK,OAAO;AAAA,MACtB;AACA,YAAM,gBAAgB,iBAClB,eAAe,CAAC,IAChB,QAAQ;AAGZ,iBAAW,QAAQ,MAAM,OAAO;AAC9B,cAAM,WAAW,gBAAgB,IAAI;AACrC,iBAAS,WAAW,KAAK,SAAS,SAAS,UAAU,OAAO;AAC5D,YAAI,SAAS,QAAQ;AACnB,mBAAS,OAAO,OAAO,KAAK,SAAS,SAAS,OAAO,MAAM,OAAO;AAAA,QACpE;AACA,iBAAS,WAAW,SAAS,SAAS,IAAI,CAAC,OAAO;AAAA,UAChD,MAAM,KAAK,SAAS,EAAE,MAAM,OAAO;AAAA,QACrC,EAAE;AACF,aAAK,MAAM,KAAK,QAAQ;AAAA,MAC1B;AAGA,iBAAW,WAAW,MAAM,UAAU;AACpC,cAAM,WAAW,gBAAgB,OAAO;AACxC,iBAAS,WAAW,KAAK,SAAS,SAAS,UAAU,OAAO;AAC5D,YAAI,SAAS,QAAQ;AACnB,mBAAS,OAAO,OAAO,KAAK,SAAS,SAAS,OAAO,MAAM,OAAO;AAAA,QACpE;AACA,iBAAS,WAAW,SAAS,SAAS,IAAI,CAAC,OAAO;AAAA,UAChD,MAAM,KAAK,SAAS,EAAE,MAAM,OAAO;AAAA,QACrC,EAAE;AACF,iBAAS,WAAW,SAAS,SAAS,IAAI,CAAC,OAAO;AAAA,UAChD,MAAM,KAAK,SAAS,EAAE,MAAM,OAAO;AAAA,QACrC,EAAE;AAEF,aAAK,qBAAqB,UAAU,aAAa;AACjD,aAAK,SAAS,KAAK,QAAQ;AAAA,MAC7B;AAGA,iBAAW,SAAS,MAAM,QAAQ;AAChC,cAAM,WAAW,gBAAgB,KAAK;AACtC,iBAAS,WAAW,KAAK,SAAS,SAAS,UAAU,OAAO;AAC5D,YAAI,SAAS,QAAQ;AACnB,mBAAS,OAAO,OAAO,KAAK,SAAS,SAAS,OAAO,MAAM,OAAO;AAAA,QACpE;AACA,iBAAS,WAAW,SAAS,SAAS,IAAI,CAAC,OAAO;AAAA,UAChD,MAAM,KAAK,SAAS,EAAE,MAAM,OAAO;AAAA,QACrC,EAAE;AACF,iBAAS,WAAW,SAAS,SAAS,IAAI,CAAC,OAAO;AAAA,UAChD,MAAM,KAAK,SAAS,EAAE,MAAM,OAAO;AAAA,QACrC,EAAE;AACF,iBAAS,YAAY,SAAS,UAAU,IAAI,CAAC,OAAO;AAAA,UAClD,MAAM,KAAK,SAAS,EAAE,MAAM,OAAO;AAAA,QACrC,EAAE;AACF,aAAK,OAAO,KAAK,QAAQ;AAAA,MAC3B;AAGA,iBAAW,SAAS,MAAM,QAAQ;AAChC,cAAM,WAAW,gBAAgB,KAAK;AACtC,iBAAS,WAAW,KAAK,SAAS,SAAS,UAAU,OAAO;AAC5D,YAAI,SAAS,QAAQ;AACnB,mBAAS,OAAO,OAAO,KAAK,SAAS,SAAS,OAAO,MAAM,OAAO;AAAA,QACpE;AACA,iBAAS,WAAW,SAAS,SAAS,IAAI,CAAC,OAAO;AAAA,UAChD,MAAM,KAAK,SAAS,EAAE,MAAM,OAAO;AAAA,QACrC,EAAE;AACF,aAAK,OAAO,KAAK,QAAQ;AAAA,MAC3B;AAGA,iBAAW,SAAS,MAAM,KAAK,UAAU;AACvC,aAAK,KAAK,SAAS,KAAK;AAAA,UACtB,MAAM,KAAK,SAAS,MAAM,MAAM,OAAO;AAAA,QACzC,CAAC;AAAA,MACH;AAGA,iBAAW,SAAS,MAAM,UAAU,UAAU;AAC5C,aAAK,UAAU,SAAS,KAAK;AAAA,UAC3B,MAAM,KAAK,SAAS,MAAM,MAAM,OAAO;AAAA,QACzC,CAAC;AAAA,MACH;AAGA,aAAO,OAAO,KAAK,OAAO,MAAM,KAAK;AAAA,IACvC;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,SAAS,KAAa,SAA6B;AACjD,UAAM,QAAQ,YAAY,KAAK,GAAG;AAClC,QAAI,CAAC,OAAO;AACV,aAAO;AAAA,IACT;AAEA,UAAM,OAAO,MAAM,CAAC;AACpB,UAAM,QAAQ,SAAS,MAAM,CAAC,GAAG,EAAE;AACnC,WAAO,KAAK,IAAI,IAAI,QAAQ,QAAQ,IAAI,CAAC;AAAA,EAC3C;AAAA;AAAA;AAAA;AAAA;AAAA,EAMQ,qBACN,SACA,eACM;AAEN,UAAM,MAAM;AACZ,UAAM,QAAQ,IAAI;AAClB,QAAI,CAAC,OAAO,IAAK;AAEjB,UAAM,QAAQ,kBAAkB,KAAK,MAAM,GAAG;AAC9C,QAAI,OAAO;AACT,YAAM,QAAQ,SAAS,MAAM,CAAC,GAAG,EAAE;AACnC,YAAM,MAAM,cAAc,QAAQ,aAAa;AAAA,IACjD;AAAA,EACF;AACF;;;ADjIO,IAAM,sBAAN,MAA0B;AAAA,EAC/B,YACmB,QACA,QACA,QACA,UAAkB,cAAc,oBACjD;AAJiB;AACA;AACA;AACA;AAAA,EAChB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAaH,MAAM,eACJ,KACA,UACA,YACA,sBACA,SACA,wBACA,aACkC;AAClC,UAAM,UAAU,IAAI,MAAM,CAAC;AAC3B,UAAM,MAAM,QAAQ,IAAI;AACxB,UAAM,gBAAY,wBAAK,KAAK,UAAU,QAAQ;AAC9C,UAAM,oBAAgB,wBAAK,KAAK,UAAU,UAAU,SAAS;AAG7D,UAAM,aAAa,MAAM,KAAK,aAAa,OAAO;AAClD,QAAI,eAAe,GAAG;AACpB,YAAM,IAAI;AAAA,QACR;AAAA,MACF;AAAA,IACF;AAGA,UAAM,SAAS,KAAK,gBAAgB,UAAU;AAC9C,SAAK,OAAO;AAAA,MACV,mCAAmC,UAAU,iBAAY,OAAO,MAAM,cAAc,KAAK,OAAO,SAAS;AAAA,IAC3G;AAGA,UAAM,SAAS,IAAI,gBAAgB;AACnC,UAAM,UAAU,MAAM,OAAO,MAAM,OAAO;AAC1C,SAAK,OAAO;AAAA,MACV;AAAA,MACA;AAAA,IACF;AAEA,UAAM,iBAAoC,CAAC;AAE3C,QAAI;AAEF,eAAS,IAAI,GAAG,IAAI,OAAO,QAAQ,KAAK;AACtC,aAAK,WAAW,WAAW;AAE3B,cAAM,CAAC,OAAO,GAAG,IAAI,OAAO,CAAC;AAC7B,cAAM,eAAW,wBAAK,eAAe,UAAU,CAAC,EAAE;AAClD,uCAAU,UAAU,EAAE,WAAW,KAAK,CAAC;AAEvC,cAAM,MAAM,MAAM,KAAK;AAAA,UACrB;AAAA,UACA,OAAO;AAAA,UACP;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,QACF;AAEA,uBAAe,KAAK,GAAG;AAAA,MACzB;AAAA,IACF,UAAE;AAEA,WAAK,OAAO,KAAK,qDAAqD;AACtE,YAAM,OAAO,KAAK;AAAA,IACpB;AAEA,SAAK,WAAW,WAAW;AAG3B,SAAK,OAAO;AAAA,MACV,6BAA6B,OAAO,MAAM;AAAA,IAC5C;AACA,UAAM,SAAS,IAAI,sBAAsB;AACzC,UAAM,iBAAiB,KAAK;AAAA,MAC1B;AAAA,MACA,OAAO;AAAA,IACT;AACA,UAAM,SAAS,OAAO,MAAM,gBAAgB,cAAc;AAE1D,SAAK,OAAO;AAAA,MACV,iCAAiC,OAAO,MAAM,MAAM,WAAW,OAAO,SAAS,MAAM,cAAc,OAAO,OAAO,MAAM,YAAY,OAAO,KAAK,OAAO,KAAK,EAAE,MAAM;AAAA,IACrK;AAGA,mCAAU,WAAW,EAAE,WAAW,KAAK,CAAC;AACxC,UAAM,gBAAY,wBAAK,WAAW,QAAQ;AAC1C,mCAAU,WAAW,EAAE,WAAW,KAAK,CAAC;AAGxC,SAAK,eAAe,eAAe,OAAO,QAAQ,SAAS;AAG3D,UAAM,iBAAa,wBAAK,WAAW,aAAa;AAChD,uCAAc,YAAY,KAAK,UAAU,MAAM,CAAC;AAEhD,QAAI;AAEF,YAAM,KAAK,iBAAiB,SAAS,SAAS;AAG9C,WAAK,wBAAwB,YAAY,SAAS;AAElD,WAAK,WAAW,WAAW;AAG3B,WAAK,OAAO;AAAA,QACV;AAAA,MACF;AACA,YAAM,WAAW,SAAS;AAAA,IAC5B,UAAE;AAEA,cAAI,4BAAW,aAAa,GAAG;AAC7B,oCAAO,eAAe,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AAAA,MACxD;AAEA,UAAI,sBAAsB;AACxB,aAAK,OAAO;AAAA,UACV;AAAA,UACA;AAAA,QACF;AACA,gBAAI,4BAAW,SAAS,GAAG;AACzB,sCAAO,WAAW,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AAAA,QACpD;AAAA,MACF,OAAO;AACL,aAAK,OAAO;AAAA,UACV;AAAA,UACA;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKA,MAAc,aACZ,YACA,aACA,WACA,SACA,SACA,UACA,SACA,wBAC0B;AAC1B,UAAM,aAAa,SAAS,aAAa,CAAC,IAAI,WAAW,WAAW,SAAS,IAAI,OAAO;AAExF,aAAS,UAAU,GAAG,WAAW,KAAK,OAAO,YAAY,WAAW;AAClE,UAAI;AACF,YAAI,UAAU,GAAG;AACf,eAAK,OAAO;AAAA,YACV,yBAAyB,UAAU,eAAe,OAAO,IAAI,KAAK,OAAO,UAAU;AAAA,UACrF;AAAA,QACF,OAAO;AACL,eAAK,OAAO;AAAA,YACV,yBAAyB,UAAU;AAAA,UACrC;AAAA,QACF;AAEA,cAAM,YAAY,KAAK,IAAI;AAG3B,cAAM,oBAAoB,uBAAuB;AAAA,UAC/C,GAAG;AAAA,UACH,YAAY,CAAC,WAAW,OAAO;AAAA,QACjC,CAAC;AAGD,cAAM,OAAO,MAAM,KAAK,OAAO,mBAAmB;AAAA,UAChD,SAAS,CAAC,EAAE,MAAM,QAAQ,KAAK,QAAQ,CAAC;AAAA,UACxC,SAAS;AAAA,UACT,QAAQ,EAAE,MAAM,MAAM;AAAA,QACxB,CAAC;AAGD,cAAM,KAAK,kBAAkB,IAAI;AAGjC,cAAM,cAAU,wBAAK,UAAU,YAAY;AAC3C,cAAM,KAAK,eAAe,KAAK,QAAQ,OAAO;AAG9C,cAAM,iBAAa,wBAAK,UAAU,WAAW;AAC7C,cAAM,qBAAiB,wBAAK,UAAU,QAAQ;AAC9C,cAAM,eAAe;AAAA,UACnB,KAAK;AAAA,UACL;AAAA,UACA;AAAA,UACA;AAAA,QACF;AAGA,cAAM,qBAAiB,wBAAK,gBAAgB,aAAa;AACzD,cAAM,MAAM,MAAM,cAA+B,KAAK,cAAc;AAGpE,gBAAI,4BAAW,OAAO,EAAG,6BAAO,SAAS,EAAE,OAAO,KAAK,CAAC;AACxD,gBAAI,4BAAW,UAAU,GAAG;AAC1B,sCAAO,YAAY,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AAAA,QACrD;AAEA,cAAM,YAAY,KAAK,IAAI,IAAI,aAAa,KAAM,QAAQ,CAAC;AAC3D,YAAI,UAAU,GAAG;AACf,eAAK,OAAO;AAAA,YACV,yBAAyB,UAAU,wBAAwB,OAAO,KAAK,OAAO;AAAA,UAChF;AAAA,QACF,OAAO;AACL,eAAK,OAAO;AAAA,YACV,yBAAyB,UAAU,gBAAgB,OAAO;AAAA,UAC5D;AAAA,QACF;AAEA,eAAO;AAAA,MACT,SAAS,OAAO;AACd,YAAI,WAAW,KAAK,OAAO,YAAY;AACrC,eAAK,OAAO;AAAA,YACV,yBAAyB,UAAU,kBAAkB,KAAK,OAAO,UAAU;AAAA,UAC7E;AACA,gBAAM;AAAA,QACR;AACA,aAAK,OAAO;AAAA,UACV,yBAAyB,UAAU,uBAAuB,UAAU,CAAC,IAAI,KAAK,OAAO,UAAU;AAAA,QACjG;AAAA,MACF;AAAA,IACF;AAGA,UAAM,IAAI,MAAM,aAAa;AAAA,EAE/B;AAAA;AAAA,EAGA,gBAAgB,YAAiC;AAC/C,QAAI,KAAK,OAAO,aAAa,GAAG;AAC9B,YAAM,IAAI,MAAM,kDAAkD;AAAA,IACpE;AAEA,UAAM,SAAsB,CAAC;AAC7B,aAAS,QAAQ,GAAG,SAAS,YAAY,SAAS,KAAK,OAAO,WAAW;AACvE,YAAM,MAAM,KAAK,IAAI,QAAQ,KAAK,OAAO,YAAY,GAAG,UAAU;AAClE,aAAO,KAAK,CAAC,OAAO,GAAG,CAAC;AAAA,IAC1B;AACA,WAAO;AAAA,EACT;AAAA;AAAA,EAGA,MAAc,aAAa,SAAkC;AAC3D,UAAM,SAAS,MAAM,WAAW,WAAW,CAAC,OAAO,CAAC;AACpD,QAAI,OAAO,SAAS,GAAG;AACrB,aAAO;AAAA,IACT;AACA,UAAM,QAAQ,OAAO,OAAO,MAAM,kBAAkB;AACpD,WAAO,QAAQ,SAAS,MAAM,CAAC,GAAG,EAAE,IAAI;AAAA,EAC1C;AAAA;AAAA,EAGA,MAAc,kBAAkB,MAOd;AAChB,UAAM,YAAY,KAAK,IAAI;AAE3B,WAAO,MAAM;AACX,UAAI,KAAK,IAAI,IAAI,YAAY,KAAK,SAAS;AACzC,cAAM,IAAI,MAAM,0CAA0C;AAAA,MAC5D;AAEA,YAAM,SAAS,MAAM,KAAK,KAAK;AAE/B,UAAI,OAAO,gBAAgB,UAAW;AAEtC,UAAI,OAAO,gBAAgB,WAAW;AACpC,YAAI,UAAU;AACd,YAAI;AACF,gBAAM,SAAS,MAAM,KAAK,UAAU;AACpC,cAAI,OAAO,QAAQ,QAAQ;AACzB,sBAAU,OAAO,OAAO,IAAI,CAAC,MAAM,EAAE,OAAO,EAAE,KAAK,IAAI;AAAA,UACzD;AAAA,QACF,QAAQ;AAAA,QAER;AACA,cAAM,IAAI,MAAM,4CAA4C,OAAO,EAAE;AAAA,MACvE;AAEA,YAAM,IAAI;AAAA,QAAQ,CAAC,YACjB,WAAW,SAAS,cAAc,gBAAgB;AAAA,MACpD;AAAA,IACF;AAAA,EACF;AAAA;AAAA,EAGA,MAAc,eAAe,QAAgB,SAAgC;AAC3E,UAAM,YAAY,MAAM,KAAK,OAAO,kBAAkB,MAAM;AAE5D,QAAI,UAAU,YAAY;AACxB,YAAM,kBAAc,mCAAkB,OAAO;AAC7C,gBAAM,2BAAS,UAAU,YAAY,WAAW;AAChD;AAAA,IACF;AAEA,QAAI,UAAU,MAAM;AAClB,gBAAM,4BAAU,SAAS,UAAU,IAAI;AACvC;AAAA,IACF;AAGA,UAAM,UAAU,KAAK,OAAO,UAAU,EAAE;AACxC,UAAM,WAAW,MAAM,MAAM,GAAG,OAAO,cAAc,MAAM,IAAI;AAAA,MAC7D,SAAS,EAAE,QAAQ,kBAAkB;AAAA,IACvC,CAAC;AAED,QAAI,CAAC,SAAS,IAAI;AAChB,YAAM,IAAI;AAAA,QACR,iCAAiC,SAAS,MAAM,IAAI,SAAS,UAAU;AAAA,MACzE;AAAA,IACF;AAEA,UAAM,SAAS,IAAI,WAAW,MAAM,SAAS,YAAY,CAAC;AAC1D,cAAM,4BAAU,SAAS,MAAM;AAAA,EACjC;AAAA;AAAA;AAAA;AAAA;AAAA,EAMQ,eACN,eACA,aACA,WACM;AAEN,QAAI,iBAAiB;AACrB,aAAS,IAAI,GAAG,IAAI,aAAa,KAAK;AACpC,YAAM,qBAAiB;AAAA,QACrB;AAAA,QACA,UAAU,CAAC;AAAA,QACX;AAAA,QACA;AAAA,MACF;AACA,UAAI,KAAC,4BAAW,cAAc,EAAG;AAEjC,YAAM,eAAW,6BAAY,cAAc,EACxC,OAAO,CAAC,MAAM,EAAE,WAAW,MAAM,KAAK,EAAE,SAAS,MAAM,CAAC,EACxD,KAAK,CAAC,GAAG,MAAM;AACd,cAAM,OAAO,SAAS,EAAE,QAAQ,QAAQ,EAAE,EAAE,QAAQ,QAAQ,EAAE,GAAG,EAAE;AACnE,cAAM,OAAO,SAAS,EAAE,QAAQ,QAAQ,EAAE,EAAE,QAAQ,QAAQ,EAAE,GAAG,EAAE;AACnE,eAAO,OAAO;AAAA,MAChB,CAAC;AAEH,iBAAW,QAAQ,UAAU;AAC3B,cAAM,UAAM,wBAAK,gBAAgB,IAAI;AACrC,cAAM,WAAO,wBAAK,WAAW,OAAO,cAAc,MAAM;AACxD,0CAAa,KAAK,IAAI;AACtB;AAAA,MACF;AAAA,IACF;AAGA,QAAI,mBAAmB;AACvB,aAAS,IAAI,GAAG,IAAI,aAAa,KAAK;AACpC,YAAM,qBAAiB;AAAA,QACrB;AAAA,QACA,UAAU,CAAC;AAAA,QACX;AAAA,QACA;AAAA,MACF;AACA,UAAI,KAAC,4BAAW,cAAc,EAAG;AAEjC,YAAM,iBAAa,6BAAY,cAAc,EAC1C,OAAO,CAAC,MAAM,EAAE,WAAW,QAAQ,KAAK,EAAE,SAAS,MAAM,CAAC,EAC1D,KAAK,CAAC,GAAG,MAAM;AACd,cAAM,OAAO;AAAA,UACX,EAAE,QAAQ,UAAU,EAAE,EAAE,QAAQ,QAAQ,EAAE;AAAA,UAC1C;AAAA,QACF;AACA,cAAM,OAAO;AAAA,UACX,EAAE,QAAQ,UAAU,EAAE,EAAE,QAAQ,QAAQ,EAAE;AAAA,UAC1C;AAAA,QACF;AACA,eAAO,OAAO;AAAA,MAChB,CAAC;AAEH,iBAAW,QAAQ,YAAY;AAC7B,cAAM,UAAM,wBAAK,gBAAgB,IAAI;AACrC,cAAM,WAAO,wBAAK,WAAW,SAAS,gBAAgB,MAAM;AAC5D,0CAAa,KAAK,IAAI;AACtB;AAAA,MACF;AAAA,IACF;AAEA,SAAK,OAAO;AAAA,MACV,mCAAmC,cAAc,UAAU,gBAAgB,mBAAmB,SAAS;AAAA,IACzG;AAAA,EACF;AAAA;AAAA,EAGA,MAAc,iBACZ,SACA,WACe;AACf,SAAK,OAAO;AAAA,MACV;AAAA,IACF;AAEA,UAAM,WAAW,IAAI,aAAa,KAAK,MAAM;AAC7C,UAAM,eAAe,MAAM,SAAS,YAAY,SAAS,SAAS;AAElE,UAAM,iBAAa,wBAAK,WAAW,aAAa;AAChD,UAAM,UAAU,aAAa;AAC7B,UAAM,YAAY;AAAA;AAAA,kEAE4C,aAAa,SAAS;AAAA;AAAA;AAAA,+BAGzD,eAAe,WAAW;AAAA;AAAA;AAAA;AAIrD,UAAM,gBAAgB,WAAW,YAAY,OAAO;AACpD,cAAM,yBAAO,SAAS,UAAU;AAEhC,SAAK,OAAO;AAAA,MACV,kCAAkC,aAAa,SAAS;AAAA,IAC1D;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQQ,wBAAwB,YAAoB,WAAyB;AAC3E,UAAM,cAAU,8BAAa,YAAY,OAAO;AAChD,UAAM,iBAAiB,oBAAI,IAAY;AACvC,UAAM,aAAa;AACnB,QAAI;AACJ,YAAQ,QAAQ,WAAW,KAAK,OAAO,OAAO,MAAM;AAClD,qBAAe,IAAI,MAAM,CAAC,EAAE,QAAQ,WAAW,EAAE,CAAC;AAAA,IACpD;AAEA,UAAM,eAAW,6BAAY,SAAS,EAAE;AAAA,MACtC,CAAC,MAAM,EAAE,WAAW,MAAM,KAAK,EAAE,SAAS,MAAM;AAAA,IAClD;AAEA,QAAI,eAAe;AACnB,eAAW,QAAQ,UAAU;AAC3B,UAAI,CAAC,eAAe,IAAI,IAAI,GAAG;AAC7B,wCAAO,wBAAK,WAAW,IAAI,GAAG,EAAE,OAAO,KAAK,CAAC;AAC7C;AAAA,MACF;AAAA,IACF;AAEA,QAAI,eAAe,GAAG;AACpB,WAAK,OAAO;AAAA,QACV,oCAAoC,YAAY,yBAAyB,eAAe,IAAI;AAAA,MAC9F;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMQ,oBACN,eACA,aACU;AACV,UAAM,UAAoB,CAAC;AAC3B,QAAI,aAAa;AACjB,aAAS,IAAI,GAAG,IAAI,aAAa,KAAK;AACpC,cAAQ,KAAK,UAAU;AACvB,YAAM,UAAM,wBAAK,eAAe,UAAU,CAAC,IAAI,UAAU,QAAQ;AACjE,YAAM,YAAQ,4BAAW,GAAG,QACxB,6BAAY,GAAG,EAAE;AAAA,QACf,CAAC,MAAM,EAAE,WAAW,MAAM,KAAK,EAAE,SAAS,MAAM;AAAA,MAClD,EAAE,SACF;AACJ,oBAAc;AAAA,IAChB;AACA,WAAO;AAAA,EACT;AAAA;AAAA,EAGQ,WAAW,QAA4B;AAC7C,QAAI,QAAQ,SAAS;AACnB,YAAM,QAAQ,IAAI,MAAM,oCAAoC;AAC5D,YAAM,OAAO;AACb,YAAM;AAAA,IACR;AAAA,EACF;AACF;;;AEhjBA,IAAAE,kBAAmC;AACnC,qBAAuB;AACvB,IAAAC,oBAAqB;AAYd,IAAM,oBAAN,MAAwB;AAAA,EAC7B,YAA6B,QAAuB;AAAvB;AAAA,EAAwB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUrD,MAAM,QAAQ,QAAgB,UAAmC;AAC/D,UAAM,YAAY,KAAK,IAAI;AAC3B,UAAM,cAAU,uBAAO;AACvB,UAAM,gBAAY,wBAAK,SAAS,GAAG,QAAQ,IAAI,SAAS,YAAY;AACpE,UAAM,iBAAa,wBAAK,SAAS,GAAG,QAAQ,IAAI,SAAS,YAAY;AAErE,QAAI;AACF,WAAK,OAAO,KAAK,iDAAiD;AAClE,YAAM,KAAK,YAAY,QAAQ,SAAS;AAExC,WAAK,OAAO,KAAK,gDAAgD;AACjE,YAAM,KAAK,kBAAkB,WAAW,UAAU;AAElD,WAAK,OAAO,KAAK,0CAA0C,UAAU;AACrE,aAAO;AAAA,IACT,UAAE;AAEA,cAAI,4BAAW,SAAS,GAAG;AACzB,oCAAO,WAAW,EAAE,OAAO,KAAK,CAAC;AAAA,MACnC;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAc,YAAY,KAAa,YAAmC;AACxE,UAAM,SAAS,MAAM,WAAW,QAAQ;AAAA,MACtC;AAAA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA;AAAA,MACA;AAAA;AAAA,MACA;AAAA,IACF,CAAC;AAED,QAAI,OAAO,SAAS,GAAG;AACrB,YAAM,IAAI;AAAA,QACR,2BAA2B,OAAO,UAAU,eAAe;AAAA,MAC7D;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAc,kBACZ,WACA,YACe;AACf,UAAM,SAAS,MAAM,WAAW,UAAU;AAAA,MACxC;AAAA,MACA,oBAAoB,QAAQ,SAAS;AAAA,MACrC;AAAA,MACA;AAAA,MACA,oBAAoB,QAAQ,SAAS;AAAA,MACrC;AAAA,IACF,CAAC;AAED,QAAI,OAAO,SAAS,GAAG;AACrB,YAAM,IAAI;AAAA,QACR,uCAAuC,OAAO,UAAU,eAAe;AAAA,MACzE;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,QAAQ,cAA4B;AAClC,YAAI,4BAAW,YAAY,GAAG;AAC5B,WAAK,OAAO;AAAA,QACV;AAAA,QACA;AAAA,MACF;AACA,kCAAO,cAAc,EAAE,OAAO,KAAK,CAAC;AAAA,IACtC;AAAA,EACF;AACF;;;AZZO,IAAM,eAAN,MAAmB;AAAA,EACxB,YACmB,QACA,QACA,yBAAkC,OAClC,UAAkB,cAAc,oBACjD;AAJiB;AACA;AACA;AACA;AAAA,EAChB;AAAA,EAEH,MAAM,QACJ,KACA,UACA,YACA,sBACA,SACA,aACkC;AAClC,SAAK,OAAO,KAAK,8BAA8B,GAAG;AAGlD,QAAI,QAAQ,qBAAqB,IAAI,WAAW,SAAS,GAAG;AAC1D,YAAM,UAAU,IAAI;AAAA,QAClB,KAAK;AAAA,QACL,KAAK;AAAA,QACL;AAAA,UACE,WAAW,QAAQ,aAAa,mBAAmB;AAAA,UACnD,YACE,QAAQ,mBAAmB,mBAAmB;AAAA,QAClD;AAAA,QACA,KAAK;AAAA,MACP;AACA,aAAO,QAAQ;AAAA,QACb;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA,CAAC,SAAS,KAAK,uBAAuB,IAAI;AAAA,QAC1C;AAAA,MACF;AAAA,IACF;AAGA,QAAI,QAAQ,eAAe;AACzB,aAAO,KAAK;AAAA,QACV;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,IACF;AAGA,WAAO,KAAK;AAAA,MACV;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYA,MAAM,oBACJ,KACA,UACA,YACA,sBACA,SACA,aACoC;AACpC,SAAK,OAAO,KAAK,sDAAsD,GAAG;AAI1E,UAAM,aAAa,QAAQ,cAAc,IAAI,wBAAwB;AACrE,UAAM,iBAAoC,EAAE,GAAG,SAAS,WAAW;AAEnE,UAAM,UAAU,IAAI,WAAW,SAAS,IAAI,IAAI,MAAM,CAAC,IAAI;AAG3D,UAAM,WAAW,MAAM,KAAK;AAAA,MAC1B;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AACA,SAAK,OAAO;AAAA,MACV,gCAAgC,SAAS,MAAM,KAAK,SAAS,MAAM;AAAA,IACrE;AAGA,QAAI,eAAe,cAAc;AAC/B,YAAM,iBAAiB,KAAK,iBAAiB,UAAU;AACvD,UAAI,gBAAgB;AAClB,uBAAe,aAAa,cAAc;AAAA,MAC5C;AAAA,IACF;AAGA,QAAI,SAAS,WAAW,OAAO;AAC7B,YAAM,KAAK;AAAA,QACT;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA,SAAS;AAAA,QACT,SAAS;AAAA,MACX;AACA,aAAO;AAAA,QACL;AAAA,QACA,kBAAkB,KAAK,iBAAiB,UAAU;AAAA,MACpD;AAAA,IACF;AAGA,UAAM,gBAAmC,SAAS,oBAC9C,EAAE,GAAG,gBAAgB,UAAU,SAAS,kBAAkB,IAC1D;AACJ,UAAM,KAAK;AAAA,MACT;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AACA,WAAO;AAAA,MACL;AAAA,MACA,kBAAkB,KAAK,iBAAiB,UAAU;AAAA,IACpD;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMQ,iBACN,YACyB;AACzB,UAAM,SAAS,WAAW,UAAU;AACpC,QAAI,OAAO,WAAW,WAAW,GAAG;AAClC,aAAO;AAAA,IACT;AACA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,MAAc,kBACZ,SACA,UACA,SACA,aACsB;AAEtB,QAAI,QAAQ,gBAAgB,CAAC,QAAQ,wBAAwB,CAAC,SAAS;AACrE,YAAM,SAAS,QAAQ,gBAAgB;AACvC,YAAM,SAAS,QAAQ,eACnB,WAAW,QAAQ,YAAY,KAC/B,CAAC,UACC,oCACA;AACN,aAAO,EAAE,QAAQ,QAAQ,cAAc,GAAG,YAAY,EAAE;AAAA,IAC1D;AAGA,UAAM,kBAAc,wBAAK,QAAQ,IAAI,GAAG,UAAU,UAAU,WAAW;AACvE,UAAM,UAAU,IAAI;AAAA,MAClB,KAAK;AAAA,MACL,IAAI,aAAa,KAAK,MAAM;AAAA,MAC5B,IAAI,iBAAiB,KAAK,MAAM;AAAA,IAClC;AAEA,QAAI;AACF,YAAM,WAAW,MAAM,QAAQ;AAAA,QAC7B;AAAA,QACA;AAAA,QACA,QAAQ;AAAA,QACR;AAAA,UACE,YAAY,QAAQ;AAAA,UACpB;AAAA,QACF;AAAA,MACF;AAGA,UAAI,QAAQ,cAAc;AACxB,eAAO;AAAA,UACL,GAAG;AAAA,UACH,QAAQ,QAAQ;AAAA,UAChB,QAAQ,WAAW,QAAQ,YAAY,KAAK,SAAS,MAAM;AAAA,QAC7D;AAAA,MACF;AAEA,aAAO;AAAA,IACT,UAAE;AAEA,cAAI,4BAAW,WAAW,GAAG;AAC3B,oCAAO,aAAa,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AAAA,MACtD;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAc,eACZ,SACA,UACA,YACA,sBACA,SACA,aACA,mBACA,qBACe;AACf,QAAI,CAAC,QAAQ,mBAAmB;AAC9B,YAAM,IAAI,MAAM,wDAAwD;AAAA,IAC1E;AACA,QAAI,CAAC,SAAS;AACZ,YAAM,IAAI,MAAM,oDAAoD;AAAA,IACtE;AAEA,UAAM,MAAM,UAAU,OAAO;AAG7B,UAAM,kBAA8C,OAAO,cAAc;AAEvE,UAAI;AACJ,UAAI;AACF,cAAMC,kBAAa,wBAAK,WAAW,aAAa;AAEhD,cAAM,aAAa,MAAM;AAAA,UACvB;AAAA,UACAA;AAAA,QACF;AACA,cAAM,gBAAgB,IAAI,iBAAiB,KAAK,MAAM;AACtD,oBAAY,MAAM,cAAc,YAAY,SAAS,UAAU;AAAA,MACjE,QAAQ;AACN,aAAK,OAAO;AAAA,UACV;AAAA,QACF;AAAA,MACF;AAGA,YAAM,iBAAa,wBAAK,WAAW,aAAa;AAChD,YAAM,oBAAgB,wBAAK,WAAW,wBAAwB;AAC9D,wCAAa,YAAY,aAAa;AAEtC,YAAM,YAAY,IAAI,iBAAiB,KAAK,MAAM;AAClD,YAAM,UAAU,eAAe,WAAW,QAAQ,mBAAoB;AAAA,QACpE,aAAa,QAAQ;AAAA,QACrB,YAAY,QAAQ;AAAA,QACpB;AAAA,QACA,cAAc,QAAQ;AAAA,QACtB,mBAAmB;AAAA,QACnB;AAAA,QACA;AAAA,MACF,CAAC;AACD,YAAM,WAAW,SAAS;AAAA,IAC5B;AAGA,UAAM,aAAgC,oBAClC,EAAE,GAAG,SAAS,UAAU,kBAAkB,IAC1C;AACJ,UAAM,KAAK;AAAA,MACT;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAEA,SAAK,OAAO,KAAK,sDAAsD;AAAA,EACzE;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAc,mBACZ,KACA,UACA,YACA,sBACA,SACA,aACkC;AAClC,SAAK,OAAO;AAAA,MACV;AAAA,IACF;AACA,UAAM,oBAAoB,IAAI,kBAAkB,KAAK,MAAM;AAC3D,QAAI,eAA8B;AAElC,QAAI;AACF,qBAAe,MAAM,kBAAkB,QAAQ,KAAK,QAAQ;AAC5D,YAAM,WAAW,UAAU,YAAY;AACvC,WAAK,OAAO;AAAA,QACV;AAAA,QACA;AAAA,MACF;AAEA,aAAO,MAAM,KAAK;AAAA,QAChB;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,IACF,UAAE;AACA,UAAI,cAAc;AAChB,0BAAkB,QAAQ,YAAY;AAAA,MACxC;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAc,oBACZ,KACA,UACA,YACA,sBACA,SACA,aACkC;AAClC,QAAI,gBAA8B;AAElC,QAAI;AACF,aAAO,MAAM,KAAK;AAAA,QAChB;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,IACF,SAAS,OAAO;AAEd,UAAI,aAAa,SAAS;AACxB,cAAM;AAAA,MACR;AAEA,sBAAgB;AAChB,WAAK,OAAO,MAAM,qCAAqC,KAAK;AAE5D,UAAI,CAAC,KAAK,wBAAwB;AAChC,cAAM;AAAA,MACR;AAAA,IACF;AAGA,SAAK,OAAO,KAAK,iDAAiD;AAClE,UAAM,oBAAoB,IAAI,kBAAkB,KAAK,MAAM;AAC3D,QAAI,eAA8B;AAElC,QAAI;AACF,qBAAe,MAAM,kBAAkB,QAAQ,KAAK,QAAQ;AAG5D,YAAM,WAAW,UAAU,YAAY;AACvC,WAAK,OAAO,KAAK,2CAA2C,QAAQ;AAEpE,YAAM,SAAS,MAAM,KAAK;AAAA,QACxB;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAEA,WAAK,OAAO,KAAK,8CAA8C;AAC/D,aAAO;AAAA,IACT,SAAS,eAAe;AACtB,WAAK,OAAO;AAAA,QACV;AAAA,QACA;AAAA,MACF;AACA,YAAM,IAAI,sBAAsB,eAAgB,aAAsB;AAAA,IACxE,UAAE;AAEA,UAAI,cAAc;AAChB,0BAAkB,QAAQ,YAAY;AAAA,MACxC;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAc,kBACZ,KACA,UACA,YACA,sBACA,SACA,aACkC;AAClC,UAAM,YAAY,KAAK,IAAI;AAC3B,UAAM,oBAAoB,KAAK,uBAAuB,OAAO;AAE7D,SAAK,OAAO;AAAA,MACV,iCAAiC,KAAK,UAAU,kBAAkB,aAAa,IAAI,CAAC;AAAA,IACtF;AACA,SAAK,OAAO;AAAA,MACV;AAAA,IACF;AACA,SAAK,OAAO,KAAK,uDAAuD;AACxE,SAAK,OAAO;AAAA,MACV;AAAA,IACF;AAGA,UAAM,EAAE,SAAS,OAAO,IAAI,MAAM,KAAK,WAAW,GAAG;AAErD,QAAI;AACF,YAAM,OAAO,MAAM,KAAK,oBAAoB,SAAS,iBAAiB;AACtE,YAAM,KAAK,kBAAkB,IAAI;AAGjC,UAAI,aAAa,SAAS;AACxB,aAAK,OAAO;AAAA,UACV;AAAA,QACF;AACA,cAAM,QAAQ,IAAI,MAAM,4BAA4B;AACpD,cAAM,OAAO;AACb,cAAM;AAAA,MACR;AAEA,YAAM,KAAK,eAAe,KAAK,MAAM;AAAA,IACvC,UAAE;AAEA,UAAI,QAAQ;AACV,aAAK,OAAO,KAAK,8CAA8C;AAC/D,cAAM,OAAO,KAAK;AAAA,MACpB;AAAA,IACF;AAEA,UAAM,MAAM,QAAQ,IAAI;AACxB,UAAM,cAAU,wBAAK,KAAK,YAAY;AACtC,UAAM,iBAAa,wBAAK,KAAK,kBAAkB;AAC/C,UAAM,gBAAY,wBAAK,KAAK,UAAU,QAAQ;AAE9C,QAAI;AACF,YAAM,KAAK,sBAAsB,SAAS,YAAY,SAAS;AAG/D,YAAM,KAAK,iBAAiB,KAAK,SAAS;AAG1C,UAAI,aAAa,SAAS;AACxB,aAAK,OAAO,KAAK,mDAAmD;AACpE,cAAM,QAAQ,IAAI,MAAM,4BAA4B;AACpD,cAAM,OAAO;AACb,cAAM;AAAA,MACR;AAGA,WAAK,OAAO,KAAK,iDAAiD;AAClE,YAAM,WAAW,SAAS;AAE1B,YAAM,WAAW,KAAK,IAAI,IAAI;AAC9B,WAAK,OAAO,KAAK,mDAAmD;AACpE,WAAK,OAAO,KAAK,8BAA8B,UAAU,IAAI;AAAA,IAC/D,UAAE;AAEA,WAAK,OAAO,KAAK,+CAA+C;AAChE,cAAI,4BAAW,OAAO,GAAG;AACvB,oCAAO,SAAS,EAAE,OAAO,KAAK,CAAC;AAAA,MACjC;AACA,cAAI,4BAAW,UAAU,GAAG;AAC1B,oCAAO,YAAY,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AAAA,MACrD;AAGA,UAAI,sBAAsB;AACxB,aAAK,OAAO;AAAA,UACV;AAAA,UACA;AAAA,QACF;AACA,gBAAI,4BAAW,SAAS,GAAG;AACzB,sCAAO,WAAW,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AAAA,QACpD;AAAA,MACF,OAAO;AACL,aAAK,OAAO,KAAK,uCAAuC,SAAS;AAAA,MACnE;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA,EAEQ,uBACN,SACmB;AACnB,WAAO;AAAA,MACL,OAAG,wBAAK,SAAS;AAAA,QACf;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF,CAAC;AAAA,MACD,YAAY,CAAC,QAAQ,MAAM;AAAA,MAC3B,mBAAmB;AAAA,MACnB,YAAY;AAAA,MACZ,aAAa;AAAA,QACX,MAAM;AAAA,QACN,MAAM,QAAQ,YAAY,CAAC,SAAS,OAAO;AAAA,QAC3C,aAAa;AAAA,QACb,WAAW;AAAA,MACb;AAAA,MACA,yBAAyB;AAAA,MACzB,sBAAsB;AAAA;AAAA,MACtB,cAAc;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAQd,WAAW;AAAA,MACX,qBAAqB;AAAA,QACnB,QAAQ;AAAA,QACR,aAAa,QAAQ;AAAA,MACvB;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAc,oBACZ,KACA,mBAC8B;AAC9B,UAAM,OAAO,MAAM,KAAK,OAAO,mBAAmB;AAAA,MAChD,SAAS;AAAA,QACP;AAAA,UACE,MAAM;AAAA,UACN;AAAA,QACF;AAAA,MACF;AAAA,MACA,SAAS;AAAA,MACT,QAAQ;AAAA,QACN,MAAM;AAAA,MACR;AAAA,IACF,CAAC;AAED,SAAK,OAAO,KAAK,gCAAgC,KAAK,MAAM,EAAE;AAC9D,SAAK,OAAO,KAAK,wCAAwC;AAEzD,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAc,WACZ,KACwD;AACxD,QAAI,IAAI,WAAW,SAAS,GAAG;AAC7B,YAAM,WAAW,IAAI,MAAM,CAAC;AAC5B,YAAM,SAAS,IAAI,gBAAgB;AACnC,YAAM,UAAU,MAAM,OAAO,MAAM,QAAQ;AAE3C,WAAK,OAAO,KAAK,6CAA6C,OAAO;AAErE,aAAO,EAAE,SAAS,OAAO;AAAA,IAC3B;AAEA,WAAO,EAAE,SAAS,IAAI;AAAA,EACxB;AAAA,EAEA,MAAc,kBAAkB,MAA0C;AACxE,UAAM,sBAAsB,KAAK,IAAI;AACrC,QAAI,mBAAmB;AAEvB,UAAM,cAAc,CAAC,WAIf;AACJ,YAAM,QAAkB,CAAC,WAAW,OAAO,WAAW,EAAE;AAExD,UAAI,OAAO,kBAAkB,QAAW;AACtC,cAAM,KAAK,aAAa,OAAO,aAAa,EAAE;AAAA,MAChD;AAEA,YAAM,OAAO,OAAO;AACpB,UAAI,MAAM;AACR,YACE,KAAK,wBAAwB,UAC7B,KAAK,oBAAoB,QACzB;AACA,gBAAM;AAAA,YACJ,aAAa,KAAK,mBAAmB,IAAI,KAAK,eAAe;AAAA,UAC/D;AAAA,QACF;AAAA,MACF;AAEA,YAAM,eAAe,oBAAoB,MAAM,KAAK,KAAK,CAAC;AAC1D,UAAI,iBAAiB,kBAAkB;AACrC,2BAAmB;AACnB,gBAAQ,OAAO,MAAM,YAAY;AAAA,MACnC;AAAA,IACF;AAEA,WAAO,MAAM;AACX,UAAI,KAAK,IAAI,IAAI,sBAAsB,KAAK,SAAS;AACnD,cAAM,IAAI,MAAM,cAAc;AAAA,MAChC;AAEA,YAAM,SAAS,MAAM,KAAK,KAAK;AAE/B,kBAAY,MAAM;AAElB,UAAI,OAAO,gBAAgB,WAAW;AACpC,aAAK,OAAO,KAAK,wCAAwC;AACzD;AAAA,MACF;AAEA,UAAI,OAAO,gBAAgB,WAAW;AAEpC,cAAM,eAAe,MAAM,KAAK,sBAAsB,IAAI;AAC1D,cAAM,UAAU,KAAK,OAAO,KAAK,IAAI,IAAI,uBAAuB,GAAI;AACpE,aAAK,OAAO;AAAA,UACV;AAAA,mCAAsC,OAAO,MAAM,YAAY;AAAA,QACjE;AACA,cAAM,IAAI,MAAM,gBAAgB,YAAY,EAAE;AAAA,MAChD;AAEA,YAAM,IAAI;AAAA,QAAQ,CAAC,YACjB,WAAW,SAAS,cAAc,gBAAgB;AAAA,MACpD;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAc,sBACZ,MACiB;AACjB,QAAI;AACF,YAAM,SAAS,MAAM,KAAK,UAAU;AACpC,UAAI,OAAO,QAAQ,QAAQ;AACzB,eAAO,OAAO,OACX,IAAI,CAAC,MAA2B,EAAE,OAAO,EACzC,KAAK,IAAI;AAAA,MACd;AAEA,aAAO,WAAW,OAAO,UAAU,SAAS;AAAA,IAE9C,SAAS,KAAK;AACZ,WAAK,OAAO,MAAM,kDAAkD,GAAG;AACvE,aAAO;AAAA,IACT;AAAA,EACF;AAAA,EAEA,MAAc,eAAe,QAA+B;AAC1D,SAAK,OAAO;AAAA,MACV;AAAA,IACF;AAEA,UAAM,YAAY,MAAM,KAAK,OAAO,kBAAkB,MAAM;AAE5D,UAAM,cAAU,wBAAK,QAAQ,IAAI,GAAG,YAAY;AAChD,SAAK,OAAO,KAAK,sCAAsC,OAAO;AAE9D,QAAI,UAAU,YAAY;AACxB,YAAM,kBAAc,mCAAkB,OAAO;AAC7C,gBAAM,2BAAS,UAAU,YAAY,WAAW;AAChD;AAAA,IACF;AAEA,QAAI,UAAU,MAAM;AAClB,gBAAM,4BAAU,SAAS,UAAU,IAAI;AACvC;AAAA,IACF;AAGA,SAAK,OAAO;AAAA,MACV;AAAA,IACF;AACA,UAAM,UAAU,KAAK,OAAO,UAAU,EAAE;AACxC,UAAM,WAAW,MAAM,MAAM,GAAG,OAAO,cAAc,MAAM,IAAI;AAAA,MAC7D,SAAS,EAAE,QAAQ,kBAAkB;AAAA,IACvC,CAAC;AAED,QAAI,CAAC,SAAS,IAAI;AAChB,YAAM,IAAI;AAAA,QACR,gCAAgC,SAAS,MAAM,IAAI,SAAS,UAAU;AAAA,MACxE;AAAA,IACF;AAEA,UAAM,SAAS,IAAI,WAAW,MAAM,SAAS,YAAY,CAAC;AAC1D,cAAM,4BAAU,SAAS,MAAM;AAAA,EACjC;AAAA,EAEA,MAAc,sBACZ,SACA,YACA,WACe;AAEf,UAAM,eAAe;AAAA,MACnB,KAAK;AAAA,MACL;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAc,iBACZ,KACA,WACe;AACf,QAAI,CAAC,IAAI,WAAW,SAAS,GAAG;AAC9B,WAAK,OAAO;AAAA,QACV;AAAA,MACF;AACA;AAAA,IACF;AAEA,UAAM,UAAU,IAAI,MAAM,CAAC;AAC3B,SAAK,OAAO;AAAA,MACV;AAAA,IACF;AAEA,UAAM,WAAW,IAAI,aAAa,KAAK,MAAM;AAC7C,UAAM,eAAe,MAAM,SAAS,YAAY,SAAS,SAAS;AAGlE,UAAM,iBAAa,wBAAK,WAAW,aAAa;AAChD,UAAM,UAAU,aAAa;AAC7B,UAAM,YAAY;AAAA;AAAA,kEAE4C,aAAa,SAAS;AAAA;AAAA;AAAA,+BAGzD,eAAe,WAAW;AAAA;AAAA;AAAA;AAIrD,UAAM,gBAAgB,WAAW,YAAY,OAAO;AACpD,cAAM,yBAAO,SAAS,UAAU;AAEhC,SAAK,OAAO;AAAA,MACV,2BAA2B,aAAa,SAAS;AAAA,IACnD;AAAA,EACF;AACF;;;AVjyBO,IAAM,YAAN,MAAgB;AAAA,EACJ;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACT,SAAkC;AAAA,EAClC;AAAA,EAER,YAAY,SAAkB;AAC5B,UAAM;AAAA,MACJ;AAAA,MACA,UAAU,WAAW;AAAA,MACrB;AAAA,MACA,sBAAsB;AAAA,MACtB,yBAAyB;AAAA,IAC3B,IAAI;AAEJ,SAAK,SAAS;AAEd,QAAI,aAAa,SAAS;AACxB,WAAK,UAAU,QAAQ;AACvB,WAAK,OAAO;AAAA,IACd,OAAO;AACL,WAAK,OAAO,QAAQ;AACpB,WAAK,UAAU;AAAA,IACjB;AAEA,SAAK,UAAU;AACf,SAAK,WAAW,gBAAY,wBAAK,QAAQ,IAAI,GAAG,OAAO;AACvD,SAAK,sBAAsB;AAC3B,SAAK,yBAAyB;AAAA,EAChC;AAAA,EAEA,MAAM,OAAsB;AAC1B,SAAK,OAAO,KAAK,6BAA6B;AAE9C,SAAK,qBAAqB;AAC1B,SAAK,iBAAiB;AACtB,SAAK,sBAAsB;AAC3B,SAAK,kBAAkB;AAGvB,QAAI,KAAK,0BAA0B,CAAC,KAAK,SAAS;AAChD,WAAK,0BAA0B;AAC/B,WAAK,0BAA0B;AAAA,IACjC,WAAW,KAAK,0BAA0B,KAAK,SAAS;AACtD,WAAK,OAAO;AAAA,QACV;AAAA,MACF;AAAA,IACF;AAEA,QAAI,KAAK,SAAS;AAChB,WAAK,OAAO,KAAK,sCAAsC,KAAK,OAAO;AACnE,WAAK,SAAS,IAAI,2BAAQ;AAAA,QACxB,KAAK,EAAE,SAAS,KAAK,SAAS,SAAS,KAAK,QAAQ;AAAA,MACtD,CAAC;AACD,YAAM,KAAK,mBAAmB;AAC9B;AAAA,IACF;AAEA,SAAK,OAAO,KAAK,wCAAwC;AACzD,QAAI;AACF,WAAK,cAAc,IAAI,mBAAmB;AAAA,QACxC,QAAQ,KAAK;AAAA,QACb,UAAU,KAAK;AAAA,QACf,MAAM,KAAK;AAAA,QACX,qBAAqB,KAAK;AAAA,MAC5B,CAAC;AAED,YAAM,KAAK,YAAY,MAAM;AAE7B,YAAM,YAAY,oBAAoB,KAAK,IAAI;AAC/C,WAAK,SAAS,IAAI,2BAAQ;AAAA,QACxB,KAAK;AAAA,UACH,SAAS;AAAA,UACT,SAAS,KAAK;AAAA,QAChB;AAAA,MACF,CAAC;AAED,YAAM,KAAK,mBAAmB;AAC9B,WAAK,OAAO,KAAK,mBAAmB;AAAA,IACtC,SAAS,OAAO;AACd,WAAK,OAAO,MAAM,sCAAsC,KAAK;AAC7D,YAAM,IAAI,MAAM,mCAAmC,KAAK,EAAE;AAAA,IAC5D;AAAA,EACF;AAAA,EAEQ,uBAA6B;AACnC,YAAI,0BAAS,MAAM,UAAU;AAC3B,YAAM,IAAI;AAAA,QACR,iEAA6D,0BAAS;AAAA,MACxE;AAAA,IACF;AAAA,EACF;AAAA,EAEQ,mBAAyB;AAC/B,QAAI;AACF,+CAAS,YAAY,EAAE,OAAO,SAAS,CAAC;AAAA,IAC1C,QAAQ;AACN,YAAM,IAAI;AAAA,QACR;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA,EAEQ,wBAA8B;AACpC,QAAI;AACF,+CAAS,mBAAmB,EAAE,OAAO,SAAS,CAAC;AAAA,IACjD,QAAQ;AACN,YAAM,IAAI;AAAA,QACR;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA,EAEQ,oBAA0B;AAChC,QAAI;AACF,YAAM,oBAAgB,qCAAS,2BAA2B;AAAA,QACxD,UAAU;AAAA,MACZ,CAAC,EAAE,KAAK;AACR,YAAM,eAAe,cAAc,MAAM,eAAe;AACxD,UAAI,cAAc;AAChB,cAAM,QAAQ,SAAS,aAAa,CAAC,CAAC;AACtC,cAAM,QAAQ,SAAS,aAAa,CAAC,CAAC;AACtC,YAAI,QAAQ,MAAO,UAAU,MAAM,QAAQ,IAAK;AAC9C,gBAAM,IAAI;AAAA,YACR,sDAAsD,aAAa;AAAA,UACrE;AAAA,QACF;AAAA,MACF;AAAA,IACF,SAAS,OAAO;AACd,UAAI,iBAAiB,SAAS,MAAM,QAAQ,SAAS,aAAa,GAAG;AACnE,cAAM;AAAA,MACR;AACA,YAAM,IAAI,MAAM,+BAA+B;AAAA,IACjD;AAAA,EACF;AAAA,EAEQ,4BAAkC;AACxC,QAAI;AACF,+CAAS,gBAAgB,EAAE,OAAO,SAAS,CAAC;AAAA,IAC9C,QAAQ;AACN,YAAM,IAAI;AAAA,QACR;AAAA,MAEF;AAAA,IACF;AAAA,EACF;AAAA,EAEQ,4BAAkC;AACxC,QAAI;AACF,+CAAS,YAAY,EAAE,OAAO,SAAS,CAAC;AAAA,IAC1C,QAAQ;AACN,YAAM,IAAI;AAAA,QACR;AAAA,MAEF;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMQ,yBAAyB,OAAyB;AACxD,QAAI,iBAAiB,OAAO;AAE1B,UAAI,MAAM,QAAQ,SAAS,cAAc,GAAG;AAC1C,eAAO;AAAA,MACT;AACA,UACE,MAAM,iBAAiB,SACvB,MAAM,MAAM,QAAQ,SAAS,cAAc,GAC3C;AACA,eAAO;AAAA,MACT;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,MAAc,gBAA+B;AAC3C,SAAK,OAAO,KAAK,kCAAkC;AAInD,UAAM,mBAAmB,kBAAkB,KAAK,QAAQ,KAAK,IAAK;AAGlE,UAAM,cAAc,IAAI,mBAAmB;AAAA,MACzC,QAAQ,KAAK;AAAA,MACb,UAAU,KAAK;AAAA,MACf,MAAM,KAAK;AAAA,MACX,qBAAqB;AAAA;AAAA,IACvB,CAAC;AAED,UAAM,YAAY,YAAY;AAG9B,SAAK,QAAQ,QAAQ;AACrB,SAAK,SAAS,IAAI,2BAAQ;AAAA,MACxB,KAAK;AAAA,QACH,SAAS,oBAAoB,KAAK,IAAI;AAAA,QACtC,SAAS,KAAK;AAAA,MAChB;AAAA,IACF,CAAC;AAED,UAAM,KAAK,mBAAmB;AAC9B,SAAK,OAAO,KAAK,2CAA2C;AAAA,EAC9D;AAAA,EAEA,MAAc,qBAAoC;AAChD,UAAM,cAAc,WAAW;AAC/B,UAAM,gBAAgB,WAAW;AACjC,UAAM,cAAc,WAAW;AAC/B,QAAI,cAAc;AAElB,aAAS,UAAU,GAAG,WAAW,aAAa,WAAW;AACvD,UAAI;AACF,cAAM,KAAK,OAAQ,OAAO;AAC1B,aAAK,OAAO,KAAK,6BAA6B;AAC9C;AAAA,MACF,QAAQ;AACN,cAAM,MAAM,KAAK,IAAI;AACrB,YAAI,MAAM,eAAe,aAAa;AACpC,eAAK,OAAO;AAAA,YACV;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,UACF;AACA,wBAAc;AAAA,QAChB;AAEA,YAAI,UAAU,aAAa;AACzB,gBAAM,IAAI,QAAQ,CAAC,YAAY,WAAW,SAAS,aAAa,CAAC;AAAA,QACnE;AAAA,MACF;AAAA,IACF;AAEA,UAAM,IAAI,MAAM,sDAAsD;AAAA,EACxE;AAAA,EAEA,MAAa,MACX,KACA,UACA,YACA,sBACA,SACA,aACkC;AAClC,QAAI,CAAC,KAAK,QAAQ;AAChB,YAAM,IAAI;AAAA,QACR;AAAA,MACF;AAAA,IACF;AAGA,QAAI,QAAQ,iBAAiB,CAAC,KAAK,SAAS;AAC1C,WAAK,0BAA0B;AAC/B,WAAK,0BAA0B;AAAA,IACjC;AAGA,UAAM,kBACJ,QAAQ,yBAAyB,UACjC,QAAQ,iBAAiB;AAE3B,QAAI,iBAAiB;AACnB,aAAO,KAAK;AAAA,QACV;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,IACF;AAGA,UAAM,aAAa,CAAC,KAAK,WAAW,KAAK,SAAS;AAClD,UAAM,cAAc,WAAW;AAC/B,QAAI,UAAU;AAEd,WAAO,WAAW,aAAa;AAC7B,UAAI;AAEF,cAAM,2BACJ,KAAK,0BAA0B,CAAC,KAAK;AACvC,cAAM,YAAY,IAAI;AAAA,UACpB,KAAK;AAAA,UACL,KAAK;AAAA,UACL;AAAA,UACA,KAAK;AAAA,QACP;AACA,eAAO,MAAM,UAAU;AAAA,UACrB;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,QACF;AAAA,MACF,SAAS,OAAO;AAEd,YAAI,aAAa,SAAS;AACxB,gBAAM;AAAA,QACR;AAGA,YACE,cACA,KAAK,yBAAyB,KAAK,KACnC,UAAU,aACV;AACA,eAAK,OAAO;AAAA,YACV;AAAA,UACF;AACA,gBAAM,KAAK,cAAc;AACzB;AACA;AAAA,QACF;AACA,cAAM;AAAA,MACR;AAAA,IACF;AAGA,WAAO;AAAA,EAET;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAc,kBACZ,KACA,UACA,YACA,sBACA,SACA,aACkC;AAElC,UAAM,aAAa,CAAC,KAAK,WAAW,KAAK,SAAS;AAClD,UAAM,cAAc,WAAW;AAC/B,QAAI,UAAU;AAEd,WAAO,WAAW,aAAa;AAC7B,UAAI;AACF,cAAM,2BACJ,KAAK,0BAA0B,CAAC,KAAK;AACvC,cAAM,YAAY,IAAI;AAAA,UACpB,KAAK;AAAA,UACL,KAAK;AAAA,UACL;AAAA,UACA,KAAK;AAAA,QACP;AACA,cAAM,SAAS,MAAM,UAAU;AAAA,UAC7B;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,QACF;AACA,eAAO,OAAO;AAAA,MAChB,SAAS,OAAO;AAEd,YAAI,aAAa,SAAS;AACxB,gBAAM;AAAA,QACR;AAGA,YACE,cACA,KAAK,yBAAyB,KAAK,KACnC,UAAU,aACV;AACA,eAAK,OAAO;AAAA,YACV;AAAA,UACF;AACA,gBAAM,KAAK,cAAc;AACzB;AACA;AAAA,QACF;AACA,cAAM;AAAA,MACR;AAAA,IACF;AAGA,WAAO;AAAA,EAET;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAa,UAAyB;AACpC,SAAK,OAAO,KAAK,0BAA0B;AAE3C,QAAI;AAEF,UAAI,CAAC,KAAK,WAAW,KAAK,MAAM;AAC9B,cAAM,mBAAmB,kBAAkB,KAAK,QAAQ,KAAK,IAAI;AAAA,MACnE;AAAA,IACF,SAAS,OAAO;AACd,WAAK,OAAO,MAAM,sCAAsC,KAAK;AAAA,IAC/D,UAAE;AAEA,WAAK,QAAQ,QAAQ;AACrB,WAAK,SAAS;AACd,WAAK,OAAO,KAAK,sBAAsB;AAAA,IACzC;AAAA,EACF;AACF;;;AuBzeA,IAAM,qBAAqB;AAO3B,IAAM,gCAAgC;AAGtC,IAAM,uBAAiC;AAAA,EACrC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAGA,IAAM,+BAAyC;AAAA,EAC7C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAGA,IAAM,+BAAyC;AAAA,EAC7C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAMA,IAAM,qCAAqC;AAG3C,IAAM,iCAAiC;AAGvC,IAAMC,gBAAe;AAGrB,IAAMC,aAAY;AAWX,IAAM,uBAAN,MAA2B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQhC,OAAO,SACL,UACA,mBACqB;AACrB,UAAM,SAA4B,CAAC;AAEnC,UAAM,eAAe,SAAS;AAAA,MAC5B,CAAC,OAAO,GAAG,SAAS,aAAa,GAAG,QAAQ,SAAS;AAAA,IACvD;AAEA,QAAI,aAAa,WAAW,GAAG;AAC7B,aAAO,EAAE,SAAS,MAAM,QAAQ,CAAC,EAAE;AAAA,IACrC;AAEA,UAAM,mBAAmB,KAAK,sBAAsB,YAAY;AAChE,QAAI,kBAAkB;AACpB,aAAO,KAAK,gBAAgB;AAAA,IAC9B;AAEA,QAAI,oBAAoB,CAAC,GAAG,WAAW,IAAI,GAAG;AAC5C,YAAM,cAAc,KAAK,oBAAoB,YAAY;AACzD,UAAI,aAAa;AACf,eAAO,KAAK,WAAW;AAAA,MACzB;AAAA,IACF;AAEA,UAAM,YAAY,KAAK,sBAAsB,YAAY;AACzD,QAAI,WAAW;AACb,aAAO,KAAK,SAAS;AAAA,IACvB;AAEA,UAAM,kBAAkB,KAAK,wBAAwB,YAAY;AACjE,QAAI,iBAAiB;AACnB,aAAO,KAAK,eAAe;AAAA,IAC7B;AAEA,WAAO,EAAE,SAAS,OAAO,WAAW,GAAG,OAAO;AAAA,EAChD;AAAA;AAAA;AAAA;AAAA,EAKA,OAAe,sBACb,UACwB;AACxB,UAAM,mBAA6B,CAAC;AAEpC,eAAW,MAAM,UAAU;AACzB,iBAAW,WAAW,sBAAsB;AAC1C,YAAI,QAAQ,KAAK,GAAG,OAAO,GAAG;AAC5B,2BAAiB,KAAK,GAAG,KAAK;AAC9B;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAEA,QAAI,iBAAiB,WAAW,EAAG,QAAO;AAE1C,WAAO;AAAA,MACL,MAAM;AAAA,MACN,SAAS,oDAAoD,iBAAiB,MAAM;AAAA,MACpF;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,OAAe,oBACb,UACwB;AACxB,UAAM,aAAa,SAAS,IAAI,CAAC,OAAO,GAAG,OAAO,EAAE,KAAK,EAAE;AAC3D,UAAM,gBAAgB,WAAW,QAAQ,OAAO,EAAE;AAElD,QAAI,cAAc,SAAS,oBAAoB;AAC7C,aAAO;AAAA,IACT;AAEA,UAAM,cAAc,WAAW,MAAMD,aAAY,GAAG,UAAU;AAC9D,UAAM,WAAW,WAAW,MAAMC,UAAS,GAAG,UAAU;AACxD,UAAM,iBAAiB,cAAc;AACrC,UAAM,QAAQ,iBAAiB,cAAc;AAE7C,QAAI,QAAQ,+BAA+B;AACzC,aAAO;AAAA,QACL,MAAM;AAAA,QACN,SAAS,mCAAmC,QAAQ,KAAK,QAAQ,CAAC,CAAC,uCAAuC,gCAAgC,GAAG;AAAA,QAC7I,kBAAkB,SAAS,IAAI,CAAC,OAAO,GAAG,KAAK;AAAA,MACjD;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,OAAe,sBACb,UACwB;AACxB,UAAM,mBAA6B,CAAC;AACpC,UAAM,cAAc;AAAA,MAClB,GAAG;AAAA,MACH,GAAG;AAAA,IACL;AAEA,eAAW,MAAM,UAAU;AACzB,iBAAW,WAAW,aAAa;AACjC,YAAI,QAAQ,KAAK,GAAG,OAAO,GAAG;AAC5B,2BAAiB,KAAK,GAAG,KAAK;AAC9B;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAEA,QAAI,iBAAiB,WAAW,EAAG,QAAO;AAE1C,WAAO;AAAA,MACL,MAAM;AAAA,MACN,SAAS,uEAAuE,iBAAiB,MAAM;AAAA,MACvG;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,OAAe,wBACb,UACwB;AACxB,UAAM,aAAa,SAAS,IAAI,CAAC,OAAO,GAAG,OAAO,EAAE,KAAK,IAAI;AAE7D,QAAI,WAAW,KAAK,EAAE,WAAW,EAAG,QAAO;AAG3C,UAAM,kBAAkB;AACxB,QAAI,wBAAwB;AAE5B,QAAI;AACJ,YAAQ,QAAQ,gBAAgB,KAAK,UAAU,OAAO,MAAM;AAC1D,YAAM,eAAe,MAAM,CAAC;AAE5B,YAAM,UAAU,MAAM,CAAC;AACvB,YAAM,QAAQ,QAAQ,MAAM,KAAK,EAAE,OAAO,CAAC,MAAM,MAAM,YAAY;AAEnE,UAAI,MAAM,UAAU,gCAAgC;AAElD,iCAAyB,QAAQ;AAAA,MACnC;AAAA,IACF;AAEA,QAAI,0BAA0B,EAAG,QAAO;AAExC,UAAM,QAAQ,wBAAwB,WAAW;AAEjD,QAAI,QAAQ,mCAAoC,QAAO;AAEvD,WAAO;AAAA,MACL,MAAM;AAAA,MACN,SAAS,4CAA4C,QAAQ,KAAK,QAAQ,CAAC,CAAC;AAAA,MAC5E,kBAAkB,SAAS,IAAI,CAAC,OAAO,GAAG,KAAK;AAAA,IACjD;AAAA,EACF;AACF;","names":["import_node_child_process","import_node_os","import_node_path","Error","import_node_fs","import_promises","import_node_path","import_node_fs","import_node_path","import_promises","import_node_child_process","import_promises","err","import_node_fs","import_node_path","import_node_fs","import_node_path","import_node_fs","import_v4","DEFAULT_MAX_RETRIES","detectedLanguages","import_node_fs","import_node_path","import_node_fs","import_promises","import_node_path","import_node_fs","import_node_path","resultPath","HANGUL_REGEX","CJK_REGEX"]}
|