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.
Files changed (124) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +260 -2
  3. package/dist/index.cjs +659 -0
  4. package/dist/index.cjs.map +1 -0
  5. package/dist/index.d.cts +293 -0
  6. package/dist/index.d.ts +293 -0
  7. package/dist/index.js +616 -0
  8. package/dist/index.js.map +1 -0
  9. package/hosting/.dockerignore +17 -0
  10. package/hosting/.gcloudignore +14 -0
  11. package/hosting/FEATURES.md +172 -0
  12. package/hosting/HOSTING.md +268 -0
  13. package/hosting/LICENSE +21 -0
  14. package/hosting/README.md +369 -0
  15. package/hosting/core/__init__.py +0 -0
  16. package/hosting/core/aadhaar_extractor.py +298 -0
  17. package/hosting/core/back_page_extractor.py +492 -0
  18. package/hosting/core/barcodes.py +125 -0
  19. package/hosting/core/card_recovery.py +244 -0
  20. package/hosting/core/document_bundle.py +82 -0
  21. package/hosting/core/document_classifier.py +233 -0
  22. package/hosting/core/document_input.py +135 -0
  23. package/hosting/core/document_registry.py +143 -0
  24. package/hosting/core/driving_licence_extractor.py +510 -0
  25. package/hosting/core/ead_recovery.py +54 -0
  26. package/hosting/core/evidence.py +54 -0
  27. package/hosting/core/form_recovery.py +61 -0
  28. package/hosting/core/jobs.py +232 -0
  29. package/hosting/core/kyc_ocr.py +132 -0
  30. package/hosting/core/kyc_validation.py +411 -0
  31. package/hosting/core/model_setup.py +13 -0
  32. package/hosting/core/mrz_parser.py +336 -0
  33. package/hosting/core/mrz_recovery.py +193 -0
  34. package/hosting/core/npr_extractor.py +410 -0
  35. package/hosting/core/nrega_extractor.py +827 -0
  36. package/hosting/core/ocr_engine.py +207 -0
  37. package/hosting/core/page_classifier.py +137 -0
  38. package/hosting/core/pan_extractor.py +183 -0
  39. package/hosting/core/pipeline.py +712 -0
  40. package/hosting/core/preprocessor.py +297 -0
  41. package/hosting/core/recovery_budget.py +67 -0
  42. package/hosting/core/redaction.py +48 -0
  43. package/hosting/core/review.html +244 -0
  44. package/hosting/core/structured_documents.py +422 -0
  45. package/hosting/core/travel_mrz.py +164 -0
  46. package/hosting/core/travel_recovery.py +187 -0
  47. package/hosting/core/validator.py +396 -0
  48. package/hosting/core/validators.py +179 -0
  49. package/hosting/core/voter_id_extractor.py +363 -0
  50. package/hosting/deploy/cloudflare/deploy.sh +44 -0
  51. package/hosting/deploy/cloudflare/package-lock.json +3088 -0
  52. package/hosting/deploy/cloudflare/package.json +29 -0
  53. package/hosting/deploy/cloudflare/src/gateway.ts +42 -0
  54. package/hosting/deploy/cloudflare/src/index.ts +21 -0
  55. package/hosting/deploy/cloudflare/test/gateway.test.ts +59 -0
  56. package/hosting/deploy/cloudflare/tsconfig.json +12 -0
  57. package/hosting/deploy/cloudflare/worker-configuration.d.ts +16253 -0
  58. package/hosting/deploy/cloudflare/wrangler.jsonc +26 -0
  59. package/hosting/deploy/cloudrun/cloudbuild.yaml +18 -0
  60. package/hosting/deploy/cloudrun/deploy.sh +71 -0
  61. package/hosting/deploy/cloudrun/render_service.py +47 -0
  62. package/hosting/deploy/common.sh +14 -0
  63. package/hosting/deploy/docker/Dockerfile +16 -0
  64. package/hosting/deploy/docker/server.py +313 -0
  65. package/hosting/deploy/lambda/Dockerfile.lambda +11 -0
  66. package/hosting/deploy/lambda/deploy.sh +22 -0
  67. package/hosting/deploy/lambda/handler.py +132 -0
  68. package/hosting/deploy/lambda/template.yaml +59 -0
  69. package/hosting/deploy/local/dev.sh +18 -0
  70. package/hosting/deploy/server/Caddyfile +5 -0
  71. package/hosting/deploy/server/compose.jobs.yaml +36 -0
  72. package/hosting/deploy/server/compose.yaml +38 -0
  73. package/hosting/deploy/server/deploy.sh +23 -0
  74. package/hosting/infra/cloudrun/main.tf +104 -0
  75. package/hosting/infra/cloudrun/terraform.tfvars.example +7 -0
  76. package/hosting/infra/cloudrun/variables.tf +55 -0
  77. package/hosting/infra/lambda/main.tf +63 -0
  78. package/hosting/infra/lambda/terraform.tfvars.example +6 -0
  79. package/hosting/infra/lambda/variables.tf +37 -0
  80. package/hosting/pyproject.toml +65 -0
  81. package/hosting/requirements.lock +134 -0
  82. package/package.json +76 -4
  83. package/python/core/__init__.py +0 -0
  84. package/python/core/aadhaar_extractor.py +298 -0
  85. package/python/core/back_page_extractor.py +492 -0
  86. package/python/core/barcodes.py +125 -0
  87. package/python/core/card_recovery.py +244 -0
  88. package/python/core/document_bundle.py +82 -0
  89. package/python/core/document_classifier.py +233 -0
  90. package/python/core/document_input.py +135 -0
  91. package/python/core/document_registry.py +143 -0
  92. package/python/core/driving_licence_extractor.py +510 -0
  93. package/python/core/ead_recovery.py +54 -0
  94. package/python/core/evidence.py +54 -0
  95. package/python/core/form_recovery.py +61 -0
  96. package/python/core/jobs.py +232 -0
  97. package/python/core/kyc_ocr.py +132 -0
  98. package/python/core/kyc_validation.py +411 -0
  99. package/python/core/model_setup.py +13 -0
  100. package/python/core/mrz_parser.py +336 -0
  101. package/python/core/mrz_recovery.py +193 -0
  102. package/python/core/npr_extractor.py +410 -0
  103. package/python/core/nrega_extractor.py +827 -0
  104. package/python/core/ocr_engine.py +207 -0
  105. package/python/core/page_classifier.py +137 -0
  106. package/python/core/pan_extractor.py +183 -0
  107. package/python/core/pipeline.py +712 -0
  108. package/python/core/preprocessor.py +297 -0
  109. package/python/core/recovery_budget.py +67 -0
  110. package/python/core/redaction.py +48 -0
  111. package/python/core/review.html +244 -0
  112. package/python/core/structured_documents.py +422 -0
  113. package/python/core/travel_mrz.py +164 -0
  114. package/python/core/travel_recovery.py +187 -0
  115. package/python/core/validator.py +396 -0
  116. package/python/core/validators.py +179 -0
  117. package/python/core/voter_id_extractor.py +363 -0
  118. package/python/pyproject.toml +34 -0
  119. package/python/requirements.lock +134 -0
  120. package/python/server.py +313 -0
  121. package/scripts/dococr.mjs +163 -0
  122. package/scripts/postinstall.mjs +173 -0
  123. package/scripts/sync-hosting.mjs +35 -0
  124. 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.
@@ -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.