1/** 2 * @typedef {Object} RecognitionResult 3 * @property {boolean} success 4 * @property {string} [rawData] 5 * @property {string} format 6 * @property {Error} [error] 7 */ 8 9import { 10 TextractClient, 11 DetectDocumentTextCommand, 12 AnalyzeDocumentCommand, 13} from './aws-textract.esm.bundle.min.js'; 14 15/** 16 * Manages a pool of AWS regions for round-robin request distribution. 17 * Each region tracks its own throttle/backoff state independently. 18 */ 19class RegionPool { 20 /** 21 * @param {string[]} regions 22 * @param {Object} [defaultCredentials] 23 */ 24 constructor(regions, defaultCredentials) { 25 this.entries = regions.map((region) => ({ 26 region, 27 credentials: defaultCredentials, 28 backoffUntil: 0, 29 consecutiveThrottles: 0, 30 })); 31 this._index = 0; 32 } 33 34 /** Get the next available region, skipping any currently in backoff. */ 35 getNext() { 36 const now = Date.now(); 37 const len = this.entries.length; 38 for (let i = 0; i < len; i++) { 39 const entry = this.entries[(this._index + i) % len]; 40 if (entry.backoffUntil <= now) { 41 this._index = ((this._index + i) % len) + 1; 42 return entry; 43 } 44 } 45 const soonest = this.entries.reduce((a, b) => (a.backoffUntil < b.backoffUntil ? a : b)); 46 return soonest; 47 } 48 49 /** Mark a region as throttled with exponential backoff. */ 50 markThrottled(entry) { 51 entry.consecutiveThrottles++; 52 entry.backoffUntil = Date.now() + Math.min(1000 * (2 ** entry.consecutiveThrottles), 32000); 53 } 54 55 /** Mark a region as successful (reset throttle state). */ 56 markSuccess(entry) { 57 entry.consecutiveThrottles = 0; 58 entry.backoffUntil = 0; 59 } 60 61 /** Returns true when every region is currently in a backoff window. */ 62 allInBackoff() { 63 const now = Date.now(); 64 return this.entries.every((e) => e.backoffUntil > now); 65 } 66} 67 68/** 69 * Browser-compatible AWS Textract recognition model for use with Scribe.js. 70 * Imports from a pre-bundled ESM file so no bare-specifier resolution is needed. 71 * Supports synchronous (single-image) recognition only. 72 */ 73export class RecognitionModelTextractBrowser { 74 static config = { 75 name: 'AWS Textract', 76 outputFormat: 'textract', 77 rateLimit: { tps: 1 }, 78 }; 79 80 static isThrottlingError(error) { 81 return error?.$metadata?.httpStatusCode === 429 82 || error?.name === 'ThrottlingException' 83 || error?.name === 'ProvisionedThroughputExceededException' 84 || error?.name === 'LimitExceededException'; 85 } 86 87 /** @type {RegionPool|null} */ 88 static _regionPool = null; 89 90 /** 91 * Lazily creates or reuses a RegionPool from options.region when it is an array. 92 * @param {Object} options 93 */ 94 static _ensureRegionPool(options) { 95 const regions = options.region; 96 if (!Array.isArray(regions) || regions.length <= 1) { 97 this._regionPool = null; 98 return; 99 } 100 const currentRegions = this._regionPool?.entries.map((e) => e.region); 101 if (!currentRegions || JSON.stringify(currentRegions) !== JSON.stringify(regions)) { 102 this._regionPool = new RegionPool(regions, options.credentials); 103 } 104 } 105 106 /** 107 * Dispatch a single image recognition request across the region pool. 108 * @param {Uint8Array} data 109 * @param {Object} options 110 * @param {boolean} analyzeLayout 111 * @param {boolean} analyzeTables 112 * @returns {Promise<RecognitionResult>} 113 */ 114 static async _recognizeWithPool(data, options, analyzeLayout, analyzeTables) { 115 const pool = this._regionPool; 116 const maxAttempts = pool.entries.length; 117 118 for (let attempt = 0; attempt < maxAttempts; attempt++) { 119 const entry = pool.getNext(); 120 121 const now = Date.now(); 122 if (entry.backoffUntil > now) { 123 await new Promise((resolve) => setTimeout(resolve, entry.backoffUntil - now)); 124 } 125 126 try { 127 const textractClient = new TextractClient({ 128 region: entry.region, 129 ...(entry.credentials && { credentials: entry.credentials }), 130 }); 131 132 let command; 133 if (analyzeLayout || analyzeTables) { 134 const FeatureTypes = []; 135 if (analyzeLayout) FeatureTypes.push('LAYOUT'); 136 if (analyzeTables) FeatureTypes.push('TABLES'); 137 command = new AnalyzeDocumentCommand({ 138 Document: { Bytes: data }, 139 FeatureTypes, 140 }); 141 } else { 142 command = new DetectDocumentTextCommand({ 143 Document: { Bytes: data }, 144 }); 145 } 146 147 const response = await textractClient.send(command); 148 pool.markSuccess(entry); 149 return { success: true, rawData: JSON.stringify(response), format: 'textract' }; 150 } catch (error) { 151 if (this.isThrottlingError(error)) { 152 pool.markThrottled(entry); 153 if (!pool.allInBackoff()) continue; 154 } 155 return { success: false, error, format: 'textract' }; 156 } 157 } 158 159 return { success: false, error: new Error('All regions exhausted'), format: 'textract' }; 160 } 161 162 /** 163 * Recognize text from an image using AWS Textract. 164 * 165 * @param {Uint8Array|ArrayBuffer} imageData - Image data 166 * @param {Object} [options] 167 * @param {boolean} [options.analyzeLayout=false] - Whether to enable layout analysis. 168 * @param {boolean} [options.analyzeTables=false] - Whether to enable table analysis. 169 * Enabling table analysis automatically enables layout analysis. 170 * @param {string|string[]} [options.region] - AWS region (e.g. 'us-east-1') or array of regions 171 * for multi-region throughput scaling (e.g. ['us-east-1', 'us-west-2']). 172 * When an array is provided, pages are distributed across regions round-robin 173 * with per-region throttle backoff. 174 * If not provided, the SDK resolves from AWS_REGION env var, ~/.aws/config, or instance metadata. 175 * @param {{accessKeyId: string, secretAccessKey: string, sessionToken?: string}} [options.credentials] - AWS credentials. 176 * @returns {Promise<RecognitionResult>} 177 */ 178 static async recognizeImage(imageData, options = {}) { 179 const data = imageData instanceof ArrayBuffer ? new Uint8Array(imageData) : imageData; 180 const analyzeTables = options.analyzeTables ?? false; 181 const analyzeLayout = options.analyzeLayout ?? false; 182 183 // Multi-region path: distribute across regions with per-region backoff. 184 this._ensureRegionPool(options); 185 if (this._regionPool) { 186 return this._recognizeWithPool(data, options, analyzeLayout, analyzeTables); 187 } 188 189 // Single-region path. 190 const region = (typeof options.region === 'string' && options.region) || undefined; 191 const credentials = options.credentials || undefined; 192 193 try { 194 const textractClient = new TextractClient({ 195 ...(region && { region }), 196 ...(credentials && { credentials }), 197 }); 198 199 let command; 200 if (analyzeLayout || analyzeTables) { 201 const FeatureTypes = []; 202 if (analyzeLayout) FeatureTypes.push('LAYOUT'); 203 if (analyzeTables) FeatureTypes.push('TABLES'); 204 command = new AnalyzeDocumentCommand({ 205 Document: { Bytes: data }, 206 FeatureTypes, 207 }); 208 } else { 209 command = new DetectDocumentTextCommand({ 210 Document: { Bytes: data }, 211 }); 212 } 213 214 const response = await textractClient.send(command); 215 return { 216 success: true, 217 rawData: JSON.stringify(response), 218 format: 'textract', 219 }; 220 } catch (error) { 221 return { 222 success: false, 223 error, 224 format: 'textract', 225 }; 226 } 227 } 228 229 static async checkAvailability() { 230 return { available: true }; 231 } 232}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.