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>
This commit is contained in:
272
studio-v2/lib/worksheet-editor/cleanup-service.ts
Normal file
272
studio-v2/lib/worksheet-editor/cleanup-service.ts
Normal file
@@ -0,0 +1,272 @@
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
Reference in New Issue
Block a user