Spaces:
Running
Running
| // Web Worker for parallel OCR image generation | |
| // This worker uses OffscreenCanvas for true multi-threaded rendering | |
| interface RenderTask { | |
| id: number | |
| index: number | |
| text: string | |
| config: { | |
| width: number | |
| height: number | |
| textColor: string | |
| direction: string | |
| backgroundStyle: string | |
| backgroundColor: string | |
| } | |
| fontFamily: string | |
| shouldAugment: boolean | |
| augValues: Record<string, number> | |
| seed: number | |
| } | |
| interface RenderResult { | |
| id: number | |
| index: number | |
| filename: string | |
| blob: Blob | |
| label: string | |
| fontName: string | |
| augmentations: string[] | |
| backgroundStyle: string | |
| isAugmented: boolean | |
| error?: string | |
| } | |
| // Seeded random for reproducibility | |
| function seededRandom(seed: number) { | |
| let s = seed | |
| return function () { | |
| s = Math.sin(s) * 10000 | |
| return s - Math.floor(s) | |
| } | |
| } | |
| // Apply augmentations to OffscreenCanvas | |
| function applyAugmentation( | |
| ctx: OffscreenCanvasRenderingContext2D, | |
| canvas: OffscreenCanvas, | |
| augValues: Record<string, number>, | |
| random: () => number | |
| ): string[] { | |
| const applied: string[] = [] | |
| // Rotation | |
| if (augValues.rotation && random() > 0.5) { | |
| const angle = (random() - 0.5) * 2 * augValues.rotation * Math.PI / 180 | |
| ctx.translate(canvas.width / 2, canvas.height / 2) | |
| ctx.rotate(angle) | |
| ctx.translate(-canvas.width / 2, -canvas.height / 2) | |
| applied.push('rotation') | |
| } | |
| // Skew | |
| if (augValues.skew && random() > 0.5) { | |
| const skewAmount = (random() - 0.5) * augValues.skew * 0.01 | |
| ctx.transform(1, skewAmount, 0, 1, 0, 0) | |
| applied.push('skew') | |
| } | |
| return applied | |
| } | |
| // Render a single sample | |
| async function renderSample(task: RenderTask): Promise<RenderResult> { | |
| const random = seededRandom(task.seed + task.index * 1000) | |
| try { | |
| // Create OffscreenCanvas | |
| const canvas = new OffscreenCanvas(task.config.width, task.config.height) | |
| const ctx = canvas.getContext('2d') | |
| if (!ctx) { | |
| throw new Error('Could not get 2d context') | |
| } | |
| // Fill background | |
| ctx.fillStyle = task.config.backgroundColor | |
| ctx.fillRect(0, 0, canvas.width, canvas.height) | |
| // Apply augmentation transforms if enabled | |
| let appliedAugmentations: string[] = [] | |
| if (task.shouldAugment) { | |
| ctx.save() | |
| appliedAugmentations = applyAugmentation(ctx, canvas, task.augValues, random) | |
| } | |
| // Set text properties | |
| const fontSize = Math.min(canvas.height * 0.6, 48) | |
| ctx.font = `${fontSize}px "${task.fontFamily}", Arial, sans-serif` | |
| ctx.fillStyle = task.config.textColor | |
| ctx.textAlign = task.config.direction === 'rtl' ? 'right' : 'left' | |
| ctx.textBaseline = 'middle' | |
| // Draw text | |
| const x = task.config.direction === 'rtl' ? canvas.width - 10 : 10 | |
| const y = canvas.height / 2 | |
| ctx.direction = task.config.direction as CanvasDirection | |
| ctx.fillText(task.text, x, y) | |
| if (task.shouldAugment) { | |
| ctx.restore() | |
| } | |
| // Post-processing augmentations | |
| if (task.shouldAugment) { | |
| // Brightness | |
| if (task.augValues.brightness && random() > 0.5) { | |
| const adjustment = 1 + (random() - 0.5) * task.augValues.brightness / 50 | |
| const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height) | |
| for (let i = 0; i < imageData.data.length; i += 4) { | |
| imageData.data[i] = Math.min(255, imageData.data[i] * adjustment) | |
| imageData.data[i + 1] = Math.min(255, imageData.data[i + 1] * adjustment) | |
| imageData.data[i + 2] = Math.min(255, imageData.data[i + 2] * adjustment) | |
| } | |
| ctx.putImageData(imageData, 0, 0) | |
| appliedAugmentations.push('brightness') | |
| } | |
| // Noise | |
| if (task.augValues.gaussian_noise && random() > 0.6) { | |
| const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height) | |
| const noiseLevel = task.augValues.gaussian_noise / 2 | |
| for (let i = 0; i < imageData.data.length; i += 4) { | |
| const noise = (random() - 0.5) * noiseLevel | |
| imageData.data[i] = Math.max(0, Math.min(255, imageData.data[i] + noise)) | |
| imageData.data[i + 1] = Math.max(0, Math.min(255, imageData.data[i + 1] + noise)) | |
| imageData.data[i + 2] = Math.max(0, Math.min(255, imageData.data[i + 2] + noise)) | |
| } | |
| ctx.putImageData(imageData, 0, 0) | |
| appliedAugmentations.push('noise') | |
| } | |
| } | |
| // Convert to blob | |
| const blob = await canvas.convertToBlob({ type: 'image/png' }) | |
| const filename = `image_${String(task.index).padStart(6, '0')}.png` | |
| return { | |
| id: task.id, | |
| index: task.index, | |
| filename, | |
| blob, | |
| label: `${filename}\t${task.text}`, | |
| fontName: task.fontFamily, | |
| augmentations: appliedAugmentations, | |
| backgroundStyle: task.config.backgroundStyle, | |
| isAugmented: task.shouldAugment | |
| } | |
| } catch (error) { | |
| return { | |
| id: task.id, | |
| index: task.index, | |
| filename: '', | |
| blob: new Blob(), | |
| label: '', | |
| fontName: '', | |
| augmentations: [], | |
| backgroundStyle: '', | |
| isAugmented: false, | |
| error: error instanceof Error ? error.message : 'Unknown error' | |
| } | |
| } | |
| } | |
| // Handle messages from main thread | |
| self.onmessage = async (e: MessageEvent) => { | |
| const { type, tasks } = e.data | |
| if (type === 'render') { | |
| // Process all tasks in this batch | |
| const results: RenderResult[] = [] | |
| for (const task of tasks as RenderTask[]) { | |
| const result = await renderSample(task) | |
| results.push(result) | |
| // Send progress for each completed task | |
| self.postMessage({ type: 'progress', result }) | |
| } | |
| // Send completion signal | |
| self.postMessage({ type: 'complete', results }) | |
| } | |
| } | |
| // Signal that worker is ready | |
| self.postMessage({ type: 'ready' }) | |