fix: Restore all files lost during destructive rebase
A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.
This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).
Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
419
studio-v2/lib/worksheet-editor/WorksheetContext.tsx
Normal file
419
studio-v2/lib/worksheet-editor/WorksheetContext.tsx
Normal file
@@ -0,0 +1,419 @@
|
||||
'use client'
|
||||
|
||||
import React, { createContext, useContext, useState, useCallback, useRef, useEffect, ReactNode } from 'react'
|
||||
import type { Canvas, Object as FabricObject } from 'fabric'
|
||||
import type {
|
||||
EditorTool,
|
||||
WorksheetDocument,
|
||||
WorksheetPage,
|
||||
PageFormat,
|
||||
HistoryEntry,
|
||||
DEFAULT_PAGE_FORMAT
|
||||
} from '@/app/worksheet-editor/types'
|
||||
|
||||
// Context Types
|
||||
interface WorksheetContextType {
|
||||
// Canvas
|
||||
canvas: Canvas | null
|
||||
setCanvas: (canvas: Canvas | null) => void
|
||||
|
||||
// Document
|
||||
document: WorksheetDocument | null
|
||||
setDocument: (doc: WorksheetDocument | null) => void
|
||||
|
||||
// Tool State
|
||||
activeTool: EditorTool
|
||||
setActiveTool: (tool: EditorTool) => void
|
||||
|
||||
// Selection
|
||||
selectedObjects: FabricObject[]
|
||||
setSelectedObjects: (objects: FabricObject[]) => void
|
||||
|
||||
// Zoom
|
||||
zoom: number
|
||||
setZoom: (zoom: number) => void
|
||||
zoomIn: () => void
|
||||
zoomOut: () => void
|
||||
zoomToFit: () => void
|
||||
|
||||
// Grid
|
||||
showGrid: boolean
|
||||
setShowGrid: (show: boolean) => void
|
||||
snapToGrid: boolean
|
||||
setSnapToGrid: (snap: boolean) => void
|
||||
gridSize: number
|
||||
setGridSize: (size: number) => void
|
||||
|
||||
// Pages
|
||||
currentPageIndex: number
|
||||
setCurrentPageIndex: (index: number) => void
|
||||
addPage: () => void
|
||||
deletePage: (index: number) => void
|
||||
|
||||
// History
|
||||
canUndo: boolean
|
||||
canRedo: boolean
|
||||
undo: () => void
|
||||
redo: () => void
|
||||
saveToHistory: (action: string) => void
|
||||
|
||||
// Save/Load
|
||||
saveDocument: () => Promise<void>
|
||||
loadDocument: (id: string) => Promise<void>
|
||||
|
||||
// Export
|
||||
exportToPDF: () => Promise<Blob>
|
||||
exportToImage: (format: 'png' | 'jpg') => Promise<Blob>
|
||||
|
||||
// Dirty State
|
||||
isDirty: boolean
|
||||
setIsDirty: (dirty: boolean) => void
|
||||
}
|
||||
|
||||
const WorksheetContext = createContext<WorksheetContextType | null>(null)
|
||||
|
||||
// Provider Props
|
||||
interface WorksheetProviderProps {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
// Generate unique ID
|
||||
const generateId = () => `ws_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
|
||||
|
||||
// Default Page Format
|
||||
const defaultPageFormat: PageFormat = {
|
||||
width: 210,
|
||||
height: 297,
|
||||
orientation: 'portrait',
|
||||
margins: { top: 15, right: 15, bottom: 15, left: 15 }
|
||||
}
|
||||
|
||||
export function WorksheetProvider({ children }: WorksheetProviderProps) {
|
||||
// Canvas State
|
||||
const [canvas, setCanvas] = useState<Canvas | null>(null)
|
||||
|
||||
// Document State
|
||||
const [document, setDocument] = useState<WorksheetDocument | null>(null)
|
||||
|
||||
// Editor State
|
||||
const [activeTool, setActiveTool] = useState<EditorTool>('select')
|
||||
const [selectedObjects, setSelectedObjects] = useState<FabricObject[]>([])
|
||||
const [zoom, setZoom] = useState(1)
|
||||
const [showGrid, setShowGrid] = useState(true)
|
||||
const [snapToGrid, setSnapToGrid] = useState(true)
|
||||
const [gridSize, setGridSize] = useState(10)
|
||||
const [currentPageIndex, setCurrentPageIndex] = useState(0)
|
||||
const [isDirty, setIsDirty] = useState(false)
|
||||
|
||||
// History State
|
||||
const historyRef = useRef<HistoryEntry[]>([])
|
||||
const historyIndexRef = useRef(-1)
|
||||
const [canUndo, setCanUndo] = useState(false)
|
||||
const [canRedo, setCanRedo] = useState(false)
|
||||
|
||||
// Initialize empty document
|
||||
useEffect(() => {
|
||||
if (!document) {
|
||||
const newDoc: WorksheetDocument = {
|
||||
id: generateId(),
|
||||
title: 'Neues Arbeitsblatt',
|
||||
pages: [{
|
||||
id: generateId(),
|
||||
index: 0,
|
||||
canvasJSON: ''
|
||||
}],
|
||||
pageFormat: defaultPageFormat,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString()
|
||||
}
|
||||
setDocument(newDoc)
|
||||
}
|
||||
}, [document])
|
||||
|
||||
// Zoom functions
|
||||
const zoomIn = useCallback(() => {
|
||||
setZoom(prev => Math.min(prev * 1.2, 4))
|
||||
}, [])
|
||||
|
||||
const zoomOut = useCallback(() => {
|
||||
setZoom(prev => Math.max(prev / 1.2, 0.25))
|
||||
}, [])
|
||||
|
||||
const zoomToFit = useCallback(() => {
|
||||
setZoom(1)
|
||||
}, [])
|
||||
|
||||
// Page functions
|
||||
const addPage = useCallback(() => {
|
||||
if (!document) return
|
||||
|
||||
const newPage: WorksheetPage = {
|
||||
id: generateId(),
|
||||
index: document.pages.length,
|
||||
canvasJSON: ''
|
||||
}
|
||||
|
||||
setDocument({
|
||||
...document,
|
||||
pages: [...document.pages, newPage],
|
||||
updatedAt: new Date().toISOString()
|
||||
})
|
||||
setCurrentPageIndex(document.pages.length)
|
||||
setIsDirty(true)
|
||||
}, [document])
|
||||
|
||||
const deletePage = useCallback((index: number) => {
|
||||
if (!document || document.pages.length <= 1) return
|
||||
|
||||
const newPages = document.pages.filter((_, i) => i !== index)
|
||||
.map((page, i) => ({ ...page, index: i }))
|
||||
|
||||
setDocument({
|
||||
...document,
|
||||
pages: newPages,
|
||||
updatedAt: new Date().toISOString()
|
||||
})
|
||||
|
||||
if (currentPageIndex >= newPages.length) {
|
||||
setCurrentPageIndex(newPages.length - 1)
|
||||
}
|
||||
setIsDirty(true)
|
||||
}, [document, currentPageIndex])
|
||||
|
||||
// History functions
|
||||
const saveToHistory = useCallback((action: string) => {
|
||||
if (!canvas) return
|
||||
|
||||
try {
|
||||
const canvasData = canvas.toJSON()
|
||||
if (!canvasData) return
|
||||
|
||||
const entry: HistoryEntry = {
|
||||
canvasJSON: JSON.stringify(canvasData),
|
||||
timestamp: Date.now(),
|
||||
action
|
||||
}
|
||||
|
||||
// Remove any future history if we're not at the end
|
||||
if (historyIndexRef.current < historyRef.current.length - 1) {
|
||||
historyRef.current = historyRef.current.slice(0, historyIndexRef.current + 1)
|
||||
}
|
||||
|
||||
historyRef.current.push(entry)
|
||||
historyIndexRef.current = historyRef.current.length - 1
|
||||
|
||||
// Limit history size
|
||||
if (historyRef.current.length > 50) {
|
||||
historyRef.current.shift()
|
||||
historyIndexRef.current--
|
||||
}
|
||||
|
||||
setCanUndo(historyIndexRef.current > 0)
|
||||
setCanRedo(false)
|
||||
setIsDirty(true)
|
||||
} catch (error) {
|
||||
console.error('Failed to save history:', error)
|
||||
}
|
||||
}, [canvas])
|
||||
|
||||
const undo = useCallback(() => {
|
||||
if (!canvas || historyIndexRef.current <= 0) return
|
||||
|
||||
historyIndexRef.current--
|
||||
const entry = historyRef.current[historyIndexRef.current]
|
||||
|
||||
canvas.loadFromJSON(JSON.parse(entry.canvasJSON), () => {
|
||||
canvas.renderAll()
|
||||
})
|
||||
|
||||
setCanUndo(historyIndexRef.current > 0)
|
||||
setCanRedo(true)
|
||||
}, [canvas])
|
||||
|
||||
const redo = useCallback(() => {
|
||||
if (!canvas || historyIndexRef.current >= historyRef.current.length - 1) return
|
||||
|
||||
historyIndexRef.current++
|
||||
const entry = historyRef.current[historyIndexRef.current]
|
||||
|
||||
canvas.loadFromJSON(JSON.parse(entry.canvasJSON), () => {
|
||||
canvas.renderAll()
|
||||
})
|
||||
|
||||
setCanUndo(true)
|
||||
setCanRedo(historyIndexRef.current < historyRef.current.length - 1)
|
||||
}, [canvas])
|
||||
|
||||
// Save/Load functions
|
||||
const saveDocument = useCallback(async () => {
|
||||
if (!canvas || !document) return
|
||||
|
||||
// Save current page state
|
||||
const currentPage = document.pages[currentPageIndex]
|
||||
currentPage.canvasJSON = JSON.stringify(canvas.toJSON())
|
||||
|
||||
// Get API base URL (use same protocol as page)
|
||||
const { hostname, protocol } = window.location
|
||||
const apiBase = hostname === 'localhost' ? 'http://localhost:8086' : `${protocol}//${hostname}:8086`
|
||||
|
||||
try {
|
||||
const response = await fetch(`${apiBase}/api/v1/worksheet/save`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(document)
|
||||
})
|
||||
|
||||
if (!response.ok) throw new Error('Failed to save')
|
||||
|
||||
const result = await response.json()
|
||||
setDocument({ ...document, id: result.id })
|
||||
setIsDirty(false)
|
||||
} catch (error) {
|
||||
console.error('Save failed:', error)
|
||||
// Fallback to localStorage
|
||||
localStorage.setItem(`worksheet_${document.id}`, JSON.stringify(document))
|
||||
setIsDirty(false)
|
||||
}
|
||||
}, [canvas, document, currentPageIndex])
|
||||
|
||||
const loadDocument = useCallback(async (id: string) => {
|
||||
const { hostname, protocol } = window.location
|
||||
const apiBase = hostname === 'localhost' ? 'http://localhost:8086' : `${protocol}//${hostname}:8086`
|
||||
|
||||
try {
|
||||
const response = await fetch(`${apiBase}/api/v1/worksheet/${id}`)
|
||||
|
||||
if (!response.ok) throw new Error('Failed to load')
|
||||
|
||||
const doc = await response.json()
|
||||
setDocument(doc)
|
||||
setCurrentPageIndex(0)
|
||||
|
||||
if (canvas && doc.pages[0]?.canvasJSON) {
|
||||
canvas.loadFromJSON(JSON.parse(doc.pages[0].canvasJSON), () => {
|
||||
canvas.renderAll()
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Load failed:', error)
|
||||
// Try localStorage fallback
|
||||
const stored = localStorage.getItem(`worksheet_${id}`)
|
||||
if (stored) {
|
||||
const doc = JSON.parse(stored)
|
||||
setDocument(doc)
|
||||
setCurrentPageIndex(0)
|
||||
}
|
||||
}
|
||||
}, [canvas])
|
||||
|
||||
// Export functions
|
||||
const exportToPDF = useCallback(async (): Promise<Blob> => {
|
||||
if (!canvas || !document) throw new Error('No canvas or document')
|
||||
|
||||
const { PDFDocument, rgb } = await import('pdf-lib')
|
||||
|
||||
const pdfDoc = await PDFDocument.create()
|
||||
|
||||
for (const page of document.pages) {
|
||||
const pdfPage = pdfDoc.addPage([
|
||||
document.pageFormat.width * 2.83465, // mm to points
|
||||
document.pageFormat.height * 2.83465
|
||||
])
|
||||
|
||||
// Export canvas as PNG and embed
|
||||
if (page.canvasJSON) {
|
||||
// Load the page content
|
||||
if (currentPageIndex !== page.index) {
|
||||
await new Promise<void>((resolve) => {
|
||||
canvas.loadFromJSON(JSON.parse(page.canvasJSON), () => {
|
||||
canvas.renderAll()
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const dataUrl = canvas.toDataURL({ format: 'png', multiplier: 2 })
|
||||
const imageBytes = await fetch(dataUrl).then(res => res.arrayBuffer())
|
||||
const image = await pdfDoc.embedPng(imageBytes)
|
||||
|
||||
pdfPage.drawImage(image, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: pdfPage.getWidth(),
|
||||
height: pdfPage.getHeight()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const pdfBytes = await pdfDoc.save()
|
||||
// Convert Uint8Array to ArrayBuffer for Blob compatibility
|
||||
const arrayBuffer = pdfBytes.slice().buffer as ArrayBuffer
|
||||
return new Blob([arrayBuffer], { type: 'application/pdf' })
|
||||
}, [canvas, document, currentPageIndex])
|
||||
|
||||
const exportToImage = useCallback(async (format: 'png' | 'jpg'): Promise<Blob> => {
|
||||
if (!canvas) throw new Error('No canvas')
|
||||
|
||||
// Fabric.js uses 'jpeg' not 'jpg'
|
||||
const fabricFormat = format === 'jpg' ? 'jpeg' : format
|
||||
const dataUrl = canvas.toDataURL({
|
||||
format: fabricFormat as 'png' | 'jpeg',
|
||||
quality: format === 'jpg' ? 0.9 : undefined,
|
||||
multiplier: 2
|
||||
})
|
||||
|
||||
const response = await fetch(dataUrl)
|
||||
return response.blob()
|
||||
}, [canvas])
|
||||
|
||||
const value: WorksheetContextType = {
|
||||
canvas,
|
||||
setCanvas,
|
||||
document,
|
||||
setDocument,
|
||||
activeTool,
|
||||
setActiveTool,
|
||||
selectedObjects,
|
||||
setSelectedObjects,
|
||||
zoom,
|
||||
setZoom,
|
||||
zoomIn,
|
||||
zoomOut,
|
||||
zoomToFit,
|
||||
showGrid,
|
||||
setShowGrid,
|
||||
snapToGrid,
|
||||
setSnapToGrid,
|
||||
gridSize,
|
||||
setGridSize,
|
||||
currentPageIndex,
|
||||
setCurrentPageIndex,
|
||||
addPage,
|
||||
deletePage,
|
||||
canUndo,
|
||||
canRedo,
|
||||
undo,
|
||||
redo,
|
||||
saveToHistory,
|
||||
saveDocument,
|
||||
loadDocument,
|
||||
exportToPDF,
|
||||
exportToImage,
|
||||
isDirty,
|
||||
setIsDirty
|
||||
}
|
||||
|
||||
return (
|
||||
<WorksheetContext.Provider value={value}>
|
||||
{children}
|
||||
</WorksheetContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function useWorksheet() {
|
||||
const context = useContext(WorksheetContext)
|
||||
if (!context) {
|
||||
throw new Error('useWorksheet must be used within a WorksheetProvider')
|
||||
}
|
||||
return context
|
||||
}
|
||||
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)
|
||||
}
|
||||
10
studio-v2/lib/worksheet-editor/index.ts
Normal file
10
studio-v2/lib/worksheet-editor/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Worksheet Editor Library
|
||||
*
|
||||
* Export context and utilities
|
||||
*/
|
||||
|
||||
export { WorksheetProvider, useWorksheet } from './WorksheetContext'
|
||||
|
||||
// Cleanup Service for handwriting removal
|
||||
export * from './cleanup-service'
|
||||
Reference in New Issue
Block a user