Spaces:
Build error
Build error
Upload 5 files
Browse files- Dockerfile +29 -0
- docker-compose.yml +30 -0
- index.html +896 -0
- main.py +656 -0
- requirements.txt +11 -0
Dockerfile
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
|
| 3 |
+
# Install system dependencies
|
| 4 |
+
RUN apt-get update && apt-get install -y \
|
| 5 |
+
libgl1-mesa-glx \
|
| 6 |
+
libglib2.0-0 \
|
| 7 |
+
libsm6 \
|
| 8 |
+
libxext6 \
|
| 9 |
+
libxrender-dev \
|
| 10 |
+
libgomp1 \
|
| 11 |
+
curl \
|
| 12 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 13 |
+
|
| 14 |
+
WORKDIR /app
|
| 15 |
+
|
| 16 |
+
# Set model cache directories
|
| 17 |
+
ENV U2NET_HOME=/app/models/u2net
|
| 18 |
+
ENV REMBG_CACHE=/app/models/rembg
|
| 19 |
+
ENV TRANSFORMERS_CACHE=/app/models/transformers
|
| 20 |
+
ENV HF_HOME=/app/models/hf
|
| 21 |
+
ENV HUGGINGFACE_HUB_CACHE=/app/models/hf
|
| 22 |
+
ENV OMP_NUM_THREADS=2
|
| 23 |
+
ENV OPENBLAS_NUM_THREADS=2
|
| 24 |
+
|
| 25 |
+
# Install Python dependencies
|
| 26 |
+
COPY requirements.txt .
|
| 27 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 28 |
+
|
| 29 |
+
# Pre-down
|
docker-compose.yml
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version: "3.9"
|
| 2 |
+
|
| 3 |
+
services:
|
| 4 |
+
bgremover:
|
| 5 |
+
build: .
|
| 6 |
+
container_name: bgremover_pro
|
| 7 |
+
ports:
|
| 8 |
+
- "7860:7860"
|
| 9 |
+
environment:
|
| 10 |
+
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
|
| 11 |
+
- U2NET_HOME=/app/models/u2net
|
| 12 |
+
- HF_HOME=/app/models/hf
|
| 13 |
+
- TRANSFORMERS_CACHE=/app/models/transformers
|
| 14 |
+
- OMP_NUM_THREADS=2
|
| 15 |
+
- OPENBLAS_NUM_THREADS=2
|
| 16 |
+
volumes:
|
| 17 |
+
# Persist downloaded models so they survive rebuilds
|
| 18 |
+
- model_cache:/app/models
|
| 19 |
+
restart: unless-stopped
|
| 20 |
+
mem_limit: 14g # leave 2GB for OS on 16GB host
|
| 21 |
+
cpus: "1.8" # leave 0.2 for OS on 2vCPU
|
| 22 |
+
healthcheck:
|
| 23 |
+
test: ["CMD", "curl", "-f", "http://localhost:7860/health"]
|
| 24 |
+
interval: 30s
|
| 25 |
+
timeout: 10s
|
| 26 |
+
retries: 3
|
| 27 |
+
start_period: 120s # allow time for model loading at startup
|
| 28 |
+
|
| 29 |
+
volumes:
|
| 30 |
+
model_cache:
|
index.html
ADDED
|
@@ -0,0 +1,896 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="ar" dir="rtl">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8"/>
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
| 6 |
+
<title>ุฅุฒุงูุฉ ุงูุฎูููุฉ ุงูุงุญุชุฑุงููุฉ</title>
|
| 7 |
+
<link rel="preconnect" href="https://fonts.googleapis.com"/>
|
| 8 |
+
<link href="https://fonts.googleapis.com/css2?family=Cairo:wght@300;400;600;700;900&family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet"/>
|
| 9 |
+
<style>
|
| 10 |
+
/* โโโ TOKENS โโโ */
|
| 11 |
+
:root {
|
| 12 |
+
--bg: #050709;
|
| 13 |
+
--bg2: #0a0d12;
|
| 14 |
+
--bg3: #0f1318;
|
| 15 |
+
--surface: #131820;
|
| 16 |
+
--surface2: #1a2030;
|
| 17 |
+
--border: #1e2836;
|
| 18 |
+
--border2: #253040;
|
| 19 |
+
--cyan: #00e5ff;
|
| 20 |
+
--cyan2: #00b8d4;
|
| 21 |
+
--green: #00ff9d;
|
| 22 |
+
--amber: #ffb300;
|
| 23 |
+
--red: #ff4757;
|
| 24 |
+
--text: #e8edf4;
|
| 25 |
+
--text2: #8899aa;
|
| 26 |
+
--text3: #4a5568;
|
| 27 |
+
--glow-c: 0 0 20px #00e5ff40, 0 0 60px #00e5ff18;
|
| 28 |
+
--glow-g: 0 0 20px #00ff9d40, 0 0 60px #00ff9d18;
|
| 29 |
+
--radius: 14px;
|
| 30 |
+
--radius-sm: 8px;
|
| 31 |
+
--font: 'Cairo', sans-serif;
|
| 32 |
+
--mono: 'JetBrains Mono', monospace;
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
/* โโโ RESET โโโ */
|
| 36 |
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
| 37 |
+
html { scroll-behavior: smooth; }
|
| 38 |
+
body {
|
| 39 |
+
font-family: var(--font);
|
| 40 |
+
background: var(--bg);
|
| 41 |
+
color: var(--text);
|
| 42 |
+
min-height: 100vh;
|
| 43 |
+
overflow-x: hidden;
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
/* โโโ BACKGROUND GRID โโโ */
|
| 47 |
+
body::before {
|
| 48 |
+
content: '';
|
| 49 |
+
position: fixed; inset: 0; z-index: 0;
|
| 50 |
+
background-image:
|
| 51 |
+
linear-gradient(rgba(0,229,255,.025) 1px, transparent 1px),
|
| 52 |
+
linear-gradient(90deg, rgba(0,229,255,.025) 1px, transparent 1px);
|
| 53 |
+
background-size: 60px 60px;
|
| 54 |
+
pointer-events: none;
|
| 55 |
+
}
|
| 56 |
+
body::after {
|
| 57 |
+
content: '';
|
| 58 |
+
position: fixed; inset: 0; z-index: 0;
|
| 59 |
+
background: radial-gradient(ellipse 80% 60% at 50% -10%, #00e5ff0d 0%, transparent 65%);
|
| 60 |
+
pointer-events: none;
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
/* โโโ SCROLLBAR โโโ */
|
| 64 |
+
::-webkit-scrollbar { width: 6px; }
|
| 65 |
+
::-webkit-scrollbar-track { background: var(--bg2); }
|
| 66 |
+
::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 99px; }
|
| 67 |
+
|
| 68 |
+
/* โโโ LAYOUT โโโ */
|
| 69 |
+
.app {
|
| 70 |
+
position: relative; z-index: 1;
|
| 71 |
+
max-width: 1200px;
|
| 72 |
+
margin: 0 auto;
|
| 73 |
+
padding: 0 20px 80px;
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
/* โโโ HEADER โโโ */
|
| 77 |
+
header {
|
| 78 |
+
padding: 32px 0 20px;
|
| 79 |
+
display: flex; align-items: center; justify-content: space-between;
|
| 80 |
+
border-bottom: 1px solid var(--border);
|
| 81 |
+
margin-bottom: 36px;
|
| 82 |
+
}
|
| 83 |
+
.logo {
|
| 84 |
+
display: flex; align-items: center; gap: 14px;
|
| 85 |
+
}
|
| 86 |
+
.logo-icon {
|
| 87 |
+
width: 48px; height: 48px; border-radius: 12px;
|
| 88 |
+
background: linear-gradient(135deg, #00e5ff22, #00ff9d15);
|
| 89 |
+
border: 1px solid #00e5ff40;
|
| 90 |
+
display: grid; place-items: center;
|
| 91 |
+
font-size: 22px;
|
| 92 |
+
box-shadow: var(--glow-c);
|
| 93 |
+
}
|
| 94 |
+
.logo-text h1 {
|
| 95 |
+
font-size: 1.5rem; font-weight: 900; letter-spacing: -0.3px;
|
| 96 |
+
background: linear-gradient(90deg, #fff 30%, var(--cyan));
|
| 97 |
+
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
|
| 98 |
+
line-height: 1.1;
|
| 99 |
+
}
|
| 100 |
+
.logo-text p { font-size: .78rem; color: var(--text2); margin-top: 2px; }
|
| 101 |
+
|
| 102 |
+
.queue-badge {
|
| 103 |
+
display: flex; align-items: center; gap: 8px;
|
| 104 |
+
background: var(--surface); border: 1px solid var(--border);
|
| 105 |
+
border-radius: 99px; padding: 8px 18px;
|
| 106 |
+
font-size: .82rem; color: var(--text2);
|
| 107 |
+
cursor: default;
|
| 108 |
+
}
|
| 109 |
+
.queue-dot {
|
| 110 |
+
width: 8px; height: 8px; border-radius: 50%;
|
| 111 |
+
background: var(--green);
|
| 112 |
+
animation: pulse 2s ease-in-out infinite;
|
| 113 |
+
}
|
| 114 |
+
@keyframes pulse { 0%,100%{opacity:1;transform:scale(1)} 50%{opacity:.5;transform:scale(.8)} }
|
| 115 |
+
|
| 116 |
+
/* โโโ MAIN GRID โโโ */
|
| 117 |
+
.main-grid {
|
| 118 |
+
display: grid;
|
| 119 |
+
grid-template-columns: 1fr 1fr;
|
| 120 |
+
gap: 24px;
|
| 121 |
+
}
|
| 122 |
+
@media (max-width: 768px) {
|
| 123 |
+
.main-grid { grid-template-columns: 1fr; }
|
| 124 |
+
header { flex-direction: column; align-items: flex-start; gap: 16px; }
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
/* โโโ PANEL โโโ */
|
| 128 |
+
.panel {
|
| 129 |
+
background: var(--surface);
|
| 130 |
+
border: 1px solid var(--border);
|
| 131 |
+
border-radius: var(--radius);
|
| 132 |
+
overflow: hidden;
|
| 133 |
+
}
|
| 134 |
+
.panel-header {
|
| 135 |
+
padding: 16px 20px;
|
| 136 |
+
border-bottom: 1px solid var(--border);
|
| 137 |
+
display: flex; align-items: center; gap: 10px;
|
| 138 |
+
font-size: .85rem; font-weight: 600; color: var(--text2);
|
| 139 |
+
text-transform: uppercase; letter-spacing: 1.5px;
|
| 140 |
+
}
|
| 141 |
+
.panel-header .icon { font-size: 1rem; }
|
| 142 |
+
.panel-body { padding: 20px; }
|
| 143 |
+
|
| 144 |
+
/* โโโ DROPZONE โโโ */
|
| 145 |
+
.dropzone {
|
| 146 |
+
border: 2px dashed var(--border2);
|
| 147 |
+
border-radius: var(--radius);
|
| 148 |
+
min-height: 220px;
|
| 149 |
+
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
| 150 |
+
gap: 14px; cursor: pointer;
|
| 151 |
+
transition: border-color .2s, background .2s;
|
| 152 |
+
position: relative; overflow: hidden;
|
| 153 |
+
background: var(--bg2);
|
| 154 |
+
}
|
| 155 |
+
.dropzone:hover, .dropzone.dragover {
|
| 156 |
+
border-color: var(--cyan);
|
| 157 |
+
background: #00e5ff08;
|
| 158 |
+
}
|
| 159 |
+
.dropzone.dragover::after {
|
| 160 |
+
content: '';
|
| 161 |
+
position: absolute; inset: 0;
|
| 162 |
+
background: linear-gradient(45deg, transparent 40%, #00e5ff08 50%, transparent 60%);
|
| 163 |
+
background-size: 200% 200%;
|
| 164 |
+
animation: scan 1.2s linear infinite;
|
| 165 |
+
}
|
| 166 |
+
@keyframes scan { 0%{background-position:200% 200%} 100%{background-position:-200% -200%} }
|
| 167 |
+
|
| 168 |
+
.dropzone-icon {
|
| 169 |
+
font-size: 3rem; filter: grayscale(0.3);
|
| 170 |
+
transition: transform .3s;
|
| 171 |
+
}
|
| 172 |
+
.dropzone:hover .dropzone-icon { transform: scale(1.1); }
|
| 173 |
+
.dropzone p { font-size: .9rem; color: var(--text2); text-align: center; }
|
| 174 |
+
.dropzone small { font-size: .75rem; color: var(--text3); }
|
| 175 |
+
.dropzone input { position: absolute; inset: 0; opacity: 0; cursor: pointer; }
|
| 176 |
+
|
| 177 |
+
/* โโโ PREVIEW โโโ */
|
| 178 |
+
.preview-wrap {
|
| 179 |
+
margin-top: 16px; display: none;
|
| 180 |
+
border-radius: var(--radius-sm); overflow: hidden;
|
| 181 |
+
border: 1px solid var(--border);
|
| 182 |
+
background: var(--bg2);
|
| 183 |
+
}
|
| 184 |
+
.preview-wrap.show { display: block; }
|
| 185 |
+
.preview-wrap img {
|
| 186 |
+
width: 100%; max-height: 260px;
|
| 187 |
+
object-fit: contain; display: block;
|
| 188 |
+
background: repeating-conic-gradient(#1a2030 0% 25%, #111820 0% 50%) 0 0 / 16px 16px;
|
| 189 |
+
}
|
| 190 |
+
.preview-meta {
|
| 191 |
+
padding: 10px 14px;
|
| 192 |
+
font-size: .78rem; color: var(--text3);
|
| 193 |
+
font-family: var(--mono);
|
| 194 |
+
display: flex; justify-content: space-between;
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
/* โโโ MODE TOGGLE โโโ */
|
| 198 |
+
.mode-section { margin-top: 18px; }
|
| 199 |
+
.mode-label {
|
| 200 |
+
font-size: .78rem; color: var(--text2);
|
| 201 |
+
text-transform: uppercase; letter-spacing: 1.5px;
|
| 202 |
+
margin-bottom: 10px; font-weight: 600;
|
| 203 |
+
}
|
| 204 |
+
.mode-toggle {
|
| 205 |
+
display: grid; grid-template-columns: 1fr 1fr;
|
| 206 |
+
gap: 10px;
|
| 207 |
+
}
|
| 208 |
+
.mode-btn {
|
| 209 |
+
background: var(--bg2); border: 1px solid var(--border2);
|
| 210 |
+
border-radius: var(--radius-sm);
|
| 211 |
+
padding: 14px 12px; cursor: pointer;
|
| 212 |
+
transition: all .2s; text-align: center;
|
| 213 |
+
color: var(--text2); font-family: var(--font);
|
| 214 |
+
}
|
| 215 |
+
.mode-btn:hover { border-color: var(--cyan2); color: var(--text); }
|
| 216 |
+
.mode-btn.active {
|
| 217 |
+
border-color: var(--cyan);
|
| 218 |
+
background: #00e5ff0f;
|
| 219 |
+
color: var(--cyan);
|
| 220 |
+
box-shadow: 0 0 14px #00e5ff22;
|
| 221 |
+
}
|
| 222 |
+
.mode-btn .mode-icon { font-size: 1.4rem; margin-bottom: 6px; }
|
| 223 |
+
.mode-btn .mode-name { font-size: .88rem; font-weight: 700; }
|
| 224 |
+
.mode-btn .mode-desc { font-size: .72rem; margin-top: 4px; opacity: .7; line-height: 1.4; }
|
| 225 |
+
|
| 226 |
+
/* โโโ SUBMIT โโโ */
|
| 227 |
+
.btn-submit {
|
| 228 |
+
width: 100%; margin-top: 16px;
|
| 229 |
+
background: linear-gradient(135deg, var(--cyan2), var(--cyan));
|
| 230 |
+
border: none; border-radius: var(--radius-sm);
|
| 231 |
+
padding: 15px 20px; cursor: pointer;
|
| 232 |
+
color: #000; font-family: var(--font); font-size: 1rem; font-weight: 800;
|
| 233 |
+
transition: all .2s; letter-spacing: .5px;
|
| 234 |
+
}
|
| 235 |
+
.btn-submit:hover:not(:disabled) {
|
| 236 |
+
transform: translateY(-1px);
|
| 237 |
+
box-shadow: 0 6px 24px #00e5ff50;
|
| 238 |
+
}
|
| 239 |
+
.btn-submit:disabled { opacity: .4; cursor: not-allowed; transform: none; }
|
| 240 |
+
|
| 241 |
+
/* โโโ PROGRESS โโโ */
|
| 242 |
+
.progress-card {
|
| 243 |
+
display: none; margin-top: 16px;
|
| 244 |
+
background: var(--bg2); border: 1px solid var(--border);
|
| 245 |
+
border-radius: var(--radius-sm); padding: 16px;
|
| 246 |
+
}
|
| 247 |
+
.progress-card.show { display: block; }
|
| 248 |
+
.progress-stage {
|
| 249 |
+
font-size: .85rem; color: var(--text2); margin-bottom: 10px;
|
| 250 |
+
display: flex; align-items: center; gap: 8px;
|
| 251 |
+
}
|
| 252 |
+
.progress-stage .spin {
|
| 253 |
+
width: 14px; height: 14px; border: 2px solid var(--border2);
|
| 254 |
+
border-top-color: var(--cyan); border-radius: 50%;
|
| 255 |
+
animation: spin .8s linear infinite; flex-shrink: 0;
|
| 256 |
+
}
|
| 257 |
+
@keyframes spin { to { transform: rotate(360deg); } }
|
| 258 |
+
.progress-bar-wrap {
|
| 259 |
+
height: 4px; background: var(--border); border-radius: 2px; overflow: hidden;
|
| 260 |
+
}
|
| 261 |
+
.progress-bar-fill {
|
| 262 |
+
height: 100%; background: linear-gradient(90deg, var(--cyan2), var(--cyan));
|
| 263 |
+
border-radius: 2px; width: 0%;
|
| 264 |
+
transition: width .4s ease;
|
| 265 |
+
position: relative; overflow: hidden;
|
| 266 |
+
}
|
| 267 |
+
.progress-bar-fill::after {
|
| 268 |
+
content: '';
|
| 269 |
+
position: absolute; top: 0; right: -100%; height: 100%; width: 100%;
|
| 270 |
+
background: linear-gradient(90deg, transparent, rgba(255,255,255,.4), transparent);
|
| 271 |
+
animation: shimmer 1.5s ease-in-out infinite;
|
| 272 |
+
}
|
| 273 |
+
@keyframes shimmer { to { right: 100%; } }
|
| 274 |
+
|
| 275 |
+
.queue-info-line {
|
| 276 |
+
font-size: .75rem; color: var(--text3); margin-top: 8px;
|
| 277 |
+
font-family: var(--mono);
|
| 278 |
+
}
|
| 279 |
+
|
| 280 |
+
/* โโโ RESULT PANEL โโโ */
|
| 281 |
+
.result-empty {
|
| 282 |
+
display: flex; flex-direction: column;
|
| 283 |
+
align-items: center; justify-content: center;
|
| 284 |
+
min-height: 400px; gap: 12px;
|
| 285 |
+
color: var(--text3); text-align: center;
|
| 286 |
+
}
|
| 287 |
+
.result-empty .empty-icon { font-size: 3rem; opacity: .3; }
|
| 288 |
+
.result-empty p { font-size: .88rem; }
|
| 289 |
+
|
| 290 |
+
/* โโโ COMPARISON SLIDER โโโ */
|
| 291 |
+
.compare-wrap {
|
| 292 |
+
display: none; position: relative;
|
| 293 |
+
border-radius: var(--radius-sm); overflow: hidden;
|
| 294 |
+
user-select: none; background: repeating-conic-gradient(#1a2030 0% 25%, #111820 0% 50%) 0 0 / 16px 16px;
|
| 295 |
+
cursor: ew-resize;
|
| 296 |
+
}
|
| 297 |
+
.compare-wrap.show { display: block; }
|
| 298 |
+
.compare-wrap img {
|
| 299 |
+
width: 100%; max-height: 380px;
|
| 300 |
+
object-fit: contain; display: block;
|
| 301 |
+
}
|
| 302 |
+
.compare-before {
|
| 303 |
+
position: absolute; inset: 0;
|
| 304 |
+
overflow: hidden;
|
| 305 |
+
}
|
| 306 |
+
.compare-before img {
|
| 307 |
+
position: absolute; top: 0; right: 0; /* RTL */
|
| 308 |
+
height: 100%; object-fit: contain;
|
| 309 |
+
width: 100%; max-height: unset;
|
| 310 |
+
background: var(--bg3);
|
| 311 |
+
}
|
| 312 |
+
.compare-clip { width: 50%; }
|
| 313 |
+
|
| 314 |
+
.compare-handle {
|
| 315 |
+
position: absolute; top: 0; bottom: 0;
|
| 316 |
+
width: 3px; background: #fff; cursor: ew-resize;
|
| 317 |
+
left: 50%; transform: translateX(-50%);
|
| 318 |
+
z-index: 10; transition: background .2s;
|
| 319 |
+
}
|
| 320 |
+
.compare-handle::before {
|
| 321 |
+
content: 'โบ';
|
| 322 |
+
position: absolute; top: 50%; left: 50%;
|
| 323 |
+
transform: translate(-50%, -50%);
|
| 324 |
+
background: #fff; color: #000;
|
| 325 |
+
width: 28px; height: 28px; border-radius: 50%;
|
| 326 |
+
display: grid; place-items: center;
|
| 327 |
+
font-size: 11px; font-weight: 900;
|
| 328 |
+
box-shadow: 0 2px 12px #0008;
|
| 329 |
+
}
|
| 330 |
+
.compare-labels {
|
| 331 |
+
position: absolute; top: 10px; left: 0; right: 0;
|
| 332 |
+
display: flex; justify-content: space-between;
|
| 333 |
+
padding: 0 14px; pointer-events: none; z-index: 5;
|
| 334 |
+
}
|
| 335 |
+
.compare-labels span {
|
| 336 |
+
background: #000a; color: #fff;
|
| 337 |
+
font-size: .72rem; padding: 3px 8px; border-radius: 4px;
|
| 338 |
+
font-family: var(--mono);
|
| 339 |
+
}
|
| 340 |
+
|
| 341 |
+
/* โโโ RESULT ACTIONS โโโ */
|
| 342 |
+
.result-actions {
|
| 343 |
+
display: none; margin-top: 14px; gap: 10px;
|
| 344 |
+
flex-wrap: wrap;
|
| 345 |
+
}
|
| 346 |
+
.result-actions.show { display: flex; }
|
| 347 |
+
.btn-dl {
|
| 348 |
+
flex: 1; min-width: 120px;
|
| 349 |
+
padding: 11px 16px;
|
| 350 |
+
border-radius: var(--radius-sm); border: none; cursor: pointer;
|
| 351 |
+
font-family: var(--font); font-size: .88rem; font-weight: 700;
|
| 352 |
+
transition: all .2s; display: flex; align-items: center; justify-content: center; gap: 6px;
|
| 353 |
+
}
|
| 354 |
+
.btn-dl.png {
|
| 355 |
+
background: var(--surface2); border: 1px solid var(--border2); color: var(--text);
|
| 356 |
+
}
|
| 357 |
+
.btn-dl.png:hover { border-color: var(--cyan); color: var(--cyan); }
|
| 358 |
+
.btn-dl.webp {
|
| 359 |
+
background: linear-gradient(135deg, #00ff9d22, #00ff9d12);
|
| 360 |
+
border: 1px solid #00ff9d40; color: var(--green);
|
| 361 |
+
}
|
| 362 |
+
.btn-dl.webp:hover { box-shadow: var(--glow-g); }
|
| 363 |
+
|
| 364 |
+
/* โโโ ANALYSIS BOX โโโ */
|
| 365 |
+
.analysis-box {
|
| 366 |
+
display: none; margin-top: 14px;
|
| 367 |
+
background: var(--bg2); border: 1px solid var(--border);
|
| 368 |
+
border-radius: var(--radius-sm); padding: 14px 16px;
|
| 369 |
+
}
|
| 370 |
+
.analysis-box.show { display: block; }
|
| 371 |
+
.analysis-title {
|
| 372 |
+
font-size: .72rem; color: var(--cyan2); font-weight: 700;
|
| 373 |
+
text-transform: uppercase; letter-spacing: 1.5px; margin-bottom: 8px;
|
| 374 |
+
display: flex; align-items: center; gap: 6px;
|
| 375 |
+
}
|
| 376 |
+
.analysis-text {
|
| 377 |
+
font-size: .82rem; color: var(--text2); line-height: 1.7;
|
| 378 |
+
white-space: pre-wrap;
|
| 379 |
+
}
|
| 380 |
+
|
| 381 |
+
/* โโโ STATS ROW โโโ */
|
| 382 |
+
.stats-row {
|
| 383 |
+
display: none; margin-top: 14px; gap: 10px;
|
| 384 |
+
}
|
| 385 |
+
.stats-row.show { display: flex; }
|
| 386 |
+
.stat-chip {
|
| 387 |
+
flex: 1; background: var(--bg2); border: 1px solid var(--border);
|
| 388 |
+
border-radius: var(--radius-sm); padding: 10px 14px; text-align: center;
|
| 389 |
+
}
|
| 390 |
+
.stat-chip .sv { font-size: 1.1rem; font-weight: 800; color: var(--cyan); font-family: var(--mono); }
|
| 391 |
+
.stat-chip .sl { font-size: .7rem; color: var(--text3); margin-top: 2px; text-transform: uppercase; letter-spacing: 1px; }
|
| 392 |
+
|
| 393 |
+
/* โโโ ERROR โโโ */
|
| 394 |
+
.error-box {
|
| 395 |
+
display: none; margin-top: 14px;
|
| 396 |
+
background: #ff475710; border: 1px solid #ff475740;
|
| 397 |
+
border-radius: var(--radius-sm); padding: 14px 16px;
|
| 398 |
+
font-size: .84rem; color: var(--red); line-height: 1.6;
|
| 399 |
+
}
|
| 400 |
+
.error-box.show { display: block; }
|
| 401 |
+
|
| 402 |
+
/* โโโ TOAST โโโ */
|
| 403 |
+
.toast-wrap {
|
| 404 |
+
position: fixed; bottom: 30px; left: 50%; transform: translateX(-50%);
|
| 405 |
+
z-index: 1000; display: flex; flex-direction: column; align-items: center; gap: 8px;
|
| 406 |
+
}
|
| 407 |
+
.toast {
|
| 408 |
+
background: var(--surface2); border: 1px solid var(--border2);
|
| 409 |
+
border-radius: 99px; padding: 10px 22px;
|
| 410 |
+
font-size: .84rem; color: var(--text);
|
| 411 |
+
animation: toastIn .3s ease forwards;
|
| 412 |
+
box-shadow: 0 4px 24px #0008;
|
| 413 |
+
max-width: 340px; text-align: center;
|
| 414 |
+
}
|
| 415 |
+
.toast.error { border-color: var(--red); color: var(--red); background: #ff47570e; }
|
| 416 |
+
.toast.success { border-color: var(--green); color: var(--green); background: #00ff9d0e; }
|
| 417 |
+
@keyframes toastIn { from{opacity:0;transform:translateY(10px)} to{opacity:1;transform:translateY(0)} }
|
| 418 |
+
@keyframes toastOut { to{opacity:0;transform:translateY(10px)} }
|
| 419 |
+
|
| 420 |
+
/* โโโ SPINNER OVERLAY โโโ */
|
| 421 |
+
.overlay {
|
| 422 |
+
display: none; position: absolute; inset: 0;
|
| 423 |
+
background: #05070980; backdrop-filter: blur(4px);
|
| 424 |
+
border-radius: var(--radius); z-index: 20;
|
| 425 |
+
align-items: center; justify-content: center;
|
| 426 |
+
flex-direction: column; gap: 14px;
|
| 427 |
+
}
|
| 428 |
+
.overlay.show { display: flex; }
|
| 429 |
+
.overlay-text { font-size: .88rem; color: var(--cyan2); }
|
| 430 |
+
</style>
|
| 431 |
+
</head>
|
| 432 |
+
<body>
|
| 433 |
+
<div class="app">
|
| 434 |
+
|
| 435 |
+
<!-- HEADER -->
|
| 436 |
+
<header>
|
| 437 |
+
<div class="logo">
|
| 438 |
+
<div class="logo-icon">โ๏ธ</div>
|
| 439 |
+
<div class="logo-text">
|
| 440 |
+
<h1>ุฅุฒุงูุฉ ุงูุฎูููุฉ AI</h1>
|
| 441 |
+
<p>ุฏูุฉ ุนุงููุฉ ุญุชู 4K โ ุจุฏูู ุชุดููู</p>
|
| 442 |
+
</div>
|
| 443 |
+
</div>
|
| 444 |
+
<div class="queue-badge">
|
| 445 |
+
<div class="queue-dot" id="queueDot"></div>
|
| 446 |
+
<span id="queueText">ุฌุงุฑู ุงูุชุญู
ูู...</span>
|
| 447 |
+
</div>
|
| 448 |
+
</header>
|
| 449 |
+
|
| 450 |
+
<!-- MAIN GRID -->
|
| 451 |
+
<div class="main-grid">
|
| 452 |
+
|
| 453 |
+
<!-- LEFT: UPLOAD -->
|
| 454 |
+
<div class="panel">
|
| 455 |
+
<div class="panel-header">
|
| 456 |
+
<span class="icon">๐ค</span>
|
| 457 |
+
ุฑูุน ุงูุตูุฑุฉ
|
| 458 |
+
</div>
|
| 459 |
+
<div class="panel-body">
|
| 460 |
+
|
| 461 |
+
<!-- Dropzone -->
|
| 462 |
+
<div class="dropzone" id="dropzone">
|
| 463 |
+
<div class="dropzone-icon">๐ผ๏ธ</div>
|
| 464 |
+
<p>ุงุณุญุจ ุตูุฑุชู ููุง ุฃู ุงููุฑ ููุงุฎุชูุงุฑ</p>
|
| 465 |
+
<small>PNG โข JPG โข WebP โข BMP โข TIFF โข AVIF โ ุญุชู 100MB</small>
|
| 466 |
+
<input type="file" id="fileInput" accept="image/*"/>
|
| 467 |
+
</div>
|
| 468 |
+
|
| 469 |
+
<!-- Preview -->
|
| 470 |
+
<div class="preview-wrap" id="previewWrap">
|
| 471 |
+
<img id="previewImg" src="" alt="ู
ุนุงููุฉ"/>
|
| 472 |
+
<div class="preview-meta">
|
| 473 |
+
<span id="previewName">โ</span>
|
| 474 |
+
<span id="previewSize">โ</span>
|
| 475 |
+
</div>
|
| 476 |
+
</div>
|
| 477 |
+
|
| 478 |
+
<!-- Mode -->
|
| 479 |
+
<div class="mode-section">
|
| 480 |
+
<div class="mode-label">ูุถุน ุงูุฅุฒุงูุฉ</div>
|
| 481 |
+
<div class="mode-toggle">
|
| 482 |
+
<button class="mode-btn active" data-mode="fast" onclick="setMode('fast')">
|
| 483 |
+
<div class="mode-icon">โก</div>
|
| 484 |
+
<div class="mode-name">ุงููุถุน ุงูุณุฑูุน</div>
|
| 485 |
+
<div class="mode-desc">ู
ุซุงูู ููุตูุฑ ุงููุงุถุญุฉ โ ูุชูุฌุฉ ููุฑูุฉ</div>
|
| 486 |
+
</button>
|
| 487 |
+
<button class="mode-btn" data-mode="thinking" onclick="setMode('thinking')">
|
| 488 |
+
<div class="mode-icon">๐ง </div>
|
| 489 |
+
<div class="mode-name">ูุถุน ุงูุชูููุฑ</div>
|
| 490 |
+
<div class="mode-desc">ุฃูุตู ุฏูุฉ โ ุดุนุฑ ูุชูุงุตูู ุฏูููุฉ โ ุญุชู ุฏูููุชูู</div>
|
| 491 |
+
</button>
|
| 492 |
+
</div>
|
| 493 |
+
</div>
|
| 494 |
+
|
| 495 |
+
<!-- Submit -->
|
| 496 |
+
<button class="btn-submit" id="submitBtn" onclick="submitImage()" disabled>
|
| 497 |
+
ุงุจุฏุฃ ุงูุฅุฒุงูุฉ
|
| 498 |
+
</button>
|
| 499 |
+
|
| 500 |
+
<!-- Progress -->
|
| 501 |
+
<div class="progress-card" id="progressCard">
|
| 502 |
+
<div class="progress-stage" id="progressStage">
|
| 503 |
+
<div class="spin"></div>
|
| 504 |
+
<span id="progressText">ุฌุงุฑู ุงูู
ุนุงูุฌุฉ...</span>
|
| 505 |
+
</div>
|
| 506 |
+
<div class="progress-bar-wrap">
|
| 507 |
+
<div class="progress-bar-fill" id="progressFill"></div>
|
| 508 |
+
</div>
|
| 509 |
+
<div class="queue-info-line" id="queueLine"></div>
|
| 510 |
+
</div>
|
| 511 |
+
|
| 512 |
+
<!-- Error -->
|
| 513 |
+
<div class="error-box" id="errorBox"></div>
|
| 514 |
+
|
| 515 |
+
</div>
|
| 516 |
+
</div>
|
| 517 |
+
|
| 518 |
+
<!-- RIGHT: RESULT -->
|
| 519 |
+
<div class="panel" style="position:relative;">
|
| 520 |
+
<div class="panel-header">
|
| 521 |
+
<span class="icon">โจ</span>
|
| 522 |
+
ุงููุชูุฌุฉ
|
| 523 |
+
<span id="resultMode" style="margin-right:auto;font-size:.7rem;color:var(--text3);"></span>
|
| 524 |
+
</div>
|
| 525 |
+
<div class="panel-body">
|
| 526 |
+
|
| 527 |
+
<!-- Empty state -->
|
| 528 |
+
<div class="result-empty" id="resultEmpty">
|
| 529 |
+
<div class="empty-icon">๐ฎ</div>
|
| 530 |
+
<p>ุงุฑูุน ุตูุฑุฉ ูุงุจุฏุฃ ุงูุฅุฒุงูุฉ ูุฑุคูุฉ ุงููุชูุฌุฉ ููุง</p>
|
| 531 |
+
</div>
|
| 532 |
+
|
| 533 |
+
<!-- Compare Slider -->
|
| 534 |
+
<div class="compare-wrap" id="compareWrap">
|
| 535 |
+
<img id="resultImg" src="" alt="ุงููุชูุฌุฉ"/>
|
| 536 |
+
<div class="compare-before" id="compareBefore">
|
| 537 |
+
<img id="originalImg" src="" alt="ุงูุฃุตููุฉ"/>
|
| 538 |
+
</div>
|
| 539 |
+
<div class="compare-handle" id="compareHandle"></div>
|
| 540 |
+
<div class="compare-labels">
|
| 541 |
+
<span>ุงููุชูุฌุฉ</span>
|
| 542 |
+
<span>ุงูุฃุตููุฉ</span>
|
| 543 |
+
</div>
|
| 544 |
+
</div>
|
| 545 |
+
|
| 546 |
+
<!-- Stats -->
|
| 547 |
+
<div class="stats-row" id="statsRow">
|
| 548 |
+
<div class="stat-chip">
|
| 549 |
+
<div class="sv" id="statTime">โ</div>
|
| 550 |
+
<div class="sl">ููุช ุงูู
ุนุงูุฌุฉ</div>
|
| 551 |
+
</div>
|
| 552 |
+
<div class="stat-chip">
|
| 553 |
+
<div class="sv" id="statSize">โ</div>
|
| 554 |
+
<div class="sl">ุญุฌู
ุงููุงุชุฌ</div>
|
| 555 |
+
</div>
|
| 556 |
+
<div class="stat-chip">
|
| 557 |
+
<div class="sv" id="statMode">โ</div>
|
| 558 |
+
<div class="sl">ุงููุถุน</div>
|
| 559 |
+
</div>
|
| 560 |
+
</div>
|
| 561 |
+
|
| 562 |
+
<!-- AI Analysis -->
|
| 563 |
+
<div class="analysis-box" id="analysisBox">
|
| 564 |
+
<div class="analysis-title">๐ค ุชุญููู ุงูุฐูุงุก ุงูุงุตุทูุงุนู</div>
|
| 565 |
+
<div class="analysis-text" id="analysisText"></div>
|
| 566 |
+
</div>
|
| 567 |
+
|
| 568 |
+
<!-- Download actions -->
|
| 569 |
+
<div class="result-actions" id="resultActions">
|
| 570 |
+
<button class="btn-dl png" onclick="download('png')">โฌ๏ธ ุชุญู
ูู PNG</button>
|
| 571 |
+
<button class="btn-dl webp" onclick="download('webp')">โฌ๏ธ ุชุญู
ูู WebP</button>
|
| 572 |
+
</div>
|
| 573 |
+
|
| 574 |
+
</div>
|
| 575 |
+
</div>
|
| 576 |
+
|
| 577 |
+
</div><!-- /main-grid -->
|
| 578 |
+
|
| 579 |
+
</div><!-- /app -->
|
| 580 |
+
|
| 581 |
+
<!-- Toast container -->
|
| 582 |
+
<div class="toast-wrap" id="toastWrap"></div>
|
| 583 |
+
|
| 584 |
+
<script>
|
| 585 |
+
/* โโโ STATE โโโ */
|
| 586 |
+
let selectedFile = null;
|
| 587 |
+
let selectedMode = 'fast';
|
| 588 |
+
let currentTaskId = null;
|
| 589 |
+
let ws = null;
|
| 590 |
+
let progressInterval = null;
|
| 591 |
+
let progressVal = 0;
|
| 592 |
+
let isDragging = false;
|
| 593 |
+
|
| 594 |
+
/* โโโ QUEUE POLLING โโโ */
|
| 595 |
+
async function pollQueue() {
|
| 596 |
+
try {
|
| 597 |
+
const r = await fetch('/queue-info');
|
| 598 |
+
const d = await r.json();
|
| 599 |
+
const dot = document.getElementById('queueDot');
|
| 600 |
+
const txt = document.getElementById('queueText');
|
| 601 |
+
txt.textContent = `ุงูุทุงุจูุฑ: ${d.waiting}/${d.max} โ ${d.processing ? 'ูุนู
ู ุงูุขู' : 'ู
ุชุงุญ'}`;
|
| 602 |
+
dot.style.background = d.waiting >= d.max ? 'var(--amber)' : 'var(--green)';
|
| 603 |
+
} catch(e) {
|
| 604 |
+
document.getElementById('queueText').textContent = 'ูุง ุงุชุตุงู';
|
| 605 |
+
}
|
| 606 |
+
}
|
| 607 |
+
pollQueue();
|
| 608 |
+
setInterval(pollQueue, 4000);
|
| 609 |
+
|
| 610 |
+
/* โโโ FILE INPUT โโโ */
|
| 611 |
+
const dropzone = document.getElementById('dropzone');
|
| 612 |
+
const fileInput = document.getElementById('fileInput');
|
| 613 |
+
|
| 614 |
+
dropzone.addEventListener('dragover', e => { e.preventDefault(); dropzone.classList.add('dragover'); });
|
| 615 |
+
dropzone.addEventListener('dragleave', () => dropzone.classList.remove('dragover'));
|
| 616 |
+
dropzone.addEventListener('drop', e => {
|
| 617 |
+
e.preventDefault();
|
| 618 |
+
dropzone.classList.remove('dragover');
|
| 619 |
+
const f = e.dataTransfer.files[0];
|
| 620 |
+
if (f) handleFile(f);
|
| 621 |
+
});
|
| 622 |
+
fileInput.addEventListener('change', () => {
|
| 623 |
+
if (fileInput.files[0]) handleFile(fileInput.files[0]);
|
| 624 |
+
});
|
| 625 |
+
|
| 626 |
+
function handleFile(f) {
|
| 627 |
+
if (!f.type.startsWith('image/')) {
|
| 628 |
+
toast('ููุณู
ุญ ููุท ุจู
ููุงุช ุงูุตูุฑ', 'error');
|
| 629 |
+
return;
|
| 630 |
+
}
|
| 631 |
+
if (f.size > 100 * 1024 * 1024) {
|
| 632 |
+
toast('ุญุฌู
ุงูู
ูู ูุชุฌุงูุฒ 100MB', 'error');
|
| 633 |
+
return;
|
| 634 |
+
}
|
| 635 |
+
selectedFile = f;
|
| 636 |
+
|
| 637 |
+
// Show preview
|
| 638 |
+
const reader = new FileReader();
|
| 639 |
+
reader.onload = e => {
|
| 640 |
+
document.getElementById('previewImg').src = e.target.result;
|
| 641 |
+
document.getElementById('previewWrap').classList.add('show');
|
| 642 |
+
document.getElementById('originalImg').src = e.target.result;
|
| 643 |
+
};
|
| 644 |
+
reader.readAsDataURL(f);
|
| 645 |
+
|
| 646 |
+
document.getElementById('previewName').textContent = f.name;
|
| 647 |
+
document.getElementById('previewSize').textContent = formatBytes(f.size);
|
| 648 |
+
document.getElementById('submitBtn').disabled = false;
|
| 649 |
+
resetResult();
|
| 650 |
+
}
|
| 651 |
+
|
| 652 |
+
/* โโโ MODE โโโ */
|
| 653 |
+
function setMode(m) {
|
| 654 |
+
selectedMode = m;
|
| 655 |
+
document.querySelectorAll('.mode-btn').forEach(b => b.classList.toggle('active', b.dataset.mode === m));
|
| 656 |
+
}
|
| 657 |
+
|
| 658 |
+
/* โโโ SUBMIT โโโ */
|
| 659 |
+
async function submitImage() {
|
| 660 |
+
if (!selectedFile) return;
|
| 661 |
+
|
| 662 |
+
const btn = document.getElementById('submitBtn');
|
| 663 |
+
btn.disabled = true;
|
| 664 |
+
resetResult();
|
| 665 |
+
showProgress('ุฌุงุฑู ุฑูุน ุงูุตูุฑุฉ...', 5);
|
| 666 |
+
|
| 667 |
+
const fd = new FormData();
|
| 668 |
+
fd.append('file', selectedFile);
|
| 669 |
+
fd.append('mode', selectedMode);
|
| 670 |
+
|
| 671 |
+
try {
|
| 672 |
+
const res = await fetch('/upload', { method: 'POST', body: fd });
|
| 673 |
+
const data = await res.json();
|
| 674 |
+
|
| 675 |
+
if (!res.ok) {
|
| 676 |
+
showError(data.detail || 'ูุดู ูู ุฑูุน ุงูุตูุฑุฉ');
|
| 677 |
+
btn.disabled = false;
|
| 678 |
+
return;
|
| 679 |
+
}
|
| 680 |
+
|
| 681 |
+
currentTaskId = data.task_id;
|
| 682 |
+
const pos = data.queue_pos;
|
| 683 |
+
|
| 684 |
+
if (pos > 1) {
|
| 685 |
+
showProgress(`ูู ุงูุทุงุจูุฑ โ ุงูู
ููุน ${pos}/${data.queue_total}`, 10);
|
| 686 |
+
setQueueLine(`ู
ูู
ุชู ุฑูู
${pos} ูู ุงูุงูุชุธุงุฑ`);
|
| 687 |
+
} else {
|
| 688 |
+
showProgress('ุฌุงุฑู ุงูุจุฏุก...', 15);
|
| 689 |
+
}
|
| 690 |
+
|
| 691 |
+
connectWebSocket(data.task_id);
|
| 692 |
+
|
| 693 |
+
} catch(e) {
|
| 694 |
+
showError('ุฎุทุฃ ูู ุงูุงุชุตุงู ุจุงูุฎุงุฏู
');
|
| 695 |
+
btn.disabled = false;
|
| 696 |
+
hideProgress();
|
| 697 |
+
}
|
| 698 |
+
}
|
| 699 |
+
|
| 700 |
+
/* โโโ WEBSOCKET โโโ */
|
| 701 |
+
function connectWebSocket(taskId) {
|
| 702 |
+
if (ws) { ws.close(); ws = null; }
|
| 703 |
+
|
| 704 |
+
const protocol = location.protocol === 'https:' ? 'wss' : 'ws';
|
| 705 |
+
ws = new WebSocket(`${protocol}://${location.host}/ws/${taskId}`);
|
| 706 |
+
|
| 707 |
+
ws.onmessage = e => {
|
| 708 |
+
const msg = JSON.parse(e.data);
|
| 709 |
+
handleWSMessage(msg);
|
| 710 |
+
};
|
| 711 |
+
ws.onerror = () => {
|
| 712 |
+
// fallback to polling
|
| 713 |
+
if (currentTaskId === taskId) startPolling(taskId);
|
| 714 |
+
};
|
| 715 |
+
ws.onclose = () => {};
|
| 716 |
+
}
|
| 717 |
+
|
| 718 |
+
function handleWSMessage(msg) {
|
| 719 |
+
switch(msg.event) {
|
| 720 |
+
case 'queued':
|
| 721 |
+
case 'position_update':
|
| 722 |
+
showProgress(`ูู ุงูุทุงุจูุฑ โ ุงูู
ููุน ${msg.position}/${msg.total}`, 10);
|
| 723 |
+
setQueueLine(`ุงูุชุธุงุฑ: ${msg.position} ู
ู ${msg.total}`);
|
| 724 |
+
break;
|
| 725 |
+
|
| 726 |
+
case 'stage':
|
| 727 |
+
const stages = {
|
| 728 |
+
'ุชุญููู': 30, 'ุชุญููู ุงูุตูุฑุฉ': 30,
|
| 729 |
+
'ุฅุฒุงูุฉ ุงูุฎูููุฉ': 60, 'ุชูููุฏ': 85,
|
| 730 |
+
};
|
| 731 |
+
let pv = 20;
|
| 732 |
+
for (const [k, v] of Object.entries(stages)) {
|
| 733 |
+
if ((msg.stage || '').includes(k)) { pv = v; break; }
|
| 734 |
+
}
|
| 735 |
+
showProgress(msg.stage || 'ู
ุนุงูุฌุฉ...', pv);
|
| 736 |
+
if (msg.analysis) setAnalysis(msg.analysis);
|
| 737 |
+
setQueueLine('');
|
| 738 |
+
break;
|
| 739 |
+
|
| 740 |
+
case 'completed':
|
| 741 |
+
onCompleted(msg);
|
| 742 |
+
break;
|
| 743 |
+
|
| 744 |
+
case 'failed':
|
| 745 |
+
onFailed(msg.error);
|
| 746 |
+
break;
|
| 747 |
+
}
|
| 748 |
+
}
|
| 749 |
+
|
| 750 |
+
/* โโโ POLLING FALLBACK โโโ */
|
| 751 |
+
function startPolling(taskId) {
|
| 752 |
+
clearInterval(progressInterval);
|
| 753 |
+
progressInterval = setInterval(async () => {
|
| 754 |
+
try {
|
| 755 |
+
const r = await fetch(`/status/${taskId}`);
|
| 756 |
+
const d = await r.json();
|
| 757 |
+
if (d.status === 'completed') { clearInterval(progressInterval); onCompleted(d); }
|
| 758 |
+
else if (d.status === 'failed') { clearInterval(progressInterval); onFailed(d.error); }
|
| 759 |
+
else if (d.status === 'pending') showProgress(`ูู ุงูุทุงุจูุฑ โ ${d.queue_pos}`, 10);
|
| 760 |
+
else if (d.status === 'processing') showProgress(d.stage || 'ู
ุนุงูุฌุฉ...', 50);
|
| 761 |
+
} catch(e) {}
|
| 762 |
+
}, 1500);
|
| 763 |
+
}
|
| 764 |
+
|
| 765 |
+
/* โโโ COMPLETED โโโ */
|
| 766 |
+
function onCompleted(msg) {
|
| 767 |
+
hideProgress();
|
| 768 |
+
document.getElementById('submitBtn').disabled = false;
|
| 769 |
+
|
| 770 |
+
const resultImg = document.getElementById('resultImg');
|
| 771 |
+
resultImg.src = `/preview/${currentTaskId}?t=${Date.now()}`;
|
| 772 |
+
resultImg.onload = () => initCompareSlider();
|
| 773 |
+
|
| 774 |
+
document.getElementById('compareWrap').classList.add('show');
|
| 775 |
+
document.getElementById('resultEmpty').style.display = 'none';
|
| 776 |
+
document.getElementById('resultActions').classList.add('show');
|
| 777 |
+
document.getElementById('statsRow').classList.add('show');
|
| 778 |
+
|
| 779 |
+
const t = parseFloat(msg.proc_time || 0);
|
| 780 |
+
document.getElementById('statTime').textContent = t ? `${t.toFixed(1)}ุซ` : 'โ';
|
| 781 |
+
document.getElementById('statSize').textContent = msg.size_kb ? `${msg.size_kb}KB` : 'โ';
|
| 782 |
+
document.getElementById('statMode').textContent = selectedMode === 'thinking' ? '๐ง ' : 'โก';
|
| 783 |
+
document.getElementById('resultMode').textContent = selectedMode === 'thinking' ? 'ูุถุน ุงูุชูููุฑ' : 'ุงููุถุน ุงูุณุฑูุน';
|
| 784 |
+
|
| 785 |
+
if (msg.analysis) setAnalysis(msg.analysis);
|
| 786 |
+
toast('ุงูุชู
ูุช ุงูุฅุฒุงูุฉ ุจูุฌุงุญ โ
', 'success');
|
| 787 |
+
}
|
| 788 |
+
|
| 789 |
+
function onFailed(error) {
|
| 790 |
+
hideProgress();
|
| 791 |
+
showError(error || 'ูุดูุช ุงูู
ุนุงูุฌุฉ');
|
| 792 |
+
document.getElementById('submitBtn').disabled = false;
|
| 793 |
+
toast('ูุดูุช ุงูู
ุนุงูุฌุฉ โ', 'error');
|
| 794 |
+
}
|
| 795 |
+
|
| 796 |
+
/* โโโ COMPARE SLIDER โโโ */
|
| 797 |
+
function initCompareSlider() {
|
| 798 |
+
const wrap = document.getElementById('compareWrap');
|
| 799 |
+
const before = document.getElementById('compareBefore');
|
| 800 |
+
const handle = document.getElementById('compareHandle');
|
| 801 |
+
let dragging = false;
|
| 802 |
+
|
| 803 |
+
function setPos(x) {
|
| 804 |
+
const rect = wrap.getBoundingClientRect();
|
| 805 |
+
// RTL: flip x
|
| 806 |
+
let rel = (rect.right - x) / rect.width; // RTL
|
| 807 |
+
rel = Math.max(0.02, Math.min(0.98, rel));
|
| 808 |
+
const pct = (rel * 100).toFixed(1);
|
| 809 |
+
before.style.width = pct + '%';
|
| 810 |
+
handle.style.left = (100 - rel * 100).toFixed(1) + '%';
|
| 811 |
+
}
|
| 812 |
+
|
| 813 |
+
handle.addEventListener('mousedown', e => { dragging = true; e.preventDefault(); });
|
| 814 |
+
wrap.addEventListener('mousedown', e => {
|
| 815 |
+
if (e.target === wrap || e.target === document.getElementById('resultImg')) {
|
| 816 |
+
dragging = true; setPos(e.clientX); e.preventDefault();
|
| 817 |
+
}
|
| 818 |
+
});
|
| 819 |
+
window.addEventListener('mousemove', e => { if (dragging) setPos(e.clientX); });
|
| 820 |
+
window.addEventListener('mouseup', () => { dragging = false; });
|
| 821 |
+
|
| 822 |
+
handle.addEventListener('touchstart', e => { dragging = true; e.preventDefault(); }, {passive:false});
|
| 823 |
+
window.addEventListener('touchmove', e => { if (dragging) setPos(e.touches[0].clientX); }, {passive:false});
|
| 824 |
+
window.addEventListener('touchend', () => { dragging = false; });
|
| 825 |
+
|
| 826 |
+
// Init at 50%
|
| 827 |
+
setPos(wrap.getBoundingClientRect().left + wrap.offsetWidth / 2);
|
| 828 |
+
}
|
| 829 |
+
|
| 830 |
+
/* โโโ DOWNLOAD โโโ */
|
| 831 |
+
function download(fmt) {
|
| 832 |
+
if (!currentTaskId) return;
|
| 833 |
+
const a = document.createElement('a');
|
| 834 |
+
a.href = `/result/${currentTaskId}?fmt=${fmt}`;
|
| 835 |
+
a.download = `nobg.${fmt}`;
|
| 836 |
+
document.body.appendChild(a); a.click(); document.body.removeChild(a);
|
| 837 |
+
}
|
| 838 |
+
|
| 839 |
+
/* โโโ UI HELPERS โโโ */
|
| 840 |
+
function showProgress(text, pct) {
|
| 841 |
+
document.getElementById('progressCard').classList.add('show');
|
| 842 |
+
document.getElementById('progressText').textContent = text;
|
| 843 |
+
document.getElementById('progressFill').style.width = Math.max(progressVal, pct) + '%';
|
| 844 |
+
progressVal = Math.max(progressVal, pct);
|
| 845 |
+
document.getElementById('errorBox').classList.remove('show');
|
| 846 |
+
}
|
| 847 |
+
function hideProgress() {
|
| 848 |
+
document.getElementById('progressFill').style.width = '100%';
|
| 849 |
+
setTimeout(() => {
|
| 850 |
+
document.getElementById('progressCard').classList.remove('show');
|
| 851 |
+
progressVal = 0;
|
| 852 |
+
document.getElementById('progressFill').style.width = '0%';
|
| 853 |
+
}, 600);
|
| 854 |
+
}
|
| 855 |
+
function setQueueLine(t) { document.getElementById('queueLine').textContent = t; }
|
| 856 |
+
function showError(msg) {
|
| 857 |
+
const box = document.getElementById('errorBox');
|
| 858 |
+
box.textContent = 'โ ๏ธ ' + msg;
|
| 859 |
+
box.classList.add('show');
|
| 860 |
+
document.getElementById('progressCard').classList.remove('show');
|
| 861 |
+
}
|
| 862 |
+
function setAnalysis(text) {
|
| 863 |
+
document.getElementById('analysisBox').classList.add('show');
|
| 864 |
+
document.getElementById('analysisText').textContent = text;
|
| 865 |
+
}
|
| 866 |
+
function resetResult() {
|
| 867 |
+
document.getElementById('compareWrap').classList.remove('show');
|
| 868 |
+
document.getElementById('resultActions').classList.remove('show');
|
| 869 |
+
document.getElementById('statsRow').classList.remove('show');
|
| 870 |
+
document.getElementById('analysisBox').classList.remove('show');
|
| 871 |
+
document.getElementById('errorBox').classList.remove('show');
|
| 872 |
+
document.getElementById('resultEmpty').style.display = '';
|
| 873 |
+
document.getElementById('progressCard').classList.remove('show');
|
| 874 |
+
progressVal = 0;
|
| 875 |
+
if (ws) { ws.close(); ws = null; }
|
| 876 |
+
clearInterval(progressInterval);
|
| 877 |
+
}
|
| 878 |
+
function formatBytes(b) {
|
| 879 |
+
if (b < 1024) return b + ' B';
|
| 880 |
+
if (b < 1048576) return (b/1024).toFixed(1) + ' KB';
|
| 881 |
+
return (b/1048576).toFixed(1) + ' MB';
|
| 882 |
+
}
|
| 883 |
+
function toast(msg, type='') {
|
| 884 |
+
const wrap = document.getElementById('toastWrap');
|
| 885 |
+
const el = document.createElement('div');
|
| 886 |
+
el.className = 'toast ' + type;
|
| 887 |
+
el.textContent = msg;
|
| 888 |
+
wrap.appendChild(el);
|
| 889 |
+
setTimeout(() => {
|
| 890 |
+
el.style.animation = 'toastOut .3s ease forwards';
|
| 891 |
+
setTimeout(() => el.remove(), 300);
|
| 892 |
+
}, 3200);
|
| 893 |
+
}
|
| 894 |
+
</script>
|
| 895 |
+
</body>
|
| 896 |
+
</html>
|
main.py
ADDED
|
@@ -0,0 +1,656 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
BG Remover Pro โ FastAPI Backend
|
| 3 |
+
Supports: Fast Mode (u2net) & Thinking Mode (BiRefNet + Claude AI)
|
| 4 |
+
Queue: max 10 waiting | Rate limiting | Anti-spam
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import asyncio
|
| 8 |
+
import base64
|
| 9 |
+
import gc
|
| 10 |
+
import io
|
| 11 |
+
import json
|
| 12 |
+
import logging
|
| 13 |
+
import os
|
| 14 |
+
import time
|
| 15 |
+
import uuid
|
| 16 |
+
from collections import defaultdict
|
| 17 |
+
from dataclasses import dataclass, field
|
| 18 |
+
from enum import Enum
|
| 19 |
+
from pathlib import Path
|
| 20 |
+
from typing import Dict, List, Optional
|
| 21 |
+
|
| 22 |
+
import anthropic
|
| 23 |
+
from fastapi import FastAPI, File, HTTPException, Request, UploadFile, WebSocket, WebSocketDisconnect
|
| 24 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 25 |
+
from fastapi.responses import JSONResponse, Response
|
| 26 |
+
from fastapi.staticfiles import StaticFiles
|
| 27 |
+
from PIL import Image, ImageFilter
|
| 28 |
+
import numpy as np
|
| 29 |
+
|
| 30 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 31 |
+
# LOGGING
|
| 32 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 33 |
+
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
|
| 34 |
+
log = logging.getLogger("bgremover")
|
| 35 |
+
|
| 36 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 37 |
+
# CONSTANTS
|
| 38 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 39 |
+
ALLOWED_MIME_TYPES = {
|
| 40 |
+
"image/jpeg", "image/jpg", "image/png", "image/webp",
|
| 41 |
+
"image/gif", "image/bmp", "image/tiff", "image/avif",
|
| 42 |
+
"image/heic", "image/heif", "image/x-png",
|
| 43 |
+
}
|
| 44 |
+
ALLOWED_EXTENSIONS = {
|
| 45 |
+
".jpg", ".jpeg", ".png", ".webp",
|
| 46 |
+
".gif", ".bmp", ".tiff", ".tif", ".avif",
|
| 47 |
+
}
|
| 48 |
+
MAX_FILE_SIZE = 100 * 1024 * 1024 # 100 MB
|
| 49 |
+
MAX_QUEUE_SIZE = 10 # max waiting tasks
|
| 50 |
+
RATE_LIMIT_WINDOW = 60 # seconds
|
| 51 |
+
RATE_LIMIT_MAX = 5 # requests per window per IP
|
| 52 |
+
MAX_ACTIVE_PER_IP = 2 # concurrent tasks per IP
|
| 53 |
+
THINKING_TIMEOUT = 120 # seconds (2 min max)
|
| 54 |
+
RESULT_TTL = 3600 # keep results for 1 hour
|
| 55 |
+
|
| 56 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 57 |
+
# ENUMS & DATA CLASSES
|
| 58 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 59 |
+
class Mode(str, Enum):
|
| 60 |
+
FAST = "fast"
|
| 61 |
+
THINKING = "thinking"
|
| 62 |
+
|
| 63 |
+
class TaskStatus(str, Enum):
|
| 64 |
+
PENDING = "pending"
|
| 65 |
+
PROCESSING = "processing"
|
| 66 |
+
COMPLETED = "completed"
|
| 67 |
+
FAILED = "failed"
|
| 68 |
+
|
| 69 |
+
@dataclass
|
| 70 |
+
class Task:
|
| 71 |
+
id: str
|
| 72 |
+
mode: Mode
|
| 73 |
+
image_data: bytes
|
| 74 |
+
filename: str
|
| 75 |
+
ip: str
|
| 76 |
+
status: TaskStatus = TaskStatus.PENDING
|
| 77 |
+
queue_pos: int = 0
|
| 78 |
+
created_at: float = field(default_factory=time.time)
|
| 79 |
+
result_png: Optional[bytes] = None
|
| 80 |
+
result_webp: Optional[bytes] = None
|
| 81 |
+
error: Optional[str] = None
|
| 82 |
+
analysis: Optional[str] = None
|
| 83 |
+
orig_size: Optional[tuple] = None
|
| 84 |
+
proc_time: Optional[float] = None
|
| 85 |
+
stage: str = "ุงูุชุธุงุฑ"
|
| 86 |
+
|
| 87 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 88 |
+
# GLOBAL STATE
|
| 89 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 90 |
+
tasks: Dict[str, Task] = {}
|
| 91 |
+
pending_queue: List[str] = []
|
| 92 |
+
queue_lock: asyncio.Lock = asyncio.Lock()
|
| 93 |
+
ws_map: Dict[str, List[WebSocket]] = defaultdict(list)
|
| 94 |
+
ip_times: Dict[str, List[float]] = defaultdict(list)
|
| 95 |
+
ip_active: Dict[str, int] = defaultdict(int)
|
| 96 |
+
current_task: Optional[str] = None
|
| 97 |
+
|
| 98 |
+
# Sessions (loaded at startup)
|
| 99 |
+
fast_session = None
|
| 100 |
+
thinking_session = None
|
| 101 |
+
anthropic_client = None
|
| 102 |
+
|
| 103 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 104 |
+
# APP
|
| 105 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 106 |
+
app = FastAPI(title="BG Remover Pro", version="2.0")
|
| 107 |
+
app.add_middleware(
|
| 108 |
+
CORSMiddleware,
|
| 109 |
+
allow_origins=["*"],
|
| 110 |
+
allow_methods=["*"],
|
| 111 |
+
allow_headers=["*"],
|
| 112 |
+
)
|
| 113 |
+
|
| 114 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 115 |
+
# STARTUP
|
| 116 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 117 |
+
@app.on_event("startup")
|
| 118 |
+
async def startup_event():
|
| 119 |
+
global fast_session, thinking_session, anthropic_client
|
| 120 |
+
|
| 121 |
+
log.info("Loading fast model (u2net)...")
|
| 122 |
+
from rembg import new_session
|
| 123 |
+
fast_session = new_session("u2net")
|
| 124 |
+
log.info("โ u2net loaded")
|
| 125 |
+
|
| 126 |
+
log.info("Loading thinking model (birefnet-general)...")
|
| 127 |
+
thinking_session = new_session("birefnet-general")
|
| 128 |
+
log.info("โ birefnet-general loaded")
|
| 129 |
+
|
| 130 |
+
api_key = os.getenv("ANTHROPIC_API_KEY", "")
|
| 131 |
+
if api_key:
|
| 132 |
+
anthropic_client = anthropic.Anthropic(api_key=api_key)
|
| 133 |
+
log.info("โ Anthropic client initialized")
|
| 134 |
+
else:
|
| 135 |
+
log.warning("ANTHROPIC_API_KEY not set โ AI analysis disabled")
|
| 136 |
+
|
| 137 |
+
asyncio.create_task(queue_worker())
|
| 138 |
+
asyncio.create_task(cleanup_worker())
|
| 139 |
+
log.info("โ Workers started")
|
| 140 |
+
|
| 141 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 142 |
+
# RATE LIMITING
|
| 143 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 144 |
+
def check_rate_limit(ip: str) -> tuple[bool, str]:
|
| 145 |
+
now = time.time()
|
| 146 |
+
ip_times[ip] = [t for t in ip_times[ip] if now - t < RATE_LIMIT_WINDOW]
|
| 147 |
+
|
| 148 |
+
if len(ip_times[ip]) >= RATE_LIMIT_MAX:
|
| 149 |
+
remaining = int(RATE_LIMIT_WINDOW - (now - ip_times[ip][0]))
|
| 150 |
+
return False, f"ุชุฌุงูุฒุช ุงูุญุฏ ุงูู
ุณู
ูุญ ุจู ({RATE_LIMIT_MAX} ุทูุจุงุช/{RATE_LIMIT_WINDOW}ุซ). ุงูุชุธุฑ {remaining}ุซ"
|
| 151 |
+
|
| 152 |
+
if ip_active[ip] >= MAX_ACTIVE_PER_IP:
|
| 153 |
+
return False, f"ูุฏูู {MAX_ACTIVE_PER_IP} ู
ูุงู
ูุดุทุฉ ุจุงููุนู. ุงูุชุธุฑ ุงูุชู
ุงููุง"
|
| 154 |
+
|
| 155 |
+
ip_times[ip].append(now)
|
| 156 |
+
return True, ""
|
| 157 |
+
|
| 158 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 159 |
+
# IMAGE VALIDATION
|
| 160 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 161 |
+
async def validate_image(file: UploadFile, data: bytes) -> tuple[bool, str]:
|
| 162 |
+
if len(data) > MAX_FILE_SIZE:
|
| 163 |
+
return False, "ุญุฌู
ุงูู
ูู ูุชุฌุงูุฒ 100MB"
|
| 164 |
+
|
| 165 |
+
fname = file.filename or ""
|
| 166 |
+
ext = Path(fname).suffix.lower()
|
| 167 |
+
if ext and ext not in ALLOWED_EXTENSIONS:
|
| 168 |
+
return False, f"ุงู
ุชุฏุงุฏ ุบูุฑ ู
ุณู
ูุญ: {ext}. ุงูู
ุณู
ูุญ: {', '.join(sorted(ALLOWED_EXTENSIONS))}"
|
| 169 |
+
|
| 170 |
+
ct = (file.content_type or "").lower().split(";")[0].strip()
|
| 171 |
+
if ct and ct not in ALLOWED_MIME_TYPES and not ct.startswith("image/"):
|
| 172 |
+
return False, f"ููุน ุงูู
ูู ุบูุฑ ู
ุณู
ูุญ: {ct}"
|
| 173 |
+
|
| 174 |
+
# Verify actual image bytes
|
| 175 |
+
try:
|
| 176 |
+
img = Image.open(io.BytesIO(data))
|
| 177 |
+
img.verify()
|
| 178 |
+
except Exception:
|
| 179 |
+
try:
|
| 180 |
+
img = Image.open(io.BytesIO(data))
|
| 181 |
+
img.load()
|
| 182 |
+
except Exception:
|
| 183 |
+
return False, "ุงูู
ูู ุชุงูู ุฃู ููุณ ุตูุฑุฉ ุตุงูุญุฉ"
|
| 184 |
+
|
| 185 |
+
return True, ""
|
| 186 |
+
|
| 187 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 188 |
+
# AI ANALYSIS (Claude)
|
| 189 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 190 |
+
async def analyze_image(image_data: bytes, mode: Mode) -> str:
|
| 191 |
+
if not anthropic_client:
|
| 192 |
+
return "ุชุญููู ุงูุฐูุงุก ุงูุงุตุทูุงุนู ุบูุฑ ู
ุชุงุญ (ANTHROPIC_API_KEY ุบูุฑ ู
ุญุฏุฏ)"
|
| 193 |
+
|
| 194 |
+
try:
|
| 195 |
+
# Resize for API if too large (saves tokens)
|
| 196 |
+
img = Image.open(io.BytesIO(image_data)).convert("RGB")
|
| 197 |
+
if max(img.size) > 1024:
|
| 198 |
+
img.thumbnail((1024, 1024), Image.LANCZOS)
|
| 199 |
+
buf = io.BytesIO()
|
| 200 |
+
img.save(buf, format="JPEG", quality=85)
|
| 201 |
+
b64 = base64.standard_b64encode(buf.getvalue()).decode()
|
| 202 |
+
|
| 203 |
+
if mode == Mode.THINKING:
|
| 204 |
+
# Extended thinking for maximum precision analysis
|
| 205 |
+
response = anthropic_client.messages.create(
|
| 206 |
+
model="claude-sonnet-4-20250514",
|
| 207 |
+
max_tokens=2000,
|
| 208 |
+
thinking={"type": "enabled", "budget_tokens": 8000},
|
| 209 |
+
messages=[{
|
| 210 |
+
"role": "user",
|
| 211 |
+
"content": [
|
| 212 |
+
{
|
| 213 |
+
"type": "image",
|
| 214 |
+
"source": {"type": "base64", "media_type": "image/jpeg", "data": b64}
|
| 215 |
+
},
|
| 216 |
+
{
|
| 217 |
+
"type": "text",
|
| 218 |
+
"text": (
|
| 219 |
+
"ุฃูุช ุฎุจูุฑ ู
ุญุชุฑู ูู ู
ุนุงูุฌุฉ ุงูุตูุฑ ูุฅุฒุงูุฉ ุงูุฎูููุงุช. ุญูู ูุฐู ุงูุตูุฑุฉ ุชุญูููุงู ุฏูููุงู:\n\n"
|
| 220 |
+
"1. **ุงูู
ูุถูุน ุงูุฑุฆูุณู**: ู
ุง ููุ (ุดุฎุตุ ุญููุงูุ ู
ูุชุฌุ ุฅูุฎ)\n"
|
| 221 |
+
"2. **ุงูุฎูููุฉ**: ุทุจูุนุชูุง ูู
ุฏู ุชุนููุฏูุง\n"
|
| 222 |
+
"3. **ุงูุญูุงู ุงูุตุนุจุฉ**: ูู ููุฌุฏ ุดุนุฑุ ูุฑุงุกุ ุดูุงููุฉุ ุธูุงูุ\n"
|
| 223 |
+
"4. **ู
ุณุชูู ุงูุตุนูุจุฉ**: ุณูู / ู
ุชูุณุท / ุตุนุจ ุฌุฏุงู\n"
|
| 224 |
+
"5. **ุชูุตูุฉ**: ู
ุง ุงูุฅุณุชุฑุงุชูุฌูุฉ ุงูู
ุซูู ูุฅุฒุงูุฉ ุงูุฎูููุฉุ\n\n"
|
| 225 |
+
"ูู ุฏูููุงู ูู
ุฎุชุตุฑุงู."
|
| 226 |
+
)
|
| 227 |
+
}
|
| 228 |
+
]
|
| 229 |
+
}]
|
| 230 |
+
)
|
| 231 |
+
else:
|
| 232 |
+
response = anthropic_client.messages.create(
|
| 233 |
+
model="claude-sonnet-4-20250514",
|
| 234 |
+
max_tokens=300,
|
| 235 |
+
messages=[{
|
| 236 |
+
"role": "user",
|
| 237 |
+
"content": [
|
| 238 |
+
{
|
| 239 |
+
"type": "image",
|
| 240 |
+
"source": {"type": "base64", "media_type": "image/jpeg", "data": b64}
|
| 241 |
+
},
|
| 242 |
+
{
|
| 243 |
+
"type": "text",
|
| 244 |
+
"text": "ู
ุง ุงูู
ูุถูุน ุงูุฑุฆูุณู ูู ูุฐู ุงูุตูุฑุฉุ ูู ุงูุฎูููุฉ ุจุณูุทุฉ ุฃู
ู
ุนูุฏุฉุ ุฌู
ูุชุงู ููุท."
|
| 245 |
+
}
|
| 246 |
+
]
|
| 247 |
+
}]
|
| 248 |
+
)
|
| 249 |
+
|
| 250 |
+
text_blocks = [b for b in response.content if b.type == "text"]
|
| 251 |
+
return text_blocks[0].text if text_blocks else "ุชู
ุงูุชุญููู"
|
| 252 |
+
|
| 253 |
+
except Exception as e:
|
| 254 |
+
log.error(f"Claude analysis error: {e}")
|
| 255 |
+
return f"ุชุนุฐุฑ ุงูุชุญููู: {str(e)[:120]}"
|
| 256 |
+
|
| 257 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 258 |
+
# BACKGROUND REMOVAL
|
| 259 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 260 |
+
def _do_remove_fast(data: bytes) -> bytes:
|
| 261 |
+
"""Fast removal using u2net โ standard quality, quick."""
|
| 262 |
+
from rembg import remove
|
| 263 |
+
return remove(
|
| 264 |
+
data,
|
| 265 |
+
session=fast_session,
|
| 266 |
+
alpha_matting=False,
|
| 267 |
+
post_process_mask=True,
|
| 268 |
+
bgcolor=None,
|
| 269 |
+
)
|
| 270 |
+
|
| 271 |
+
def _do_remove_thinking(data: bytes) -> bytes:
|
| 272 |
+
"""
|
| 273 |
+
Thinking removal using BiRefNet + alpha matting.
|
| 274 |
+
Multi-pass for maximum edge precision.
|
| 275 |
+
"""
|
| 276 |
+
from rembg import remove
|
| 277 |
+
|
| 278 |
+
# Pass 1: BiRefNet segmentation with alpha matting
|
| 279 |
+
result_bytes = remove(
|
| 280 |
+
data,
|
| 281 |
+
session=thinking_session,
|
| 282 |
+
alpha_matting=True,
|
| 283 |
+
alpha_matting_foreground_threshold=240,
|
| 284 |
+
alpha_matting_background_threshold=10,
|
| 285 |
+
alpha_matting_erode_size=10,
|
| 286 |
+
post_process_mask=True,
|
| 287 |
+
bgcolor=None,
|
| 288 |
+
)
|
| 289 |
+
|
| 290 |
+
# Pass 2: Alpha channel refinement
|
| 291 |
+
try:
|
| 292 |
+
result_img = Image.open(io.BytesIO(result_bytes)).convert("RGBA")
|
| 293 |
+
r, g, b, alpha = result_img.split()
|
| 294 |
+
|
| 295 |
+
# Denoise alpha channel โ reduces haloing artifacts
|
| 296 |
+
alpha_arr = np.array(alpha, dtype=np.float32)
|
| 297 |
+
|
| 298 |
+
# Bilateral-style smoothing on edge regions
|
| 299 |
+
# Only smooth near-edge pixels (20โ200), keep full opacity/transparency
|
| 300 |
+
edge_mask = (alpha_arr > 20) & (alpha_arr < 235)
|
| 301 |
+
if edge_mask.any():
|
| 302 |
+
from PIL import ImageFilter
|
| 303 |
+
alpha_smooth = alpha.filter(ImageFilter.SMOOTH_MORE)
|
| 304 |
+
alpha_arr2 = np.array(alpha_smooth, dtype=np.float32)
|
| 305 |
+
# Blend only at edge pixels
|
| 306 |
+
alpha_arr[edge_mask] = (
|
| 307 |
+
alpha_arr[edge_mask] * 0.4 + alpha_arr2[edge_mask] * 0.6
|
| 308 |
+
)
|
| 309 |
+
|
| 310 |
+
alpha_final = Image.fromarray(alpha_arr.clip(0, 255).astype(np.uint8))
|
| 311 |
+
final_img = Image.merge("RGBA", (r, g, b, alpha_final))
|
| 312 |
+
|
| 313 |
+
out = io.BytesIO()
|
| 314 |
+
final_img.save(out, format="PNG", optimize=False, compress_level=1)
|
| 315 |
+
return out.getvalue()
|
| 316 |
+
except Exception as e:
|
| 317 |
+
log.warning(f"Pass 2 refinement failed (returning pass 1): {e}")
|
| 318 |
+
return result_bytes
|
| 319 |
+
|
| 320 |
+
async def run_removal(task: Task) -> bytes:
|
| 321 |
+
loop = asyncio.get_event_loop()
|
| 322 |
+
if task.mode == Mode.FAST:
|
| 323 |
+
return await loop.run_in_executor(None, _do_remove_fast, task.image_data)
|
| 324 |
+
else:
|
| 325 |
+
return await asyncio.wait_for(
|
| 326 |
+
loop.run_in_executor(None, _do_remove_thinking, task.image_data),
|
| 327 |
+
timeout=THINKING_TIMEOUT,
|
| 328 |
+
)
|
| 329 |
+
|
| 330 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 331 |
+
# WEBSOCKET BROADCAST
|
| 332 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 333 |
+
async def broadcast(task_id: str, payload: dict):
|
| 334 |
+
dead = []
|
| 335 |
+
for ws in ws_map.get(task_id, []):
|
| 336 |
+
try:
|
| 337 |
+
await ws.send_json(payload)
|
| 338 |
+
except Exception:
|
| 339 |
+
dead.append(ws)
|
| 340 |
+
for ws in dead:
|
| 341 |
+
try:
|
| 342 |
+
ws_map[task_id].remove(ws)
|
| 343 |
+
except ValueError:
|
| 344 |
+
pass
|
| 345 |
+
|
| 346 |
+
async def broadcast_all_positions():
|
| 347 |
+
"""Notify all waiting tasks of their new queue positions."""
|
| 348 |
+
async with queue_lock:
|
| 349 |
+
for i, tid in enumerate(pending_queue):
|
| 350 |
+
await broadcast(tid, {
|
| 351 |
+
"event": "position_update",
|
| 352 |
+
"position": i + 1,
|
| 353 |
+
"total": len(pending_queue),
|
| 354 |
+
})
|
| 355 |
+
|
| 356 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 357 |
+
# QUEUE WORKER
|
| 358 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 359 |
+
async def queue_worker():
|
| 360 |
+
global current_task
|
| 361 |
+
log.info("Queue worker started")
|
| 362 |
+
|
| 363 |
+
while True:
|
| 364 |
+
task_id = None
|
| 365 |
+
|
| 366 |
+
async with queue_lock:
|
| 367 |
+
if pending_queue:
|
| 368 |
+
task_id = pending_queue.pop(0)
|
| 369 |
+
t = tasks.get(task_id)
|
| 370 |
+
if t:
|
| 371 |
+
t.status = TaskStatus.PROCESSING
|
| 372 |
+
t.stage = "ุชุญููู ุงูุตูุฑุฉ"
|
| 373 |
+
t.queue_pos = 0
|
| 374 |
+
current_task = task_id
|
| 375 |
+
# Update remaining positions
|
| 376 |
+
for i, tid in enumerate(pending_queue):
|
| 377 |
+
if tid in tasks:
|
| 378 |
+
tasks[tid].queue_pos = i + 1
|
| 379 |
+
|
| 380 |
+
if not task_id:
|
| 381 |
+
await asyncio.sleep(0.3)
|
| 382 |
+
continue
|
| 383 |
+
|
| 384 |
+
task = tasks.get(task_id)
|
| 385 |
+
if not task:
|
| 386 |
+
current_task = None
|
| 387 |
+
continue
|
| 388 |
+
|
| 389 |
+
start = time.time()
|
| 390 |
+
try:
|
| 391 |
+
# Step 1: AI analysis
|
| 392 |
+
await broadcast(task_id, {"event": "stage", "stage": "ุชุญููู ุงูุตูุฑุฉ ุจุงูุฐูุงุก ุงูุงุตุทูุงุนู..."})
|
| 393 |
+
task.stage = "ุชุญููู"
|
| 394 |
+
task.analysis = await analyze_image(task.image_data, task.mode)
|
| 395 |
+
|
| 396 |
+
# Step 2: Background removal
|
| 397 |
+
stage_msg = (
|
| 398 |
+
"ุฅุฒุงูุฉ ุงูุฎูููุฉ โ ูุถุน ุงูุชูููุฑ ุงูุนู
ูู (ุญุชู ุฏูููุชูู)..."
|
| 399 |
+
if task.mode == Mode.THINKING
|
| 400 |
+
else "ุฅุฒุงูุฉ ุงูุฎูููุฉ โ ุงููุถุน ุงูุณุฑูุน..."
|
| 401 |
+
)
|
| 402 |
+
await broadcast(task_id, {"event": "stage", "stage": stage_msg, "analysis": task.analysis})
|
| 403 |
+
task.stage = "ุฅุฒุงูุฉ ุงูุฎูููุฉ"
|
| 404 |
+
|
| 405 |
+
result_bytes = await run_removal(task)
|
| 406 |
+
task.result_png = result_bytes
|
| 407 |
+
|
| 408 |
+
# Step 3: Generate WebP lossless
|
| 409 |
+
await broadcast(task_id, {"event": "stage", "stage": "ุชูููุฏ ู
ูู WebP..."})
|
| 410 |
+
result_img = Image.open(io.BytesIO(result_bytes)).convert("RGBA")
|
| 411 |
+
webp_buf = io.BytesIO()
|
| 412 |
+
result_img.save(webp_buf, format="WEBP", lossless=True, quality=100)
|
| 413 |
+
task.result_webp = webp_buf.getvalue()
|
| 414 |
+
|
| 415 |
+
task.proc_time = time.time() - start
|
| 416 |
+
task.status = TaskStatus.COMPLETED
|
| 417 |
+
task.stage = "ู
ูุชู
ู"
|
| 418 |
+
|
| 419 |
+
log.info(f"Task {task_id[:8]} completed in {task.proc_time:.1f}s ({task.mode})")
|
| 420 |
+
await broadcast(task_id, {
|
| 421 |
+
"event": "completed",
|
| 422 |
+
"task_id": task_id,
|
| 423 |
+
"proc_time": f"{task.proc_time:.1f}",
|
| 424 |
+
"analysis": task.analysis,
|
| 425 |
+
"size_kb": len(task.result_png) // 1024,
|
| 426 |
+
})
|
| 427 |
+
|
| 428 |
+
except asyncio.TimeoutError:
|
| 429 |
+
task.status = TaskStatus.FAILED
|
| 430 |
+
task.error = "ุงูุชูุช ู
ููุฉ ุงูู
ุนุงูุฌุฉ (120 ุซุงููุฉ). ุฌุฑุจ ุงููุถุน ุงูุณุฑูุน"
|
| 431 |
+
log.warning(f"Task {task_id[:8]} timed out")
|
| 432 |
+
await broadcast(task_id, {"event": "failed", "error": task.error})
|
| 433 |
+
|
| 434 |
+
except Exception as exc:
|
| 435 |
+
task.status = TaskStatus.FAILED
|
| 436 |
+
task.error = str(exc)
|
| 437 |
+
log.error(f"Task {task_id[:8]} failed: {exc}", exc_info=True)
|
| 438 |
+
await broadcast(task_id, {"event": "failed", "error": str(exc)[:300]})
|
| 439 |
+
|
| 440 |
+
finally:
|
| 441 |
+
ip_active[task.ip] = max(0, ip_active[task.ip] - 1)
|
| 442 |
+
current_task = None
|
| 443 |
+
del task.image_data # free memory immediately
|
| 444 |
+
gc.collect()
|
| 445 |
+
|
| 446 |
+
await broadcast_all_positions()
|
| 447 |
+
await asyncio.sleep(0.1)
|
| 448 |
+
|
| 449 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 450 |
+
# CLEANUP WORKER โ removes old results
|
| 451 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 452 |
+
async def cleanup_worker():
|
| 453 |
+
while True:
|
| 454 |
+
await asyncio.sleep(300)
|
| 455 |
+
now = time.time()
|
| 456 |
+
stale = [
|
| 457 |
+
tid for tid, t in tasks.items()
|
| 458 |
+
if now - t.created_at > RESULT_TTL
|
| 459 |
+
and t.status in (TaskStatus.COMPLETED, TaskStatus.FAILED)
|
| 460 |
+
]
|
| 461 |
+
for tid in stale:
|
| 462 |
+
del tasks[tid]
|
| 463 |
+
if stale:
|
| 464 |
+
log.info(f"Cleaned up {len(stale)} old tasks")
|
| 465 |
+
|
| 466 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 467 |
+
# WEBSOCKET ENDPOINT
|
| 468 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 469 |
+
@app.websocket("/ws/{task_id}")
|
| 470 |
+
async def ws_endpoint(websocket: WebSocket, task_id: str):
|
| 471 |
+
await websocket.accept()
|
| 472 |
+
ws_map[task_id].append(websocket)
|
| 473 |
+
|
| 474 |
+
# Send current state immediately
|
| 475 |
+
task = tasks.get(task_id)
|
| 476 |
+
if task:
|
| 477 |
+
if task.status == TaskStatus.COMPLETED:
|
| 478 |
+
await websocket.send_json({"event": "completed", "task_id": task_id, "proc_time": str(task.proc_time or 0), "analysis": task.analysis})
|
| 479 |
+
elif task.status == TaskStatus.FAILED:
|
| 480 |
+
await websocket.send_json({"event": "failed", "error": task.error})
|
| 481 |
+
elif task.status == TaskStatus.PENDING:
|
| 482 |
+
await websocket.send_json({"event": "queued", "position": task.queue_pos, "total": len(pending_queue)})
|
| 483 |
+
elif task.status == TaskStatus.PROCESSING:
|
| 484 |
+
await websocket.send_json({"event": "stage", "stage": task.stage})
|
| 485 |
+
|
| 486 |
+
try:
|
| 487 |
+
while True:
|
| 488 |
+
await asyncio.wait_for(websocket.receive_text(), timeout=60)
|
| 489 |
+
except (WebSocketDisconnect, asyncio.TimeoutError):
|
| 490 |
+
pass
|
| 491 |
+
finally:
|
| 492 |
+
try:
|
| 493 |
+
ws_map[task_id].remove(websocket)
|
| 494 |
+
except ValueError:
|
| 495 |
+
pass
|
| 496 |
+
|
| 497 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 498 |
+
# HTTP ENDPOINTS
|
| 499 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 500 |
+
@app.get("/health")
|
| 501 |
+
async def health():
|
| 502 |
+
return {"status": "ok", "queue": len(pending_queue), "processing": current_task is not None}
|
| 503 |
+
|
| 504 |
+
@app.get("/")
|
| 505 |
+
async def root():
|
| 506 |
+
from fastapi.responses import FileResponse
|
| 507 |
+
return FileResponse("static/index.html")
|
| 508 |
+
|
| 509 |
+
@app.post("/upload")
|
| 510 |
+
async def upload(
|
| 511 |
+
request: Request,
|
| 512 |
+
file: UploadFile = File(...),
|
| 513 |
+
mode: str = "fast",
|
| 514 |
+
):
|
| 515 |
+
ip = request.client.host or "unknown"
|
| 516 |
+
|
| 517 |
+
# Validate mode
|
| 518 |
+
if mode not in (Mode.FAST, Mode.THINKING):
|
| 519 |
+
raise HTTPException(400, "ูุถุน ุบูุฑ ุตุงูุญ. ุงุณุชุฎุฏู
'fast' ุฃู 'thinking'")
|
| 520 |
+
|
| 521 |
+
# Rate limit
|
| 522 |
+
allowed, msg = check_rate_limit(ip)
|
| 523 |
+
if not allowed:
|
| 524 |
+
raise HTTPException(429, msg)
|
| 525 |
+
|
| 526 |
+
# Queue capacity
|
| 527 |
+
async with queue_lock:
|
| 528 |
+
if len(pending_queue) >= MAX_QUEUE_SIZE:
|
| 529 |
+
raise HTTPException(503, f"ุงูุทุงุจูุฑ ู
ู
ุชูุฆ ({MAX_QUEUE_SIZE}/{MAX_QUEUE_SIZE}). ูุฑุฌู ุงูุงูุชุธุงุฑ")
|
| 530 |
+
|
| 531 |
+
# Read & validate
|
| 532 |
+
data = await file.read()
|
| 533 |
+
valid, err = await validate_image(file, data)
|
| 534 |
+
if not valid:
|
| 535 |
+
# Refund the rate limit slot
|
| 536 |
+
ip_times[ip].pop() if ip_times[ip] else None
|
| 537 |
+
raise HTTPException(400, err)
|
| 538 |
+
|
| 539 |
+
# Image metadata
|
| 540 |
+
img = Image.open(io.BytesIO(data))
|
| 541 |
+
orig_size = img.size
|
| 542 |
+
|
| 543 |
+
# Create task
|
| 544 |
+
task_id = str(uuid.uuid4())
|
| 545 |
+
task = Task(
|
| 546 |
+
id=task_id,
|
| 547 |
+
mode=Mode(mode),
|
| 548 |
+
image_data=data,
|
| 549 |
+
filename=file.filename or "image",
|
| 550 |
+
ip=ip,
|
| 551 |
+
orig_size=orig_size,
|
| 552 |
+
)
|
| 553 |
+
|
| 554 |
+
async with queue_lock:
|
| 555 |
+
tasks[task_id] = task
|
| 556 |
+
pending_queue.append(task_id)
|
| 557 |
+
task.queue_pos = len(pending_queue)
|
| 558 |
+
ip_active[ip] += 1
|
| 559 |
+
|
| 560 |
+
log.info(f"New task {task_id[:8]} | mode={mode} | size={orig_size} | ip={ip}")
|
| 561 |
+
|
| 562 |
+
return JSONResponse({
|
| 563 |
+
"task_id": task_id,
|
| 564 |
+
"queue_pos": task.queue_pos,
|
| 565 |
+
"queue_total": len(pending_queue),
|
| 566 |
+
"mode": mode,
|
| 567 |
+
"image_size": f"{orig_size[0]}ร{orig_size[1]}",
|
| 568 |
+
"filename": file.filename,
|
| 569 |
+
})
|
| 570 |
+
|
| 571 |
+
@app.get("/status/{task_id}")
|
| 572 |
+
async def status(task_id: str):
|
| 573 |
+
task = tasks.get(task_id)
|
| 574 |
+
if not task:
|
| 575 |
+
raise HTTPException(404, "ุงูู
ูู
ุฉ ุบูุฑ ู
ูุฌูุฏุฉ ุฃู ุงูุชูุช ุตูุงุญูุชูุง")
|
| 576 |
+
|
| 577 |
+
base = {
|
| 578 |
+
"task_id": task_id,
|
| 579 |
+
"status": task.status.value,
|
| 580 |
+
"mode": task.mode.value,
|
| 581 |
+
"filename": task.filename,
|
| 582 |
+
}
|
| 583 |
+
if task.status == TaskStatus.PENDING:
|
| 584 |
+
base.update({"queue_pos": task.queue_pos, "queue_total": len(pending_queue) + (1 if current_task else 0)})
|
| 585 |
+
elif task.status == TaskStatus.PROCESSING:
|
| 586 |
+
base.update({"stage": task.stage})
|
| 587 |
+
elif task.status == TaskStatus.COMPLETED:
|
| 588 |
+
base.update({"proc_time": task.proc_time, "analysis": task.analysis, "size_kb": len(task.result_png or b"") // 1024})
|
| 589 |
+
elif task.status == TaskStatus.FAILED:
|
| 590 |
+
base.update({"error": task.error})
|
| 591 |
+
return JSONResponse(base)
|
| 592 |
+
|
| 593 |
+
@app.get("/result/{task_id}")
|
| 594 |
+
async def result(task_id: str, fmt: str = "png"):
|
| 595 |
+
task = tasks.get(task_id)
|
| 596 |
+
if not task:
|
| 597 |
+
raise HTTPException(404, "ุงูู
ูู
ุฉ ุบูุฑ ู
ูุฌูุฏุฉ")
|
| 598 |
+
if task.status != TaskStatus.COMPLETED:
|
| 599 |
+
raise HTTPException(400, f"ุงูู
ูู
ุฉ ูู
ุชูุชู
ู. ุงูุญุงูุฉ: {task.status.value}")
|
| 600 |
+
|
| 601 |
+
stem = Path(task.filename).stem
|
| 602 |
+
if fmt == "webp" and task.result_webp:
|
| 603 |
+
return Response(
|
| 604 |
+
content=task.result_webp,
|
| 605 |
+
media_type="image/webp",
|
| 606 |
+
headers={"Content-Disposition": f'attachment; filename="{stem}_nobg.webp"'},
|
| 607 |
+
)
|
| 608 |
+
return Response(
|
| 609 |
+
content=task.result_png,
|
| 610 |
+
media_type="image/png",
|
| 611 |
+
headers={"Content-Disposition": f'attachment; filename="{stem}_nobg.png"'},
|
| 612 |
+
)
|
| 613 |
+
|
| 614 |
+
@app.get("/preview/{task_id}")
|
| 615 |
+
async def preview(task_id: str):
|
| 616 |
+
"""Inline preview (no Content-Disposition) for display in browser."""
|
| 617 |
+
task = tasks.get(task_id)
|
| 618 |
+
if not task or task.status != TaskStatus.COMPLETED:
|
| 619 |
+
raise HTTPException(404, "ุงููุชูุฌุฉ ุบูุฑ ู
ุชุงุญุฉ")
|
| 620 |
+
return Response(content=task.result_png, media_type="image/png")
|
| 621 |
+
|
| 622 |
+
@app.get("/queue-info")
|
| 623 |
+
async def queue_info():
|
| 624 |
+
return JSONResponse({
|
| 625 |
+
"waiting": len(pending_queue),
|
| 626 |
+
"max": MAX_QUEUE_SIZE,
|
| 627 |
+
"free_slots": MAX_QUEUE_SIZE - len(pending_queue),
|
| 628 |
+
"processing": current_task is not None,
|
| 629 |
+
"total_tasks": len(tasks),
|
| 630 |
+
})
|
| 631 |
+
|
| 632 |
+
@app.delete("/task/{task_id}")
|
| 633 |
+
async def cancel_task(task_id: str, request: Request):
|
| 634 |
+
task = tasks.get(task_id)
|
| 635 |
+
if not task:
|
| 636 |
+
raise HTTPException(404, "ุงูู
ูู
ุฉ ุบูุฑ ู
ูุฌูุฏุฉ")
|
| 637 |
+
if task.status == TaskStatus.PROCESSING:
|
| 638 |
+
raise HTTPException(400, "ูุง ูู
ูู ุฅูุบุงุก ู
ูู
ุฉ ููุฏ ุงูู
ุนุงูุฌุฉ")
|
| 639 |
+
|
| 640 |
+
async with queue_lock:
|
| 641 |
+
if task_id in pending_queue:
|
| 642 |
+
pending_queue.remove(task_id)
|
| 643 |
+
ip_active[task.ip] = max(0, ip_active[task.ip] - 1)
|
| 644 |
+
if task_id in tasks:
|
| 645 |
+
del tasks[task_id]
|
| 646 |
+
|
| 647 |
+
await broadcast_all_positions()
|
| 648 |
+
return JSONResponse({"message": "ุชู
ุฅูุบุงุก ุงูู
ูู
ุฉ"})
|
| 649 |
+
|
| 650 |
+
# Mount static files
|
| 651 |
+
app.mount("/static", StaticFiles(directory="static"), name="static")
|
| 652 |
+
|
| 653 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 654 |
+
if __name__ == "__main__":
|
| 655 |
+
import uvicorn
|
| 656 |
+
uvicorn.run(app, host="0.0.0.0", port=7860, loop="asyncio")
|
requirements.txt
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi==0.115.6
|
| 2 |
+
uvicorn[standard]==0.32.1
|
| 3 |
+
python-multipart==0.0.20
|
| 4 |
+
Pillow==11.1.0
|
| 5 |
+
rembg[gpu]==2.0.59
|
| 6 |
+
onnxruntime==1.20.1
|
| 7 |
+
anthropic==0.42.0
|
| 8 |
+
numpy==1.26.4
|
| 9 |
+
aiofiles==24.1.0
|
| 10 |
+
pymatting==1.1.12
|
| 11 |
+
scipy==1.14.1
|