LogicGoInfotechSpaces commited on
Commit
73a3788
·
verified ·
1 Parent(s): 0aa80d5

Create MEDIA_CLICKS_APP.PY

Browse files
Files changed (1) hide show
  1. MEDIA_CLICKS_APP.PY +548 -0
MEDIA_CLICKS_APP.PY ADDED
@@ -0,0 +1,548 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import io
3
+ import json
4
+ import traceback
5
+ from datetime import datetime,timedelta
6
+ from typing import Optional
7
+ import time
8
+ import uuid
9
+ import boto3
10
+ from fastapi import FastAPI, File, UploadFile, Form, HTTPException, Request, Depends
11
+ from fastapi.responses import StreamingResponse, JSONResponse
12
+ from fastapi.middleware.cors import CORSMiddleware
13
+ from pydantic import BaseModel
14
+ from pymongo import MongoClient
15
+ import gridfs
16
+ from gridfs.errors import NoFile
17
+ from bson.objectid import ObjectId
18
+ from PIL import Image
19
+ from fastapi.concurrency import run_in_threadpool
20
+ import shutil
21
+ import firebase_admin
22
+ from firebase_admin import credentials, auth
23
+ from PIL import Image
24
+ from huggingface_hub import InferenceClient
25
+ # ---------------------------------------------------------------------
26
+ # Load Firebase Config from env (stringified JSON)
27
+ # ---------------------------------------------------------------------
28
+ firebase_config_json = os.getenv("firebase_config")
29
+ if not firebase_config_json:
30
+ raise RuntimeError("❌ Missing Firebase config in environment variable 'firebase_config'")
31
+
32
+ try:
33
+ firebase_creds_dict = json.loads(firebase_config_json)
34
+ cred = credentials.Certificate(firebase_creds_dict)
35
+ firebase_admin.initialize_app(cred)
36
+ except Exception as e:
37
+ raise RuntimeError(f"Failed to initialize Firebase Admin SDK: {e}")
38
+
39
+ # ---------------------------------------------------------------------
40
+ # Hugging Face setup
41
+ # ---------------------------------------------------------------------
42
+ HF_TOKEN = os.getenv("HF_TOKEN")
43
+ if not HF_TOKEN:
44
+ raise RuntimeError("HF_TOKEN not set in environment variables")
45
+
46
+ hf_client = InferenceClient(token=HF_TOKEN)
47
+ # ---------------------------------------------------------------------
48
+ # MODEL SELECTION
49
+ # ---------------------------------------------------------------------
50
+ genai = None # ✅ IMPORTANT: module-level declaration
51
+ MODEL = os.getenv("IMAGE_MODEL", "GEMINI").upper()
52
+ GEMINI_FORCE_CATEGORY_ID = "69368e741224bcb6bdb98076"
53
+ # ---------------------------------------------------------------------
54
+ # Gemini setup (ONLY used if MODEL == "GEMINI")
55
+ # ---------------------------------------------------------------------
56
+ GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
57
+ GEMINI_IMAGE_MODEL = os.getenv("GEMINI_IMAGE_MODEL", "gemini-2.5-flash-image")
58
+
59
+ # ---------------------------------------------------------------------
60
+ # MongoDB setup
61
+ # ---------------------------------------------------------------------
62
+ # MONGODB_URI=os.getenv("MONGODB_URI")
63
+ # DB_NAME = "polaroid_db"
64
+
65
+ # mongo = MongoClient(MONGODB_URI)
66
+ # db = mongo[DB_NAME]
67
+ # fs = gridfs.GridFS(db)
68
+ # logs_collection = db["logs"]
69
+ # ---------------- Logs MongoDB (NEW) ----------------
70
+ MONGODB_URI = os.getenv("MONGODB_URI")
71
+ POLAROID_LOGS_MONGO_URL = os.getenv("POLAROID_LOGS_MONGO_URL")
72
+
73
+ DB_NAME = "polaroid_db"
74
+
75
+ # Main DB (images, gridfs)
76
+ mongo = MongoClient(MONGODB_URI)
77
+ db = mongo[DB_NAME]
78
+ fs = gridfs.GridFS(db)
79
+
80
+ # Separate Logs DB
81
+ logs_client = None
82
+ logs_collection = None
83
+
84
+ if POLAROID_LOGS_MONGO_URL:
85
+ logs_client = MongoClient(POLAROID_LOGS_MONGO_URL)
86
+ logs_db = logs_client["logs"] # FORCE DB NAME
87
+ logs_collection = logs_db["polaroid"] # FORCE COLLECTION
88
+ else:
89
+ raise RuntimeError("POLAROID_LOGS_MONGO_URL not set")
90
+ # ---------------------------------------------------------------------
91
+ # DigitalOcean Spaces setup
92
+ # ---------------------------------------------------------------------
93
+ DO_SPACES_KEY = os.getenv("DO_SPACES_KEY")
94
+ DO_SPACES_SECRET = os.getenv("DO_SPACES_SECRET")
95
+ DO_SPACES_REGION = os.getenv("DO_SPACES_REGION", "blr1")
96
+ DO_SPACES_BUCKET = os.getenv("DO_SPACES_BUCKET")
97
+ DO_SPACES_ENDPOINT = os.getenv("DO_SPACES_ENDPOINT", f"https://{DO_SPACES_REGION}.digitaloceanspaces.com")
98
+
99
+ if not DO_SPACES_KEY or not DO_SPACES_SECRET:
100
+ raise RuntimeError("Missing DigitalOcean Spaces credentials in environment variables")
101
+
102
+ # Initialize S3 client for DigitalOcean Spaces
103
+ s3_client = boto3.client(
104
+ 's3',
105
+ region_name=DO_SPACES_REGION,
106
+ endpoint_url=DO_SPACES_ENDPOINT,
107
+ aws_access_key_id=DO_SPACES_KEY,
108
+ aws_secret_access_key=DO_SPACES_SECRET
109
+ )
110
+
111
+ # ---------------------------------------------------------------------
112
+ # FastAPI app setup
113
+ # ---------------------------------------------------------------------
114
+ app = FastAPI(title="Qwen Image Edit API with Firebase Auth")
115
+ app.add_middleware(
116
+ CORSMiddleware,
117
+ allow_origins=["*"],
118
+ allow_credentials=True,
119
+ allow_methods=["*"],
120
+ allow_headers=["*"],
121
+ )
122
+
123
+ # ---------------------------------------------------------------------
124
+ # Auth dependency
125
+ # ---------------------------------------------------------------------
126
+ async def verify_firebase_token(request: Request):
127
+ """Middleware-like dependency to verify Firebase JWT from Authorization header."""
128
+ auth_header = request.headers.get("Authorization")
129
+ if not auth_header or not auth_header.startswith("Bearer "):
130
+ raise HTTPException(status_code=401, detail="Missing or invalid Authorization header")
131
+
132
+ id_token = auth_header.split("Bearer ")[1]
133
+ try:
134
+ decoded_token = auth.verify_id_token(id_token)
135
+ request.state.user = decoded_token
136
+ return decoded_token
137
+ except Exception as e:
138
+ raise HTTPException(status_code=401, detail=f"Invalid or expired Firebase token: {e}")
139
+
140
+ # ---------------------------------------------------------------------
141
+ # Models
142
+ # ---------------------------------------------------------------------
143
+ class HealthResponse(BaseModel):
144
+ status: str
145
+ db: str
146
+ model: str
147
+
148
+ # --------------------- UTILS ---------------------
149
+ def resize_image_if_needed(img: Image.Image, max_size=(1024, 1024)) -> Image.Image:
150
+ """
151
+ Resize image to fit within max_size while keeping aspect ratio.
152
+ """
153
+ if img.width > max_size[0] or img.height > max_size[1]:
154
+ img.thumbnail(max_size, Image.ANTIALIAS)
155
+ return img
156
+ # ---------------------------------------------------------------------
157
+ # Lazy Gemini Initialization
158
+ # ---------------------------------------------------------------------
159
+ _genai_initialized = False
160
+ def init_gemini():
161
+ global _genai_initialized, genai
162
+
163
+ if _genai_initialized:
164
+ return
165
+
166
+ if not GEMINI_API_KEY:
167
+ raise RuntimeError("❌ GEMINI_API_KEY not set")
168
+
169
+ import google.generativeai as genai
170
+ genai.configure(api_key=GEMINI_API_KEY)
171
+
172
+ _genai_initialized = True
173
+
174
+ def expiry(hours: int):
175
+ """Return UTC datetime for TTL expiration"""
176
+ return datetime.utcnow() + timedelta(hours=hours)
177
+
178
+ def prepare_image(file_bytes: bytes) -> Image.Image:
179
+ """
180
+ Open image and resize if larger than 1024x1024
181
+ """
182
+ img = Image.open(io.BytesIO(file_bytes)).convert("RGB")
183
+
184
+ # ✅ MIN SIZE CHECK
185
+ if img.width < 200 or img.height < 200:
186
+ raise HTTPException(
187
+ status_code=400,
188
+ detail="Image size is below 200x200 pixels. Please upload a larger image."
189
+ )
190
+ img = Image.open(io.BytesIO(file_bytes)).convert("RGB")
191
+ img = resize_image_if_needed(img, max_size=(1024, 1024))
192
+ return img
193
+
194
+ def upload_to_digitalocean(image_bytes: bytes, folder: str, filename: str) -> str:
195
+ """
196
+ Upload image to DigitalOcean Spaces and return the public URL.
197
+ folder: 'source' or 'results'
198
+ """
199
+ key = f"valentine/{folder}/{filename}"
200
+ try:
201
+ s3_client.put_object(
202
+ Bucket=DO_SPACES_BUCKET,
203
+ Key=key,
204
+ Body=image_bytes,
205
+ ContentType="image/jpeg" if filename.endswith('.jpg') else "image/png",
206
+ ACL='public-read'
207
+ )
208
+ url = f"{DO_SPACES_ENDPOINT}/{DO_SPACES_BUCKET}/{key}"
209
+ return url
210
+ except Exception as e:
211
+ raise RuntimeError(f"Failed to upload to DigitalOcean Spaces: {e}")
212
+
213
+ def generate_image_id() -> str:
214
+ """
215
+ Generate a random image ID (same format as MongoDB ObjectId for consistency)
216
+ """
217
+ return str(uuid.uuid4().hex[:24])
218
+
219
+ MAX_COMPRESSED_SIZE = 2 * 1024 * 1024 # 2 MB
220
+ def compress_pil_image_to_2mb(
221
+ pil_img: Image.Image,
222
+ max_dim: int = 1280
223
+ ) -> bytes:
224
+ """
225
+ Resize + compress PIL image to <= 2MB.
226
+ Returns JPEG bytes.
227
+ """
228
+ img = pil_img.convert("RGB")
229
+
230
+ # Resize (maintain aspect ratio)
231
+ img.thumbnail((max_dim, max_dim), Image.LANCZOS)
232
+
233
+ quality = 85
234
+ buffer = io.BytesIO()
235
+
236
+ while quality >= 40:
237
+ buffer.seek(0)
238
+ buffer.truncate()
239
+
240
+ img.save(
241
+ buffer,
242
+ format="JPEG",
243
+ quality=quality,
244
+ optimize=True,
245
+ progressive=True
246
+ )
247
+
248
+ if buffer.tell() <= MAX_COMPRESSED_SIZE:
249
+ break
250
+
251
+ quality -= 5
252
+
253
+ return buffer.getvalue()
254
+
255
+ def run_image_generation(
256
+ image1: Image.Image,
257
+ prompt: str,
258
+ image2: Optional[Image.Image] = None,
259
+ force_model: Optional[str] = None
260
+ ) -> Image.Image:
261
+ """
262
+ Unified image generation interface.
263
+ QWEN -> merges images if image2 exists
264
+ GEMINI -> passes images separately
265
+ """
266
+
267
+ effective_model = (force_model or MODEL).upper()
268
+
269
+ # ---------------- QWEN ----------------
270
+ if effective_model == "QWEN":
271
+
272
+ # ✅ Merge images ONLY for QWEN
273
+ if image2:
274
+ total_width = image1.width + image2.width
275
+ max_height = max(image1.height, image2.height)
276
+ merged = Image.new("RGB", (total_width, max_height))
277
+ merged.paste(image1, (0, 0))
278
+ merged.paste(image2, (image1.width, 0))
279
+ else:
280
+ merged = image1
281
+
282
+ return hf_client.image_to_image(
283
+ image=merged,
284
+ prompt=prompt,
285
+ model="Qwen/Qwen-Image-Edit"
286
+ )
287
+
288
+ # ---------------- GEMINI ----------------
289
+ elif effective_model == "GEMINI":
290
+ init_gemini()
291
+ model = genai.GenerativeModel(GEMINI_IMAGE_MODEL)
292
+
293
+ parts = [prompt]
294
+
295
+ def add_image(img: Image.Image):
296
+ buf = io.BytesIO()
297
+ img.save(buf, format="PNG")
298
+ buf.seek(0)
299
+ parts.append({
300
+ "mime_type": "image/png",
301
+ "data": buf.getvalue()
302
+ })
303
+
304
+ add_image(image1)
305
+
306
+ if image2:
307
+ add_image(image2)
308
+
309
+ response = model.generate_content(parts)
310
+
311
+ # -------- SAFE IMAGE EXTRACTION --------
312
+ import base64
313
+
314
+ image_bytes = None
315
+ for candidate in response.candidates:
316
+ for part in candidate.content.parts:
317
+ if hasattr(part, "inline_data") and part.inline_data:
318
+ data = part.inline_data.data
319
+ image_bytes = (
320
+ data if isinstance(data, (bytes, bytearray))
321
+ else base64.b64decode(data)
322
+ )
323
+ break
324
+ if image_bytes:
325
+ break
326
+
327
+ if not image_bytes:
328
+ raise RuntimeError("Gemini did not return an image")
329
+
330
+ img = Image.open(io.BytesIO(image_bytes))
331
+ img.verify()
332
+ return Image.open(io.BytesIO(image_bytes)).convert("RGB")
333
+
334
+ else:
335
+ raise RuntimeError(f"Unsupported IMAGE_MODEL: {effective_model}")
336
+
337
+
338
+
339
+ # ---------------------------------------------------------------------
340
+ # Endpoints
341
+ # ---------------------------------------------------------------------
342
+ @app.get("/")
343
+ async def root():
344
+ """Root endpoint"""
345
+ return {
346
+ "success": True,
347
+ "message": "Polaroid,Kiddo,Makeup,Hairstyle API",
348
+ "data": {
349
+ "version": "1.0.1",
350
+ "Product Name":"Beauty Camera - GlowCam AI Studio",
351
+ "Released By" : "LogicGo Infotech"
352
+ }
353
+ }
354
+
355
+ @app.get("/health", response_model=HealthResponse)
356
+ def health():
357
+ """Public health check"""
358
+ mongo.admin.command("ping")
359
+ return HealthResponse(status="ok", db=db.name, model="Qwen/Qwen-Image-Edit")
360
+
361
+ @app.post("/generate")
362
+ async def generate(
363
+ prompt: str = Form(...),
364
+ image1: UploadFile = File(...),
365
+ image2: Optional[UploadFile] = File(None),
366
+ user_id: Optional[str] = Form(None),
367
+ category_id: Optional[str] = Form(None),
368
+ appname: Optional[str] = Form(None),
369
+ user=Depends(verify_firebase_token)
370
+ ):
371
+ start_time = time.time()
372
+
373
+ # -------------------------
374
+ # 1. VALIDATE & READ IMAGES
375
+ # -------------------------
376
+ try:
377
+ img1_bytes = await image1.read()
378
+ pil_img1 = prepare_image(img1_bytes)
379
+ input1_id = generate_image_id()
380
+ input1_filename = f"{input1_id}_{image1.filename}"
381
+ input1_url = upload_to_digitalocean(img1_bytes, "source", input1_filename)
382
+ except Exception as e:
383
+ raise HTTPException(400, f"Failed to read first image: {e}")
384
+
385
+ img2_bytes = None
386
+ input2_id = None
387
+ input2_url = None
388
+ pil_img2 = None
389
+
390
+ if image2:
391
+ try:
392
+ img2_bytes = await image2.read()
393
+ pil_img2 = prepare_image(img2_bytes)
394
+ input2_id = generate_image_id()
395
+ input2_filename = f"{input2_id}_{image2.filename}"
396
+ input2_url = upload_to_digitalocean(img2_bytes, "source", input2_filename)
397
+ except Exception as e:
398
+ raise HTTPException(400, f"Failed to read second image: {e}")
399
+
400
+ # -------------------------
401
+ # 3. HF INFERENCE
402
+ # -------------------------
403
+ try:
404
+ # --------------------------------------------------
405
+ # MODEL OVERRIDE BASED ON CATEGORY
406
+ # --------------------------------------------------
407
+ force_model = None
408
+
409
+ if category_id == GEMINI_FORCE_CATEGORY_ID:
410
+ force_model = "GEMINI"
411
+
412
+ pil_output = run_image_generation(
413
+ image1=pil_img1,
414
+ image2=pil_img2,
415
+ prompt=prompt,
416
+ force_model="GEMINI" if category_id == GEMINI_FORCE_CATEGORY_ID else None
417
+ )
418
+
419
+
420
+ except Exception as e:
421
+ response_time_ms = round((time.time() - start_time) * 1000)
422
+ # logs_collection.insert_one({
423
+ # "timestamp": datetime.utcnow(),
424
+ # "status": "failure",
425
+ # "input1_id": input1_id,
426
+ # "input2_id": input2_id if input2_id else None,
427
+ # "prompt": prompt,
428
+ # "user_email": user.get("email"),
429
+ # "error": str(e),
430
+ # "response_time_ms": response_time_ms,
431
+ # "appname": appname
432
+ #
433
+ if logs_collection is not None:
434
+ logs_collection.insert_one({
435
+ "endpoint": "/generate",
436
+ "status": "fail",
437
+ "response_time_ms": float(response_time_ms),
438
+ "timestamp": datetime.utcnow(),
439
+ "appname": appname if appname else "None",
440
+ "error": str(e)
441
+ })
442
+ raise HTTPException(500, f"Inference failed: {e}")
443
+
444
+ # -------------------------
445
+ # 5. SAVE OUTPUT IMAGE
446
+ # -------------------------
447
+ output_object_id = ObjectId()
448
+ output_image_id = str(output_object_id)
449
+ out_buf = io.BytesIO()
450
+ pil_output.save(out_buf, format="PNG")
451
+ out_bytes = out_buf.getvalue()
452
+
453
+ out_filename = f"{output_image_id}_result.png"
454
+ out_url = upload_to_digitalocean(out_bytes, "results", out_filename)
455
+ try:
456
+ fs.put(
457
+ out_bytes,
458
+ _id=output_object_id,
459
+ filename=out_filename,
460
+ content_type="image/png",
461
+ metadata={
462
+ "user_email": user.get("email"),
463
+ "prompt": prompt,
464
+ "created_at": datetime.utcnow()
465
+ }
466
+ )
467
+ except Exception as e:
468
+ raise HTTPException(500, f"Failed to store output image in GridFS: {e}")
469
+
470
+ # -------------------------
471
+ # 5b. SAVE COMPRESSED IMAGE
472
+ # -------------------------
473
+ compressed_bytes = compress_pil_image_to_2mb(
474
+ pil_output,
475
+ max_dim=1280
476
+ )
477
+
478
+ compressed_filename = f"{output_image_id}_compressed.jpg"
479
+ compressed_url = upload_to_digitalocean(compressed_bytes, "results", compressed_filename)
480
+
481
+
482
+ response_time_ms = round((time.time() - start_time) * 1000)
483
+
484
+ # -------------------------
485
+ # 6. LOG SUCCESS
486
+ # -------------------------
487
+ # logs_collection.insert_one({
488
+ # "timestamp": datetime.utcnow(),
489
+ # "status": "success",
490
+ # "input1_id": input1_id,
491
+ # "input2_id": input2_id if input2_id else None,
492
+ # "output_id": output_image_id,
493
+ # "prompt": prompt,
494
+ # "user_email": user.get("email"),
495
+ # "response_time_ms": response_time_ms,
496
+ # "appname": appname
497
+ # })
498
+ if logs_collection is not None:
499
+ logs_collection.insert_one({
500
+ "endpoint": "/generate",
501
+ "status": "success",
502
+ "response_time_ms": float(response_time_ms),
503
+ "timestamp": datetime.utcnow(),
504
+ "appname": appname if appname else "None",
505
+ "error": None
506
+ })
507
+ return JSONResponse({
508
+ "output_image_id": output_image_id,
509
+ "user": user.get("email"),
510
+ "response_time_ms": response_time_ms,
511
+ "Compressed_Image_URL": compressed_url
512
+ })
513
+
514
+
515
+ # Image endpoint removed - images are now stored directly in DigitalOcean Spaces
516
+ # and are publicly accessible via the Compressed_Image_URL returned in the /generate response
517
+ @app.get("/image/{image_id}")
518
+ async def download_image(
519
+ image_id: str
520
+ ):
521
+ try:
522
+ file_obj_id = ObjectId(image_id)
523
+ except Exception:
524
+ raise HTTPException(status_code=400, detail="Invalid image id format")
525
+
526
+ def _read_from_gridfs():
527
+ grid_out = fs.get(file_obj_id)
528
+ return grid_out.read(), grid_out.filename, getattr(grid_out, "content_type", "image/png")
529
+
530
+ try:
531
+ file_bytes, filename, content_type = await run_in_threadpool(_read_from_gridfs)
532
+ except NoFile:
533
+ raise HTTPException(status_code=404, detail="Image not found")
534
+ except Exception as e:
535
+ raise HTTPException(status_code=500, detail=f"Failed to download image: {e}")
536
+
537
+ return StreamingResponse(
538
+ io.BytesIO(file_bytes),
539
+ media_type=content_type or "image/png",
540
+ headers={"Content-Disposition": f'attachment; filename="{filename or f"{image_id}.png"}"'}
541
+ )
542
+
543
+ # ---------------------------------------------------------------------
544
+ # Run locally
545
+ # ---------------------------------------------------------------------
546
+ if __name__ == "__main__":
547
+ import uvicorn
548
+ uvicorn.run("app:app", host="0.0.0.0", port=7860, reload=True)