hadadrjt commited on
Commit
360d36a
·
0 Parent(s):

api: Initial.

Browse files

This endpoint is designed for compatibility with Next-Gen Spaces.

* Non-Production release.

* BETA.

Files changed (4) hide show
  1. Dockerfile +26 -0
  2. README.md +14 -0
  3. app.py +459 -0
  4. requirements.txt +4 -0
Dockerfile ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #
2
+ # SPDX-FileCopyrightText: Hadad <hadad@linuxmail.org>
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ #
5
+
6
+ FROM hadadrjt/ubuntu:latest
7
+
8
+ WORKDIR /usr/src/app
9
+
10
+ COPY . .
11
+
12
+ RUN pip install -r requirements.txt
13
+
14
+ RUN useradd -m app \
15
+ && chown -R app:app /usr/src/app \
16
+ && chmod -R u+rwX /usr/src/app \
17
+ && passwd -l root \
18
+ && usermod -s /usr/sbin/nologin root
19
+
20
+ EXPOSE 7860
21
+
22
+ USER app
23
+
24
+ ENTRYPOINT []
25
+
26
+ CMD ["python", "app.py"]
README.md ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: J.A.R.V.I.S.
3
+ license: apache-2.0
4
+ license_link: https://huggingface.co/hadadrjt/JARVIS/blob/main/LICENSE
5
+ emoji: 👀
6
+ colorFrom: green
7
+ colorTo: green
8
+ sdk: docker
9
+ app_port: 7860
10
+ pinned: true
11
+ short_description: API Endpoint
12
+ models:
13
+ - hadadrjt/JARVIS
14
+ ---
app.py ADDED
@@ -0,0 +1,459 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #
2
+ # SPDX-FileCopyrightText: Hadad <hadad@linuxmail.org>
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ #
5
+
6
+ import uuid
7
+ import time
8
+ import json
9
+ import asyncio
10
+ import logging
11
+ from typing import Optional, List, Union, Dict, Any, Literal
12
+ from fastapi import FastAPI, HTTPException, Request, status
13
+ from fastapi.middleware.cors import CORSMiddleware
14
+ from fastapi.responses import StreamingResponse, JSONResponse
15
+ from pydantic import BaseModel, Field, ValidationError
16
+ from gradio_client import Client
17
+ from contextlib import asynccontextmanager
18
+
19
+ logging.basicConfig(
20
+ level=logging.INFO,
21
+ format="%(asctime)s | %(levelname)s | %(name)s | %(threadName)s | %(message)s",
22
+ handlers=[logging.StreamHandler()]
23
+ )
24
+ logger = logging.getLogger("api_gateway")
25
+
26
+ class SessionData:
27
+ def __init__(self):
28
+ self.system: str = ""
29
+ self.history: List[Dict[str, Any]] = []
30
+ self.last_access: float = time.time()
31
+ self.active_tasks: Dict[str, asyncio.Task] = {}
32
+
33
+ class SessionManager:
34
+ def __init__(self):
35
+ self.sessions: Dict[str, Dict[str, SessionData]] = {}
36
+ self.lock = asyncio.Lock()
37
+
38
+ async def cleanup(self):
39
+ while True:
40
+ await asyncio.sleep(60)
41
+ async with self.lock:
42
+ now = time.time()
43
+ expired = []
44
+ for user, sessions in list(self.sessions.items()):
45
+ for sid, data in list(sessions.items()):
46
+ if now - data.last_access > 300:
47
+ expired.append((user, sid))
48
+ for task_id, task in data.active_tasks.items():
49
+ if not task.done():
50
+ task.cancel()
51
+ logger.info(f"Cancelled active task {task_id} for expired session {sid}")
52
+ for user, sid in expired:
53
+ if user in self.sessions and sid in self.sessions[user]:
54
+ del self.sessions[user][sid]
55
+ if not self.sessions[user]:
56
+ del self.sessions[user]
57
+ logger.info(f"Session expired: user={user} session={sid}")
58
+
59
+ async def get_session(self, user: Optional[str], session_id: Optional[str]) -> (str, str, SessionData):
60
+ async with self.lock:
61
+ if not user:
62
+ user = str(uuid.uuid4())
63
+ logger.debug(f"Generated new user ID: {user}")
64
+ if user not in self.sessions:
65
+ self.sessions[user] = {}
66
+ if not session_id or session_id not in self.sessions[user]:
67
+ session_id = str(uuid.uuid4())
68
+ self.sessions[user][session_id] = SessionData()
69
+ logger.info(f"Created new session: user={user} session={session_id}")
70
+ session = self.sessions[user][session_id]
71
+ session.last_access = time.time()
72
+ logger.debug(f"Session accessed: user={user} session={session_id} history_length={len(session.history)}")
73
+ return user, session_id, session
74
+
75
+ session_manager = SessionManager()
76
+
77
+ async def refresh_client(app: FastAPI):
78
+ while True:
79
+ await asyncio.sleep(1)
80
+ async with app.state.client_lock:
81
+ if app.state.client is None:
82
+ await asyncio.sleep(1)
83
+ continue
84
+ while True:
85
+ await asyncio.sleep(15)
86
+ async with app.state.client_lock:
87
+ if app.state.client is not None:
88
+ try:
89
+ old_client = app.state.client
90
+ app.state.client = None
91
+ del old_client
92
+ app.state.client = Client("https://hadadrjt-ai.hf.space/")
93
+ logger.info("Refreshed Gradio client connection")
94
+ except Exception as e:
95
+ logger.error(f"Error refreshing Gradio client: {e}", exc_info=True)
96
+ app.state.client = None
97
+ await asyncio.sleep(5)
98
+ else:
99
+ break
100
+
101
+ @asynccontextmanager
102
+ async def lifespan(app: FastAPI):
103
+ app.state.session_manager = session_manager
104
+ app.state.client = None
105
+ app.state.client_lock = asyncio.Lock()
106
+ app.state.refresh_task = asyncio.create_task(refresh_client(app))
107
+ logger.info("App lifespan started, refresh client task running")
108
+ try:
109
+ yield
110
+ finally:
111
+ app.state.refresh_task.cancel()
112
+ await asyncio.sleep(0.1)
113
+ logger.info("App lifespan ended, refresh client task cancelled")
114
+
115
+ app = FastAPI(
116
+ title="J.A.R.V.I.S. OpenAI-Compatible API",
117
+ version="2.1.3-0625",
118
+ lifespan=lifespan,
119
+ )
120
+
121
+ app.add_middleware(
122
+ CORSMiddleware,
123
+ allow_origins=["*"],
124
+ allow_methods=["GET", "POST", "OPTIONS", "HEAD"],
125
+ allow_headers=["*"],
126
+ )
127
+
128
+ class Function(BaseModel):
129
+ name: str
130
+ description: Optional[str]
131
+ parameters: Dict[str, Any]
132
+
133
+ class Tool(BaseModel):
134
+ type: Literal["function"] = "function"
135
+ function: Function
136
+
137
+ class ToolCall(BaseModel):
138
+ id: str
139
+ type: Literal["function"]
140
+ function: Dict[str, str]
141
+
142
+ class Message(BaseModel):
143
+ role: str = Field(..., pattern="^(system|user|assistant|tool|function)$")
144
+ content: Optional[Union[str, List[Dict[str, Any]]]]
145
+ name: Optional[str] = None
146
+ tool_calls: Optional[List[ToolCall]] = None
147
+ tool_call_id: Optional[str] = None
148
+
149
+ class CommonParams(BaseModel):
150
+ model: str
151
+ stream: bool = False
152
+ user: Optional[str] = None
153
+ session_id: Optional[str] = None
154
+ top_p: Optional[float] = None
155
+ top_k: Optional[int] = None
156
+ temperature: Optional[float] = None
157
+ max_tokens: Optional[int] = None
158
+ max_new_tokens: Optional[int] = None
159
+ presence_penalty: Optional[float] = None
160
+ frequency_penalty: Optional[float] = None
161
+ repetition_penalty: Optional[float] = None
162
+ logit_bias: Optional[Dict[str, float]] = None
163
+ repeat_penalty: Optional[float] = None
164
+ seed: Optional[int] = None
165
+ tools: Optional[List[Tool]] = None
166
+ tool_choice: Optional[Union[str, Dict[str, str]]] = None
167
+ functions: Optional[List[Function]] = None
168
+ function_call: Optional[Union[str, Dict[str, str]]] = None
169
+
170
+ class ChatCompletionRequest(CommonParams):
171
+ messages: List[Message]
172
+
173
+ class CompletionRequest(CommonParams):
174
+ prompt: Union[str, List[str]]
175
+
176
+ class EmbeddingRequest(BaseModel):
177
+ model: str
178
+ input: Union[str, List[str]]
179
+ user: Optional[str] = None
180
+
181
+ class RouterRequest(CommonParams):
182
+ endpoint: Optional[str] = "chat/completions"
183
+ messages: Optional[List[Message]] = None
184
+ prompt: Optional[Union[str, List[str]]] = None
185
+ input: Optional[Union[str, List[str]]] = None
186
+
187
+ def sanitize_messages(messages: List[Message]) -> List[Message]:
188
+ cleaned = []
189
+ for m in messages:
190
+ if isinstance(m.content, list):
191
+ texts = [c.get("text", "") for c in m.content if isinstance(c, dict) and c.get("type") == "text"]
192
+ if texts:
193
+ cleaned.append(Message(role=m.role, content=" ".join(texts)))
194
+ elif isinstance(m.content, str):
195
+ cleaned.append(m)
196
+ return cleaned
197
+
198
+ def map_messages(system: str, history: List[Dict[str, Any]], new_msgs: List[Message]) -> str:
199
+ msgs = []
200
+ if system:
201
+ msgs.append({"role": "system", "content": system})
202
+ msgs.extend(history)
203
+ msgs.extend([m.model_dump() for m in new_msgs if m.role != "system"])
204
+ text = ""
205
+ for m in msgs:
206
+ text += f"{m.get('role','')}:{m.get('content','')}\n"
207
+ return text.strip()
208
+
209
+ async def get_client(app: FastAPI) -> Client:
210
+ async with app.state.client_lock:
211
+ if app.state.client is None:
212
+ try:
213
+ app.state.client = Client("https://hadadrjt-ai.hf.space/")
214
+ logger.info("Created Gradio client connection on demand")
215
+ except Exception as e:
216
+ logger.error(f"Failed to create Gradio client: {e}", exc_info=True)
217
+ raise HTTPException(status_code=502, detail="Failed to connect to upstream Gradio app")
218
+ return app.state.client
219
+
220
+ async def call_gradio(client: Client, params: dict):
221
+ for attempt in range(3):
222
+ try:
223
+ logger.debug(f"Calling Gradio attempt {attempt+1}")
224
+ return await asyncio.to_thread(lambda: client.submit(**params))
225
+ except Exception as e:
226
+ logger.warning(f"Gradio call failed attempt {attempt+1}: {e}", exc_info=True)
227
+ await asyncio.sleep(0.2 * (attempt + 1))
228
+ logger.error("Gradio upstream error after 3 attempts")
229
+ raise HTTPException(status_code=502, detail="Upstream Gradio app error")
230
+
231
+ async def stream_response(job, session_id: str, session_history: List[Dict[str, Any]], new_messages: List[Message], response_type: str):
232
+ partial = ""
233
+ try:
234
+ chunks = await asyncio.to_thread(lambda: list(job))
235
+ except Exception as e:
236
+ logger.error(f"Streaming error: {e}", exc_info=True)
237
+ chunks = []
238
+ for chunk in chunks:
239
+ try:
240
+ if isinstance(chunk, list):
241
+ response = next((item.get('content') for item in chunk if isinstance(item, dict) and 'content' in item), str(chunk))
242
+ else:
243
+ response = str(chunk)
244
+ token = response[len(partial):] if response.startswith(partial) else response
245
+ partial = response
246
+ if response_type == "chat":
247
+ data = {
248
+ "id": str(uuid.uuid4()),
249
+ "object": "chat.completion.chunk",
250
+ "choices": [{"delta": {"content": token}, "index": 0, "finish_reason": None}],
251
+ "session_id": session_id
252
+ }
253
+ else:
254
+ data = {
255
+ "id": str(uuid.uuid4()),
256
+ "object": "text_completion.chunk",
257
+ "choices": [{"text": token, "index": 0, "finish_reason": None}],
258
+ "session_id": session_id
259
+ }
260
+ yield f"data: {json.dumps(data)}\n\n"
261
+ except Exception as e:
262
+ logger.error(f"Chunk yield error: {e}", exc_info=True)
263
+ continue
264
+ session_history.extend([m.model_dump() for m in new_messages if m.role != "system"])
265
+ session_history.append({"role": "assistant", "content": partial})
266
+ done_data = {
267
+ "id": str(uuid.uuid4()),
268
+ "object": f"{response_type}.completion.chunk",
269
+ "choices": [{"delta" if response_type=="chat" else "text": {} if response_type=="chat" else "", "index": 0, "finish_reason": "stop"}],
270
+ "session_id": session_id
271
+ }
272
+ yield f"data: {json.dumps(done_data)}\n\n"
273
+
274
+ @app.post("/v1/chat/completions")
275
+ async def chat_completions(req: ChatCompletionRequest):
276
+ user, session_id, session = await session_manager.get_session(req.user, req.session_id)
277
+ req.messages = sanitize_messages(req.messages)
278
+ for m in req.messages:
279
+ if m.role == "system":
280
+ session.system = m.content
281
+ break
282
+ text = map_messages(session.system, session.history, req.messages)
283
+ params = {
284
+ "message": text,
285
+ "model_label": req.model,
286
+ "api_name": "/api",
287
+ "top_p": req.top_p,
288
+ "top_k": req.top_k,
289
+ "temperature": req.temperature,
290
+ "max_tokens": req.max_tokens,
291
+ "max_new_tokens": req.max_new_tokens,
292
+ "presence_penalty": req.presence_penalty,
293
+ "frequency_penalty": req.frequency_penalty,
294
+ "repetition_penalty": req.repetition_penalty,
295
+ "repeat_penalty": req.repeat_penalty,
296
+ "logit_bias": req.logit_bias,
297
+ "seed": req.seed,
298
+ "functions": req.functions or req.tools,
299
+ "function_call": req.function_call or req.tool_choice,
300
+ }
301
+ params = {k: v for k, v in params.items() if v is not None}
302
+ logger.info(f"Chat completion request user={user} session={session_id} model={req.model} stream={req.stream}")
303
+ client = await get_client(app)
304
+ if req.stream:
305
+ job = await call_gradio(client, params)
306
+ generator = stream_response(job, session_id, session.history, req.messages, "chat")
307
+ return StreamingResponse(generator, media_type="text/event-stream")
308
+ else:
309
+ loop = asyncio.get_running_loop()
310
+ try:
311
+ result = await loop.run_in_executor(None, lambda: client.predict(**params))
312
+ except Exception as e:
313
+ logger.error(f"Gradio predict error: {e}", exc_info=True)
314
+ raise HTTPException(status_code=502, detail="Upstream Gradio app error")
315
+ session.history.extend([m.model_dump() for m in req.messages if m.role != "system"])
316
+ session.history.append({"role": "assistant", "content": result})
317
+ logger.info(f"Chat completion response sent user={user} session={session_id}")
318
+ return {
319
+ "id": str(uuid.uuid4()),
320
+ "object": "chat.completion",
321
+ "choices": [{"message": {"role": "assistant", "content": result}}],
322
+ "session_id": session_id
323
+ }
324
+
325
+ @app.post("/v1/completions")
326
+ async def completions(req: CompletionRequest):
327
+ user, session_id, _ = await session_manager.get_session(req.user, req.session_id)
328
+ prompt = req.prompt if isinstance(req.prompt, str) else "\n".join(req.prompt)
329
+ params = {
330
+ "message": prompt,
331
+ "model_label": req.model,
332
+ "api_name": "/api",
333
+ "top_p": req.top_p,
334
+ "top_k": req.top_k,
335
+ "temperature": req.temperature,
336
+ "max_tokens": req.max_tokens,
337
+ "max_new_tokens": req.max_new_tokens,
338
+ "presence_penalty": req.presence_penalty,
339
+ "frequency_penalty": req.frequency_penalty,
340
+ "repetition_penalty": req.repetition_penalty,
341
+ "repeat_penalty": req.repeat_penalty,
342
+ "logit_bias": req.logit_bias,
343
+ "seed": req.seed,
344
+ }
345
+ params = {k: v for k, v in params.items() if v is not None}
346
+ logger.info(f"Completion request user={user} session={session_id} model={req.model} stream={req.stream}")
347
+ client = await get_client(app)
348
+ if req.stream:
349
+ job = await call_gradio(client, params)
350
+ generator = stream_response(job, session_id, [], [], "text")
351
+ return StreamingResponse(generator, media_type="text/event-stream")
352
+ else:
353
+ loop = asyncio.get_running_loop()
354
+ try:
355
+ result = await loop.run_in_executor(None, lambda: client.predict(**params))
356
+ except Exception as e:
357
+ logger.error(f"Gradio predict error: {e}", exc_info=True)
358
+ raise HTTPException(status_code=502, detail="Upstream Gradio app error")
359
+ logger.info(f"Completion response sent user={user} session={session_id}")
360
+ return {"id": str(uuid.uuid4()), "object": "text_completion", "choices": [{"text": result}]}
361
+
362
+ @app.post("/v1/embeddings")
363
+ async def embeddings(req: EmbeddingRequest):
364
+ inputs = req.input if isinstance(req.input, list) else [req.input]
365
+ embeddings = [[0.0] * 768 for _ in inputs]
366
+ logger.info(f"Embedding request model={req.model} inputs_count={len(inputs)}")
367
+ return {"object": "list", "data": [{"embedding": emb, "index": i} for i, emb in enumerate(embeddings)]}
368
+
369
+ @app.get("/v1/models")
370
+ async def get_models():
371
+ logger.info("Models list requested")
372
+ return {"object": "list", "data": [{"id": "Q8_K_XL", "object": "model", "owned_by": "J.A.R.V.I.S."}]}
373
+
374
+ @app.get("/v1/history")
375
+ async def get_history(user: Optional[str] = None, session_id: Optional[str] = None):
376
+ user = user or "anonymous"
377
+ sessions = session_manager.sessions
378
+ logger.info(f"History requested user={user} session={session_id}")
379
+ if user in sessions and session_id and session_id in sessions[user]:
380
+ return {"user": user, "session_id": session_id, "history": sessions[user][session_id].history}
381
+ return {"user": user, "session_id": session_id, "history": []}
382
+
383
+ @app.post("/v1/responses/cancel")
384
+ async def cancel_response(user: Optional[str], session_id: Optional[str], task_id: Optional[str]):
385
+ user = user or "anonymous"
386
+ if not task_id:
387
+ logger.warning(f"Cancel response missing task_id user={user} session={session_id}")
388
+ raise HTTPException(status_code=400, detail="Missing task_id for cancellation")
389
+ async with session_manager.lock:
390
+ if user in session_manager.sessions and session_id in session_manager.sessions[user]:
391
+ session = session_manager.sessions[user][session_id]
392
+ task = session.active_tasks.get(task_id)
393
+ if task and not task.done():
394
+ task.cancel()
395
+ logger.info(f"Cancelled task {task_id} for user={user} session={session_id}")
396
+ return {"message": f"Cancelled task {task_id}"}
397
+ logger.warning(f"Task not found or already completed task_id={task_id} user={user} session={session_id}")
398
+ raise HTTPException(status_code=404, detail="Task not found or already completed")
399
+
400
+ @app.api_route("/v1", methods=["POST", "GET", "OPTIONS", "HEAD"])
401
+ async def router(request: Request):
402
+ if request.method == "POST":
403
+ try:
404
+ body_json = await request.json()
405
+ except Exception:
406
+ logger.error("Invalid JSON body in router POST")
407
+ raise HTTPException(status_code=400, detail="Invalid JSON body")
408
+ try:
409
+ body = RouterRequest(**body_json)
410
+ except ValidationError as e:
411
+ logger.error(f"Validation error in router POST: {e.errors()}")
412
+ raise HTTPException(status_code=422, detail=e.errors())
413
+ endpoint = body.endpoint or "chat/completions"
414
+ logger.info(f"Router POST to endpoint={endpoint}")
415
+ if endpoint == "chat/completions":
416
+ if not body.model or not body.messages:
417
+ raise HTTPException(status_code=422, detail="Missing 'model' or 'messages'")
418
+ req_obj = ChatCompletionRequest(**body.dict())
419
+ return await chat_completions(req_obj)
420
+ elif endpoint == "completions":
421
+ if not body.model or not body.prompt:
422
+ raise HTTPException(status_code=422, detail="Missing 'model' or 'prompt'")
423
+ req_obj = CompletionRequest(**body.dict())
424
+ return await completions(req_obj)
425
+ elif endpoint == "embeddings":
426
+ if not body.model or body.input is None:
427
+ raise HTTPException(status_code=422, detail="Missing 'model' or 'input'")
428
+ req_obj = EmbeddingRequest(**body.dict())
429
+ return await embeddings(req_obj)
430
+ elif endpoint == "models":
431
+ return await get_models()
432
+ elif endpoint == "history":
433
+ return await get_history(body.user, body.session_id)
434
+ elif endpoint == "responses/cancel":
435
+ return await cancel_response(body.user, body.session_id, body.session_id)
436
+ else:
437
+ logger.warning(f"Router POST unknown endpoint: {endpoint}")
438
+ raise HTTPException(status_code=404, detail="Endpoint not found")
439
+ else:
440
+ logger.info(f"Router {request.method} called - only POST supported with JSON body")
441
+ return JSONResponse({"message": "Send POST request with JSON body"}, status_code=status.HTTP_405_METHOD_NOT_ALLOWED)
442
+
443
+ @app.get("/")
444
+ async def root():
445
+ return {
446
+ "endpoints": [
447
+ "/v1/chat/completions",
448
+ "/v1/completions",
449
+ "/v1/embeddings",
450
+ "/v1/models",
451
+ "/v1/history",
452
+ "/v1/responses/cancel",
453
+ "/v1"
454
+ ]
455
+ }
456
+
457
+ if __name__ == "__main__":
458
+ import uvicorn
459
+ uvicorn.run(app, host="0.0.0.0", port=7860)
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ fastapi
2
+ gradio-client
3
+ pydantic
4
+ uvicorn