Skydata001 commited on
Commit
7d6a5e2
ยท
verified ยท
1 Parent(s): 3dcf03f

Upload 5 files

Browse files
Files changed (5) hide show
  1. Dockerfile +29 -0
  2. docker-compose.yml +30 -0
  3. index.html +896 -0
  4. main.py +656 -0
  5. 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