document-ocr 0.0.0-stage → 3.1.3
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/LICENSE +21 -0
- package/README.md +260 -2
- package/dist/index.cjs +659 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +293 -0
- package/dist/index.d.ts +293 -0
- package/dist/index.js +616 -0
- package/dist/index.js.map +1 -0
- package/hosting/.dockerignore +17 -0
- package/hosting/.gcloudignore +14 -0
- package/hosting/FEATURES.md +172 -0
- package/hosting/HOSTING.md +268 -0
- package/hosting/LICENSE +21 -0
- package/hosting/README.md +369 -0
- package/hosting/core/__init__.py +0 -0
- package/hosting/core/aadhaar_extractor.py +298 -0
- package/hosting/core/back_page_extractor.py +492 -0
- package/hosting/core/barcodes.py +125 -0
- package/hosting/core/card_recovery.py +244 -0
- package/hosting/core/document_bundle.py +82 -0
- package/hosting/core/document_classifier.py +233 -0
- package/hosting/core/document_input.py +135 -0
- package/hosting/core/document_registry.py +143 -0
- package/hosting/core/driving_licence_extractor.py +510 -0
- package/hosting/core/ead_recovery.py +54 -0
- package/hosting/core/evidence.py +54 -0
- package/hosting/core/form_recovery.py +61 -0
- package/hosting/core/jobs.py +232 -0
- package/hosting/core/kyc_ocr.py +132 -0
- package/hosting/core/kyc_validation.py +411 -0
- package/hosting/core/model_setup.py +13 -0
- package/hosting/core/mrz_parser.py +336 -0
- package/hosting/core/mrz_recovery.py +193 -0
- package/hosting/core/npr_extractor.py +410 -0
- package/hosting/core/nrega_extractor.py +827 -0
- package/hosting/core/ocr_engine.py +207 -0
- package/hosting/core/page_classifier.py +137 -0
- package/hosting/core/pan_extractor.py +183 -0
- package/hosting/core/pipeline.py +712 -0
- package/hosting/core/preprocessor.py +297 -0
- package/hosting/core/recovery_budget.py +67 -0
- package/hosting/core/redaction.py +48 -0
- package/hosting/core/review.html +244 -0
- package/hosting/core/structured_documents.py +422 -0
- package/hosting/core/travel_mrz.py +164 -0
- package/hosting/core/travel_recovery.py +187 -0
- package/hosting/core/validator.py +396 -0
- package/hosting/core/validators.py +179 -0
- package/hosting/core/voter_id_extractor.py +363 -0
- package/hosting/deploy/cloudflare/deploy.sh +44 -0
- package/hosting/deploy/cloudflare/package-lock.json +3088 -0
- package/hosting/deploy/cloudflare/package.json +29 -0
- package/hosting/deploy/cloudflare/src/gateway.ts +42 -0
- package/hosting/deploy/cloudflare/src/index.ts +21 -0
- package/hosting/deploy/cloudflare/test/gateway.test.ts +59 -0
- package/hosting/deploy/cloudflare/tsconfig.json +12 -0
- package/hosting/deploy/cloudflare/worker-configuration.d.ts +16253 -0
- package/hosting/deploy/cloudflare/wrangler.jsonc +26 -0
- package/hosting/deploy/cloudrun/cloudbuild.yaml +18 -0
- package/hosting/deploy/cloudrun/deploy.sh +71 -0
- package/hosting/deploy/cloudrun/render_service.py +47 -0
- package/hosting/deploy/common.sh +14 -0
- package/hosting/deploy/docker/Dockerfile +16 -0
- package/hosting/deploy/docker/server.py +313 -0
- package/hosting/deploy/lambda/Dockerfile.lambda +11 -0
- package/hosting/deploy/lambda/deploy.sh +22 -0
- package/hosting/deploy/lambda/handler.py +132 -0
- package/hosting/deploy/lambda/template.yaml +59 -0
- package/hosting/deploy/local/dev.sh +18 -0
- package/hosting/deploy/server/Caddyfile +5 -0
- package/hosting/deploy/server/compose.jobs.yaml +36 -0
- package/hosting/deploy/server/compose.yaml +38 -0
- package/hosting/deploy/server/deploy.sh +23 -0
- package/hosting/infra/cloudrun/main.tf +104 -0
- package/hosting/infra/cloudrun/terraform.tfvars.example +7 -0
- package/hosting/infra/cloudrun/variables.tf +55 -0
- package/hosting/infra/lambda/main.tf +63 -0
- package/hosting/infra/lambda/terraform.tfvars.example +6 -0
- package/hosting/infra/lambda/variables.tf +37 -0
- package/hosting/pyproject.toml +65 -0
- package/hosting/requirements.lock +134 -0
- package/package.json +76 -4
- package/python/core/__init__.py +0 -0
- package/python/core/aadhaar_extractor.py +298 -0
- package/python/core/back_page_extractor.py +492 -0
- package/python/core/barcodes.py +125 -0
- package/python/core/card_recovery.py +244 -0
- package/python/core/document_bundle.py +82 -0
- package/python/core/document_classifier.py +233 -0
- package/python/core/document_input.py +135 -0
- package/python/core/document_registry.py +143 -0
- package/python/core/driving_licence_extractor.py +510 -0
- package/python/core/ead_recovery.py +54 -0
- package/python/core/evidence.py +54 -0
- package/python/core/form_recovery.py +61 -0
- package/python/core/jobs.py +232 -0
- package/python/core/kyc_ocr.py +132 -0
- package/python/core/kyc_validation.py +411 -0
- package/python/core/model_setup.py +13 -0
- package/python/core/mrz_parser.py +336 -0
- package/python/core/mrz_recovery.py +193 -0
- package/python/core/npr_extractor.py +410 -0
- package/python/core/nrega_extractor.py +827 -0
- package/python/core/ocr_engine.py +207 -0
- package/python/core/page_classifier.py +137 -0
- package/python/core/pan_extractor.py +183 -0
- package/python/core/pipeline.py +712 -0
- package/python/core/preprocessor.py +297 -0
- package/python/core/recovery_budget.py +67 -0
- package/python/core/redaction.py +48 -0
- package/python/core/review.html +244 -0
- package/python/core/structured_documents.py +422 -0
- package/python/core/travel_mrz.py +164 -0
- package/python/core/travel_recovery.py +187 -0
- package/python/core/validator.py +396 -0
- package/python/core/validators.py +179 -0
- package/python/core/voter_id_extractor.py +363 -0
- package/python/pyproject.toml +34 -0
- package/python/requirements.lock +134 -0
- package/python/server.py +313 -0
- package/scripts/dococr.mjs +163 -0
- package/scripts/postinstall.mjs +173 -0
- package/scripts/sync-hosting.mjs +35 -0
- package/scripts/sync-python.sh +20 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/errors.ts","../src/image.ts","../src/retry.ts","../src/local-server.ts","../src/client.ts"],"sourcesContent":["/** Preserve transport status separately from the server's human-readable message. */\nexport class HttpError extends Error {\n constructor(readonly status: number, message: string) {\n super(message)\n this.name = 'HttpError'\n }\n\n get retryable(): boolean {\n return this.status === 408 || this.status === 429 || this.status >= 500\n }\n}\n\n/** Invalid client input cannot succeed by retrying the same request. */\nexport class InputValidationError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'InputValidationError'\n }\n}\n","import type { ImageInput } from './types'\nimport { HttpError, InputValidationError } from './errors'\n\nfunction isHttpUrl(input: string): boolean {\n return /^https?:\\/\\//i.test(input)\n}\n\nasync function fetchImage(url: string, signal?: AbortSignal): Promise<Response> {\n const res = await fetch(url, { signal })\n if (!res.ok) {\n await res.body?.cancel()\n throw new HttpError(res.status, `Failed to fetch image from URL: ${res.status}`)\n }\n return res\n}\n\n/**\n * Normalise any supported image input to a base64 string.\n */\nexport async function normalizeToBase64(input: ImageInput, signal?: AbortSignal): Promise<string> {\n // Already a base64 string\n if (typeof input === 'string' && !isHttpUrl(input)) {\n validateBase64(input)\n return input\n }\n\n // URL — fetch first\n if (typeof input === 'string' && isHttpUrl(input)) {\n const res = await fetchImage(input, signal)\n const buf = await res.arrayBuffer()\n return arrayBufferToBase64(buf)\n }\n\n // Buffer (Node.js)\n if (typeof Buffer !== 'undefined' && Buffer.isBuffer(input)) {\n return input.toString('base64')\n }\n\n // ArrayBuffer\n if (input instanceof ArrayBuffer) {\n return arrayBufferToBase64(input)\n }\n\n // Blob / File\n if (typeof Blob !== 'undefined' && input instanceof Blob) {\n const buf = await input.arrayBuffer()\n return arrayBufferToBase64(buf)\n }\n\n throw new InputValidationError('Unsupported image input type')\n}\n\n/**\n * Normalise any supported image input to a Blob for FormData upload.\n */\nexport async function normalizeToBlob(input: ImageInput, signal?: AbortSignal): Promise<Blob> {\n // Blob / File\n if (typeof Blob !== 'undefined' && input instanceof Blob) {\n if (input.type.startsWith('image/') || input.type === 'application/pdf') return input\n const header = new Uint8Array(await input.slice(0, 16).arrayBuffer())\n return new Blob([input], { type: mediaType(header) })\n }\n\n // Buffer (Node.js) — copy to a fresh ArrayBuffer-backed Uint8Array. This\n // avoids ReadableStream issues in Node 24+, only copies the Buffer's own\n // window (respecting byteOffset/byteLength on a subarray view), and yields a\n // BlobPart typed over ArrayBuffer (not the SharedArrayBuffer-inclusive\n // ArrayBufferLike that `input.buffer` widens to).\n if (typeof Buffer !== 'undefined' && Buffer.isBuffer(input)) {\n const copy = new Uint8Array(input)\n return new Blob([copy], { type: mediaType(copy) })\n }\n\n // ArrayBuffer\n if (input instanceof ArrayBuffer) {\n return new Blob([input], { type: mediaType(new Uint8Array(input)) })\n }\n\n // URL — fetch\n if (typeof input === 'string' && isHttpUrl(input)) {\n const res = await fetchImage(input, signal)\n return normalizeToBlob(await res.blob(), signal)\n }\n\n // Base64 string\n if (typeof input === 'string') {\n validateBase64(input)\n const binary = atob(input)\n const bytes = new Uint8Array(binary.length)\n for (let i = 0; i < binary.length; i++) {\n bytes[i] = binary.charCodeAt(i)\n }\n return new Blob([bytes], { type: mediaType(bytes) })\n }\n\n throw new InputValidationError('Unsupported image input type')\n}\n\nfunction validateBase64(value: string): void {\n if (!value || !/^[A-Za-z0-9+/]*={0,2}$/.test(value) || value.length % 4 === 1\n || (value.includes('=') && value.length % 4 !== 0)) {\n throw new InputValidationError('Image string must be base64 or an HTTP(S) URL')\n }\n}\n\nfunction mediaType(bytes: Uint8Array): string {\n const header = String.fromCharCode(...bytes.subarray(0, 16))\n if (header.startsWith('%PDF-')) return 'application/pdf'\n if (header.startsWith('\\x89PNG\\r\\n\\x1a\\n')) return 'image/png'\n if (header.startsWith('GIF87a') || header.startsWith('GIF89a')) return 'image/gif'\n if (header.startsWith('II*\\x00') || header.startsWith('MM\\x00*')) return 'image/tiff'\n if (header.startsWith('RIFF') && header.slice(8, 12) === 'WEBP') return 'image/webp'\n if (header.startsWith('BM')) return 'image/bmp'\n if (header.slice(4, 8) === 'ftyp') return header.slice(8, 12) === 'avif' ? 'image/avif' : 'image/heif'\n // Preserve the historic Buffer behavior for other image encodings; the\n // server validates actual bytes. Untyped multipart blobs are rejected there.\n return 'image/jpeg'\n}\n\nfunction arrayBufferToBase64(buffer: ArrayBuffer): string {\n if (typeof Buffer !== 'undefined') {\n return Buffer.from(buffer).toString('base64')\n }\n // Browser\n const bytes = new Uint8Array(buffer)\n let binary = ''\n for (let i = 0; i < bytes.byteLength; i++) {\n binary += String.fromCharCode(bytes[i])\n }\n return btoa(binary)\n}\n","import { HttpError, InputValidationError } from './errors'\n\nexport interface RetryOptions {\n retries: number\n timeoutMs: number\n}\n\nexport function validateRetryOptions({ retries, timeoutMs }: RetryOptions): void {\n if (!Number.isInteger(retries) || retries < 0) {\n throw new RangeError('retries must be a non-negative integer')\n }\n if (!Number.isFinite(timeoutMs) || timeoutMs <= 0 || timeoutMs > 2_147_483_647) {\n throw new RangeError('timeoutMs must be between 0 and 2147483647 milliseconds (exclusive of 0)')\n }\n}\n\nexport async function withRetry<T>(\n fn: (signal: AbortSignal) => Promise<T>,\n options: RetryOptions,\n): Promise<T> {\n validateRetryOptions(options)\n const { retries, timeoutMs } = options\n let lastError: Error | undefined\n\n for (let attempt = 0; attempt <= retries; attempt++) {\n const controller = new AbortController()\n let timeout: ReturnType<typeof setTimeout> | undefined\n const deadline = new Promise<never>((_, reject) => {\n timeout = setTimeout(() => {\n const error = new Error(`OCR request timed out after ${timeoutMs}ms`)\n error.name = 'TimeoutError'\n reject(error)\n controller.abort(error)\n }, timeoutMs)\n })\n\n try {\n return await Promise.race([fn(controller.signal), deadline])\n } catch (err) {\n lastError = err instanceof Error ? err : new Error(String(err))\n\n if (lastError instanceof InputValidationError || (lastError instanceof HttpError && !lastError.retryable)) {\n throw lastError\n }\n\n } finally {\n clearTimeout(timeout)\n }\n\n if (attempt < retries) {\n // Exponential backoff: 500ms, 1s, 2s...\n const delay = Math.min(500 * Math.pow(2, attempt), 5000)\n await new Promise((r) => setTimeout(r, delay))\n }\n }\n\n throw lastError ?? new Error('All retries exhausted')\n}\n","import { type ChildProcess, spawn } from 'node:child_process'\nimport { createServer } from 'node:net'\nimport { existsSync } from 'node:fs'\nimport { join, dirname } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { setTimeout as delay } from 'node:timers/promises'\n\n// Resolve package directory — works for both ESM and CJS builds\nfunction resolvePackageDir(): string {\n // In ESM context, use import.meta.url\n // In CJS context, use __dirname\n try {\n if (typeof import.meta?.url === 'string') {\n // dist/index.js -> package root\n return join(dirname(fileURLToPath(import.meta.url)), '..')\n }\n } catch {\n // fallback\n }\n // CJS fallback: __dirname is dist/, go up one level\n if (typeof __dirname === 'string') {\n return join(__dirname, '..')\n }\n throw new Error('Cannot resolve document-ocr package directory')\n}\n\nconst HEALTH_POLL_INTERVAL_MS = 500\nconst HEALTH_TIMEOUT_MS = 120_000 // 120s for first-time model download\n\n/**\n * Finds a free port by briefly binding to port 0.\n */\nfunction findFreePort(): Promise<number> {\n return new Promise((resolve, reject) => {\n const srv = createServer()\n srv.listen(0, '127.0.0.1', () => {\n const addr = srv.address()\n if (addr && typeof addr === 'object') {\n const port = addr.port\n srv.close(() => resolve(port))\n } else {\n srv.close(() => reject(new Error('Could not determine free port')))\n }\n })\n srv.on('error', reject)\n })\n}\n\n/**\n * Manages a local Python FastAPI server for passport OCR.\n *\n * Uses a module-level singleton so multiple PassportOCR instances\n * share a single server process.\n */\nexport class LocalServer {\n private process: ChildProcess | null = null\n private ready: Promise<string> | null = null\n private startupAbort: AbortController | null = null\n private cleanupHandler: (() => void) | null = null\n\n /**\n * Ensure the local Python server is running and return its endpoint URL.\n */\n async ensureRunning(): Promise<string> {\n // If already starting or running, return the existing promise\n if (this.ready) {\n return this.ready\n }\n\n const controller = new AbortController()\n this.startupAbort = controller\n const ready = this._start(controller)\n this.ready = ready\n\n // If start fails, clear the promise so we can retry\n ready.catch(() => {\n if (this.ready === ready) this.ready = null\n })\n\n return ready\n }\n\n private async _start(controller: AbortController): Promise<string> {\n const packageDir = resolvePackageDir()\n const pythonDir = join(packageDir, 'python')\n const venvDir = join(packageDir, '.venv')\n\n // Check that setup was completed\n const markerFile = join(venvDir, '.setup-complete')\n if (!existsSync(markerFile)) {\n throw new Error(\n 'document-ocr: Python environment not set up. ' +\n 'Run \"npm rebuild document-ocr\" or ensure Python 3.14 and uv are installed. ' +\n 'Check that the postinstall script ran successfully.',\n )\n }\n\n // Find the venv python executable\n const isWindows = process.platform === 'win32'\n const pythonExe = isWindows\n ? join(venvDir, 'Scripts', 'python.exe')\n : join(venvDir, 'bin', 'python')\n\n if (!existsSync(pythonExe)) {\n throw new Error(\n `document-ocr: Python executable not found at ${pythonExe}. ` +\n 'Run \"npm rebuild document-ocr\" to re-create the virtual environment.',\n )\n }\n\n // Find a free port\n const port = await findFreePort()\n controller.signal.throwIfAborted()\n const endpoint = `http://127.0.0.1:${port}`\n\n // Spawn the uvicorn server\n const args = [\n '-m',\n 'uvicorn',\n 'server:app',\n '--host',\n '127.0.0.1',\n '--port',\n String(port),\n '--log-level',\n 'warning',\n ]\n\n const child = spawn(pythonExe, args, {\n cwd: pythonDir,\n env: {\n ...process.env,\n PYTHONPATH: pythonDir,\n PADDLE_PDX_DISABLE_MODEL_SOURCE_CHECK: 'True',\n // Suppress paddle logging noise\n GLOG_minloglevel: '2',\n },\n stdio: ['ignore', 'pipe', 'pipe'],\n // Detach on Windows to avoid blocking\n detached: isWindows,\n })\n this.process = child\n\n // Drain child output so startup can't deadlock on a full pipe buffer.\n let processLogs = ''\n const appendLogs = (chunk: Buffer) => {\n processLogs += chunk.toString()\n if (processLogs.length > 4096) {\n processLogs = processLogs.slice(-4096)\n }\n }\n\n child.stdout?.on('data', appendLogs)\n child.stderr?.on('data', appendLogs)\n\n const cleanup = () => child.kill('SIGTERM')\n this.cleanupHandler = cleanup\n process.once('exit', cleanup)\n\n // Handle unexpected exit\n const exitPromise = new Promise<never>((_, reject) => {\n const failed = (error: Error) => {\n process.removeListener('exit', cleanup)\n if (this.cleanupHandler === cleanup) this.cleanupHandler = null\n if (this.process === child) {\n this.ready = null\n this.process = null\n }\n controller.abort(error)\n reject(error)\n }\n child.once('error', failed)\n child.once('exit', (code, signal) => failed(new Error(\n `document-ocr: Python server exited (code=${code}, signal=${signal}).\\n${processLogs}`,\n )))\n })\n\n try {\n return await Promise.race([this._waitForReady(endpoint, controller.signal), exitPromise])\n } catch (error) {\n if (this.process === child) await this.stop()\n throw error\n } finally {\n if (controller.signal.aborted) process.removeListener('exit', cleanup)\n }\n }\n\n private async _waitForReady(endpoint: string, signal?: AbortSignal): Promise<string> {\n const readinessUrl = `${endpoint}/ready`\n const startTime = Date.now()\n\n while (Date.now() - startTime < HEALTH_TIMEOUT_MS) {\n signal?.throwIfAborted()\n let terminalFailure: string | null = null\n const probeController = new AbortController()\n const cancelProbe = () => probeController.abort(signal?.reason)\n signal?.addEventListener('abort', cancelProbe, { once: true })\n const timeout = setTimeout(() => probeController.abort(), 2000)\n try {\n const res = await fetch(readinessUrl, {\n signal: probeController.signal,\n })\n const body = (await res.json().catch(() => null)) as {\n status?: unknown\n error?: unknown\n } | null\n signal?.throwIfAborted()\n if (res.ok && body?.status === 'ready') {\n return endpoint\n }\n if (body?.status === 'model_init_failed') {\n const detail =\n typeof body.error === 'string'\n ? body.error.replace(/\\s+/g, ' ').slice(0, 500)\n : 'configured OCR model initialization failed'\n terminalFailure = detail\n }\n } catch {\n signal?.throwIfAborted()\n // Server not ready yet, keep polling\n } finally {\n clearTimeout(timeout)\n signal?.removeEventListener('abort', cancelProbe)\n }\n\n if (terminalFailure) {\n throw new Error(\n `document-ocr: OCR model initialization failed: ${terminalFailure}`,\n )\n }\n\n await delay(HEALTH_POLL_INTERVAL_MS, undefined, { signal })\n }\n\n // Timed out\n throw new Error(\n `document-ocr: Python server did not become ready within ${HEALTH_TIMEOUT_MS / 1000}s. ` +\n 'This might happen on first run while models are being downloaded.',\n )\n }\n\n /**\n * Stop the local Python server if running.\n */\n async stop(): Promise<void> {\n if (this.cleanupHandler) {\n process.removeListener('exit', this.cleanupHandler)\n this.cleanupHandler = null\n }\n this.startupAbort?.abort(new Error('document-ocr: local server stopped'))\n this.startupAbort = null\n this.ready = null\n\n if (this.process) {\n const proc = this.process\n this.process = null\n if (proc.exitCode !== null || proc.signalCode !== null) return\n await new Promise<void>((resolve) => {\n const done = () => {\n clearTimeout(forceKill)\n clearTimeout(deadline)\n proc.removeListener('exit', done)\n proc.removeListener('error', done)\n resolve()\n }\n const forceKill = setTimeout(() => {\n try { proc.kill('SIGKILL') } catch { done() }\n }, 5000)\n const deadline = setTimeout(done, 6000)\n proc.once('exit', done)\n proc.once('error', done)\n try {\n if (!proc.kill('SIGTERM')) done()\n } catch {\n done()\n }\n })\n }\n }\n}\n\n// Module-level singleton — shared across all PassportOCR instances\nlet _instance: LocalServer | null = null\n\nexport function getLocalServer(): LocalServer {\n if (!_instance) {\n _instance = new LocalServer()\n }\n return _instance\n}\n","import type {\n BatchScanResult, DocumentCatalog, DocumentOCROptions, DocumentScanResult, DocumentScanGroupResult,\n ImageInput, ScanOptions, S3ImageInput, JobOptions, OCRJob,\n} from './types'\nimport { normalizeToBase64, normalizeToBlob } from './image'\nimport { validateRetryOptions, withRetry } from './retry'\nimport { HttpError, InputValidationError } from './errors'\nimport { getLocalServer } from './local-server'\n\nconst MAX_LAMBDA_PAYLOAD_BYTES = 6 * 1024 * 1024\n\nexport class DocumentOCR {\n private mode: 'local' | 'http' | 'lambda'\n private endpoint?: string\n private functionName?: string\n private timeoutMs: number\n private retries: number\n private apiKey?: string\n private authHeaders?: DocumentOCROptions['authHeaders']\n\n constructor(options: DocumentOCROptions = {}) {\n this.mode = options.mode ?? 'local'\n this.endpoint = options.endpoint\n this.functionName = options.functionName\n this.timeoutMs = options.timeoutMs ?? 30000\n this.retries = options.retries ?? 2\n this.apiKey = options.apiKey\n this.authHeaders = options.authHeaders\n validateRetryOptions({ retries: this.retries, timeoutMs: this.timeoutMs })\n if (this.mode === 'http' && !this.endpoint) throw new Error('endpoint is required for http mode')\n if (this.mode === 'lambda' && !this.functionName) throw new Error('functionName is required for lambda mode')\n if (this.mode === 'local' && this.endpoint) {\n console.warn('document-ocr: endpoint is ignored in local mode. ' +\n 'Use mode: \"http\" if you want to connect to an external server.')\n }\n }\n\n async scan(image: ImageInput, options: ScanOptions = {}): Promise<DocumentScanResult> {\n const parameters = scanParameters(options)\n return this.retry(async (signal) => {\n if (this.mode === 'lambda') {\n const base64 = await normalizeToBase64(image, signal)\n return this.invokeLambda({ image_base64: base64, ...parameters }, signal)\n }\n const form = new FormData()\n form.append('image', await normalizeToBlob(image, signal), 'document')\n appendParameters(form, parameters)\n const { status, data } = await this.request('/scan', signal, { method: 'POST', body: form })\n return parseScanResponse(status, data)\n })\n }\n\n async scanBatch(images: ImageInput[], options: ScanOptions = {}): Promise<BatchScanResult> {\n return this.uploadMany('/scan/batch', images, options)\n }\n\n async scanDocument(images: ImageInput[], options: ScanOptions = {}): Promise<DocumentScanGroupResult> {\n return this.uploadMany('/scan/document', images, options) as Promise<DocumentScanGroupResult>\n }\n\n private async uploadMany(path: string, images: ImageInput[], options: ScanOptions): Promise<BatchScanResult> {\n this.requireHttp(path === '/scan/batch' ? 'scanBatch' : 'scanDocument')\n if (!Array.isArray(images) || images.length === 0) throw new InputValidationError('scanBatch requires at least one image')\n const parameters = scanParameters(options)\n return this.retry(async (signal) => {\n const form = new FormData()\n for (const [index, image] of images.entries()) {\n form.append('images', await normalizeToBlob(image, signal), `document-${index}`)\n }\n appendParameters(form, parameters)\n const { status, data } = await this.request(path, signal, { method: 'POST', body: form })\n const body = asObject(data)\n const valid = body !== null && ['success', 'partial', 'failure'].includes(String(body.status))\n && Array.isArray(body.results) && body.results.every(isScanResult) && Array.isArray(body.errors)\n checkStatus(status, data, status === 422 && valid)\n if (!valid) throw new Error('Invalid OCR response: expected a batch scan result')\n if (path === '/scan/document' && (!asObject(body.documentFields) || !Array.isArray(body.conflicts))) {\n throw new Error('Invalid OCR response: expected grouped document fields and conflicts')\n }\n return data as BatchScanResult\n })\n }\n\n async documents(): Promise<DocumentCatalog> {\n this.requireHttp('documents')\n return this.retry(async (signal) => {\n const { status, data } = await this.request('/documents', signal)\n checkStatus(status, data)\n const body = asObject(data)\n if (!body || typeof body.schemaVersion !== 'number' || !Array.isArray(body.documents) || !asObject(body.capabilities)) {\n throw new Error('Invalid OCR response: expected a document catalog')\n }\n return data as DocumentCatalog\n })\n }\n\n async enqueueJob(images: ImageInput[], options: JobOptions = {}): Promise<OCRJob> {\n this.requireHttp('enqueueJob')\n if (!Array.isArray(images) || images.length === 0) throw new InputValidationError('enqueueJob requires at least one image')\n const parameters = scanParameters(options)\n for (const key of ['grouped', 'notify'] as const) {\n if (options[key] !== undefined) {\n if (typeof options[key] !== 'boolean') throw new InputValidationError(`${key} must be boolean`)\n parameters[key] = options[key]\n }\n }\n // Submission creates persistent work. Do not retry it automatically without\n // server-side idempotency: a lost response could otherwise enqueue twice.\n return withRetry(async (signal) => {\n const form = new FormData()\n for (const [index, image] of images.entries()) form.append('images', await normalizeToBlob(image, signal), `document-${index}`)\n appendParameters(form, parameters)\n const { status, data } = await this.request('/jobs', signal, { method: 'POST', body: form })\n checkStatus(status, data)\n return parseJob(data)\n }, { retries: 0, timeoutMs: this.timeoutMs })\n }\n\n async job(id: string): Promise<OCRJob> {\n this.requireHttp('job')\n const path = jobPath(id)\n return this.retry(async (signal) => {\n const { status, data } = await this.request(path, signal)\n checkStatus(status, data)\n return parseJob(data)\n })\n }\n\n async deleteJob(id: string): Promise<{ deleted: true }> {\n this.requireHttp('deleteJob')\n const path = jobPath(id)\n return this.retry(async (signal) => {\n const { status, data } = await this.request(path, signal, { method: 'DELETE' })\n checkStatus(status, data)\n if (asObject(data)?.deleted !== true) throw new Error('Invalid OCR response: expected deletion confirmation')\n return { deleted: true }\n })\n }\n\n async scanS3(image: S3ImageInput, options: ScanOptions = {}): Promise<DocumentScanResult> {\n if (this.mode !== 'lambda') throw new InputValidationError('scanS3 is available only in lambda mode')\n if (!image || typeof image.bucket !== 'string' || !image.bucket.trim()\n || typeof image.key !== 'string' || !image.key.trim()\n || (image.versionId !== undefined && (typeof image.versionId !== 'string' || !image.versionId))) {\n throw new InputValidationError('scanS3 requires bucket, key, and an optional non-empty versionId')\n }\n const parameters = scanParameters(options)\n const s3 = { bucket: image.bucket, key: image.key, ...(image.versionId === undefined ? {} : { version_id: image.versionId }) }\n return this.retry((signal) => this.invokeLambda({ s3, ...parameters }, signal))\n }\n\n async stop(): Promise<void> {\n if (this.mode === 'local') await getLocalServer().stop()\n }\n\n private requireHttp(method: string): void {\n if (this.mode === 'lambda') throw new InputValidationError(`${method} requires local or http mode`)\n }\n\n private retry<T>(operation: (signal: AbortSignal) => Promise<T>): Promise<T> {\n return withRetry(operation, { retries: this.retries, timeoutMs: this.timeoutMs })\n }\n\n private async request(path: string, signal: AbortSignal, init: RequestInit = {}): Promise<{ status: number, data: unknown }> {\n const endpoint = this.mode === 'local' ? await getLocalServer().ensureRunning() : this.endpoint!\n const url = `${endpoint.replace(/\\/+$/, '')}${path}`\n signal.throwIfAborted()\n const headers = new Headers(this.apiKey ? { Authorization: `Bearer ${this.apiKey}` } : {})\n if (this.authHeaders) {\n new Headers(await this.authHeaders(url, signal)).forEach((value, key) => headers.set(key, value))\n }\n signal.throwIfAborted()\n const response = await fetch(url, { ...init, headers, signal })\n const data: unknown = await response.json().catch(() => null)\n return { status: response.status, data }\n }\n\n private async invokeLambda(event: Record<string, unknown>, signal: AbortSignal): Promise<DocumentScanResult> {\n const payload = JSON.stringify(event)\n if (new TextEncoder().encode(payload).byteLength > MAX_LAMBDA_PAYLOAD_BYTES) {\n throw new InputValidationError('Lambda payload exceeds 6 MiB; upload the image to S3 and use scanS3()')\n }\n signal.throwIfAborted()\n const { LambdaClient, InvokeCommand } = await import('@aws-sdk/client-lambda')\n signal.throwIfAborted()\n const client = new LambdaClient({})\n const command = new InvokeCommand({ FunctionName: this.functionName, Payload: payload })\n try {\n const response = await client.send(command, { abortSignal: signal })\n if (response.FunctionError) throw new Error(`Lambda invocation failed: ${response.FunctionError}`)\n const result = JSON.parse(new TextDecoder().decode(response.Payload))\n if (typeof result?.statusCode === 'number') {\n let body: unknown = result.body\n if (typeof body === 'string') {\n try { body = JSON.parse(body) } catch { body = null }\n }\n return parseScanResponse(result.statusCode, body)\n }\n return parseScanResponse(200, result)\n } finally {\n client.destroy()\n }\n }\n}\n\nfunction scanParameters(options: ScanOptions): Record<string, string | boolean> {\n if (!options || typeof options !== 'object') throw new InputValidationError('scan options must be an object')\n const result: Record<string, string | boolean> = {}\n for (const [key, wireKey] of [['documentType', 'document_type'], ['country', 'country']] as const) {\n const value = options[key]\n if (value !== undefined) {\n if (typeof value !== 'string' || !value.trim()) throw new InputValidationError(`${key} must be a non-empty string`)\n result[wireKey] = value\n }\n }\n if (options.includeEvidence !== undefined) {\n if (typeof options.includeEvidence !== 'boolean') throw new InputValidationError('includeEvidence must be boolean')\n result.include_evidence = options.includeEvidence\n }\n return result\n}\n\nfunction appendParameters(form: FormData, parameters: Record<string, string | boolean>): void {\n for (const [key, value] of Object.entries(parameters)) form.append(key, String(value))\n}\n\nfunction jobPath(id: string): string {\n if (typeof id !== 'string' || !/^[A-Za-z0-9_-]+$/.test(id)) throw new InputValidationError('job id must contain letters, digits, hyphens, or underscores')\n return `/jobs/${encodeURIComponent(id)}`\n}\n\nfunction parseJob(data: unknown): OCRJob {\n const body = asObject(data)\n if (!body || typeof body.id !== 'string' || !['queued', 'running', 'succeeded', 'failed'].includes(String(body.status))\n || typeof body.createdAt !== 'number' || typeof body.expiresAt !== 'number' || typeof body.attempts !== 'number'\n || (body.error !== null && typeof body.error !== 'string')) throw new Error('Invalid OCR response: expected a job')\n return data as OCRJob\n}\n\nfunction asObject(data: unknown): Record<string, unknown> | null {\n return data !== null && typeof data === 'object' && !Array.isArray(data) ? data as Record<string, unknown> : null\n}\n\nfunction isScanResult(data: unknown): boolean {\n const body = asObject(data)\n return body !== null && ['success', 'failure', 'unsupported_page'].includes(String(body.status))\n && typeof body.documentType === 'string' && typeof body.pageType === 'string' && Array.isArray(body.errors)\n}\n\nfunction checkStatus(status: number, data: unknown, allow422 = false): void {\n if ((status >= 200 && status < 300) || allow422) return\n const body = asObject(data)\n const message = typeof body?.error === 'string' ? body.error\n : typeof body?.detail === 'string' ? body.detail : `OCR request failed: ${status}`\n throw new HttpError(status, message)\n}\n\nfunction parseScanResponse(status: number, data: unknown): DocumentScanResult {\n const valid = isScanResult(data)\n checkStatus(status, data, status === 422 && valid)\n if (!valid) throw new Error('Invalid OCR response: expected a document scan result')\n return data as DocumentScanResult\n}\n\nexport const PassportOCR = DocumentOCR\n"],"mappings":"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","names":["delay"]}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Directory exceptions are followed by child exclusions for legacy Docker too.
|
|
2
|
+
**
|
|
3
|
+
!requirements.lock
|
|
4
|
+
!core/
|
|
5
|
+
core/**
|
|
6
|
+
!core/*.py
|
|
7
|
+
!core/review.html
|
|
8
|
+
!deploy/
|
|
9
|
+
deploy/**
|
|
10
|
+
!deploy/docker/
|
|
11
|
+
deploy/docker/**
|
|
12
|
+
!deploy/docker/Dockerfile
|
|
13
|
+
!deploy/docker/server.py
|
|
14
|
+
!deploy/lambda/
|
|
15
|
+
deploy/lambda/**
|
|
16
|
+
!deploy/lambda/Dockerfile.lambda
|
|
17
|
+
!deploy/lambda/handler.py
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Cloud Build receives only OCR source and its container build definition.
|
|
2
|
+
**
|
|
3
|
+
!requirements.lock
|
|
4
|
+
!.dockerignore
|
|
5
|
+
!core/
|
|
6
|
+
!core/**/*.py
|
|
7
|
+
!core/review.html
|
|
8
|
+
!deploy/
|
|
9
|
+
!deploy/docker/
|
|
10
|
+
!deploy/docker/Dockerfile
|
|
11
|
+
!deploy/docker/Dockerfile.dockerignore
|
|
12
|
+
!deploy/docker/server.py
|
|
13
|
+
!deploy/cloudrun/
|
|
14
|
+
!deploy/cloudrun/cloudbuild.yaml
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# Document OCR 3.1 implementation
|
|
2
|
+
|
|
3
|
+
This working tree adds country-aware extraction, eight experimental document
|
|
4
|
+
profiles, review tools and deployable worker kits. The original per-document
|
|
5
|
+
result blocks remain compatible. Read [HOSTING.md](HOSTING.md) for infrastructure
|
|
6
|
+
and the [SDK guide](packages/passport-ocr/README.md) for Node.js usage.
|
|
7
|
+
|
|
8
|
+
## Documents and evidence
|
|
9
|
+
|
|
10
|
+
| Profile | Implemented extraction | Coverage boundary |
|
|
11
|
+
|---|---|---|
|
|
12
|
+
| `us_driver_license`, `us_state_id` | AAMVA PDF417 directory/version parsing; barcode/printed-field reconciliation; US dates, state and address | Versions 01–11 are parsed; synthetic payload/image coverage does not establish accuracy across all states or historical layouts |
|
|
13
|
+
| `passport_card` | ICAO TD1 three-line MRZ with check digits and calendar validation | Supported travel-card prefixes only; not an arbitrary national-ID parser |
|
|
14
|
+
| `us_green_card`, `us_ead` | Supported TD1 card MRZs, USCIS/card numbers and labeled visual fields | Historical artwork/layout coverage remains unmeasured |
|
|
15
|
+
| `visa` | MRV-A and MRV-B parsing, component checks, labeled visual fields | MRZ expiry is not the holder's authorized period of stay |
|
|
16
|
+
| `us_i94` | I-94 number, holder, admission class/date and admit-until date or `D/S` | Conservative English labeled records; no CBP lookup |
|
|
17
|
+
| `us_w9` | Name, business name, EIN/SSN type and value, address | Labeled English forms; no signature, tax-status or IRS verification |
|
|
18
|
+
| Existing passport books | Existing TD3 path accepts US issuer codes and alphanumeric document numbers | The new US profile work does not certify every US passport generation |
|
|
19
|
+
|
|
20
|
+
`GET /documents` is the machine-readable capability catalog. It lists field
|
|
21
|
+
names, required fields, alternatives, extraction sources and experimental flags.
|
|
22
|
+
It also reports whether barcode decoding and persistent jobs are available.
|
|
23
|
+
|
|
24
|
+
Python and HTTP hints are `document_type` and `country`. SDK hints are
|
|
25
|
+
`documentType` and `country`. Country values normalize to ISO alpha-2; `USA` and
|
|
26
|
+
`IND` are accepted. `driving_licence` plus `US` selects `us_driver_license`.
|
|
27
|
+
Hints restrict routing and never turn an unidentified page into a valid record.
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from core.pipeline import scan
|
|
31
|
+
|
|
32
|
+
result = scan('card.png', document_type='us_state_id', country='US',
|
|
33
|
+
include_evidence=True).to_dict()
|
|
34
|
+
print(result['documentFields'])
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
New profiles return `schemaVersion: 1`, `documentFields`, `issuingCountry`,
|
|
38
|
+
`issuingRegion` and `checks`. Legacy field blocks remain present. Legacy scans
|
|
39
|
+
also receive the new envelope when hints or evidence are requested. API field
|
|
40
|
+
and check names use camelCase; Python internals use snake_case.
|
|
41
|
+
|
|
42
|
+
`include_evidence=true` adds `fieldEvidence` and `imageSize: [width, height]`.
|
|
43
|
+
Each evidence item contains the observed text, a four-point bounding polygon,
|
|
44
|
+
source (`ocr`, `pdf_text`, `mrz`, `pdf417`), and extraction confidence. These are
|
|
45
|
+
heuristics/source scores, not calibrated probabilities of correctness or identity.
|
|
46
|
+
Evidence is supplied only when a source match exists. An MRZ/barcode region may
|
|
47
|
+
support several fields. Conflicting barcode/printed values prevent success.
|
|
48
|
+
Checksums and PDF417 structure do not authenticate an issuer; new profiles expose
|
|
49
|
+
`issuerAuthenticated: false`.
|
|
50
|
+
|
|
51
|
+
## PDFs, batches and grouped pages
|
|
52
|
+
|
|
53
|
+
Digital PDFs use their native text layer first, falling back to OCR for an
|
|
54
|
+
unidentified page. The response warns `PDF_NATIVE_TEXT_NOT_VISUALLY_VERIFIED`:
|
|
55
|
+
hidden text can disagree with the rendered document. Review the rendered source
|
|
56
|
+
when the distinction matters. PDF coordinate conversion uses PDFium's own
|
|
57
|
+
transform, including CropBox offsets. Rotated PDF pages use rendered OCR.
|
|
58
|
+
|
|
59
|
+
- `POST /scan`: one multipart `image`; a multi-page PDF scans its first page and
|
|
60
|
+
reports `PDF_ADDITIONAL_PAGES_IGNORED`.
|
|
61
|
+
- `POST /scan/batch`: repeated multipart `images`; independent inputs, in order.
|
|
62
|
+
Each PDF has the same first-page behavior as `/scan`.
|
|
63
|
+
- `POST /scan/document`: repeated `images` belonging to one document/holder;
|
|
64
|
+
expands PDFs into pages and reconciles their fields. Returns `results`, merged
|
|
65
|
+
`documentFields`, `conflicts` and `errors`. Different document families or
|
|
66
|
+
conflicting values fail reconciliation; conflicting fields are not silently
|
|
67
|
+
selected. Pages without a shared identifier still depend on the caller's
|
|
68
|
+
association. This is not a general packet-splitting or household-table merger.
|
|
69
|
+
|
|
70
|
+
Synchronous requests have a 10 MiB combined upload cap, at most ten input/pages,
|
|
71
|
+
and a 60-second queue-plus-execution deadline. A timed-out native operation keeps
|
|
72
|
+
its concurrency slot until it exits. Use jobs for longer persistent-server work.
|
|
73
|
+
Rendered PDFs are bounded to 20 million pixels/page and 40 million/document;
|
|
74
|
+
input images are bounded to 40 million pixels. Encrypted PDFs are rejected.
|
|
75
|
+
|
|
76
|
+
Single scans return 422 for extraction failure, with the complete result.
|
|
77
|
+
Batch/group responses return 200 with `success`, `partial` or `failure` in their
|
|
78
|
+
body; HTTP errors represent a failed request rather than an incomplete record.
|
|
79
|
+
|
|
80
|
+
## Review and redaction
|
|
81
|
+
|
|
82
|
+
Open `/review` on a reachable server. Enter its API token if configured, choose a
|
|
83
|
+
file and extract. The screen displays original source text/regions, editable
|
|
84
|
+
values and the original JSON. Exported JSON preserves the original extraction
|
|
85
|
+
and a separate correction list; CSV includes original and corrected values and
|
|
86
|
+
escapes spreadsheet formulas. The token and corrections remain in tab memory.
|
|
87
|
+
|
|
88
|
+
`POST /preview` returns the normalized first-page PNG used by evidence geometry.
|
|
89
|
+
`POST /redact` takes `image` and a JSON `boxes` form field containing selected
|
|
90
|
+
four-point polygons **in that normalized coordinate plane**, not the original
|
|
91
|
+
camera image. It burns black rectangles into a metadata-free PNG. It removes the
|
|
92
|
+
original PDF text layer by rasterizing; it does not automatically find every
|
|
93
|
+
sensitive value, photograph, background identifier or later PDF page. Review the
|
|
94
|
+
exported PNG before sharing. The original input is not modified.
|
|
95
|
+
|
|
96
|
+
The static `/review` page is public so a browser can reach its token-entry form;
|
|
97
|
+
all document, preview, redaction and job routes enforce `API_TOKEN` when set.
|
|
98
|
+
Platform IAM may impose an additional authentication layer. Responses are marked
|
|
99
|
+
`Cache-Control: no-store`. Synchronous inputs/results are not persisted by the
|
|
100
|
+
application; the browser owns its selected file and exports.
|
|
101
|
+
|
|
102
|
+
## Optional persistent jobs
|
|
103
|
+
|
|
104
|
+
Jobs require a persistent local server volume and a **separate worker process**.
|
|
105
|
+
They remain unavailable on the default Cloud Run, Lambda and Cloudflare presets.
|
|
106
|
+
See the opt-in Compose jobs profile in [HOSTING.md](HOSTING.md). Do not put this
|
|
107
|
+
SQLite queue on an ephemeral disk or network filesystem.
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
export DOCUMENT_OCR_JOBS_DIR=/var/lib/document-ocr
|
|
111
|
+
# Generate once, securely store it, and give the same key to API and workers.
|
|
112
|
+
export DOCUMENT_OCR_JOB_KEY="$(python -c 'from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())')"
|
|
113
|
+
export DOCUMENT_OCR_JOB_RETENTION_SECONDS=86400
|
|
114
|
+
python -m core.jobs worker
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Keep the key across restarts; replacing it cannot decrypt existing work. A stored
|
|
118
|
+
encrypted key sentinel rejects a mismatched key before any job is claimed. Key
|
|
119
|
+
rotation/migration is not implemented. Retention may be configured from 60 seconds
|
|
120
|
+
to seven days; the default is one day. The worker removes expired rows, and the
|
|
121
|
+
API never returns expired records. Deletion/expiry cannot stop native code that
|
|
122
|
+
is already running, but its eventual result cannot recreate the deleted job.
|
|
123
|
+
|
|
124
|
+
`POST /jobs` accepts repeated `images`, scan options, `grouped` and `notify` form
|
|
125
|
+
fields, and returns 202. `GET /jobs/{id}` polls; `DELETE /jobs/{id}` deletes queued
|
|
126
|
+
inputs/results. The queue holds at most 1,000 retained jobs. Inputs and results
|
|
127
|
+
are encrypted with Fernet; input ciphertext is cleared on completion. Metadata
|
|
128
|
+
such as job ID, status and timestamps remains visible in the database. All users
|
|
129
|
+
of the same configured API token share one queue: this is a single-tenant server.
|
|
130
|
+
|
|
131
|
+
Statuses are `queued`, `running`, `succeeded`, `failed`. `succeeded` means the job
|
|
132
|
+
executed; inspect its nested scan result for extraction success. A crashed worker's
|
|
133
|
+
lease is reclaimable after one hour, up to three attempts. Ordinary processing
|
|
134
|
+
errors fail the job; they are not retried indefinitely. Notifications use
|
|
135
|
+
at-least-once delivery with a stable event ID and up to six attempts, five minutes
|
|
136
|
+
apart, within retention.
|
|
137
|
+
|
|
138
|
+
Webhooks go only to the operator-configured `DOCUMENT_OCR_WEBHOOK_URL` (HTTPS),
|
|
139
|
+
with a `DOCUMENT_OCR_WEBHOOK_SECRET` of at least 32 characters. Clients cannot
|
|
140
|
+
supply callback URLs. Request `notify=true` when enqueuing. The body contains only
|
|
141
|
+
`id` and `status`; fetch the result through the authenticated API. Verify
|
|
142
|
+
`X-Document-OCR-Signature: sha256=<HMAC>` over the exact bytes
|
|
143
|
+
`<X-Document-OCR-Timestamp>.<body>`, enforce timestamp freshness, and deduplicate
|
|
144
|
+
`X-Document-OCR-Event`. Redirects are disabled. Set both webhook variables on the
|
|
145
|
+
API and worker so submission and delivery share configuration.
|
|
146
|
+
|
|
147
|
+
## Deployment and next work
|
|
148
|
+
|
|
149
|
+
The kits include Cloud Run, Lambda with inline/S3 input, Cloudflare Worker plus
|
|
150
|
+
native Container, and Compose/Caddy server hosting, with Terraform roots for
|
|
151
|
+
Cloud Run and Lambda. The npm CLI scaffolds source and pinned dependencies,
|
|
152
|
+
checks prerequisites, and dispatches a selected target; it does not publish or
|
|
153
|
+
create cloud resources during `init` or `deploy --dry-run`.
|
|
154
|
+
|
|
155
|
+
Not implemented by this change: bank-statement transaction tables, utility-bill
|
|
156
|
+
and payslip schemas, GST/Udyam/incorporation/cheque profiles, automatic packet
|
|
157
|
+
splitting, text-line deskew/orientation search, issuer verification, multi-tenant
|
|
158
|
+
queues, and cloud-native distributed job backends. Aadhaar Secure QR signature
|
|
159
|
+
verification also remains deferred: it needs the correct current QR trust chain,
|
|
160
|
+
a verified format specification and signed fixtures; the paperless offline e-KYC
|
|
161
|
+
certificate must not be assumed to verify Secure QR data. [UIDAI's certificate
|
|
162
|
+
catalog](https://uidai.gov.in/en/data-and-download) lists these as separate
|
|
163
|
+
certificate families.
|
|
164
|
+
|
|
165
|
+
The new [structured-document benchmark](benchmarks/STRUCTURED_DATASET.md) enforces
|
|
166
|
+
independent groups, negative controls and exact identifier/date gates for all eight
|
|
167
|
+
new profiles; its empty example deliberately cannot pass.
|
|
168
|
+
|
|
169
|
+
The next accuracy gate is a consented image corpus with separate tuning/locked
|
|
170
|
+
splits, holder/state/layout/language slices, negative controls, and per-field
|
|
171
|
+
identifier/date exact match. Synthetic parser/transport tests and a clean build
|
|
172
|
+
are useful regression checks, not a production accuracy percentage.
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
# Hosting Document OCR
|
|
2
|
+
|
|
3
|
+
Cloud Run is the simplest managed HTTP deployment. Lambda supports IAM-authenticated
|
|
4
|
+
function invocation and private S3 inputs. Cloudflare uses a Worker **and a Linux
|
|
5
|
+
container**; ordinary Python Workers do not run this native ONNX/OpenCV stack.
|
|
6
|
+
A Docker Compose preset runs the HTTP API on your own server with Caddy HTTPS.
|
|
7
|
+
|
|
8
|
+
All HTTP presets expose port 8000 internally and preserve `/health`, `/ready`,
|
|
9
|
+
`/documents`, `/scan`, `/scan/document`, `/scan/batch`, and `/review`. The deployed
|
|
10
|
+
API's `/documents` response describes its current capabilities. Persistent jobs
|
|
11
|
+
are opt-in for the server preset; cloud presets do not enable a local job store.
|
|
12
|
+
|
|
13
|
+
## Shared configuration
|
|
14
|
+
|
|
15
|
+
| Input | Default | Meaning |
|
|
16
|
+
|---|---|---|
|
|
17
|
+
| `DOCUMENT_OCR_NAME` | `document-ocr` | Service/stack name |
|
|
18
|
+
| `DOCUMENT_OCR_KYC_LANGS` | `en` | Up to four of `en,latin,devanagari,ka,ta,te` |
|
|
19
|
+
| `DOCUMENT_OCR_PROFILE` | `economy` | `economy` or `warm` |
|
|
20
|
+
| `IMAGE_URI` | build source | Optional provider-compatible prebuilt image |
|
|
21
|
+
|
|
22
|
+
The profile changes Cloud Run minimum instances and Cloudflare idle time. Lambda
|
|
23
|
+
and the continuously running server preset retain their declared resource settings.
|
|
24
|
+
|
|
25
|
+
Images install `requirements.lock` and run `python -m core.model_setup` at build
|
|
26
|
+
time. Set the language list **before building**. When using a prebuilt image,
|
|
27
|
+
its bundled models must match the configured languages. Images should be pinned
|
|
28
|
+
by digest for repeatable production releases. The HTTP and Lambda entrypoints
|
|
29
|
+
use separate images, sharing the same core Python implementation.
|
|
30
|
+
|
|
31
|
+
`.dockerignore` and `.gcloudignore` allow only required source into builds. They
|
|
32
|
+
exclude local virtual environments, npm dependencies, credentials, and private
|
|
33
|
+
identity-document fixtures. Keep these files when copying deployment templates.
|
|
34
|
+
|
|
35
|
+
At runtime, `DOCUMENT_OCR_RECOVERY_BUDGET_SECONDS` (default `5`) bounds MRZ
|
|
36
|
+
recovery: once a scan has run that long it starts no further re-reads and returns
|
|
37
|
+
the best evidence it has. Set it on the service, not at build time. Use `off` for
|
|
38
|
+
no limit or `0` to disable recovery. On 2 vCPU, failed passport scans otherwise
|
|
39
|
+
spent 10+ seconds in re-reads that rarely changed the outcome.
|
|
40
|
+
|
|
41
|
+
## Google Cloud Run
|
|
42
|
+
|
|
43
|
+
Prerequisites: a billed Google Cloud project, `gcloud`, Python 3, authenticated CLI
|
|
44
|
+
credentials, and permission to enable services/create service accounts, builds,
|
|
45
|
+
repositories and Cloud Run services. Local Docker is not required.
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
export GCP_PROJECT=your-project
|
|
49
|
+
export GCP_REGION=asia-south1
|
|
50
|
+
export GCP_INVOKER_MEMBER=serviceAccount:caller@your-project.iam.gserviceaccount.com
|
|
51
|
+
export DOCUMENT_OCR_KYC_LANGS=en
|
|
52
|
+
bash deploy/cloudrun/deploy.sh
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The wrapper enables APIs, creates the image repository and runtime identity as
|
|
56
|
+
needed, builds remotely for Linux/amd64, then deploys the definition produced by
|
|
57
|
+
`deploy/cloudrun/render_service.py`. Both deployment and build use this one
|
|
58
|
+
configuration path. Existing public invoker bindings are removed before updating
|
|
59
|
+
an existing service; IAM failures stop deployment. No public invocation is enabled.
|
|
60
|
+
|
|
61
|
+
Optional inputs:
|
|
62
|
+
|
|
63
|
+
- `GCP_AR_REPO`: Artifact Registry repository name.
|
|
64
|
+
- `DOCUMENT_OCR_SERVICE_ACCOUNT`: existing runtime service-account email.
|
|
65
|
+
- `GCP_API_TOKEN_SECRET`: existing Secret Manager secret ID containing an optional
|
|
66
|
+
application Bearer token. `GCP_API_TOKEN_VERSION` defaults to `latest`.
|
|
67
|
+
- `DOCUMENT_OCR_MAX_INSTANCES`: scale-out cap, default 10.
|
|
68
|
+
- `DOCUMENT_OCR_PROFILE=warm`: maintain one instance; economy allows scale to zero.
|
|
69
|
+
|
|
70
|
+
The initial sizing is two vCPUs, 2 GiB memory, concurrency one, and startup CPU
|
|
71
|
+
boost. OCR work is serialized within each process. Benchmark before changing
|
|
72
|
+
concurrency or reducing memory. A warm profile has ongoing idle-instance cost.
|
|
73
|
+
|
|
74
|
+
The endpoint requires a Google-signed ID token whose audience is the service URL.
|
|
75
|
+
A static application API token cannot satisfy Cloud Run IAM. For local inspection:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
gcloud run services proxy document-ocr --project "$GCP_PROJECT" --region "$GCP_REGION"
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
For application calls, use the SDK's asynchronous header provider with a Google
|
|
82
|
+
auth library. Put Google's token in `X-Serverless-Authorization` when the app also
|
|
83
|
+
uses `Authorization` for its own `API_TOKEN`. Do not store a short-lived Google
|
|
84
|
+
ID token as a permanent API key. See [Google service authentication](https://docs.cloud.google.com/run/docs/authenticating/service-to-service).
|
|
85
|
+
|
|
86
|
+
## AWS Lambda
|
|
87
|
+
|
|
88
|
+
Prerequisites: AWS CLI credentials, AWS SAM CLI, Docker with Linux/amd64 support,
|
|
89
|
+
and permission to create the CloudFormation stack, Lambda role and image repo.
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
export AWS_REGION=ap-south-1
|
|
93
|
+
export AWS_STACK_NAME=document-ocr
|
|
94
|
+
export DOCUMENT_OCR_KYC_LANGS=en
|
|
95
|
+
bash deploy/lambda/deploy.sh
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The SAM template fixes the Dockerfile path relative to the repository build
|
|
99
|
+
context. It creates an x86_64 image function with 2 GiB memory, 120-second timeout,
|
|
100
|
+
and a bounded concurrency of 10. The printed function name works with SDK
|
|
101
|
+
`mode: 'lambda'`. Callers need `lambda:InvokeFunction` permission. The execution
|
|
102
|
+
role and caller role are separate; no public Function URL is created.
|
|
103
|
+
|
|
104
|
+
To read larger documents from an existing private S3 bucket:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
export DOCUMENT_OCR_S3_BUCKET=your-private-documents
|
|
108
|
+
export DOCUMENT_OCR_S3_PREFIX=uploads/
|
|
109
|
+
bash deploy/lambda/deploy.sh
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
The role grants `s3:GetObject` only within that prefix; it cannot list, upload or
|
|
113
|
+
delete objects. Upload from your application using its own appropriate identity.
|
|
114
|
+
For SSE-KMS objects, separately grant the runtime role decrypt permission on the
|
|
115
|
+
specific KMS key. Direct synchronous invocation has a 6 MiB request limit, leaving
|
|
116
|
+
approximately 4.5 MiB for raw bytes when encoded as base64; use S3 for larger
|
|
117
|
+
inputs. [AWS limits](https://docs.aws.amazon.com/lambda/latest/dg/gettingstarted-limits.html)
|
|
118
|
+
|
|
119
|
+
`IMAGE_URI` may reference an already-built Lambda-compatible image in ECR in the
|
|
120
|
+
same region. It must target one architecture. Runtime writes must use `/tmp`;
|
|
121
|
+
models are bundled to avoid runtime downloads. The HTTP Docker image alone is
|
|
122
|
+
not a Lambda runtime. [Lambda image requirements](https://docs.aws.amazon.com/lambda/latest/dg/images-create.html)
|
|
123
|
+
|
|
124
|
+
AWS Lambda Web Adapter is another valid architecture for exposing the same
|
|
125
|
+
FastAPI service through a Function URL/API Gateway, but this preset intentionally
|
|
126
|
+
retains the existing direct-invoke handler and its S3 contract. The adapter is not
|
|
127
|
+
required or installed here. [Official Web Adapter](https://github.com/aws/aws-lambda-web-adapter)
|
|
128
|
+
|
|
129
|
+
## Cloudflare Worker and Container
|
|
130
|
+
|
|
131
|
+
Prerequisites: a Workers Paid plan, Node.js 26+, Docker with Buildx, and Wrangler
|
|
132
|
+
credentials. The Worker authenticates document requests before selecting a container; the
|
|
133
|
+
container runs native Python on Linux/amd64. Plain Python Workers are not an
|
|
134
|
+
interchangeable target. [Container setup](https://developers.cloudflare.com/containers/get-started/)
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
npm ci --prefix deploy/cloudflare --ignore-scripts
|
|
138
|
+
cd deploy/cloudflare
|
|
139
|
+
npx wrangler login
|
|
140
|
+
npx wrangler secret put API_TOKEN
|
|
141
|
+
npm run deploy
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
The secret command prompts for the value; it is not stored in source. For a custom
|
|
145
|
+
`DOCUMENT_OCR_NAME`, pass that same name with `wrangler secret put API_TOKEN --name
|
|
146
|
+
YOUR_NAME`. Required-secret validation prevents unauthenticated deployment. For
|
|
147
|
+
local development, place the token in an ignored `.dev.vars` and use `npm run dev`.
|
|
148
|
+
Alternatively, supply `API_TOKEN` in the environment when running the deployment
|
|
149
|
+
wrapper. It passes the secret through a temporary private file and removes that
|
|
150
|
+
file on exit; no secret value is placed in command-line arguments.
|
|
151
|
+
|
|
152
|
+
The deployment wrapper consumes the shared configuration variables. Set
|
|
153
|
+
`DOCUMENT_OCR_MAX_INSTANCES` to select a fixed routing pool (default two). The
|
|
154
|
+
preset uses `standard-2` instances. `economy` sleeps idle containers after ten
|
|
155
|
+
minutes; `warm` extends that to one hour, and does **not** promise a permanently
|
|
156
|
+
warm instance. Built-in demand-based container autoscaling is not assumed.
|
|
157
|
+
[Scaling and routing](https://developers.cloudflare.com/containers/configuration/scaling-and-routing/)
|
|
158
|
+
|
|
159
|
+
Outbound internet access in the OCR container is disabled; all selected models
|
|
160
|
+
must exist in the image. Uploads/responses stream through the Worker and successful
|
|
161
|
+
and partial responses use `Cache-Control: no-store`. Document routes and
|
|
162
|
+
health/readiness probes require the Bearer token. `GET /review` serves the public
|
|
163
|
+
static review screen so a browser can load its token-entry form; document data
|
|
164
|
+
still requires authentication.
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
npm run types
|
|
168
|
+
npm run typecheck
|
|
169
|
+
npm test
|
|
170
|
+
npm run check # Worker/config dry-run; does not build or deploy containers
|
|
171
|
+
npm run check:image # also builds the image locally; requires Buildx and disk space
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
The generated `worker-configuration.d.ts` is produced by Wrangler, not handwritten.
|
|
175
|
+
Wrangler owns the Worker/container and Durable Object migrations. Avoid managing
|
|
176
|
+
the same Worker concurrently with Terraform; Terraform can separately own DNS or
|
|
177
|
+
Access resources.
|
|
178
|
+
|
|
179
|
+
## Your own server with HTTPS
|
|
180
|
+
|
|
181
|
+
Prerequisites: Docker Compose on the target server, a DNS name pointing to it,
|
|
182
|
+
and ports 80/443 reachable for Caddy certificates and HTTPS. Run the command on
|
|
183
|
+
that server, or use an existing Docker remote context.
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
export DOMAIN=ocr.example.com
|
|
187
|
+
export API_TOKEN='replace-with-a-random-private-token'
|
|
188
|
+
bash deploy/server/deploy.sh
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Supply real secrets through your shell or a private environment file. Caddy keeps
|
|
192
|
+
certificates in named volumes. The OCR container is reachable only on the Compose
|
|
193
|
+
network; Caddy exposes HTTPS. `IMAGE_URI` avoids building on the server. To stop
|
|
194
|
+
without deleting volumes:
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
docker compose -p document-ocr -f deploy/server/compose.yaml down
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Optional encrypted persistent jobs
|
|
201
|
+
|
|
202
|
+
The ordinary HTTP preset has jobs disabled. To enable them, generate a Fernet key
|
|
203
|
+
using the installed Python dependencies, retain it securely, and start the jobs
|
|
204
|
+
overlay:
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
export DOCUMENT_OCR_JOB_KEY="$(python -c 'from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())')"
|
|
208
|
+
export DOCUMENT_OCR_JOBS_ENABLED=true
|
|
209
|
+
bash deploy/server/deploy.sh
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
The API and a separate `python -m core.jobs worker` process share the `ocr_jobs`
|
|
213
|
+
volume and key. Preserve both across upgrades. Losing the key makes retained data
|
|
214
|
+
unreadable; do not generate a new key on every restart. Back up encrypted storage
|
|
215
|
+
and its key separately. Cloud Run, Lambda and Cloudflare presets do not claim
|
|
216
|
+
persistent job support from ephemeral container filesystems.
|
|
217
|
+
|
|
218
|
+
Retention defaults to 24 hours; set `DOCUMENT_OCR_JOB_RETENTION_SECONDS` to a
|
|
219
|
+
value from 60 to 604800. Optional completion notifications use one operator-owned
|
|
220
|
+
HTTPS endpoint: set `DOCUMENT_OCR_WEBHOOK_URL` and a
|
|
221
|
+
`DOCUMENT_OCR_WEBHOOK_SECRET` of at least 32 characters, then enqueue with
|
|
222
|
+
`notify: true`. The API and worker both receive these settings. Notifications
|
|
223
|
+
contain only the job ID/status, with a timestamp and HMAC-SHA256 signature;
|
|
224
|
+
receivers must validate the signature and deduplicate `X-Document-OCR-Event`.
|
|
225
|
+
|
|
226
|
+
## Terraform
|
|
227
|
+
|
|
228
|
+
`infra/cloudrun` and `infra/lambda` are independent Terraform roots. Copy the
|
|
229
|
+
relevant `terraform.tfvars.example` to a private `terraform.tfvars`, supplying a
|
|
230
|
+
prebuilt image URI and provider/project configuration:
|
|
231
|
+
|
|
232
|
+
```bash
|
|
233
|
+
terraform -chdir=infra/cloudrun init
|
|
234
|
+
terraform -chdir=infra/cloudrun plan
|
|
235
|
+
terraform -chdir=infra/cloudrun apply
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Use `infra/lambda` for AWS. Each root creates a repository for future image builds,
|
|
239
|
+
a runtime role/service identity and the service/function. The first `image_uri`
|
|
240
|
+
must already exist in a suitable registry; Terraform does not build images. If
|
|
241
|
+
you already used a wrapper to create resources of these names, import them into
|
|
242
|
+
Terraform or choose a different name. Do not let two deployment systems own the
|
|
243
|
+
same resources. Provider versions are recorded in `.terraform.lock.hcl`.
|
|
244
|
+
|
|
245
|
+
Cloud Run accepts only named IAM invokers and references an existing optional
|
|
246
|
+
Secret Manager secret. Lambda accepts an optional input bucket/prefix and grants
|
|
247
|
+
only prefix-scoped reads. Secret contents are never Terraform variables. Choose
|
|
248
|
+
a protected remote state backend before team use; templates deliberately do not
|
|
249
|
+
hardcode an account-specific backend.
|
|
250
|
+
|
|
251
|
+
Validate without applying:
|
|
252
|
+
|
|
253
|
+
```bash
|
|
254
|
+
terraform -chdir=infra/cloudrun init -backend=false
|
|
255
|
+
terraform -chdir=infra/cloudrun validate
|
|
256
|
+
terraform -chdir=infra/lambda init -backend=false
|
|
257
|
+
terraform -chdir=infra/lambda validate
|
|
258
|
+
sam validate --template-file deploy/lambda/template.yaml --lint
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
## Verification before production
|
|
262
|
+
|
|
263
|
+
After deployment, check `/ready`, make an authenticated scan with a synthetic
|
|
264
|
+
fixture, confirm missing/wrong credentials are rejected, and measure cold-start
|
|
265
|
+
latency and peak memory for your enabled languages. Never upload real identity
|
|
266
|
+
documents as test fixtures to public services. Image builds, platform IAM and live
|
|
267
|
+
endpoint behavior require verification in your own cloud account; a configuration
|
|
268
|
+
or Terraform validation alone does not establish a working cloud deployment.
|
package/hosting/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Piyush Gambhir
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|