bep40 commited on
Commit
7e1f5dc
·
verified ·
1 Parent(s): e3acd20

Add FastAPI backend proxy

Browse files
Files changed (1) hide show
  1. app.py +212 -0
app.py ADDED
@@ -0,0 +1,212 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import base64
3
+ import io
4
+ import json
5
+ from typing import Optional
6
+ from fastapi import FastAPI, HTTPException, Request, Response
7
+ from fastapi.staticfiles import StaticFiles
8
+ from fastapi.middleware.cors import CORSMiddleware
9
+ from pydantic import BaseModel
10
+ import httpx
11
+ import asyncio
12
+
13
+ HF_TOKEN = os.environ.get("HF_TOKEN", "")
14
+ if not HF_TOKEN:
15
+ print("WARNING: HF_TOKEN not set! AI features will fail.")
16
+
17
+ app = FastAPI(title="Comic AI Generator", version="2.0")
18
+
19
+ # Mount static files (the frontend)
20
+ app.mount("/", StaticFiles(directory="static", html=True), name="static")
21
+
22
+ # ================= MODELS =================
23
+ class TextGenRequest(BaseModel):
24
+ model: str
25
+ prompt: str
26
+ max_new_tokens: int = 512
27
+ temperature: float = 0.7
28
+
29
+ class ChatRequest(BaseModel):
30
+ model: str
31
+ messages: list
32
+ max_tokens: int = 1024
33
+ temperature: float = 0.3
34
+
35
+ class ImageGenRequest(BaseModel):
36
+ model: str
37
+ prompt: str
38
+ negative_prompt: str = ""
39
+ width: int = 1024
40
+ height: int = 1024
41
+
42
+ class InpaintRequest(BaseModel):
43
+ model: str
44
+ prompt: str
45
+ image_base64: str
46
+ mask_base64: str
47
+ negative_prompt: str = ""
48
+ width: int = 1024
49
+ height: int = 1024
50
+
51
+ class TTSRequest(BaseModel):
52
+ model: str
53
+ text: str
54
+
55
+ # ================= PROXY HELPERS =================
56
+ async def hf_api_request(url: str, payload: dict):
57
+ headers = {
58
+ "Authorization": f"Bearer {HF_TOKEN}",
59
+ "Content-Type": "application/json"
60
+ }
61
+ for attempt in range(3):
62
+ try:
63
+ async with httpx.AsyncClient(timeout=120.0) as client:
64
+ response = await client.post(url, json=payload, headers=headers, timeout=120)
65
+ if response.status_code == 200:
66
+ return response.json()
67
+ if response.status_code == 503 and attempt < 2:
68
+ print(f"Model loading (503), retrying in 15s...")
69
+ await asyncio.sleep(15)
70
+ continue
71
+ error_text = response.text[:500]
72
+ print(f"HF API Error {response.status_code}: {error_text}")
73
+ raise HTTPException(status_code=response.status_code, detail=f"HF API Error {response.status_code}: {error_text}")
74
+ except httpx.RequestError as e:
75
+ print(f"Request error attempt {attempt+1}: {e}")
76
+ if attempt == 2:
77
+ raise HTTPException(status_code=500, detail=f"Network error: {str(e)}")
78
+ await asyncio.sleep(5)
79
+
80
+ async def hf_binary_request(url: str, payload: dict):
81
+ headers = {
82
+ "Authorization": f"Bearer {HF_TOKEN}",
83
+ "Content-Type": "application/json"
84
+ }
85
+ for attempt in range(3):
86
+ try:
87
+ async with httpx.AsyncClient(timeout=120.0) as client:
88
+ response = await client.post(url, json=payload, headers=headers, timeout=120)
89
+ if response.status_code == 200:
90
+ return Response(content=response.content, media_type="image/png")
91
+ if response.status_code == 503 and attempt < 2:
92
+ await asyncio.sleep(15)
93
+ continue
94
+ raise HTTPException(status_code=response.status_code, detail=f"Image API Error {response.status_code}")
95
+ except httpx.RequestError as e:
96
+ if attempt == 2:
97
+ raise HTTPException(status_code=500, detail=str(e))
98
+ await asyncio.sleep(5)
99
+
100
+ # ================= ENDPOINTS =================
101
+ @app.post("/api/text")
102
+ async def generate_text(req: TextGenRequest):
103
+ url = f"https://api-inference.huggingface.co/models/{req.model}"
104
+ payload = {
105
+ "inputs": req.prompt,
106
+ "parameters": {
107
+ "max_new_tokens": req.max_new_tokens,
108
+ "temperature": req.temperature,
109
+ "return_full_text": False
110
+ }
111
+ }
112
+ result = await hf_api_request(url, payload)
113
+ return result
114
+
115
+ @app.post("/api/chat")
116
+ async def chat(req: ChatRequest):
117
+ url = "https://router.huggingface.co/v1/chat/completions"
118
+ payload = {
119
+ "model": req.model,
120
+ "messages": req.messages,
121
+ "max_tokens": req.max_tokens,
122
+ "temperature": req.temperature
123
+ }
124
+ result = await hf_api_request(url, payload)
125
+ return result
126
+
127
+ @app.post("/api/image")
128
+ async def generate_image(req: ImageGenRequest):
129
+ url = f"https://api-inference.huggingface.co/models/{req.model}"
130
+ payload = {
131
+ "inputs": req.prompt,
132
+ "parameters": {
133
+ "negative_prompt": req.negative_prompt,
134
+ "width": req.width,
135
+ "height": req.height,
136
+ "num_inference_steps": 30,
137
+ "guidance_scale": 7.5
138
+ }
139
+ }
140
+ return await hf_binary_request(url, payload)
141
+
142
+ @app.post("/api/inpaint")
143
+ async def inpaint_image(req: InpaintRequest):
144
+ url = f"https://api-inference.huggingface.co/models/{req.model}"
145
+ payload = {
146
+ "inputs": req.prompt,
147
+ "parameters": {
148
+ "image": f"data:image/png;base64,{req.image_base64}",
149
+ "mask": f"data:image/png;base64,{req.mask_base64}",
150
+ "negative_prompt": req.negative_prompt,
151
+ "num_inference_steps": 30,
152
+ "guidance_scale": 7.5,
153
+ "width": req.width,
154
+ "height": req.height
155
+ }
156
+ }
157
+ return await hf_binary_request(url, payload)
158
+
159
+ @app.post("/api/tts")
160
+ async def text_to_speech(req: TTSRequest):
161
+ url = f"https://api-inference.huggingface.co/models/{req.model}"
162
+ payload = {"inputs": req.text}
163
+
164
+ for attempt in range(3):
165
+ try:
166
+ async with httpx.AsyncClient(timeout=60.0) as client:
167
+ response = await client.post(url, json=payload,
168
+ headers={"Authorization": f"Bearer {HF_TOKEN}", "Content-Type": "application/json"},
169
+ timeout=60
170
+ )
171
+ if response.status_code == 200:
172
+ return Response(content=response.content, media_type="audio/wav")
173
+ if response.status_code == 503 and attempt < 2:
174
+ await asyncio.sleep(10)
175
+ continue
176
+ raise HTTPException(status_code=response.status_code, detail=f"TTS error: {response.status_code}")
177
+ except Exception as e:
178
+ if attempt == 2:
179
+ raise HTTPException(status_code=500, detail=str(e))
180
+ await asyncio.sleep(3)
181
+
182
+ @app.get("/api/health")
183
+ async def health_check():
184
+ if not HF_TOKEN:
185
+ return {"status": "error", "message": "HF_TOKEN not configured in backend"}
186
+ try:
187
+ async with httpx.AsyncClient(timeout=30.0) as client:
188
+ response = await client.post(
189
+ "https://api-inference.huggingface.co/models/Qwen/Qwen3-0.6B",
190
+ json={"inputs": "Hi", "parameters": {"max_new_tokens": 3}},
191
+ headers={"Authorization": f"Bearer {HF_TOKEN}"},
192
+ timeout=30
193
+ )
194
+ if response.status_code == 200:
195
+ return {"status": "ok", "message": "API connected successfully"}
196
+ elif response.status_code == 503:
197
+ return {"status": "loading", "message": "Model is warming up, please wait ~1 min"}
198
+ else:
199
+ return {"status": "error", "message": f"HF API returned {response.status_code}"}
200
+ except Exception as e:
201
+ return {"status": "error", "message": f"Connection failed: {str(e)}"}
202
+
203
+ @app.get("/api/models")
204
+ async def list_models():
205
+ return {
206
+ "text_model": "Qwen/Qwen3-0.6B",
207
+ "vision_model": "Qwen/Qwen3-VL-2B-Instruct",
208
+ "image_model": "stabilityai/stable-diffusion-xl-base-1.0",
209
+ "inpaint_model": "diffusers/stable-diffusion-xl-1.0-inpainting-0.1",
210
+ "tts_model": "microsoft/speecht5_tts",
211
+ "token_configured": bool(HF_TOKEN)
212
+ }