Files
breakpilot-lehrer/studio-v2/lib/worksheet-editor/cleanup-service.ts
Benjamin Boenisch 5a31f52310 Initial commit: breakpilot-lehrer - Lehrer KI Platform
Services: Admin-Lehrer, Backend-Lehrer, Studio v2, Website,
Klausur-Service, School-Service, Voice-Service, Geo-Service,
BreakPilot Drive, Agent-Core

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 23:47:26 +01:00

273 lines
6.8 KiB
TypeScript

/**
* Worksheet Cleanup Service
*
* API client for the worksheet cleanup endpoints:
* - Handwriting detection
* - Handwriting removal (inpainting)
* - Layout reconstruction
*
* All processing happens on the local Mac Mini server.
*/
export interface CleanupCapabilities {
opencv_available: boolean
lama_available: boolean
paddleocr_available: boolean
}
export interface DetectionResult {
has_handwriting: boolean
confidence: number
handwriting_ratio: number
detection_method: string
mask_base64?: string
}
export interface PreviewResult {
has_handwriting: boolean
confidence: number
handwriting_ratio: number
image_width: number
image_height: number
estimated_times_ms: {
detection: number
inpainting: number
reconstruction: number
total: number
}
capabilities: {
lama_available: boolean
}
}
export interface PipelineResult {
success: boolean
handwriting_detected: boolean
handwriting_removed: boolean
layout_reconstructed: boolean
cleaned_image_base64?: string
fabric_json?: any
metadata: {
detection?: {
confidence: number
handwriting_ratio: number
method: string
}
inpainting?: {
method_used: string
processing_time_ms: number
}
layout?: {
element_count: number
table_count: number
page_width: number
page_height: number
}
}
}
export type InpaintingMethod = 'auto' | 'opencv_telea' | 'opencv_ns' | 'lama'
export interface CleanupOptions {
removeHandwriting: boolean
reconstructLayout: boolean
inpaintingMethod: InpaintingMethod
}
/**
* Get the API base URL for the klausur-service
*/
function getApiUrl(): string {
if (typeof window === 'undefined') return 'http://localhost:8086'
const { hostname, protocol } = window.location
return hostname === 'localhost' ? 'http://localhost:8086' : `${protocol}//${hostname}:8086`
}
/**
* Get available cleanup capabilities on the server
*/
export async function getCapabilities(): Promise<CleanupCapabilities> {
const response = await fetch(`${getApiUrl()}/api/v1/worksheet/capabilities`)
if (!response.ok) {
throw new Error(`HTTP ${response.status}`)
}
return response.json()
}
/**
* Quick preview of cleanup without full processing
*/
export async function previewCleanup(file: File): Promise<PreviewResult> {
const formData = new FormData()
formData.append('image', file)
const response = await fetch(`${getApiUrl()}/api/v1/worksheet/preview-cleanup`, {
method: 'POST',
body: formData
})
if (!response.ok) {
const error = await response.json().catch(() => ({ detail: 'Unknown error' }))
throw new Error(error.detail || `HTTP ${response.status}`)
}
return response.json()
}
/**
* Detect handwriting in an image
*/
export async function detectHandwriting(
file: File,
options: { returnMask?: boolean; minConfidence?: number } = {}
): Promise<DetectionResult> {
const formData = new FormData()
formData.append('image', file)
formData.append('return_mask', String(options.returnMask ?? true))
formData.append('min_confidence', String(options.minConfidence ?? 0.3))
const response = await fetch(`${getApiUrl()}/api/v1/worksheet/detect-handwriting`, {
method: 'POST',
body: formData
})
if (!response.ok) {
const error = await response.json().catch(() => ({ detail: 'Unknown error' }))
throw new Error(error.detail || `HTTP ${response.status}`)
}
return response.json()
}
/**
* Get the handwriting detection mask as an image blob
*/
export async function getHandwritingMask(file: File): Promise<Blob> {
const formData = new FormData()
formData.append('image', file)
const response = await fetch(`${getApiUrl()}/api/v1/worksheet/detect-handwriting/mask`, {
method: 'POST',
body: formData
})
if (!response.ok) {
throw new Error(`HTTP ${response.status}`)
}
return response.blob()
}
/**
* Remove handwriting from an image
*/
export async function removeHandwriting(
file: File,
options: {
mask?: File
method?: InpaintingMethod
returnBase64?: boolean
} = {}
): Promise<{ imageBlob?: Blob; imageBase64?: string; metadata: any }> {
const formData = new FormData()
formData.append('image', file)
formData.append('method', options.method ?? 'auto')
formData.append('return_base64', String(options.returnBase64 ?? false))
if (options.mask) {
formData.append('mask', options.mask)
}
const response = await fetch(`${getApiUrl()}/api/v1/worksheet/remove-handwriting`, {
method: 'POST',
body: formData
})
if (!response.ok) {
const error = await response.json().catch(() => ({ detail: 'Unknown error' }))
throw new Error(error.detail || `HTTP ${response.status}`)
}
if (options.returnBase64) {
const data = await response.json()
return {
imageBase64: data.image_base64,
metadata: data.metadata
}
} else {
const imageBlob = await response.blob()
const methodUsed = response.headers.get('X-Method-Used') || 'unknown'
const processingTime = parseFloat(response.headers.get('X-Processing-Time-Ms') || '0')
return {
imageBlob,
metadata: {
method_used: methodUsed,
processing_time_ms: processingTime
}
}
}
}
/**
* Run the full cleanup pipeline
*/
export async function runCleanupPipeline(
file: File,
options: CleanupOptions = {
removeHandwriting: true,
reconstructLayout: true,
inpaintingMethod: 'auto'
}
): Promise<PipelineResult> {
const formData = new FormData()
formData.append('image', file)
formData.append('remove_handwriting', String(options.removeHandwriting))
formData.append('reconstruct', String(options.reconstructLayout))
formData.append('inpainting_method', options.inpaintingMethod)
const response = await fetch(`${getApiUrl()}/api/v1/worksheet/cleanup-pipeline`, {
method: 'POST',
body: formData
})
if (!response.ok) {
const error = await response.json().catch(() => ({ detail: 'Unknown error' }))
throw new Error(error.detail || `HTTP ${response.status}`)
}
return response.json()
}
/**
* Helper to convert base64 to blob
*/
export function base64ToBlob(base64: string, mimeType: string = 'image/png'): Blob {
const byteCharacters = atob(base64)
const byteArrays = []
for (let offset = 0; offset < byteCharacters.length; offset += 512) {
const slice = byteCharacters.slice(offset, offset + 512)
const byteNumbers = new Array(slice.length)
for (let i = 0; i < slice.length; i++) {
byteNumbers[i] = slice.charCodeAt(i)
}
const byteArray = new Uint8Array(byteNumbers)
byteArrays.push(byteArray)
}
return new Blob(byteArrays, { type: mimeType })
}
/**
* Helper to create object URL from base64
*/
export function base64ToObjectUrl(base64: string, mimeType: string = 'image/png'): string {
const blob = base64ToBlob(base64, mimeType)
return URL.createObjectURL(blob)
}