feat: Sprint 2 — TrOCR ONNX, PP-DocLayout, Model Management
D2: TrOCR ONNX export script (printed + handwritten, int8 quantization) D3: PP-DocLayout ONNX export script (download or Docker-based conversion) B3: Model Management admin page (PyTorch vs ONNX status, benchmarks, config) A4: TrOCR ONNX service with runtime routing (auto/pytorch/onnx via TROCR_BACKEND) A5: PP-DocLayout ONNX detection with OpenCV fallback (via GRAPHIC_DETECT_BACKEND) B4: Structure Detection UI toggle (OpenCV vs PP-DocLayout) with class color coding C3: TrOCR-ONNX.md documentation C4: OCR-Pipeline.md ONNX section added C5: mkdocs.yml nav updated, optimum added to requirements.txt Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
550
admin-lehrer/app/(admin)/ai/model-management/page.tsx
Normal file
550
admin-lehrer/app/(admin)/ai/model-management/page.tsx
Normal file
@@ -0,0 +1,550 @@
|
||||
'use client'
|
||||
|
||||
/**
|
||||
* Model Management Page
|
||||
*
|
||||
* Manage ML model backends (PyTorch vs ONNX), view status,
|
||||
* run benchmarks, and configure inference settings.
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { PagePurpose } from '@/components/common/PagePurpose'
|
||||
import { AIToolsSidebarResponsive } from '@/components/ai/AIToolsSidebar'
|
||||
|
||||
const KLAUSUR_API = '/klausur-api'
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type BackendMode = 'auto' | 'pytorch' | 'onnx'
|
||||
type ModelStatus = 'available' | 'not_found' | 'loading' | 'error'
|
||||
type Tab = 'overview' | 'benchmarks' | 'configuration'
|
||||
|
||||
interface ModelInfo {
|
||||
name: string
|
||||
key: string
|
||||
pytorch: { status: ModelStatus; size_mb: number; ram_mb: number }
|
||||
onnx: { status: ModelStatus; size_mb: number; ram_mb: number; quantized: boolean }
|
||||
}
|
||||
|
||||
interface BenchmarkRow {
|
||||
model: string
|
||||
backend: string
|
||||
quantization: string
|
||||
size_mb: number
|
||||
ram_mb: number
|
||||
inference_ms: number
|
||||
load_time_s: number
|
||||
}
|
||||
|
||||
interface StatusInfo {
|
||||
active_backend: BackendMode
|
||||
loaded_models: string[]
|
||||
cache_hits: number
|
||||
cache_misses: number
|
||||
uptime_s: number
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mock data (used when backend is not available)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const MOCK_MODELS: ModelInfo[] = [
|
||||
{
|
||||
name: 'TrOCR Printed',
|
||||
key: 'trocr_printed',
|
||||
pytorch: { status: 'available', size_mb: 892, ram_mb: 1800 },
|
||||
onnx: { status: 'available', size_mb: 234, ram_mb: 620, quantized: true },
|
||||
},
|
||||
{
|
||||
name: 'TrOCR Handwritten',
|
||||
key: 'trocr_handwritten',
|
||||
pytorch: { status: 'available', size_mb: 892, ram_mb: 1800 },
|
||||
onnx: { status: 'not_found', size_mb: 0, ram_mb: 0, quantized: false },
|
||||
},
|
||||
{
|
||||
name: 'PP-DocLayout',
|
||||
key: 'pp_doclayout',
|
||||
pytorch: { status: 'not_found', size_mb: 0, ram_mb: 0 },
|
||||
onnx: { status: 'available', size_mb: 48, ram_mb: 180, quantized: false },
|
||||
},
|
||||
]
|
||||
|
||||
const MOCK_BENCHMARKS: BenchmarkRow[] = [
|
||||
{ model: 'TrOCR Printed', backend: 'PyTorch', quantization: 'FP32', size_mb: 892, ram_mb: 1800, inference_ms: 142, load_time_s: 3.2 },
|
||||
{ model: 'TrOCR Printed', backend: 'ONNX', quantization: 'INT8', size_mb: 234, ram_mb: 620, inference_ms: 38, load_time_s: 0.8 },
|
||||
{ model: 'TrOCR Handwritten', backend: 'PyTorch', quantization: 'FP32', size_mb: 892, ram_mb: 1800, inference_ms: 156, load_time_s: 3.4 },
|
||||
{ model: 'PP-DocLayout', backend: 'ONNX', quantization: 'FP32', size_mb: 48, ram_mb: 180, inference_ms: 22, load_time_s: 0.3 },
|
||||
]
|
||||
|
||||
const MOCK_STATUS: StatusInfo = {
|
||||
active_backend: 'auto',
|
||||
loaded_models: ['trocr_printed (ONNX)', 'pp_doclayout (ONNX)'],
|
||||
cache_hits: 1247,
|
||||
cache_misses: 83,
|
||||
uptime_s: 86400,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function StatusBadge({ status }: { status: ModelStatus }) {
|
||||
const cls =
|
||||
status === 'available'
|
||||
? 'bg-emerald-100 text-emerald-800 border-emerald-200'
|
||||
: status === 'loading'
|
||||
? 'bg-blue-100 text-blue-800 border-blue-200'
|
||||
: status === 'not_found'
|
||||
? 'bg-slate-100 text-slate-500 border-slate-200'
|
||||
: 'bg-red-100 text-red-800 border-red-200'
|
||||
const label =
|
||||
status === 'available' ? 'Verfuegbar'
|
||||
: status === 'loading' ? 'Laden...'
|
||||
: status === 'not_found' ? 'Nicht vorhanden'
|
||||
: 'Fehler'
|
||||
return (
|
||||
<span className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium border ${cls}`}>
|
||||
{label}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
function formatBytes(mb: number) {
|
||||
if (mb === 0) return '--'
|
||||
if (mb >= 1000) return `${(mb / 1000).toFixed(1)} GB`
|
||||
return `${mb} MB`
|
||||
}
|
||||
|
||||
function formatUptime(seconds: number) {
|
||||
const h = Math.floor(seconds / 3600)
|
||||
const m = Math.floor((seconds % 3600) / 60)
|
||||
if (h > 0) return `${h}h ${m}m`
|
||||
return `${m}m`
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Component
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export default function ModelManagementPage() {
|
||||
const [tab, setTab] = useState<Tab>('overview')
|
||||
const [models, setModels] = useState<ModelInfo[]>(MOCK_MODELS)
|
||||
const [benchmarks, setBenchmarks] = useState<BenchmarkRow[]>(MOCK_BENCHMARKS)
|
||||
const [status, setStatus] = useState<StatusInfo>(MOCK_STATUS)
|
||||
const [backend, setBackend] = useState<BackendMode>('auto')
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [benchmarkRunning, setBenchmarkRunning] = useState(false)
|
||||
const [usingMock, setUsingMock] = useState(false)
|
||||
|
||||
// Load status
|
||||
const loadStatus = useCallback(async () => {
|
||||
try {
|
||||
const res = await fetch(`${KLAUSUR_API}/api/v1/models/status`)
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
setStatus(data)
|
||||
setBackend(data.active_backend || 'auto')
|
||||
setUsingMock(false)
|
||||
} else {
|
||||
setUsingMock(true)
|
||||
}
|
||||
} catch {
|
||||
setUsingMock(true)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Load models
|
||||
const loadModels = useCallback(async () => {
|
||||
try {
|
||||
const res = await fetch(`${KLAUSUR_API}/api/v1/models`)
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
if (data.models?.length) setModels(data.models)
|
||||
}
|
||||
} catch {
|
||||
// Keep mock data
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Load benchmarks
|
||||
const loadBenchmarks = useCallback(async () => {
|
||||
try {
|
||||
const res = await fetch(`${KLAUSUR_API}/api/v1/models/benchmarks`)
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
if (data.benchmarks?.length) setBenchmarks(data.benchmarks)
|
||||
}
|
||||
} catch {
|
||||
// Keep mock data
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
loadStatus()
|
||||
loadModels()
|
||||
loadBenchmarks()
|
||||
}, [loadStatus, loadModels, loadBenchmarks])
|
||||
|
||||
// Save backend preference
|
||||
const saveBackend = async (mode: BackendMode) => {
|
||||
setBackend(mode)
|
||||
setSaving(true)
|
||||
try {
|
||||
await fetch(`${KLAUSUR_API}/api/v1/models/backend`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ backend: mode }),
|
||||
})
|
||||
await loadStatus()
|
||||
} catch {
|
||||
// Silently handle — mock mode
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
// Run benchmark
|
||||
const runBenchmark = async () => {
|
||||
setBenchmarkRunning(true)
|
||||
try {
|
||||
const res = await fetch(`${KLAUSUR_API}/api/v1/models/benchmark`, {
|
||||
method: 'POST',
|
||||
})
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
if (data.benchmarks?.length) setBenchmarks(data.benchmarks)
|
||||
}
|
||||
await loadBenchmarks()
|
||||
} catch {
|
||||
// Keep existing data
|
||||
} finally {
|
||||
setBenchmarkRunning(false)
|
||||
}
|
||||
}
|
||||
|
||||
const tabs: { key: Tab; label: string }[] = [
|
||||
{ key: 'overview', label: 'Uebersicht' },
|
||||
{ key: 'benchmarks', label: 'Benchmarks' },
|
||||
{ key: 'configuration', label: 'Konfiguration' },
|
||||
]
|
||||
|
||||
return (
|
||||
<AIToolsSidebarResponsive>
|
||||
<div className="max-w-7xl mx-auto p-6 space-y-6">
|
||||
<PagePurpose
|
||||
title="Model Management"
|
||||
purpose="Verwaltung der ML-Modelle fuer OCR und Layout-Erkennung. Vergleich von PyTorch- und ONNX-Backends, Benchmark-Tests und Backend-Konfiguration."
|
||||
audience={['Entwickler', 'DevOps']}
|
||||
defaultCollapsed
|
||||
architecture={{
|
||||
services: ['klausur-service (FastAPI, Port 8086)'],
|
||||
databases: ['Dateisystem (Modell-Dateien)'],
|
||||
}}
|
||||
relatedPages={[
|
||||
{ name: 'OCR Pipeline', href: '/ai/ocr-pipeline', description: 'OCR-Pipeline ausfuehren' },
|
||||
{ name: 'OCR Vergleich', href: '/ai/ocr-compare', description: 'OCR-Methoden vergleichen' },
|
||||
{ name: 'GPU Infrastruktur', href: '/ai/gpu', description: 'GPU-Ressourcen verwalten' },
|
||||
]}
|
||||
/>
|
||||
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-slate-900">Model Management</h1>
|
||||
<p className="text-sm text-slate-500 mt-1">
|
||||
{models.length} Modelle konfiguriert
|
||||
{usingMock && (
|
||||
<span className="ml-2 text-xs bg-amber-100 text-amber-700 px-1.5 py-0.5 rounded">
|
||||
Mock-Daten (Backend nicht erreichbar)
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Status Cards */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<div className="bg-white rounded-lg border border-slate-200 px-4 py-3">
|
||||
<p className="text-xs text-slate-500 uppercase font-medium">Aktives Backend</p>
|
||||
<p className="text-lg font-semibold text-slate-900 mt-1">{status.active_backend.toUpperCase()}</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-lg border border-slate-200 px-4 py-3">
|
||||
<p className="text-xs text-slate-500 uppercase font-medium">Geladene Modelle</p>
|
||||
<p className="text-lg font-semibold text-slate-900 mt-1">{status.loaded_models.length}</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-lg border border-slate-200 px-4 py-3">
|
||||
<p className="text-xs text-slate-500 uppercase font-medium">Cache Hit-Rate</p>
|
||||
<p className="text-lg font-semibold text-slate-900 mt-1">
|
||||
{status.cache_hits + status.cache_misses > 0
|
||||
? `${((status.cache_hits / (status.cache_hits + status.cache_misses)) * 100).toFixed(1)}%`
|
||||
: '--'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-lg border border-slate-200 px-4 py-3">
|
||||
<p className="text-xs text-slate-500 uppercase font-medium">Uptime</p>
|
||||
<p className="text-lg font-semibold text-slate-900 mt-1">{formatUptime(status.uptime_s)}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="border-b border-slate-200">
|
||||
<nav className="flex gap-4">
|
||||
{tabs.map(t => (
|
||||
<button
|
||||
key={t.key}
|
||||
onClick={() => setTab(t.key)}
|
||||
className={`pb-3 px-1 text-sm font-medium border-b-2 transition-colors ${
|
||||
tab === t.key
|
||||
? 'border-teal-500 text-teal-600'
|
||||
: 'border-transparent text-slate-500 hover:text-slate-700'
|
||||
}`}
|
||||
>
|
||||
{t.label}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{/* Overview Tab */}
|
||||
{tab === 'overview' && (
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-sm font-medium text-slate-700">Verfuegbare Modelle</h3>
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{models.map(m => (
|
||||
<div key={m.key} className="bg-white rounded-lg border border-slate-200 overflow-hidden">
|
||||
<div className="px-4 py-3 border-b border-slate-100">
|
||||
<h4 className="font-semibold text-slate-900">{m.name}</h4>
|
||||
<p className="text-xs text-slate-400 mt-0.5 font-mono">{m.key}</p>
|
||||
</div>
|
||||
<div className="px-4 py-3 space-y-3">
|
||||
{/* PyTorch */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs font-medium text-slate-600 w-16">PyTorch</span>
|
||||
<StatusBadge status={m.pytorch.status} />
|
||||
</div>
|
||||
{m.pytorch.status === 'available' && (
|
||||
<span className="text-xs text-slate-400">
|
||||
{formatBytes(m.pytorch.size_mb)} / {formatBytes(m.pytorch.ram_mb)} RAM
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{/* ONNX */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs font-medium text-slate-600 w-16">ONNX</span>
|
||||
<StatusBadge status={m.onnx.status} />
|
||||
</div>
|
||||
{m.onnx.status === 'available' && (
|
||||
<span className="text-xs text-slate-400">
|
||||
{formatBytes(m.onnx.size_mb)} / {formatBytes(m.onnx.ram_mb)} RAM
|
||||
{m.onnx.quantized && (
|
||||
<span className="ml-1 text-xs bg-violet-100 text-violet-700 px-1 rounded">INT8</span>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Loaded Models List */}
|
||||
{status.loaded_models.length > 0 && (
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-slate-700 mb-2">Aktuell geladen</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{status.loaded_models.map((m, i) => (
|
||||
<span key={i} className="inline-flex items-center px-3 py-1 rounded-full text-sm bg-teal-50 text-teal-700 border border-teal-200">
|
||||
{m}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Benchmarks Tab */}
|
||||
{tab === 'benchmarks' && (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm font-medium text-slate-700">PyTorch vs ONNX Vergleich</h3>
|
||||
<button
|
||||
onClick={runBenchmark}
|
||||
disabled={benchmarkRunning}
|
||||
className="inline-flex items-center gap-2 px-4 py-2 bg-teal-600 text-white rounded-lg hover:bg-teal-700 disabled:opacity-50 disabled:cursor-not-allowed text-sm font-medium transition-colors"
|
||||
>
|
||||
{benchmarkRunning ? (
|
||||
<>
|
||||
<svg className="animate-spin h-4 w-4" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
|
||||
</svg>
|
||||
Benchmark laeuft...
|
||||
</>
|
||||
) : (
|
||||
'Benchmark starten'
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-lg border border-slate-200 overflow-hidden">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-slate-200 bg-slate-50 text-left text-slate-500">
|
||||
<th className="px-4 py-3 font-medium">Modell</th>
|
||||
<th className="px-4 py-3 font-medium">Backend</th>
|
||||
<th className="px-4 py-3 font-medium">Quantisierung</th>
|
||||
<th className="px-4 py-3 font-medium text-right">Groesse</th>
|
||||
<th className="px-4 py-3 font-medium text-right">RAM</th>
|
||||
<th className="px-4 py-3 font-medium text-right">Inferenz</th>
|
||||
<th className="px-4 py-3 font-medium text-right">Ladezeit</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{benchmarks.map((b, i) => (
|
||||
<tr key={i} className="border-b border-slate-100 hover:bg-slate-50">
|
||||
<td className="px-4 py-3 font-medium text-slate-900">{b.model}</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${
|
||||
b.backend === 'ONNX'
|
||||
? 'bg-violet-100 text-violet-700'
|
||||
: 'bg-orange-100 text-orange-700'
|
||||
}`}>
|
||||
{b.backend}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-slate-600">{b.quantization}</td>
|
||||
<td className="px-4 py-3 text-right text-slate-600">{formatBytes(b.size_mb)}</td>
|
||||
<td className="px-4 py-3 text-right text-slate-600">{formatBytes(b.ram_mb)}</td>
|
||||
<td className="px-4 py-3 text-right">
|
||||
<span className={`font-mono ${b.inference_ms < 50 ? 'text-emerald-600' : b.inference_ms < 100 ? 'text-amber-600' : 'text-red-600'}`}>
|
||||
{b.inference_ms} ms
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right text-slate-500">{b.load_time_s.toFixed(1)}s</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{benchmarks.length === 0 && (
|
||||
<div className="text-center py-12 text-slate-400">
|
||||
<p className="text-lg">Keine Benchmark-Daten</p>
|
||||
<p className="text-sm mt-1">Klicken Sie "Benchmark starten" um einen Vergleich durchzufuehren.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Configuration Tab */}
|
||||
{tab === 'configuration' && (
|
||||
<div className="space-y-6">
|
||||
{/* Backend Selector */}
|
||||
<div className="bg-white rounded-lg border border-slate-200 p-5">
|
||||
<h3 className="text-sm font-semibold text-slate-900 mb-1">Inference Backend</h3>
|
||||
<p className="text-sm text-slate-500 mb-4">
|
||||
Waehlen Sie welches Backend fuer die Modell-Inferenz verwendet werden soll.
|
||||
</p>
|
||||
<div className="space-y-3">
|
||||
{([
|
||||
{
|
||||
mode: 'auto' as const,
|
||||
label: 'Auto',
|
||||
desc: 'ONNX wenn verfuegbar, Fallback auf PyTorch.',
|
||||
},
|
||||
{
|
||||
mode: 'pytorch' as const,
|
||||
label: 'PyTorch',
|
||||
desc: 'Immer PyTorch verwenden. Hoeherer RAM-Verbrauch, volle Flexibilitaet.',
|
||||
},
|
||||
{
|
||||
mode: 'onnx' as const,
|
||||
label: 'ONNX',
|
||||
desc: 'Immer ONNX verwenden. Schneller und weniger RAM, Fehler wenn nicht vorhanden.',
|
||||
},
|
||||
] as const).map(opt => (
|
||||
<label
|
||||
key={opt.mode}
|
||||
className={`flex items-start gap-3 p-3 rounded-lg border cursor-pointer transition-colors ${
|
||||
backend === opt.mode
|
||||
? 'border-teal-300 bg-teal-50'
|
||||
: 'border-slate-200 hover:bg-slate-50'
|
||||
}`}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="backend"
|
||||
value={opt.mode}
|
||||
checked={backend === opt.mode}
|
||||
onChange={() => saveBackend(opt.mode)}
|
||||
disabled={saving}
|
||||
className="mt-1 text-teal-600 focus:ring-teal-500"
|
||||
/>
|
||||
<div>
|
||||
<span className="font-medium text-slate-900">{opt.label}</span>
|
||||
<p className="text-sm text-slate-500 mt-0.5">{opt.desc}</p>
|
||||
</div>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
{saving && (
|
||||
<p className="text-xs text-teal-600 mt-3">Speichere...</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Model Details Table */}
|
||||
<div className="bg-white rounded-lg border border-slate-200 p-5">
|
||||
<h3 className="text-sm font-semibold text-slate-900 mb-4">Modell-Details</h3>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-slate-200 text-left text-slate-500">
|
||||
<th className="pb-2 font-medium">Modell</th>
|
||||
<th className="pb-2 font-medium">PyTorch</th>
|
||||
<th className="pb-2 font-medium text-right">Groesse (PT)</th>
|
||||
<th className="pb-2 font-medium">ONNX</th>
|
||||
<th className="pb-2 font-medium text-right">Groesse (ONNX)</th>
|
||||
<th className="pb-2 font-medium text-right">Einsparung</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{models.map(m => {
|
||||
const ptAvail = m.pytorch.status === 'available'
|
||||
const oxAvail = m.onnx.status === 'available'
|
||||
const savings = ptAvail && oxAvail && m.pytorch.size_mb > 0
|
||||
? Math.round((1 - m.onnx.size_mb / m.pytorch.size_mb) * 100)
|
||||
: null
|
||||
return (
|
||||
<tr key={m.key} className="border-b border-slate-100">
|
||||
<td className="py-2.5 font-medium text-slate-900">{m.name}</td>
|
||||
<td className="py-2.5"><StatusBadge status={m.pytorch.status} /></td>
|
||||
<td className="py-2.5 text-right text-slate-500">{ptAvail ? formatBytes(m.pytorch.size_mb) : '--'}</td>
|
||||
<td className="py-2.5"><StatusBadge status={m.onnx.status} /></td>
|
||||
<td className="py-2.5 text-right text-slate-500">{oxAvail ? formatBytes(m.onnx.size_mb) : '--'}</td>
|
||||
<td className="py-2.5 text-right">
|
||||
{savings !== null ? (
|
||||
<span className="text-emerald-600 font-medium">-{savings}%</span>
|
||||
) : (
|
||||
<span className="text-slate-300">--</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</AIToolsSidebarResponsive>
|
||||
)
|
||||
}
|
||||
@@ -233,6 +233,15 @@ export interface ExcludeRegion {
|
||||
label?: string
|
||||
}
|
||||
|
||||
export interface DocLayoutRegion {
|
||||
x: number
|
||||
y: number
|
||||
w: number
|
||||
h: number
|
||||
class_name: string
|
||||
confidence: number
|
||||
}
|
||||
|
||||
export interface StructureResult {
|
||||
image_width: number
|
||||
image_height: number
|
||||
@@ -246,6 +255,9 @@ export interface StructureResult {
|
||||
word_count: number
|
||||
border_ghosts_removed?: number
|
||||
duration_seconds: number
|
||||
/** PP-DocLayout regions (only present when method=ppdoclayout) */
|
||||
layout_regions?: DocLayoutRegion[]
|
||||
detection_method?: 'opencv' | 'ppdoclayout'
|
||||
}
|
||||
|
||||
export interface StructureBox {
|
||||
|
||||
Reference in New Issue
Block a user