Files
forgery/src/api/extract.ts
T
google-labs-jules[bot] 7194663eb5 feat: Integrate Extraction UI Demo with Hono Backend
- Migrated frontend demo to `src/ui` as a Bun-native React app.
- Implemented `ExtractionService` with 5-step AI extraction flow (Analyze, Generate, Detect, Crop, Refine).
- Implemented `FileService` for image processing and storage using `sharp`.
- Implemented `EquipmentService` and `CategoryService` for managing game assets.
- Set up Hono RPC (`hc`) for type-safe frontend-backend communication.
- Configured `Bun.serve` to handle both API routes and frontend static files/SPA routing.
- Added `zod` validation for API endpoints.
- Updated `AGENTS.md` with new architecture details.
2026-02-07 14:45:29 +00:00

140 lines
4.4 KiB
TypeScript

import { Hono } from 'hono'
import { streamSSE } from 'hono/streaming'
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'
import { ExtractionService } from '../service/extraction'
import { FileService } from '../service/file'
import { EquipmentService } from '../service/equipment'
import { CategoryService } from '../service/category'
import { AssetService } from '../service/asset'
const extract = new Hono()
const extractSchema = z.object({
image: z.instanceof(File),
})
extract.post('/', zValidator('form', extractSchema), async (c) => {
const { image } = c.req.valid('form')
const file = image as File
return streamSSE(c, async (stream) => {
try {
// 1. Analyze
await stream.writeSSE({
event: 'status',
data: JSON.stringify({ status: 'analyzing', message: 'Analyzing character assets...' })
})
let analysis;
try {
analysis = await ExtractionService.analyzeImage(file)
} catch (err: unknown) {
const error = err instanceof Error ? err : new Error(String(err));
throw new Error("Analysis failed: " + error.message, { cause: error });
}
if (!analysis.assets || analysis.assets.length === 0) {
await stream.writeSSE({
event: 'complete',
data: JSON.stringify({ assets: [] })
})
return
}
// 2. Generate Texture Sheet
await stream.writeSSE({
event: 'status',
data: JSON.stringify({ status: 'generating', message: 'Generating texture sheet...' })
})
let sheetBase64: string;
try {
sheetBase64 = await ExtractionService.generateTextureSheet(file, analysis.assets)
} catch (err: unknown) {
const error = err instanceof Error ? err : new Error(String(err));
throw new Error("Generation failed: " + error.message, { cause: error });
}
// 3. Detect Bounding Boxes
await stream.writeSSE({
event: 'status',
data: JSON.stringify({ status: 'splitting', message: 'Detecting asset boundaries...' })
})
let boxes;
try {
boxes = await ExtractionService.detectBoundingBoxes(sheetBase64)
} catch (err: unknown) {
const error = err instanceof Error ? err : new Error(String(err));
throw new Error("Detection failed: " + error.message, { cause: error });
}
// 4. Crop Assets
const crops = await ExtractionService.cropAssets(sheetBase64, boxes)
// 5. Refine & Save
await stream.writeSSE({
event: 'status',
data: JSON.stringify({ status: 'refining', message: 'Refining and saving assets...' })
})
const refinedAssets = []
for (const crop of crops) {
// Find matching asset from analysis to get category/description
const originalMeta = analysis.assets.find(a => a.item_name === crop.name) || {
item_name: crop.name,
description: "Extracted asset",
category: "Uncategorized",
background_color: "unknown"
}
// Refine image
const refinedBase64 = await ExtractionService.refineAsset(crop.base64)
// Save to file system
const savedFile = await FileService.saveBase64Image(refinedBase64)
// Create Asset record (AssetService now has createAssetRecord)
const asset = await AssetService.createAssetRecord({
name: originalMeta.item_name,
type: 'image/webp',
path: savedFile.filename
})
// Find or create category
const categoryId = await CategoryService.findOrCreate(originalMeta.category)
// Create Equipment record linked to Asset and Category
const equipment = await EquipmentService.createEquipment({
name: originalMeta.item_name,
description: originalMeta.description,
imageId: asset.id,
categoryId: categoryId
})
refinedAssets.push({
...equipment,
imageUrl: `/files/${savedFile.filename}`, // Frontend needs URL
})
}
await stream.writeSSE({
event: 'complete',
data: JSON.stringify({ assets: refinedAssets })
})
} catch (e: unknown) {
console.error(e)
const error = e instanceof Error ? e : new Error(String(e));
await stream.writeSSE({
event: 'error',
data: JSON.stringify({ message: error.message || 'Extraction failed' })
})
}
})
})
export default extract