init backend
This commit is contained in:
1
.dockerignore
Normal file
1
.dockerignore
Normal file
@@ -0,0 +1 @@
|
|||||||
|
python_service/venv
|
||||||
36
.gitignore
vendored
Normal file
36
.gitignore
vendored
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# dependencies (bun install)
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
# output
|
||||||
|
out
|
||||||
|
dist
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# code coverage
|
||||||
|
coverage
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# logs
|
||||||
|
logs
|
||||||
|
_.log
|
||||||
|
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||||
|
|
||||||
|
# dotenv environment variable files
|
||||||
|
.env
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# caches
|
||||||
|
.eslintcache
|
||||||
|
.cache
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# IntelliJ based IDEs
|
||||||
|
.idea
|
||||||
|
|
||||||
|
# Finder (MacOS) folder config
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
**/venv
|
||||||
15
README.md
Normal file
15
README.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# desktop
|
||||||
|
|
||||||
|
To install dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun install
|
||||||
|
```
|
||||||
|
|
||||||
|
To run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run index.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
This project was created using `bun init` in bun v1.3.5. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
|
||||||
26
bun.lock
Normal file
26
bun.lock
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"lockfileVersion": 1,
|
||||||
|
"configVersion": 1,
|
||||||
|
"workspaces": {
|
||||||
|
"": {
|
||||||
|
"name": "desktop",
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/bun": "latest",
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": "^5",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"packages": {
|
||||||
|
"@types/bun": ["@types/bun@1.3.5", "", { "dependencies": { "bun-types": "1.3.5" } }, "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w=="],
|
||||||
|
|
||||||
|
"@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="],
|
||||||
|
|
||||||
|
"bun-types": ["bun-types@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="],
|
||||||
|
|
||||||
|
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||||
|
|
||||||
|
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||||
|
}
|
||||||
|
}
|
||||||
31
dockerfile
Normal file
31
dockerfile
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# --- Base image with Node + Python ---
|
||||||
|
FROM ubuntu:22.04
|
||||||
|
|
||||||
|
# --- Install system dependencies ---
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
curl python3 python3-pip git build-essential libgl1 libglvnd0 ffmpeg unzip \
|
||||||
|
&& apt-get clean \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# --- Install Bun ---
|
||||||
|
RUN curl -fsSL https://bun.sh/install | bash
|
||||||
|
ENV PATH="/root/.bun/bin:${PATH}"
|
||||||
|
|
||||||
|
# --- Install Python dependencies ---
|
||||||
|
RUN pip3 install --upgrade pip
|
||||||
|
RUN pip3 install torch --index-url https://download.pytorch.org/whl/cpu
|
||||||
|
RUN pip3 install --no-cache-dir fastapi uvicorn pillow open-clip-torch numpy faiss-cpu python-multipart
|
||||||
|
|
||||||
|
# --- Copy project files ---
|
||||||
|
WORKDIR /app
|
||||||
|
COPY python_service ./python_service
|
||||||
|
COPY src ./src
|
||||||
|
COPY package.json .
|
||||||
|
COPY bun.lock .
|
||||||
|
|
||||||
|
# --- Expose ports ---
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
# --- Start services ---
|
||||||
|
# Use & to run Python worker in background, Bun frontend as main process
|
||||||
|
CMD python3 python_service/app.py & bun run src/index.ts
|
||||||
15
package.json
Normal file
15
package.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"name": "desktop",
|
||||||
|
"module": "src/index.ts",
|
||||||
|
"type": "module",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "bun run src/index.ts"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/bun": "latest"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": "^5"
|
||||||
|
}
|
||||||
|
}
|
||||||
81
python_service/app.py
Normal file
81
python_service/app.py
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
from fastapi import FastAPI, File, UploadFile
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from PIL import Image
|
||||||
|
import torch, open_clip, numpy as np, faiss
|
||||||
|
import io
|
||||||
|
|
||||||
|
app = FastAPI(title="Pokemon Card Image Service")
|
||||||
|
|
||||||
|
# --- Allow CORS for Bun / Flutter ---
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=["*"],
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
BASE = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
|
||||||
|
# --- Configuration ---
|
||||||
|
FAISS_INDEX_FILE = os.path.join(BASE, "card_index.faiss")
|
||||||
|
EMBEDDINGS_FILE = os.path.join(BASE, "embeddings.npy")
|
||||||
|
IDS_FILE = os.path.join(BASE, "ids.npy")
|
||||||
|
TOP_K = 5
|
||||||
|
|
||||||
|
# --- Load CLIP model ---
|
||||||
|
device = "cuda" if torch.cuda.is_available() else "cpu"
|
||||||
|
print("Loading model on device:", device)
|
||||||
|
model, _, preprocess = open_clip.create_model_and_transforms(
|
||||||
|
"ViT-L-14", pretrained="laion2b_s32b_b82k"
|
||||||
|
)
|
||||||
|
model = model.to(device).eval()
|
||||||
|
|
||||||
|
# --- Load FAISS index ---
|
||||||
|
print("Loading FAISS index from disk...")
|
||||||
|
index = faiss.read_index(FAISS_INDEX_FILE)
|
||||||
|
|
||||||
|
# --- Load IDs / metadata ---
|
||||||
|
ids = np.load(IDS_FILE)
|
||||||
|
metadata = {idx: {"id": ids[idx], "name": ids[idx]} for idx in range(len(ids))}
|
||||||
|
|
||||||
|
print("Service ready! FAISS index contains", index.ntotal, "cards.")
|
||||||
|
|
||||||
|
# --- Helper: encode image ---
|
||||||
|
def encode_image_bytes(image_bytes):
|
||||||
|
img = Image.open(io.BytesIO(image_bytes)).convert("RGB")
|
||||||
|
print("Image loaded successfully:", img.size)
|
||||||
|
with torch.no_grad():
|
||||||
|
emb = model.encode_image(preprocess(img).unsqueeze(0).to(device))
|
||||||
|
emb = emb.cpu().numpy()
|
||||||
|
faiss.normalize_L2(emb)
|
||||||
|
print("Embedding shape:", emb.shape)
|
||||||
|
return emb
|
||||||
|
|
||||||
|
# --- API endpoint ---
|
||||||
|
@app.post("/query")
|
||||||
|
async def query_image(file: UploadFile = File(...)):
|
||||||
|
try:
|
||||||
|
image_bytes = await file.read()
|
||||||
|
print(f"Received {len(image_bytes)} bytes from {file.filename}")
|
||||||
|
|
||||||
|
emb = encode_image_bytes(image_bytes)
|
||||||
|
|
||||||
|
# --- FAISS search ---
|
||||||
|
D, I = index.search(emb, TOP_K)
|
||||||
|
print("FAISS distances:", D)
|
||||||
|
print("FAISS indices:", I)
|
||||||
|
|
||||||
|
results = [metadata[int(i)] for i in I[0]]
|
||||||
|
return JSONResponse(content={"results": results})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print("ERROR during query:", e)
|
||||||
|
return JSONResponse(content={"error": str(e)}, status_code=500)
|
||||||
|
|
||||||
|
# --- Run server ---
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import uvicorn
|
||||||
|
uvicorn.run(app, host="0.0.0.0", port=5001)
|
||||||
21
src/embeddings.ts
Normal file
21
src/embeddings.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import type { Card } from "./types";
|
||||||
|
|
||||||
|
// Placeholder cards array
|
||||||
|
let cards: Card[] = [
|
||||||
|
{ id: "swsh1-1", name: "Celebi V", set: "Swsh1", number: "1", imageUrl: "https://assets.tcgdex.net/de/swsh/swsh1/1/high.png" },
|
||||||
|
{ id: "swsh12-001", name: "Bluzuk", set: "Swsh12", number: "001", imageUrl: "https://assets.tcgdex.net/de/swsh/swsh12/001/high.png" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function loadCards() {
|
||||||
|
// Placeholder: you can later load embeddings.npy + FAISS
|
||||||
|
console.log("Cards module loaded (currently empty)");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function queryCardById(id: string): Card | null {
|
||||||
|
return cards.find(c => c.id === id) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example placeholder: return top N matches
|
||||||
|
export function queryCardByEmbedding(/* embedding */): Card[] {
|
||||||
|
return cards.slice(0, 5); // dummy top 5
|
||||||
|
}
|
||||||
64
src/index.ts
Normal file
64
src/index.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import { serve, spawn } from 'bun';
|
||||||
|
import { queryCardByEmbedding, queryCardById } from './embeddings';
|
||||||
|
import type { Card } from './types';
|
||||||
|
|
||||||
|
const PYTHON_SERVICE = "http://localhost:5001/query";
|
||||||
|
|
||||||
|
const server = serve({
|
||||||
|
routes: {
|
||||||
|
"/api/cards/query-image": {
|
||||||
|
async POST(req) {
|
||||||
|
try {
|
||||||
|
const formData = await req.formData();
|
||||||
|
const file = formData.get("file") as File;
|
||||||
|
if (!file) return new Response(JSON.stringify({ error: "No file uploaded" }), { status: 400 });
|
||||||
|
|
||||||
|
// Forward directly to Python microservice
|
||||||
|
const body = new FormData();
|
||||||
|
body.append("file", file);
|
||||||
|
|
||||||
|
const resp = await fetch(PYTHON_SERVICE, { method: "POST", body });
|
||||||
|
const json = await resp.json();
|
||||||
|
|
||||||
|
return new Response(JSON.stringify(json), { headers: { "Content-Type": "application/json" } });
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
return new Response(JSON.stringify({ error: "Failed to query image" }), { status: 500 });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"/api/cards/:id": async (req) => {
|
||||||
|
const { id } = req.params;
|
||||||
|
const card = queryCardById(id);
|
||||||
|
if (!card) return new Response(JSON.stringify({ error: "Card not found" }), { status: 404, headers: { "Content-Type": "application/json" } });
|
||||||
|
return new Response(JSON.stringify(card), { headers: { "Content-Type": "application/json" } });
|
||||||
|
},
|
||||||
|
"/*": async () => {
|
||||||
|
return new Response("<h1>Pokemon Card Backend</h1>", { headers: { "Content-Type": "text/html" } });
|
||||||
|
},
|
||||||
|
|
||||||
|
/* "/api/cards/query": {
|
||||||
|
async POST(req) {
|
||||||
|
try {
|
||||||
|
const { embedding } = await req.json() as { embedding: number[] };
|
||||||
|
if (!embedding) return new Response(JSON.stringify({ error: "Missing embedding" }), { status: 400, headers: { "Content-Type": "application/json" } });
|
||||||
|
const results: Card[] = queryCardByEmbedding(embedding);
|
||||||
|
return new Response(JSON.stringify(results), { headers: { "Content-Type": "application/json" } });
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error querying card:", err);
|
||||||
|
return new Response(JSON.stringify({ error: "Failed to query card" }), { status: 500, headers: { "Content-Type": "application/json" } });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}, */
|
||||||
|
},
|
||||||
|
|
||||||
|
development: process.env.NODE_ENV !== 'production' && {
|
||||||
|
// Enable browser hot reloading in development
|
||||||
|
hmr: true,
|
||||||
|
|
||||||
|
// Echo console logs from the browser to the server
|
||||||
|
console: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`🚀 Server running at ${server.url}`);
|
||||||
10
src/types.ts
Normal file
10
src/types.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
export interface Card {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
set: string;
|
||||||
|
number: string;
|
||||||
|
rarity?: string;
|
||||||
|
variant?: string;
|
||||||
|
foil?: boolean;
|
||||||
|
imageUrl: string;
|
||||||
|
}
|
||||||
29
tsconfig.json
Normal file
29
tsconfig.json
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
// Environment setup & latest features
|
||||||
|
"lib": ["ESNext"],
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "Preserve",
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"allowJs": true,
|
||||||
|
|
||||||
|
// Bundler mode
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
// Best practices
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"noImplicitOverride": true,
|
||||||
|
|
||||||
|
// Some stricter flags (disabled by default)
|
||||||
|
"noUnusedLocals": false,
|
||||||
|
"noUnusedParameters": false,
|
||||||
|
"noPropertyAccessFromIndexSignature": false
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user