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>
273 lines
6.8 KiB
TypeScript
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)
|
|
}
|