PageSourceSearch

https://scribeocr.com/scribe.js/cloud-adapters/aws-textract/RecognitionModelAwsTextractBrowser.js

js scribeocr.com collected 2026-09-25 20:04:40 UTC 7,593 bytes, 232 lines download raw bytes

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.