Upload 4 files
Browse files- Dockerfile +27 -0
- README.md +50 -10
- app.py +1178 -0
- requirements.txt +3 -0
Dockerfile
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
|
| 3 |
+
# Dipendenze sistema per curl_cffi (compila C bindings)
|
| 4 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 5 |
+
gcc \
|
| 6 |
+
libffi-dev \
|
| 7 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 8 |
+
|
| 9 |
+
# HF Spaces richiede un utente non-root
|
| 10 |
+
RUN useradd -m -u 1000 user
|
| 11 |
+
USER user
|
| 12 |
+
ENV HOME=/home/user \
|
| 13 |
+
PATH=/home/user/.local/bin:$PATH
|
| 14 |
+
|
| 15 |
+
WORKDIR $HOME/app
|
| 16 |
+
|
| 17 |
+
# Copia requirements e installa
|
| 18 |
+
COPY --chown=user requirements.txt .
|
| 19 |
+
RUN pip install --no-cache-dir --user -r requirements.txt
|
| 20 |
+
|
| 21 |
+
# Copia app
|
| 22 |
+
COPY --chown=user . .
|
| 23 |
+
|
| 24 |
+
# HF Spaces espone SEMPRE porta 7860
|
| 25 |
+
EXPOSE 7860
|
| 26 |
+
|
| 27 |
+
CMD ["python", "app.py"]
|
README.md
CHANGED
|
@@ -1,10 +1,50 @@
|
|
| 1 |
-
---
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
\---
|
| 2 |
+
|
| 3 |
+
title: Krea-2 API
|
| 4 |
+
|
| 5 |
+
emoji: ⚡
|
| 6 |
+
|
| 7 |
+
colorFrom: purple
|
| 8 |
+
|
| 9 |
+
colorTo: pink
|
| 10 |
+
|
| 11 |
+
sdk: docker
|
| 12 |
+
|
| 13 |
+
app\_port: 7860
|
| 14 |
+
|
| 15 |
+
pinned: false
|
| 16 |
+
|
| 17 |
+
license: mit
|
| 18 |
+
|
| 19 |
+
\---
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
\# Krea-2 API
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
REST API for AI image generation via Krea-2 model.
|
| 28 |
+
|
| 29 |
+
Proxy-load-balanced, agent-ready, no authentication required.
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
\## Endpoints
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
\- `GET /generate?prompt=...` — generate image
|
| 38 |
+
|
| 39 |
+
\- `GET /generate?prompt=...\&n=5` — 5 variants in parallel
|
| 40 |
+
|
| 41 |
+
\- `GET /docs` — Swagger UI
|
| 42 |
+
|
| 43 |
+
\- `GET /tool-schema` — Tool schema for AI agents
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
\## Usage
|
| 48 |
+
|
| 49 |
+
GET https://elmarcito-krea2-api.hf.space/generate?prompt=a+cyberpunk+cat
|
| 50 |
+
|
app.py
ADDED
|
@@ -0,0 +1,1178 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Krea-2 as-a-Service — HTTP API wrapper
|
| 4 |
+
Espone il client krea/Krea-2 come REST API GET, ideale per agent AI / tool calling.
|
| 5 |
+
|
| 6 |
+
Uso:
|
| 7 |
+
python api.py # avvia server su :7860
|
| 8 |
+
python api.py --port 8080 --host 0.0.0.0
|
| 9 |
+
|
| 10 |
+
Endpoints principali (tutti GET):
|
| 11 |
+
GET / → info + docs
|
| 12 |
+
GET /health → healthcheck
|
| 13 |
+
GET /generate?prompt=... → genera 1 immagine (sync)
|
| 14 |
+
GET /generate?prompt=...&n=5 → genera N in parallelo
|
| 15 |
+
GET /batch?prompts=a|b|c → batch da lista pipe-separated
|
| 16 |
+
GET /jobs/{job_id} → stato job async
|
| 17 |
+
GET /jobs/{job_id}/result → risultato quando pronto
|
| 18 |
+
GET /image/{filename} → scarica file generato
|
| 19 |
+
GET /stats → statistiche DB proxy
|
| 20 |
+
GET /openapi.json → schema OpenAPI per agent
|
| 21 |
+
|
| 22 |
+
pip install curl_cffi fastapi uvicorn
|
| 23 |
+
"""
|
| 24 |
+
|
| 25 |
+
import argparse
|
| 26 |
+
import asyncio
|
| 27 |
+
import json
|
| 28 |
+
import os
|
| 29 |
+
import re
|
| 30 |
+
import sys
|
| 31 |
+
import time
|
| 32 |
+
import uuid
|
| 33 |
+
import random
|
| 34 |
+
import string
|
| 35 |
+
import sqlite3
|
| 36 |
+
import threading
|
| 37 |
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
| 38 |
+
from dataclasses import dataclass, field, asdict
|
| 39 |
+
from pathlib import Path
|
| 40 |
+
from typing import Optional, Literal
|
| 41 |
+
from datetime import datetime
|
| 42 |
+
|
| 43 |
+
from curl_cffi import requests as cffi_requests
|
| 44 |
+
from fastapi import FastAPI, HTTPException, Query, BackgroundTasks, Request
|
| 45 |
+
from fastapi.responses import JSONResponse, FileResponse, HTMLResponse
|
| 46 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 47 |
+
import uvicorn
|
| 48 |
+
|
| 49 |
+
# ═══════════════════════════════════════════════════════════════
|
| 50 |
+
# CONFIG
|
| 51 |
+
# ═══════════════════════════════════════════════════════════════
|
| 52 |
+
|
| 53 |
+
HF_BASE = "https://huggingface.co"
|
| 54 |
+
SPACE_BASE = "https://krea-krea-2.hf.space"
|
| 55 |
+
SPACE_ID = "krea/Krea-2"
|
| 56 |
+
JWT_URL = f"{HF_BASE}/api/spaces/{SPACE_ID}/jwt"
|
| 57 |
+
JOIN_URL = f"{SPACE_BASE}/gradio_api/queue/join"
|
| 58 |
+
DATA_URL = f"{SPACE_BASE}/gradio_api/queue/data"
|
| 59 |
+
|
| 60 |
+
FN_RESOLUTION = 3
|
| 61 |
+
FN_GENERATE = 4
|
| 62 |
+
|
| 63 |
+
DEFAULT_WORKERS = 30
|
| 64 |
+
WAVE_SIZE = 25
|
| 65 |
+
WAVE_DELAY_MS = 150
|
| 66 |
+
TOKEN_TIMEOUT = 8
|
| 67 |
+
STREAM_TIMEOUT = 90
|
| 68 |
+
|
| 69 |
+
DB_PATH = Path("proxy_pool.db")
|
| 70 |
+
IMAGES_DIR = Path("generated_images")
|
| 71 |
+
IMAGES_DIR.mkdir(exist_ok=True)
|
| 72 |
+
|
| 73 |
+
BAN_TTL_SECONDS = 3600 * 6
|
| 74 |
+
GOOD_TTL_DAYS = 7
|
| 75 |
+
MIN_GOOD_RATIO = 0.4
|
| 76 |
+
MAX_PARALLEL_JOBS = 10
|
| 77 |
+
|
| 78 |
+
# Job registry TTL (dopo quanto pulire i job vecchi)
|
| 79 |
+
JOB_TTL_SECONDS = 3600 # 1h
|
| 80 |
+
|
| 81 |
+
PROXYSCRAPE_URLS = [
|
| 82 |
+
"https://api.proxyscrape.com/v2/?request=displayproxies&protocol=http&timeout=3000&country=all&ssl=all&anonymity=all",
|
| 83 |
+
"https://api.proxyscrape.com/v2/?request=displayproxies&protocol=socks4&timeout=3000&country=all&ssl=all&anonymity=all",
|
| 84 |
+
"https://api.proxyscrape.com/v2/?request=displayproxies&protocol=socks5&timeout=3000&country=all&ssl=all&anonymity=all",
|
| 85 |
+
]
|
| 86 |
+
|
| 87 |
+
IMPERSONATE_POOL = [
|
| 88 |
+
("chrome124", "Windows NT 10.0; Win64; x64", "Chrome/124.0.0.0"),
|
| 89 |
+
("chrome123", "Macintosh; Intel Mac OS X 10_15_7", "Chrome/123.0.0.0"),
|
| 90 |
+
("chrome120", "X11; Linux x86_64", "Chrome/120.0.0.0"),
|
| 91 |
+
("chrome116", "Windows NT 10.0; Win64; x64", "Chrome/116.0.0.0"),
|
| 92 |
+
]
|
| 93 |
+
|
| 94 |
+
LANGS = [
|
| 95 |
+
"en-US,en;q=0.9", "it-IT,it;q=0.9,en;q=0.8",
|
| 96 |
+
"en-GB,en;q=0.9", "fr-FR,fr;q=0.9,en;q=0.8",
|
| 97 |
+
"de-DE,de;q=0.9,en;q=0.8", "es-ES,es;q=0.9,en;q=0.8",
|
| 98 |
+
]
|
| 99 |
+
|
| 100 |
+
RESOLUTIONS = {
|
| 101 |
+
"square": (1024, 1024),
|
| 102 |
+
"portrait": (1024, 1536),
|
| 103 |
+
"landscape": (1536, 1024),
|
| 104 |
+
"square2k": (2048, 2048),
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
# ═══════════════════════════════════════════════════════════════
|
| 109 |
+
# PROXY DB (SQLite)
|
| 110 |
+
# ═══════════════════════════════════════════════════════════════
|
| 111 |
+
|
| 112 |
+
class ProxyDB:
|
| 113 |
+
def __init__(self, path=DB_PATH):
|
| 114 |
+
self.path = path
|
| 115 |
+
self._lock = threading.Lock()
|
| 116 |
+
self._init_db()
|
| 117 |
+
|
| 118 |
+
def _conn(self):
|
| 119 |
+
conn = sqlite3.connect(self.path, timeout=10, check_same_thread=False)
|
| 120 |
+
conn.execute("PRAGMA journal_mode=WAL")
|
| 121 |
+
conn.execute("PRAGMA synchronous=NORMAL")
|
| 122 |
+
return conn
|
| 123 |
+
|
| 124 |
+
def _init_db(self):
|
| 125 |
+
with self._conn() as c:
|
| 126 |
+
c.execute("""
|
| 127 |
+
CREATE TABLE IF NOT EXISTS proxies (
|
| 128 |
+
url TEXT PRIMARY KEY, protocol TEXT NOT NULL,
|
| 129 |
+
successes INTEGER DEFAULT 0, failures INTEGER DEFAULT 0,
|
| 130 |
+
avg_latency_ms REAL DEFAULT 0.0,
|
| 131 |
+
last_success REAL DEFAULT 0, last_failure REAL DEFAULT 0,
|
| 132 |
+
banned_until REAL DEFAULT 0,
|
| 133 |
+
first_seen REAL DEFAULT 0, score REAL DEFAULT 0
|
| 134 |
+
)""")
|
| 135 |
+
c.execute("CREATE INDEX IF NOT EXISTS idx_score ON proxies(score DESC)")
|
| 136 |
+
c.commit()
|
| 137 |
+
|
| 138 |
+
def upsert_many(self, proxies):
|
| 139 |
+
with self._lock, self._conn() as c:
|
| 140 |
+
now = time.time()
|
| 141 |
+
for p in proxies:
|
| 142 |
+
c.execute("INSERT OR IGNORE INTO proxies (url, protocol, first_seen) VALUES (?, ?, ?)",
|
| 143 |
+
(p["url"], p["protocol"], now))
|
| 144 |
+
c.commit()
|
| 145 |
+
|
| 146 |
+
def get_ranked(self, limit, min_good_ratio=0.4, exclude: set = None):
|
| 147 |
+
exclude = exclude or set()
|
| 148 |
+
with self._lock, self._conn() as c:
|
| 149 |
+
now = time.time()
|
| 150 |
+
n_good = int(limit * min_good_ratio)
|
| 151 |
+
n_new = limit - n_good
|
| 152 |
+
good = c.execute("""
|
| 153 |
+
SELECT url, protocol, successes, failures, avg_latency_ms, score FROM proxies
|
| 154 |
+
WHERE banned_until < ? AND successes > 0
|
| 155 |
+
ORDER BY score DESC, avg_latency_ms ASC LIMIT ?
|
| 156 |
+
""", (now, n_good * 2)).fetchall()
|
| 157 |
+
new_ones = c.execute("""
|
| 158 |
+
SELECT url, protocol, successes, failures, avg_latency_ms, score FROM proxies
|
| 159 |
+
WHERE banned_until < ? AND successes = 0 AND failures < 3
|
| 160 |
+
ORDER BY RANDOM() LIMIT ?
|
| 161 |
+
""", (now, n_new * 2)).fetchall()
|
| 162 |
+
rows = [r for r in list(good) + list(new_ones) if r[0] not in exclude][:limit]
|
| 163 |
+
return [{"url": r[0], "protocol": r[1], "successes": r[2],
|
| 164 |
+
"failures": r[3], "avg_latency_ms": r[4], "score": r[5]}
|
| 165 |
+
for r in rows]
|
| 166 |
+
|
| 167 |
+
def mark_success(self, url, latency_ms):
|
| 168 |
+
with self._lock, self._conn() as c:
|
| 169 |
+
row = c.execute("SELECT successes, avg_latency_ms FROM proxies WHERE url=?", (url,)).fetchone()
|
| 170 |
+
if row:
|
| 171 |
+
s, old_lat = row
|
| 172 |
+
new_s = s + 1
|
| 173 |
+
new_lat = old_lat * 0.7 + latency_ms * 0.3 if old_lat > 0 else latency_ms
|
| 174 |
+
score = new_s * 100 - (new_lat / 100)
|
| 175 |
+
c.execute("UPDATE proxies SET successes=?, avg_latency_ms=?, last_success=?, banned_until=0, score=? WHERE url=?",
|
| 176 |
+
(new_s, new_lat, time.time(), score, url))
|
| 177 |
+
c.commit()
|
| 178 |
+
|
| 179 |
+
def mark_failure(self, url, ban=False):
|
| 180 |
+
with self._lock, self._conn() as c:
|
| 181 |
+
row = c.execute("SELECT successes, failures FROM proxies WHERE url=?", (url,)).fetchone()
|
| 182 |
+
if not row: return
|
| 183 |
+
s, f = row
|
| 184 |
+
new_f = f + 1
|
| 185 |
+
banned_until = 0
|
| 186 |
+
if ban or (new_f >= 3 and s == 0):
|
| 187 |
+
banned_until = time.time() + BAN_TTL_SECONDS
|
| 188 |
+
score = s * 100 - new_f * 10
|
| 189 |
+
c.execute("UPDATE proxies SET failures=?, last_failure=?, banned_until=?, score=? WHERE url=?",
|
| 190 |
+
(new_f, time.time(), banned_until, score, url))
|
| 191 |
+
c.commit()
|
| 192 |
+
|
| 193 |
+
def stats(self):
|
| 194 |
+
with self._lock, self._conn() as c:
|
| 195 |
+
r = c.execute("""SELECT COUNT(*), SUM(CASE WHEN successes>0 THEN 1 ELSE 0 END),
|
| 196 |
+
SUM(CASE WHEN banned_until>? THEN 1 ELSE 0 END),
|
| 197 |
+
SUM(CASE WHEN successes=0 AND failures=0 THEN 1 ELSE 0 END)
|
| 198 |
+
FROM proxies""", (time.time(),)).fetchone()
|
| 199 |
+
return {"total": r[0] or 0, "good": r[1] or 0,
|
| 200 |
+
"banned": r[2] or 0, "untested": r[3] or 0}
|
| 201 |
+
|
| 202 |
+
DB = ProxyDB()
|
| 203 |
+
|
| 204 |
+
|
| 205 |
+
def _fetch_one(url):
|
| 206 |
+
try:
|
| 207 |
+
r = cffi_requests.get(url, timeout=8, impersonate="chrome120")
|
| 208 |
+
if r.status_code == 200:
|
| 209 |
+
protocol = "http"
|
| 210 |
+
if "socks4" in url: protocol = "socks4"
|
| 211 |
+
elif "socks5" in url: protocol = "socks5"
|
| 212 |
+
out = []
|
| 213 |
+
for line in r.text.strip().split("\n"):
|
| 214 |
+
line = line.strip()
|
| 215 |
+
if ":" not in line: continue
|
| 216 |
+
ip_port = line.split()[0]
|
| 217 |
+
proxy_url = (f"{protocol}://{ip_port}" if protocol != "http"
|
| 218 |
+
else f"http://{ip_port}")
|
| 219 |
+
out.append({"url": proxy_url, "protocol": protocol})
|
| 220 |
+
return out
|
| 221 |
+
except Exception: pass
|
| 222 |
+
return []
|
| 223 |
+
|
| 224 |
+
def refresh_proxy_pool():
|
| 225 |
+
all_proxies = []
|
| 226 |
+
with ThreadPoolExecutor(max_workers=3) as pool:
|
| 227 |
+
for chunk in pool.map(_fetch_one, PROXYSCRAPE_URLS):
|
| 228 |
+
all_proxies.extend(chunk)
|
| 229 |
+
DB.upsert_many(all_proxies)
|
| 230 |
+
return len(all_proxies)
|
| 231 |
+
|
| 232 |
+
|
| 233 |
+
# ═══════════════════════════════════════════════════════════════
|
| 234 |
+
# UTILS
|
| 235 |
+
# ═════════════════════════════���═════════════════════════════════
|
| 236 |
+
|
| 237 |
+
def _rand_hash(n=11):
|
| 238 |
+
return "".join(random.choices(string.ascii_lowercase + string.digits, k=n))
|
| 239 |
+
|
| 240 |
+
def _new_identity():
|
| 241 |
+
imp, os_str, ver = random.choice(IMPERSONATE_POOL)
|
| 242 |
+
ua = f"Mozilla/5.0 ({os_str}) AppleWebKit/537.36 (KHTML, like Gecko) {ver} Safari/537.36"
|
| 243 |
+
return {"impersonate": imp, "ua": ua,
|
| 244 |
+
"accept_lang": random.choice(LANGS),
|
| 245 |
+
"zerogpu_uuid": str(uuid.uuid4())}
|
| 246 |
+
|
| 247 |
+
def _resolution_label(w, h):
|
| 248 |
+
if w == h and w >= 2048: return "Square · 2K"
|
| 249 |
+
if w == h: return "Square · 1024"
|
| 250 |
+
if w > h: return "Landscape · 1024"
|
| 251 |
+
return "Portrait · 1024"
|
| 252 |
+
|
| 253 |
+
|
| 254 |
+
# ═══════════════════════════════════════════════════════════════
|
| 255 |
+
# JOB STATE (identico al client, senza UI)
|
| 256 |
+
# ═══════════════════════════════════════════════════════════════
|
| 257 |
+
|
| 258 |
+
@dataclass
|
| 259 |
+
class WorkerInfo:
|
| 260 |
+
id: int
|
| 261 |
+
proxy: str = ""
|
| 262 |
+
status: str = "idle"
|
| 263 |
+
step: int = 0
|
| 264 |
+
total_steps: int = 8
|
| 265 |
+
started_at: float = 0.0
|
| 266 |
+
|
| 267 |
+
@dataclass
|
| 268 |
+
class GenJob:
|
| 269 |
+
job_id: str
|
| 270 |
+
prompt: str
|
| 271 |
+
negative_prompt: str = ""
|
| 272 |
+
model: str = "Turbo"
|
| 273 |
+
steps: int = 8
|
| 274 |
+
guidance: float = 0.0
|
| 275 |
+
width: int = 1024
|
| 276 |
+
height: int = 1024
|
| 277 |
+
seed: int = 0
|
| 278 |
+
randomize: bool = True
|
| 279 |
+
|
| 280 |
+
status: str = "pending" # pending|running|success|failed
|
| 281 |
+
created_at: float = 0.0
|
| 282 |
+
started_at: float = 0.0
|
| 283 |
+
completed_at: float = 0.0
|
| 284 |
+
|
| 285 |
+
# risultati
|
| 286 |
+
filename: str = "" # nome file generato
|
| 287 |
+
file_path: str = "" # path assoluto
|
| 288 |
+
file_size: int = 0
|
| 289 |
+
hf_url: str = "" # URL originale su HF
|
| 290 |
+
local_url: str = "" # URL da chiamare per scaricare
|
| 291 |
+
result_seed: int = 0
|
| 292 |
+
error: str = ""
|
| 293 |
+
|
| 294 |
+
# dettagli race
|
| 295 |
+
winner_proxy: str = ""
|
| 296 |
+
winner_latency_ms: float = 0.0
|
| 297 |
+
workers_launched: int = 0
|
| 298 |
+
workers_dead: int = 0
|
| 299 |
+
tokens_ok: int = 0
|
| 300 |
+
|
| 301 |
+
# runtime (non serializzati in API)
|
| 302 |
+
workers: dict = field(default_factory=dict)
|
| 303 |
+
winner_event: threading.Event = field(default_factory=threading.Event)
|
| 304 |
+
lock: threading.Lock = field(default_factory=threading.Lock)
|
| 305 |
+
|
| 306 |
+
def to_public_dict(self) -> dict:
|
| 307 |
+
d = {
|
| 308 |
+
"job_id": self.job_id,
|
| 309 |
+
"status": self.status,
|
| 310 |
+
"prompt": self.prompt,
|
| 311 |
+
"negative_prompt": self.negative_prompt,
|
| 312 |
+
"params": {
|
| 313 |
+
"model": self.model, "steps": self.steps,
|
| 314 |
+
"guidance": self.guidance,
|
| 315 |
+
"width": self.width, "height": self.height,
|
| 316 |
+
"seed": self.seed, "randomize": self.randomize,
|
| 317 |
+
},
|
| 318 |
+
"timing": {
|
| 319 |
+
"created_at": self.created_at,
|
| 320 |
+
"started_at": self.started_at or None,
|
| 321 |
+
"completed_at": self.completed_at or None,
|
| 322 |
+
"duration_s": (self.completed_at - self.started_at) if self.completed_at else None,
|
| 323 |
+
"queue_wait_s": (self.started_at - self.created_at) if self.started_at else None,
|
| 324 |
+
},
|
| 325 |
+
"workers_stats": {
|
| 326 |
+
"launched": self.workers_launched,
|
| 327 |
+
"dead": self.workers_dead,
|
| 328 |
+
"tokens_ok": self.tokens_ok,
|
| 329 |
+
},
|
| 330 |
+
}
|
| 331 |
+
if self.status == "success":
|
| 332 |
+
d["result"] = {
|
| 333 |
+
"filename": self.filename,
|
| 334 |
+
"file_size_bytes": self.file_size,
|
| 335 |
+
"seed": self.result_seed,
|
| 336 |
+
"hf_url": self.hf_url,
|
| 337 |
+
"local_url": self.local_url,
|
| 338 |
+
"winner_proxy": self.winner_proxy,
|
| 339 |
+
"winner_latency_ms": self.winner_latency_ms,
|
| 340 |
+
}
|
| 341 |
+
elif self.status == "failed":
|
| 342 |
+
d["error"] = self.error
|
| 343 |
+
return d
|
| 344 |
+
|
| 345 |
+
|
| 346 |
+
# Registry globale dei job
|
| 347 |
+
JOBS: dict[str, GenJob] = {}
|
| 348 |
+
JOBS_LOCK = threading.Lock()
|
| 349 |
+
USED_PROXIES: set = set()
|
| 350 |
+
USED_PROXIES_LOCK = threading.Lock()
|
| 351 |
+
|
| 352 |
+
|
| 353 |
+
def cleanup_old_jobs():
|
| 354 |
+
"""Rimuove job più vecchi di JOB_TTL_SECONDS."""
|
| 355 |
+
now = time.time()
|
| 356 |
+
with JOBS_LOCK:
|
| 357 |
+
to_del = [jid for jid, j in JOBS.items()
|
| 358 |
+
if (j.completed_at or j.created_at) < now - JOB_TTL_SECONDS]
|
| 359 |
+
for jid in to_del:
|
| 360 |
+
del JOBS[jid]
|
| 361 |
+
return len(to_del)
|
| 362 |
+
|
| 363 |
+
|
| 364 |
+
# ═══════════════════════════════════════════════════════════════
|
| 365 |
+
# WORKER (invariato dal client v9)
|
| 366 |
+
# ═════════════════════════════════════════════════════════════���═
|
| 367 |
+
|
| 368 |
+
def worker_thread(job: GenJob, worker_id: int, proxy: dict):
|
| 369 |
+
st = job.workers[worker_id]
|
| 370 |
+
st.proxy = proxy["url"]
|
| 371 |
+
st.total_steps = job.steps
|
| 372 |
+
|
| 373 |
+
if job.winner_event.is_set():
|
| 374 |
+
st.status = "killed"; return None
|
| 375 |
+
|
| 376 |
+
t_start = time.time()
|
| 377 |
+
ident = _new_identity()
|
| 378 |
+
proxies = {"http": proxy["url"], "https": proxy["url"]}
|
| 379 |
+
st.status = "connecting"
|
| 380 |
+
|
| 381 |
+
try:
|
| 382 |
+
s = cffi_requests.Session(impersonate=ident["impersonate"], proxies=proxies)
|
| 383 |
+
s.headers.update({"user-agent": ident["ua"], "accept-language": ident["accept_lang"]})
|
| 384 |
+
except Exception:
|
| 385 |
+
st.status = "dead"
|
| 386 |
+
with job.lock: job.workers_dead += 1
|
| 387 |
+
DB.mark_failure(proxy["url"], ban=True)
|
| 388 |
+
return None
|
| 389 |
+
|
| 390 |
+
try:
|
| 391 |
+
# TOKEN
|
| 392 |
+
try:
|
| 393 |
+
r = s.get(JWT_URL, headers={
|
| 394 |
+
"accept": "*/*", "referer": f"{HF_BASE}/spaces/{SPACE_ID}",
|
| 395 |
+
"origin": HF_BASE}, timeout=TOKEN_TIMEOUT)
|
| 396 |
+
if r.status_code != 200: raise Exception()
|
| 397 |
+
token = (r.json().get("token") or r.json().get("jwt"))
|
| 398 |
+
if not token: raise Exception()
|
| 399 |
+
except Exception:
|
| 400 |
+
st.status = "dead"
|
| 401 |
+
with job.lock: job.workers_dead += 1
|
| 402 |
+
DB.mark_failure(proxy["url"])
|
| 403 |
+
return None
|
| 404 |
+
|
| 405 |
+
if job.winner_event.is_set(): st.status = "killed"; return None
|
| 406 |
+
st.status = "token"
|
| 407 |
+
with job.lock: job.tokens_ok += 1
|
| 408 |
+
|
| 409 |
+
auth = {"accept": "*/*", "content-type": "application/json",
|
| 410 |
+
"origin": SPACE_BASE, "referer": f"{SPACE_BASE}/?__theme=system",
|
| 411 |
+
"x-gradio-server": f"{SPACE_BASE}/", "x-gradio-user": "app",
|
| 412 |
+
"x-zerogpu-token": token, "x-zerogpu-uuid": ident["zerogpu_uuid"]}
|
| 413 |
+
|
| 414 |
+
# RESOLUTION
|
| 415 |
+
sh1 = _rand_hash()
|
| 416 |
+
try:
|
| 417 |
+
s.post(f"{JOIN_URL}?__theme=system", json={
|
| 418 |
+
"data": [_resolution_label(job.width, job.height)],
|
| 419 |
+
"fn_index": FN_RESOLUTION, "trigger_id": 10, "session_hash": sh1,
|
| 420 |
+
}, headers=auth, timeout=8)
|
| 421 |
+
except Exception: pass
|
| 422 |
+
|
| 423 |
+
if job.winner_event.is_set(): st.status = "killed"; return None
|
| 424 |
+
|
| 425 |
+
# GENERATE
|
| 426 |
+
sh2 = _rand_hash()
|
| 427 |
+
try:
|
| 428 |
+
r = s.post(f"{JOIN_URL}?__theme=system", json={
|
| 429 |
+
"data": [job.prompt, job.negative_prompt or None, job.model,
|
| 430 |
+
job.steps, job.guidance, job.width, job.height,
|
| 431 |
+
job.seed, job.randomize],
|
| 432 |
+
"fn_index": FN_GENERATE, "trigger_id": 7, "session_hash": sh2,
|
| 433 |
+
}, headers=auth, timeout=10)
|
| 434 |
+
if r.status_code != 200:
|
| 435 |
+
st.status = "dead"
|
| 436 |
+
with job.lock: job.workers_dead += 1
|
| 437 |
+
DB.mark_failure(proxy["url"])
|
| 438 |
+
return None
|
| 439 |
+
except Exception:
|
| 440 |
+
st.status = "dead"
|
| 441 |
+
with job.lock: job.workers_dead += 1
|
| 442 |
+
DB.mark_failure(proxy["url"])
|
| 443 |
+
return None
|
| 444 |
+
|
| 445 |
+
st.status = "queued"; st.started_at = time.time()
|
| 446 |
+
|
| 447 |
+
if job.winner_event.is_set(): st.status = "killed"; return None
|
| 448 |
+
|
| 449 |
+
# STREAM
|
| 450 |
+
try:
|
| 451 |
+
stream_r = s.get(f"{DATA_URL}?session_hash={sh2}", headers={
|
| 452 |
+
"accept": "text/event-stream",
|
| 453 |
+
"referer": f"{SPACE_BASE}/?__theme=system",
|
| 454 |
+
"x-gradio-server": f"{SPACE_BASE}/"}, stream=True, timeout=STREAM_TIMEOUT)
|
| 455 |
+
if stream_r.status_code != 200:
|
| 456 |
+
st.status = "dead"; DB.mark_failure(proxy["url"]); return None
|
| 457 |
+
except Exception:
|
| 458 |
+
st.status = "dead"; DB.mark_failure(proxy["url"]); return None
|
| 459 |
+
|
| 460 |
+
st.status = "streaming"
|
| 461 |
+
|
| 462 |
+
for raw in stream_r.iter_lines():
|
| 463 |
+
if job.winner_event.is_set(): st.status = "killed"; return None
|
| 464 |
+
if not raw: continue
|
| 465 |
+
if isinstance(raw, bytes): raw = raw.decode("utf-8", errors="ignore")
|
| 466 |
+
if not raw.startswith("data:"): continue
|
| 467 |
+
body = raw[5:].strip()
|
| 468 |
+
if not body: continue
|
| 469 |
+
try: evt = json.loads(body)
|
| 470 |
+
except: continue
|
| 471 |
+
|
| 472 |
+
msg = evt.get("msg")
|
| 473 |
+
if msg == "progress":
|
| 474 |
+
pd = evt.get("progress_data") or []
|
| 475 |
+
if pd and pd[0].get("index") is not None:
|
| 476 |
+
st.step = pd[0]["index"] + 1
|
| 477 |
+
st.total_steps = pd[0]["length"]
|
| 478 |
+
|
| 479 |
+
elif msg == "process_completed":
|
| 480 |
+
out = evt.get("output", {})
|
| 481 |
+
if out.get("error") or not out.get("data"):
|
| 482 |
+
st.status = "dead"
|
| 483 |
+
with job.lock: job.workers_dead += 1
|
| 484 |
+
DB.mark_failure(proxy["url"])
|
| 485 |
+
return None
|
| 486 |
+
|
| 487 |
+
latency = (time.time() - t_start) * 1000
|
| 488 |
+
DB.mark_success(proxy["url"], latency)
|
| 489 |
+
with job.lock:
|
| 490 |
+
if not job.winner_event.is_set():
|
| 491 |
+
job.winner_event.set()
|
| 492 |
+
job.winner_proxy = proxy["url"]
|
| 493 |
+
job.winner_latency_ms = latency
|
| 494 |
+
st.status = "done"
|
| 495 |
+
return out
|
| 496 |
+
return None
|
| 497 |
+
|
| 498 |
+
elif msg in ("unexpected_error", "close_stream"):
|
| 499 |
+
st.status = "dead"; DB.mark_failure(proxy["url"])
|
| 500 |
+
return None
|
| 501 |
+
|
| 502 |
+
st.status = "dead"
|
| 503 |
+
DB.mark_failure(proxy["url"])
|
| 504 |
+
return None
|
| 505 |
+
except Exception:
|
| 506 |
+
st.status = "dead"
|
| 507 |
+
with job.lock: job.workers_dead += 1
|
| 508 |
+
DB.mark_failure(proxy["url"])
|
| 509 |
+
return None
|
| 510 |
+
finally:
|
| 511 |
+
try: s.close()
|
| 512 |
+
except: pass
|
| 513 |
+
|
| 514 |
+
|
| 515 |
+
def execute_job(job: GenJob, n_workers: int = DEFAULT_WORKERS,
|
| 516 |
+
base_url: str = ""):
|
| 517 |
+
"""Esegue un job completo. Chiamata sync bloccante."""
|
| 518 |
+
job.status = "running"
|
| 519 |
+
job.started_at = time.time()
|
| 520 |
+
|
| 521 |
+
# Prendi proxy escludendo quelli usati da altri job in corso
|
| 522 |
+
with USED_PROXIES_LOCK:
|
| 523 |
+
proxies = DB.get_ranked(limit=n_workers,
|
| 524 |
+
min_good_ratio=MIN_GOOD_RATIO,
|
| 525 |
+
exclude=USED_PROXIES)
|
| 526 |
+
for p in proxies:
|
| 527 |
+
USED_PROXIES.add(p["url"])
|
| 528 |
+
|
| 529 |
+
if not proxies:
|
| 530 |
+
job.status = "failed"
|
| 531 |
+
job.error = "No proxies available in pool. Try /admin/refresh"
|
| 532 |
+
job.completed_at = time.time()
|
| 533 |
+
return
|
| 534 |
+
|
| 535 |
+
n_workers = min(n_workers, len(proxies))
|
| 536 |
+
job.workers_launched = n_workers
|
| 537 |
+
for i in range(n_workers):
|
| 538 |
+
job.workers[i] = WorkerInfo(id=i, total_steps=job.steps)
|
| 539 |
+
|
| 540 |
+
try:
|
| 541 |
+
with ThreadPoolExecutor(max_workers=n_workers) as pool:
|
| 542 |
+
futures = []
|
| 543 |
+
for wave_start in range(0, n_workers, WAVE_SIZE):
|
| 544 |
+
if job.winner_event.is_set(): break
|
| 545 |
+
wave_end = min(wave_start + WAVE_SIZE, n_workers)
|
| 546 |
+
for i in range(wave_start, wave_end):
|
| 547 |
+
futures.append(pool.submit(worker_thread, job, i, proxies[i]))
|
| 548 |
+
time.sleep(WAVE_DELAY_MS / 1000)
|
| 549 |
+
|
| 550 |
+
for f in as_completed(futures):
|
| 551 |
+
result = f.result()
|
| 552 |
+
if result and result.get("data"):
|
| 553 |
+
data = result["data"]
|
| 554 |
+
img = data[0]
|
| 555 |
+
job.result_seed = data[1] if len(data) > 1 else 0
|
| 556 |
+
url = img.get("url") if isinstance(img, dict) else img
|
| 557 |
+
if not url.startswith("http"):
|
| 558 |
+
url = f"{SPACE_BASE}/gradio_api/file={url}"
|
| 559 |
+
job.hf_url = url
|
| 560 |
+
|
| 561 |
+
# Download nel filesystem locale
|
| 562 |
+
try:
|
| 563 |
+
filename = f"{job.job_id}.png"
|
| 564 |
+
file_path = IMAGES_DIR / filename
|
| 565 |
+
ident = _new_identity()
|
| 566 |
+
with cffi_requests.Session(impersonate=ident["impersonate"]) as s:
|
| 567 |
+
s.headers["user-agent"] = ident["ua"]
|
| 568 |
+
r = s.get(url, timeout=60)
|
| 569 |
+
r.raise_for_status()
|
| 570 |
+
file_path.write_bytes(r.content)
|
| 571 |
+
job.filename = filename
|
| 572 |
+
job.file_path = str(file_path.absolute())
|
| 573 |
+
job.file_size = len(r.content)
|
| 574 |
+
job.local_url = f"{base_url}/image/{filename}"
|
| 575 |
+
job.status = "success"
|
| 576 |
+
except Exception as e:
|
| 577 |
+
job.status = "failed"
|
| 578 |
+
job.error = f"download failed: {e}"
|
| 579 |
+
break
|
| 580 |
+
finally:
|
| 581 |
+
with USED_PROXIES_LOCK:
|
| 582 |
+
for p in proxies:
|
| 583 |
+
USED_PROXIES.discard(p["url"])
|
| 584 |
+
if job.status == "running":
|
| 585 |
+
job.status = "failed"
|
| 586 |
+
job.error = "No worker succeeded (all proxies failed or quota exceeded)"
|
| 587 |
+
job.completed_at = time.time()
|
| 588 |
+
|
| 589 |
+
|
| 590 |
+
def build_job(prompt: str, **kwargs) -> GenJob:
|
| 591 |
+
"""Factory di GenJob con validazioni."""
|
| 592 |
+
# Risoluzione: preset o custom
|
| 593 |
+
width = kwargs.get("width") or 1024
|
| 594 |
+
height = kwargs.get("height") or 1024
|
| 595 |
+
if kwargs.get("resolution"):
|
| 596 |
+
preset = kwargs["resolution"]
|
| 597 |
+
if preset not in RESOLUTIONS:
|
| 598 |
+
raise ValueError(f"resolution must be one of {list(RESOLUTIONS.keys())}")
|
| 599 |
+
width, height = RESOLUTIONS[preset]
|
| 600 |
+
|
| 601 |
+
# Validazione parametri (da schema /gradio_api/info)
|
| 602 |
+
model = kwargs.get("model", "Turbo")
|
| 603 |
+
if model not in ("Turbo", "Raw"):
|
| 604 |
+
raise ValueError("model must be 'Turbo' or 'Raw'")
|
| 605 |
+
|
| 606 |
+
steps = int(kwargs.get("steps", 8))
|
| 607 |
+
if not 1 <= steps <= 50:
|
| 608 |
+
raise ValueError("steps must be between 1 and 50")
|
| 609 |
+
|
| 610 |
+
guidance = float(kwargs.get("guidance", 0.0))
|
| 611 |
+
if not 0.0 <= guidance <= 10.0:
|
| 612 |
+
raise ValueError("guidance must be between 0.0 and 10.0")
|
| 613 |
+
|
| 614 |
+
if not 512 <= width <= 2048:
|
| 615 |
+
raise ValueError("width must be between 512 and 2048")
|
| 616 |
+
if not 512 <= height <= 2048:
|
| 617 |
+
raise ValueError("height must be between 512 and 2048")
|
| 618 |
+
|
| 619 |
+
seed = kwargs.get("seed")
|
| 620 |
+
randomize = seed is None or seed == 0
|
| 621 |
+
if not randomize:
|
| 622 |
+
seed = int(seed)
|
| 623 |
+
if not 0 <= seed <= 2147483647:
|
| 624 |
+
raise ValueError("seed must be between 0 and 2147483647")
|
| 625 |
+
else:
|
| 626 |
+
seed = random.randint(0, 2**31 - 1)
|
| 627 |
+
|
| 628 |
+
job_id = f"job_{int(time.time()*1000)}_{_rand_hash(6)}"
|
| 629 |
+
job = GenJob(
|
| 630 |
+
job_id=job_id,
|
| 631 |
+
prompt=prompt,
|
| 632 |
+
negative_prompt=kwargs.get("negative_prompt", ""),
|
| 633 |
+
model=model, steps=steps, guidance=guidance,
|
| 634 |
+
width=width, height=height,
|
| 635 |
+
seed=seed, randomize=randomize,
|
| 636 |
+
created_at=time.time(),
|
| 637 |
+
)
|
| 638 |
+
with JOBS_LOCK:
|
| 639 |
+
JOBS[job_id] = job
|
| 640 |
+
return job
|
| 641 |
+
|
| 642 |
+
|
| 643 |
+
# ═══════════════════════════════════════════════════════════════
|
| 644 |
+
# FASTAPI APP
|
| 645 |
+
# ═══════════════════════════════════════════════════════════════
|
| 646 |
+
|
| 647 |
+
app = FastAPI(
|
| 648 |
+
title="Krea-2 API",
|
| 649 |
+
description=(
|
| 650 |
+
"REST API for AI image generation via krea/Krea-2 HuggingFace Space.\n"
|
| 651 |
+
"Optimized for agentic AI tool-calling: all endpoints are GET, "
|
| 652 |
+
"structured JSON responses, OpenAPI schema available at /openapi.json"
|
| 653 |
+
),
|
| 654 |
+
version="1.0.0",
|
| 655 |
+
docs_url="/docs",
|
| 656 |
+
redoc_url="/redoc",
|
| 657 |
+
openapi_url="/openapi.json",
|
| 658 |
+
)
|
| 659 |
+
|
| 660 |
+
app.add_middleware(
|
| 661 |
+
CORSMiddleware,
|
| 662 |
+
allow_origins=["*"],
|
| 663 |
+
allow_methods=["*"],
|
| 664 |
+
allow_headers=["*"],
|
| 665 |
+
)
|
| 666 |
+
|
| 667 |
+
|
| 668 |
+
def get_base_url(request: Request) -> str:
|
| 669 |
+
"""Costruisce l'URL base per link assoluti."""
|
| 670 |
+
scheme = request.headers.get("x-forwarded-proto", request.url.scheme)
|
| 671 |
+
host = request.headers.get("host", request.url.hostname)
|
| 672 |
+
return f"{scheme}://{host}"
|
| 673 |
+
|
| 674 |
+
|
| 675 |
+
# ─── HOME / INFO ──────────────────────────────────────────────
|
| 676 |
+
|
| 677 |
+
@app.get("/", response_class=HTMLResponse)
|
| 678 |
+
def home(request: Request):
|
| 679 |
+
"""Landing page con documentazione HTML."""
|
| 680 |
+
base = get_base_url(request)
|
| 681 |
+
stats = DB.stats()
|
| 682 |
+
n_jobs = len(JOBS)
|
| 683 |
+
n_running = sum(1 for j in JOBS.values() if j.status == "running")
|
| 684 |
+
|
| 685 |
+
return f"""<!DOCTYPE html>
|
| 686 |
+
<html><head><title>Krea-2 API</title>
|
| 687 |
+
<style>
|
| 688 |
+
body{{font-family:system-ui,-apple-system,sans-serif;max-width:900px;
|
| 689 |
+
margin:2rem auto;padding:0 1rem;background:#0d0d1a;color:#e0e0f0;line-height:1.6}}
|
| 690 |
+
h1{{background:linear-gradient(90deg,#00ffdc,#ff50dc);-webkit-background-clip:text;
|
| 691 |
+
-webkit-text-fill-color:transparent;font-size:2.5rem;margin-bottom:0}}
|
| 692 |
+
.subtitle{{color:#a0a0c0;margin-top:0}}
|
| 693 |
+
code{{background:#1a1a2e;padding:.2rem .4rem;border-radius:3px;color:#00ffdc}}
|
| 694 |
+
pre{{background:#1a1a2e;padding:1rem;border-radius:6px;overflow-x:auto;
|
| 695 |
+
border-left:3px solid #ff50dc}}
|
| 696 |
+
.endpoint{{background:#151525;padding:.8rem 1rem;margin:.5rem 0;
|
| 697 |
+
border-radius:6px;border-left:3px solid #00ffdc}}
|
| 698 |
+
.method{{color:#00ffdc;font-weight:bold}}
|
| 699 |
+
a{{color:#ff50dc}}
|
| 700 |
+
.stats{{display:grid;grid-template-columns:repeat(4,1fr);gap:1rem;margin:1rem 0}}
|
| 701 |
+
.stat{{background:#151525;padding:1rem;border-radius:6px;text-align:center}}
|
| 702 |
+
.stat b{{font-size:1.8rem;display:block;color:#00ffdc}}
|
| 703 |
+
.badge{{display:inline-block;padding:.15rem .5rem;border-radius:3px;
|
| 704 |
+
font-size:.75rem;background:#00ffdc;color:#0d0d1a;font-weight:bold}}
|
| 705 |
+
</style></head><body>
|
| 706 |
+
<h1>⚡ Krea-2 API</h1>
|
| 707 |
+
<p class="subtitle">AI image generation as a service — anonymous, proxy-load-balanced, agent-ready</p>
|
| 708 |
+
|
| 709 |
+
<div class="stats">
|
| 710 |
+
<div class="stat"><b>{stats['total']}</b>proxies</div>
|
| 711 |
+
<div class="stat"><b>{stats['good']}</b>good</div>
|
| 712 |
+
<div class="stat"><b>{n_jobs}</b>total jobs</div>
|
| 713 |
+
<div class="stat"><b>{n_running}</b>running</div>
|
| 714 |
+
</div>
|
| 715 |
+
|
| 716 |
+
<h2>🔌 Quick Start</h2>
|
| 717 |
+
|
| 718 |
+
<div class="endpoint">
|
| 719 |
+
<span class="method">GET</span> <code>/generate?prompt=a+cyberpunk+cat</code>
|
| 720 |
+
<span class="badge">SYNC</span>
|
| 721 |
+
<p>Blocca fino al completamento (~5-30s), ritorna JSON con URL immagine.</p>
|
| 722 |
+
</div>
|
| 723 |
+
|
| 724 |
+
<div class="endpoint">
|
| 725 |
+
<span class="method">GET</span> <code>/generate?prompt=...&async=true</code>
|
| 726 |
+
<span class="badge">ASYNC</span>
|
| 727 |
+
<p>Ritorna subito <code>job_id</code>, poi polling con <code>/jobs/{{job_id}}</code>.</p>
|
| 728 |
+
</div>
|
| 729 |
+
|
| 730 |
+
<div class="endpoint">
|
| 731 |
+
<span class="method">GET</span> <code>/generate?prompt=...&n=5</code>
|
| 732 |
+
<span class="badge">BATCH</span>
|
| 733 |
+
<p>Genera N varianti in parallelo (max 10).</p>
|
| 734 |
+
</div>
|
| 735 |
+
|
| 736 |
+
<h2>📋 All Endpoints</h2>
|
| 737 |
+
<pre>GET / → this page
|
| 738 |
+
GET /health → health check
|
| 739 |
+
GET /docs → Swagger UI
|
| 740 |
+
GET /openapi.json → OpenAPI schema (for agents)
|
| 741 |
+
|
| 742 |
+
GET /generate → generate image(s)
|
| 743 |
+
GET /batch → batch from prompts list
|
| 744 |
+
GET /jobs → list all jobs
|
| 745 |
+
GET /jobs/{{job_id}} → job status
|
| 746 |
+
GET /jobs/{{job_id}}/result → job result (waits if running)
|
| 747 |
+
GET /image/{{filename}} → download generated PNG
|
| 748 |
+
|
| 749 |
+
GET /stats → proxy pool stats
|
| 750 |
+
GET /admin/refresh → refresh proxy pool
|
| 751 |
+
GET /admin/cleanup → cleanup old jobs</pre>
|
| 752 |
+
|
| 753 |
+
<h2>🎯 Example — Agent tool call</h2>
|
| 754 |
+
<pre>curl "{base}/generate?prompt=sunset&model=Turbo&steps=8"</pre>
|
| 755 |
+
|
| 756 |
+
<pre>{{
|
| 757 |
+
"job_id": "job_1734567890123_abc123",
|
| 758 |
+
"status": "success",
|
| 759 |
+
"prompt": "sunset",
|
| 760 |
+
"result": {{
|
| 761 |
+
"filename": "job_1734567890123_abc123.png",
|
| 762 |
+
"local_url": "{base}/image/job_1734567890123_abc123.png",
|
| 763 |
+
"hf_url": "https://krea-krea-2.hf.space/...",
|
| 764 |
+
"seed": 823641299
|
| 765 |
+
}}
|
| 766 |
+
}}</pre>
|
| 767 |
+
|
| 768 |
+
<p style="margin-top:2rem;color:#666;text-align:center">
|
| 769 |
+
<a href="/docs">Swagger docs</a> · <a href="/openapi.json">OpenAPI</a> · <a href="/stats">Stats</a>
|
| 770 |
+
</p>
|
| 771 |
+
</body></html>
|
| 772 |
+
"""
|
| 773 |
+
|
| 774 |
+
|
| 775 |
+
@app.get("/health")
|
| 776 |
+
def health():
|
| 777 |
+
"""Health check per load balancer / orchestrator."""
|
| 778 |
+
return {
|
| 779 |
+
"status": "ok",
|
| 780 |
+
"service": "krea-2-api",
|
| 781 |
+
"version": "1.0.0",
|
| 782 |
+
"timestamp": datetime.utcnow().isoformat() + "Z",
|
| 783 |
+
"uptime_s": time.time() - START_TIME,
|
| 784 |
+
}
|
| 785 |
+
|
| 786 |
+
|
| 787 |
+
# ─── GENERATE (main endpoint) ──────────────────────────────────
|
| 788 |
+
|
| 789 |
+
@app.get("/generate")
|
| 790 |
+
def generate(
|
| 791 |
+
request: Request,
|
| 792 |
+
background_tasks: BackgroundTasks,
|
| 793 |
+
prompt: str = Query(..., description="Text prompt to generate image from",
|
| 794 |
+
min_length=1, max_length=2000, example="a cyberpunk cat"),
|
| 795 |
+
negative_prompt: str = Query("", description="What to avoid in generation"),
|
| 796 |
+
model: Literal["Turbo", "Raw"] = Query("Turbo",
|
| 797 |
+
description="Turbo = fast (8 steps), Raw = quality (more steps)"),
|
| 798 |
+
steps: int = Query(8, ge=1, le=50,
|
| 799 |
+
description="Denoising steps (Turbo: 4-8, Raw: 20-50)"),
|
| 800 |
+
guidance: float = Query(0.0, ge=0.0, le=10.0,
|
| 801 |
+
description="Classifier-free guidance scale"),
|
| 802 |
+
width: int = Query(1024, ge=512, le=2048),
|
| 803 |
+
height: int = Query(1024, ge=512, le=2048),
|
| 804 |
+
resolution: Optional[Literal["square","portrait","landscape","square2k"]] = Query(
|
| 805 |
+
None, description="Preset resolution (overrides width/height)"),
|
| 806 |
+
seed: int = Query(0, ge=0, le=2147483647,
|
| 807 |
+
description="Random seed (0 = random for each generation)"),
|
| 808 |
+
n: int = Query(1, ge=1, le=MAX_PARALLEL_JOBS,
|
| 809 |
+
description=f"Number of images to generate in parallel (max {MAX_PARALLEL_JOBS})"),
|
| 810 |
+
async_mode: bool = Query(False, alias="async",
|
| 811 |
+
description="If true, return job_id immediately; poll /jobs/{id} for result"),
|
| 812 |
+
workers: int = Query(DEFAULT_WORKERS, ge=10, le=200,
|
| 813 |
+
description="Proxy race workers per job (higher = faster but heavier)"),
|
| 814 |
+
):
|
| 815 |
+
"""
|
| 816 |
+
**Generate 1 or more AI images from a text prompt.**
|
| 817 |
+
|
| 818 |
+
Ideal for agentic AI tools. Two modes:
|
| 819 |
+
- `async=false` (default): wait for completion, return full result
|
| 820 |
+
- `async=true`: return `job_id`, poll `/jobs/{job_id}` for status
|
| 821 |
+
|
| 822 |
+
Batch: use `n=5` to generate 5 variants of the same prompt in parallel.
|
| 823 |
+
"""
|
| 824 |
+
base_url = get_base_url(request)
|
| 825 |
+
|
| 826 |
+
try:
|
| 827 |
+
jobs = []
|
| 828 |
+
for i in range(n):
|
| 829 |
+
job = build_job(
|
| 830 |
+
prompt=prompt, negative_prompt=negative_prompt,
|
| 831 |
+
model=model, steps=steps, guidance=guidance,
|
| 832 |
+
width=width, height=height,
|
| 833 |
+
resolution=resolution,
|
| 834 |
+
seed=(seed if seed > 0 else None),
|
| 835 |
+
)
|
| 836 |
+
jobs.append(job)
|
| 837 |
+
except ValueError as e:
|
| 838 |
+
raise HTTPException(status_code=400, detail=str(e))
|
| 839 |
+
|
| 840 |
+
if async_mode:
|
| 841 |
+
# Lancia in background, ritorna subito i job_ids
|
| 842 |
+
for job in jobs:
|
| 843 |
+
background_tasks.add_task(execute_job, job, workers, base_url)
|
| 844 |
+
return JSONResponse({
|
| 845 |
+
"status": "queued",
|
| 846 |
+
"job_ids": [j.job_id for j in jobs],
|
| 847 |
+
"count": len(jobs),
|
| 848 |
+
"poll_urls": [f"{base_url}/jobs/{j.job_id}" for j in jobs],
|
| 849 |
+
"message": f"Poll GET /jobs/{{job_id}} to check status",
|
| 850 |
+
})
|
| 851 |
+
|
| 852 |
+
# Sync mode: aspetta tutti
|
| 853 |
+
if len(jobs) == 1:
|
| 854 |
+
execute_job(jobs[0], workers, base_url)
|
| 855 |
+
job = jobs[0]
|
| 856 |
+
if job.status == "success":
|
| 857 |
+
return job.to_public_dict()
|
| 858 |
+
raise HTTPException(status_code=500, detail=job.to_public_dict())
|
| 859 |
+
|
| 860 |
+
# Batch sync
|
| 861 |
+
with ThreadPoolExecutor(max_workers=n) as pool:
|
| 862 |
+
futs = {pool.submit(execute_job, j, workers, base_url): j for j in jobs}
|
| 863 |
+
for f in as_completed(futs): pass
|
| 864 |
+
|
| 865 |
+
results = [j.to_public_dict() for j in jobs]
|
| 866 |
+
n_ok = sum(1 for j in jobs if j.status == "success")
|
| 867 |
+
return {
|
| 868 |
+
"status": "batch_complete",
|
| 869 |
+
"total": len(jobs),
|
| 870 |
+
"success": n_ok,
|
| 871 |
+
"failed": len(jobs) - n_ok,
|
| 872 |
+
"results": results,
|
| 873 |
+
}
|
| 874 |
+
|
| 875 |
+
|
| 876 |
+
@app.get("/batch")
|
| 877 |
+
def batch(
|
| 878 |
+
request: Request,
|
| 879 |
+
background_tasks: BackgroundTasks,
|
| 880 |
+
prompts: str = Query(...,
|
| 881 |
+
description="Prompts separated by '|' (max 10)",
|
| 882 |
+
example="a cat|a dog|a bird"),
|
| 883 |
+
model: Literal["Turbo", "Raw"] = Query("Turbo"),
|
| 884 |
+
steps: int = Query(8, ge=1, le=50),
|
| 885 |
+
guidance: float = Query(0.0, ge=0.0, le=10.0),
|
| 886 |
+
resolution: Optional[Literal["square","portrait","landscape","square2k"]] = None,
|
| 887 |
+
async_mode: bool = Query(False, alias="async"),
|
| 888 |
+
workers: int = Query(DEFAULT_WORKERS, ge=10, le=200),
|
| 889 |
+
):
|
| 890 |
+
"""
|
| 891 |
+
**Batch generation** — different prompts in parallel.
|
| 892 |
+
|
| 893 |
+
Separate prompts with `|` character.
|
| 894 |
+
"""
|
| 895 |
+
prompt_list = [p.strip() for p in prompts.split("|") if p.strip()]
|
| 896 |
+
if not prompt_list:
|
| 897 |
+
raise HTTPException(400, "No valid prompts provided")
|
| 898 |
+
if len(prompt_list) > MAX_PARALLEL_JOBS:
|
| 899 |
+
raise HTTPException(400, f"Max {MAX_PARALLEL_JOBS} prompts per batch")
|
| 900 |
+
|
| 901 |
+
base_url = get_base_url(request)
|
| 902 |
+
|
| 903 |
+
try:
|
| 904 |
+
jobs = [build_job(
|
| 905 |
+
prompt=p, model=model, steps=steps, guidance=guidance,
|
| 906 |
+
resolution=resolution
|
| 907 |
+
) for p in prompt_list]
|
| 908 |
+
except ValueError as e:
|
| 909 |
+
raise HTTPException(400, str(e))
|
| 910 |
+
|
| 911 |
+
if async_mode:
|
| 912 |
+
for job in jobs:
|
| 913 |
+
background_tasks.add_task(execute_job, job, workers, base_url)
|
| 914 |
+
return {
|
| 915 |
+
"status": "queued",
|
| 916 |
+
"count": len(jobs),
|
| 917 |
+
"job_ids": [j.job_id for j in jobs],
|
| 918 |
+
"poll_urls": [f"{base_url}/jobs/{j.job_id}" for j in jobs],
|
| 919 |
+
}
|
| 920 |
+
|
| 921 |
+
with ThreadPoolExecutor(max_workers=len(jobs)) as pool:
|
| 922 |
+
futs = {pool.submit(execute_job, j, workers, base_url): j for j in jobs}
|
| 923 |
+
for f in as_completed(futs): pass
|
| 924 |
+
|
| 925 |
+
return {
|
| 926 |
+
"status": "batch_complete",
|
| 927 |
+
"total": len(jobs),
|
| 928 |
+
"success": sum(1 for j in jobs if j.status == "success"),
|
| 929 |
+
"results": [j.to_public_dict() for j in jobs],
|
| 930 |
+
}
|
| 931 |
+
|
| 932 |
+
|
| 933 |
+
# ─── JOBS ────────────────────────────────────────────────────
|
| 934 |
+
|
| 935 |
+
@app.get("/jobs")
|
| 936 |
+
def list_jobs(
|
| 937 |
+
status: Optional[Literal["pending","running","success","failed"]] = None,
|
| 938 |
+
limit: int = Query(50, ge=1, le=500),
|
| 939 |
+
):
|
| 940 |
+
"""List all jobs (optionally filtered by status)."""
|
| 941 |
+
with JOBS_LOCK:
|
| 942 |
+
items = list(JOBS.values())
|
| 943 |
+
items.sort(key=lambda j: j.created_at, reverse=True)
|
| 944 |
+
if status:
|
| 945 |
+
items = [j for j in items if j.status == status]
|
| 946 |
+
items = items[:limit]
|
| 947 |
+
return {
|
| 948 |
+
"total": len(JOBS),
|
| 949 |
+
"returned": len(items),
|
| 950 |
+
"jobs": [j.to_public_dict() for j in items],
|
| 951 |
+
}
|
| 952 |
+
|
| 953 |
+
|
| 954 |
+
@app.get("/jobs/{job_id}")
|
| 955 |
+
def get_job(job_id: str):
|
| 956 |
+
"""Get status/details of a single job by ID."""
|
| 957 |
+
with JOBS_LOCK:
|
| 958 |
+
job = JOBS.get(job_id)
|
| 959 |
+
if not job:
|
| 960 |
+
raise HTTPException(404, f"Job {job_id} not found")
|
| 961 |
+
return job.to_public_dict()
|
| 962 |
+
|
| 963 |
+
|
| 964 |
+
@app.get("/jobs/{job_id}/result")
|
| 965 |
+
def get_job_result(
|
| 966 |
+
job_id: str,
|
| 967 |
+
wait: bool = Query(True, description="Block until job completes (max 90s)"),
|
| 968 |
+
timeout: int = Query(90, ge=1, le=300),
|
| 969 |
+
):
|
| 970 |
+
"""
|
| 971 |
+
Get the result of a job. If `wait=true` and job is still running,
|
| 972 |
+
blocks until completion or timeout.
|
| 973 |
+
"""
|
| 974 |
+
with JOBS_LOCK:
|
| 975 |
+
job = JOBS.get(job_id)
|
| 976 |
+
if not job:
|
| 977 |
+
raise HTTPException(404, f"Job {job_id} not found")
|
| 978 |
+
|
| 979 |
+
if wait and job.status in ("pending", "running"):
|
| 980 |
+
deadline = time.time() + timeout
|
| 981 |
+
while time.time() < deadline and job.status in ("pending", "running"):
|
| 982 |
+
time.sleep(0.5)
|
| 983 |
+
if job.status in ("pending", "running"):
|
| 984 |
+
raise HTTPException(408, "Timeout waiting for job")
|
| 985 |
+
|
| 986 |
+
if job.status == "success":
|
| 987 |
+
return job.to_public_dict()
|
| 988 |
+
raise HTTPException(500, job.to_public_dict())
|
| 989 |
+
|
| 990 |
+
|
| 991 |
+
# ─── IMAGE DOWNLOAD ──────────────────────────────────────────
|
| 992 |
+
|
| 993 |
+
@app.get("/image/{filename}")
|
| 994 |
+
def get_image(filename: str):
|
| 995 |
+
"""Download a generated image PNG."""
|
| 996 |
+
# sanitize: solo nomi tipo job_xxx.png
|
| 997 |
+
if not re.match(r"^job_[a-zA-Z0-9_]+\.png$", filename):
|
| 998 |
+
raise HTTPException(400, "Invalid filename")
|
| 999 |
+
file_path = IMAGES_DIR / filename
|
| 1000 |
+
if not file_path.exists():
|
| 1001 |
+
raise HTTPException(404, "Image not found (may have expired)")
|
| 1002 |
+
return FileResponse(file_path, media_type="image/png", filename=filename)
|
| 1003 |
+
|
| 1004 |
+
|
| 1005 |
+
# ─── STATS & ADMIN ───────────────────────────────────────────
|
| 1006 |
+
|
| 1007 |
+
@app.get("/stats")
|
| 1008 |
+
def stats():
|
| 1009 |
+
"""Statistics: proxy DB + jobs registry."""
|
| 1010 |
+
db_stats = DB.stats()
|
| 1011 |
+
with JOBS_LOCK:
|
| 1012 |
+
job_stats = {
|
| 1013 |
+
"total": len(JOBS),
|
| 1014 |
+
"pending": sum(1 for j in JOBS.values() if j.status == "pending"),
|
| 1015 |
+
"running": sum(1 for j in JOBS.values() if j.status == "running"),
|
| 1016 |
+
"success": sum(1 for j in JOBS.values() if j.status == "success"),
|
| 1017 |
+
"failed": sum(1 for j in JOBS.values() if j.status == "failed"),
|
| 1018 |
+
}
|
| 1019 |
+
return {
|
| 1020 |
+
"proxy_db": db_stats,
|
| 1021 |
+
"jobs": job_stats,
|
| 1022 |
+
"used_proxies_now": len(USED_PROXIES),
|
| 1023 |
+
"uptime_s": time.time() - START_TIME,
|
| 1024 |
+
}
|
| 1025 |
+
|
| 1026 |
+
|
| 1027 |
+
@app.get("/admin/refresh")
|
| 1028 |
+
def admin_refresh():
|
| 1029 |
+
"""Force refresh of proxy pool from ProxyScrape."""
|
| 1030 |
+
n = refresh_proxy_pool()
|
| 1031 |
+
return {"status": "ok", "new_proxies_fetched": n, "db_stats": DB.stats()}
|
| 1032 |
+
|
| 1033 |
+
|
| 1034 |
+
@app.get("/admin/cleanup")
|
| 1035 |
+
def admin_cleanup():
|
| 1036 |
+
"""Clean up old jobs from registry."""
|
| 1037 |
+
n = cleanup_old_jobs()
|
| 1038 |
+
return {"status": "ok", "jobs_removed": n, "jobs_remaining": len(JOBS)}
|
| 1039 |
+
|
| 1040 |
+
|
| 1041 |
+
# ═══════════════════════════════════════════════════════════════
|
| 1042 |
+
# OPENAPI TOOL SCHEMA (per agent AI)
|
| 1043 |
+
# ═══════════════════════════════════════════════════════════════
|
| 1044 |
+
|
| 1045 |
+
@app.get("/tool-schema")
|
| 1046 |
+
def tool_schema(request: Request):
|
| 1047 |
+
"""
|
| 1048 |
+
OpenAI/Anthropic-compatible tool schema for agent integration.
|
| 1049 |
+
Copy this JSON directly into your agent's tool definitions.
|
| 1050 |
+
"""
|
| 1051 |
+
base = get_base_url(request)
|
| 1052 |
+
return {
|
| 1053 |
+
"openai_function_call": {
|
| 1054 |
+
"type": "function",
|
| 1055 |
+
"function": {
|
| 1056 |
+
"name": "generate_image",
|
| 1057 |
+
"description": (
|
| 1058 |
+
"Generate an AI image from a text description using Krea-2 model. "
|
| 1059 |
+
"Returns a URL where the image can be downloaded. "
|
| 1060 |
+
"Fast (5-30 seconds), no login required."
|
| 1061 |
+
),
|
| 1062 |
+
"parameters": {
|
| 1063 |
+
"type": "object",
|
| 1064 |
+
"properties": {
|
| 1065 |
+
"prompt": {"type": "string",
|
| 1066 |
+
"description": "Detailed text description of the image to generate"},
|
| 1067 |
+
"model": {"type": "string", "enum": ["Turbo", "Raw"],
|
| 1068 |
+
"description": "Turbo=fast, Raw=high quality", "default": "Turbo"},
|
| 1069 |
+
"steps": {"type": "integer", "minimum": 1, "maximum": 50,
|
| 1070 |
+
"description": "Denoising steps", "default": 8},
|
| 1071 |
+
"resolution": {"type": "string",
|
| 1072 |
+
"enum": ["square", "portrait", "landscape", "square2k"],
|
| 1073 |
+
"default": "square"},
|
| 1074 |
+
"n": {"type": "integer", "minimum": 1, "maximum": 10,
|
| 1075 |
+
"description": "Number of variants", "default": 1},
|
| 1076 |
+
},
|
| 1077 |
+
"required": ["prompt"],
|
| 1078 |
+
},
|
| 1079 |
+
},
|
| 1080 |
+
"api_endpoint": f"GET {base}/generate",
|
| 1081 |
+
},
|
| 1082 |
+
"anthropic_tool": {
|
| 1083 |
+
"name": "generate_image",
|
| 1084 |
+
"description": "Generate AI images from text prompts via Krea-2",
|
| 1085 |
+
"input_schema": {
|
| 1086 |
+
"type": "object",
|
| 1087 |
+
"properties": {
|
| 1088 |
+
"prompt": {"type": "string"},
|
| 1089 |
+
"model": {"type": "string", "enum": ["Turbo", "Raw"]},
|
| 1090 |
+
"steps": {"type": "integer"},
|
| 1091 |
+
"resolution": {"type": "string",
|
| 1092 |
+
"enum": ["square", "portrait", "landscape", "square2k"]},
|
| 1093 |
+
"n": {"type": "integer"},
|
| 1094 |
+
},
|
| 1095 |
+
"required": ["prompt"],
|
| 1096 |
+
},
|
| 1097 |
+
"api_endpoint": f"GET {base}/generate",
|
| 1098 |
+
},
|
| 1099 |
+
"langchain_tool_example": (
|
| 1100 |
+
"from langchain.tools import tool\n"
|
| 1101 |
+
"import requests\n\n"
|
| 1102 |
+
"@tool\n"
|
| 1103 |
+
"def generate_image(prompt: str, n: int = 1) -> str:\n"
|
| 1104 |
+
" '''Generate AI images. Returns URLs.'''\n"
|
| 1105 |
+
f" r = requests.get('{base}/generate', params={{'prompt': prompt, 'n': n}})\n"
|
| 1106 |
+
" data = r.json()\n"
|
| 1107 |
+
" if data['status'] == 'success':\n"
|
| 1108 |
+
" return data['result']['local_url']\n"
|
| 1109 |
+
" return [j['result']['local_url'] for j in data['results']]"
|
| 1110 |
+
),
|
| 1111 |
+
}
|
| 1112 |
+
|
| 1113 |
+
|
| 1114 |
+
# ════════════════════════════���══════════════════════════════════
|
| 1115 |
+
# STARTUP
|
| 1116 |
+
# ═══════════════════════════════════════════════════════════════
|
| 1117 |
+
|
| 1118 |
+
START_TIME = time.time()
|
| 1119 |
+
|
| 1120 |
+
|
| 1121 |
+
@app.on_event("startup")
|
| 1122 |
+
async def startup():
|
| 1123 |
+
"""Refresh iniziale del pool proxy."""
|
| 1124 |
+
print(f"\n{'='*60}")
|
| 1125 |
+
print(f" ⚡ KREA-2 API STARTING")
|
| 1126 |
+
print(f"{'='*60}")
|
| 1127 |
+
print(f" → Refreshing proxy pool from ProxyScrape...")
|
| 1128 |
+
n = refresh_proxy_pool()
|
| 1129 |
+
s = DB.stats()
|
| 1130 |
+
print(f" → {n} new proxies fetched")
|
| 1131 |
+
print(f" → DB: {s['total']} total ({s['good']} good, {s['banned']} banned)")
|
| 1132 |
+
print(f" → Images directory: {IMAGES_DIR.absolute()}")
|
| 1133 |
+
print(f" → Ready ✓\n")
|
| 1134 |
+
|
| 1135 |
+
# Cleanup periodico background
|
| 1136 |
+
async def periodic_cleanup():
|
| 1137 |
+
while True:
|
| 1138 |
+
await asyncio.sleep(600) # ogni 10min
|
| 1139 |
+
n = cleanup_old_jobs()
|
| 1140 |
+
if n > 0:
|
| 1141 |
+
print(f" [cleanup] removed {n} old jobs")
|
| 1142 |
+
|
| 1143 |
+
asyncio.create_task(periodic_cleanup())
|
| 1144 |
+
|
| 1145 |
+
|
| 1146 |
+
# ═══════════════════════════════════════════════════════════════
|
| 1147 |
+
# MAIN
|
| 1148 |
+
# ═══════════════════════════════════════════════════════════════
|
| 1149 |
+
|
| 1150 |
+
def main():
|
| 1151 |
+
parser = argparse.ArgumentParser(description="Krea-2 REST API server")
|
| 1152 |
+
parser.add_argument("--host", default="0.0.0.0", help="Bind host")
|
| 1153 |
+
parser.add_argument("--port", type=int, default=7860, help="Bind port")
|
| 1154 |
+
parser.add_argument("--reload", action="store_true", help="Auto-reload on changes")
|
| 1155 |
+
parser.add_argument("--workers", type=int, default=1,
|
| 1156 |
+
help="Uvicorn workers (>1 disabilita stato in-memory condiviso)")
|
| 1157 |
+
args = parser.parse_args()
|
| 1158 |
+
|
| 1159 |
+
if os.name == "nt": os.system("")
|
| 1160 |
+
|
| 1161 |
+
print(f"\n Starting on http://{args.host}:{args.port}")
|
| 1162 |
+
print(f" Docs: http://{args.host}:{args.port}/docs")
|
| 1163 |
+
print(f" OpenAPI: http://{args.host}:{args.port}/openapi.json\n")
|
| 1164 |
+
|
| 1165 |
+
uvicorn.run(
|
| 1166 |
+
"api:app" if args.reload else app,
|
| 1167 |
+
host=args.host,
|
| 1168 |
+
port=args.port,
|
| 1169 |
+
reload=args.reload,
|
| 1170 |
+
workers=args.workers if not args.reload else 1,
|
| 1171 |
+
log_level="info",
|
| 1172 |
+
)
|
| 1173 |
+
|
| 1174 |
+
|
| 1175 |
+
if __name__ == "__main__":
|
| 1176 |
+
import os
|
| 1177 |
+
if os.name == "nt": os.system("")
|
| 1178 |
+
uvicorn.run(app, host="0.0.0.0", port=7860, log_level="info")
|
requirements.txt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
curl_cffi>=0.7.0
|
| 2 |
+
fastapi>=0.104.0
|
| 3 |
+
uvicorn[standard]>=0.24.0
|