cards/apps/api/src/index.ts
Till JS 1212b62613 feat(cards): Deck-Generierung aus Bildern und PDFs via Vision-LLM
Neuer Endpoint POST /api/v1/decks/from-image akzeptiert bis zu 5 Bilder
(PNG/JPG/WebP, max 10 MiB je) oder PDFs (max 30 MiB je) als multipart/form-data.
Alle Dateien werden in einem einzigen mana-llm Vision-Call verarbeitet
(mana/vision → llava → Gemini 2.5-flash → GPT-4o Fallback-Chain).

PDFs werden von Gemini nativ verstanden (Layout, Tabellen, Bilder im Dokument)
ohne Zwischenschritt über Text-Extraktion oder Rendering. Der google.py-Provider
reicht den MIME-Type aus dem data:-URI direkt an types.Part.from_bytes() weiter.

- llm-client: chatVisionJson() mit images[]-Array (mehrere Bilder/Dokumente)
- decks-generate: GeneratedDeckSchema + insertGeneratedDeck() exportiert
- decks-from-image: neuer Route-Handler, MIME-Filter für image/* + application/pdf
- index: neue Route gemountet
- client.ts: apiForm() für multipart-Uploads ohne JSON.stringify
- decks.ts: generateDeckFromImage(files, opts)
- NewDeckCard + /decks/new: Dropzone mit Multi-File, Thumbnail-Strip, PDF-Icon

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 15:21:35 +02:00

89 lines
3.7 KiB
TypeScript

import { Hono } from 'hono';
import { cors } from 'hono/cors';
import { manifestRoute } from './routes/manifest.ts';
import { healthRoute } from './routes/health.ts';
import { decksRouter } from './routes/decks.ts';
import { cardsRouter } from './routes/cards.ts';
import { reviewsRouter } from './routes/reviews.ts';
import { shareRouter } from './routes/share.ts';
import { toolsRouter } from './routes/tools.ts';
import { searchRouter } from './routes/search.ts';
import { dsgvoRouter } from './routes/dsgvo.ts';
import { meRouter } from './routes/me.ts';
import { mediaRouter } from './routes/media.ts';
import { decksGenerateRouter } from './routes/decks-generate.ts';
import { decksFromImageRouter } from './routes/decks-from-image.ts';
import { authorsRouter as marketplaceAuthorsRouter } from './routes/marketplace/authors.ts';
import { marketplaceDecksRouter } from './routes/marketplace/decks.ts';
import { exploreRouter as marketplaceExploreRouter } from './routes/marketplace/explore.ts';
import { engagementRouter as marketplaceEngagementRouter } from './routes/marketplace/engagement.ts';
import { subscriptionsRouter as marketplaceSubscriptionsRouter } from './routes/marketplace/subscriptions.ts';
import { forkRouter as marketplaceForkRouter } from './routes/marketplace/fork.ts';
import { pullRequestsRouter as marketplacePullRequestsRouter } from './routes/marketplace/pull-requests.ts';
import { discussionsRouter as marketplaceDiscussionsRouter } from './routes/marketplace/discussions.ts';
const app = new Hono();
app.use(
'*',
cors({
origin: (origin) => {
if (!origin) return origin;
// Dev: localhost-Ports erlaubt. Prod: explizite Whitelist.
if (/^https?:\/\/localhost(:\d+)?$/.test(origin)) return origin;
if (/^https?:\/\/127\.0\.0\.1(:\d+)?$/.test(origin)) return origin;
if (origin === 'https://cardecky.mana.how') return origin;
return null;
},
allowHeaders: ['Content-Type', 'X-User-Id', 'Authorization', 'X-Service-Key'],
allowMethods: ['GET', 'POST', 'PATCH', 'DELETE', 'OPTIONS'],
credentials: true,
})
);
app.route('/', healthRoute);
app.route('/.well-known/mana-app.json', manifestRoute);
app.route('/api/v1/decks', decksRouter());
app.route('/api/v1/cards', cardsRouter());
app.route('/api/v1/reviews', reviewsRouter());
app.route('/api/v1/share', shareRouter());
app.route('/api/v1/tools', toolsRouter());
app.route('/api/v1/search', searchRouter());
app.route('/api/v1/dsgvo', dsgvoRouter());
app.route('/api/v1/me', meRouter());
app.route('/api/v1/media', mediaRouter());
app.route('/api/v1/decks/generate', decksGenerateRouter());
app.route('/api/v1/decks/from-image', decksFromImageRouter());
// Marketplace (Phase 12). Eigenes pgSchema, additive Routen unter /v1/marketplace/*.
// Plan: docs/playbooks/MARKETPLACE_RESTORE.md.
//
// Mount-Reihenfolge ist signifikant: spezifischere Routes vor /authors
// und /decks, damit z.B. /marketplace/me/subscriptions nicht zu authors
// oder decks geroutet wird.
app.route('/api/v1/marketplace', marketplaceExploreRouter());
app.route('/api/v1/marketplace', marketplaceEngagementRouter());
app.route('/api/v1/marketplace', marketplaceSubscriptionsRouter());
app.route('/api/v1/marketplace', marketplaceForkRouter());
app.route('/api/v1/marketplace', marketplacePullRequestsRouter());
app.route('/api/v1/marketplace', marketplaceDiscussionsRouter());
app.route('/api/v1/marketplace/authors', marketplaceAuthorsRouter());
app.route('/api/v1/marketplace/decks', marketplaceDecksRouter());
app.get('/', (c) =>
c.json({
app: 'cards',
version: process.env.CARDS_API_VERSION ?? '0.0.0',
see: '/.well-known/mana-app.json',
})
);
const port = Number(process.env.CARDS_API_PORT ?? 3081);
console.log(`[cards-api] listening on http://localhost:${port}`);
export default {
port,
fetch: app.fetch,
};