AkashKumarave commited on
Commit
613bf73
·
verified ·
1 Parent(s): 0a3b563

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +363 -180
app.py CHANGED
@@ -1,194 +1,377 @@
1
- import json
 
 
 
 
2
  import os
3
  import time
4
- import uuid
5
- import tempfile
 
 
 
 
 
 
 
6
  from PIL import Image
7
- import gradio as gr
8
- import base64
9
- import mimetypes
10
  from io import BytesIO
11
  from google import genai
12
 
13
- def generate(text, images, api_key=None, model="gemini-2.5-flash-image-preview"):
14
- # Initialize client using provided api_key or fallback to environment variable
15
- api_key = api_key.strip() if api_key and api_key.strip() != "" else os.environ.get("GEMINI_API_KEY", "AIzaSyDL5Rilo7ptJpUOZdY6wy8PJYUcVcnDADs")
16
- client = genai.Client(api_key=api_key)
17
-
18
- # Prepare contents with images first, then text
19
- contents = images + [text]
20
-
21
- response = client.models.generate_content(
22
- model=model,
23
- contents=contents,
24
- )
25
-
26
- text_response = ""
27
- image_path = None
28
-
29
- for part in response.candidates[0].content.parts:
30
- if part.text is not None:
31
- text_response += part.text + "\n"
32
- elif part.inline_data is not None:
33
- # Create a temporary file to store the generated image
34
- with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp:
35
- temp_path = tmp.name
36
- generated_image = Image.open(BytesIO(part.inline_data.data))
37
- generated_image.save(temp_path)
38
- image_path = temp_path
39
- print(f"Generated image saved to: {temp_path} with prompt: {text}")
40
-
41
- return image_path, text_response
42
-
43
- def load_uploaded_images(uploaded_files):
44
- """Load and display uploaded images immediately"""
45
- uploaded_images = []
46
- if uploaded_files:
47
- for file in uploaded_files:
48
- if file.name.lower().endswith(('.png', '.jpg', '.jpeg', '.webp')):
49
- img = Image.open(file.name)
50
- if img.mode == "RGBA":
51
- img = img.convert("RGBA")
52
- uploaded_images.append(img)
53
- return uploaded_images
54
-
55
- def process_image_and_prompt(uploaded_files, prompt, gemini_api_key):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
  try:
57
- input_text = prompt
58
- model = "gemini-2.5-flash-image-preview"
59
-
60
- # Load images from uploaded files
61
- images = []
62
- uploaded_images = []
63
- if uploaded_files:
64
- for file in uploaded_files:
65
- if file.name.lower().endswith(('.png', '.jpg', '.jpeg', '.webp')):
66
- img = Image.open(file.name)
67
- if img.mode == "RGBA":
68
- img = img.convert("RGBA")
69
- images.append(img)
70
- uploaded_images.append(img)
71
-
72
- if not images:
73
- raise gr.Error("Please upload at least one image", duration=5)
74
-
75
- # Call generate with images and prompt
76
- image_path, text_response = generate(text=input_text, images=images, api_key=gemini_api_key, model=model)
 
 
 
77
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
  if image_path:
79
- # Load and convert the generated image if needed
80
- result_img = Image.open(image_path)
81
- if result_img.mode == "RGBA":
82
- result_img = result_img.convert("RGBA")
83
- return uploaded_images, [result_img], "" # Return uploaded images, generated image, and empty text output
84
  else:
85
- # Return uploaded images, no generated image, and the text response
86
- return uploaded_images, None, text_response
87
  except Exception as e:
88
- raise gr.Error(f"Error: {e}", duration=5)
89
-
90
- # Build a Blocks-based interface with a custom HTML header and CSS
91
- with gr.Blocks(css="style.css") as demo:
92
- # Custom HTML header with proper class for styling
93
- gr.HTML(
94
- """
95
- <div class="header-container">
96
- <div>
97
- <img src="https://www.gstatic.com/lamda/images/gemini_favicon_f069958c85030456e93de685481c559f160ea06b.png" alt="Gemini logo">
98
- </div>
99
- <div>
100
- <h1>Gemini for Image Editing</h1>
101
- <p>Powered by <a href="https://gradio.app/">Gradio</a> |
102
- <a href="https://huggingface.co/spaces/ameerazam08/Gemini-Image-Edit?duplicate=true">Duplicate</a> this Repo |
103
- <a href="https://aistudio.google.com/apikey">Get an API Key</a> |
104
- Follow me on Twitter: <a href="https://x.com/Ameerazam18">Ameerazam18</a></p>
105
- </div>
106
- </div>
107
- """
108
- )
109
-
110
- with gr.Accordion("API Configuration", open=False, elem_classes="config-accordion"):
111
- gr.Markdown("""
112
- - **Issue:** Sometimes the model returns text instead of an image.
113
- ### Steps to Address:
114
- 1. **Duplicate the Repository**
115
- - Create a separate copy for modifications.
116
- 2. **Use Your Own Gemini API Key**
117
- - You **must** configure your own Gemini key for generation!
118
- """)
119
-
120
- with gr.Accordion("Usage Instructions", open=False, elem_classes="instructions-accordion"):
121
- gr.Markdown("""
122
- ### Usage
123
- - Upload an image and enter a prompt to generate outputs.
124
- - If text is returned instead of an image, it will appear in the text output.
125
- - Upload only PNG, JPG, JPEG, or WEBP images.
126
- - **Do not use NSFW images!**
127
- """)
128
-
129
- with gr.Row(elem_classes="main-content"):
130
- with gr.Column(elem_classes="input-column"):
131
- image_input = gr.File(
132
- file_types=["image"],
133
- file_count="multiple",
134
- label="Upload Images",
135
- elem_id="image-input",
136
- elem_classes="upload-box"
137
- )
138
- gemini_api_key = gr.Textbox(
139
- lines=1,
140
- placeholder="Enter Gemini API Key (optional)",
141
- label="Gemini API Key (optional)",
142
- elem_classes="api-key-input"
143
- )
144
- prompt_input = gr.Textbox(
145
- lines=2,
146
- placeholder="Enter prompt here...",
147
- label="Prompt",
148
- elem_classes="prompt-input"
149
- )
150
- submit_btn = gr.Button("Generate", elem_classes="generate-btn")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
 
152
- with gr.Column(elem_classes="output-column"):
153
- uploaded_gallery = gr.Gallery(label="Uploaded Images", elem_classes="uploaded-gallery")
154
- output_gallery = gr.Gallery(label="Generated Outputs", elem_classes="output-gallery")
155
- output_text = gr.Textbox(
156
- label="Gemini Output",
157
- placeholder="Text response will appear here if no image is generated.",
158
- elem_classes="output-text"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
  )
160
 
161
- # Set up the interaction with three outputs
162
- submit_btn.click(
163
- fn=process_image_and_prompt,
164
- inputs=[image_input, prompt_input, gemini_api_key],
165
- outputs=[uploaded_gallery, output_gallery, output_text],
166
- )
167
-
168
- # Update uploaded gallery immediately when files are uploaded
169
- image_input.upload(
170
- fn=load_uploaded_images,
171
- inputs=[image_input],
172
- outputs=[uploaded_gallery],
173
- )
174
-
175
- gr.Markdown("## Try these examples", elem_classes="gr-examples-header")
176
-
177
- examples = [
178
- ["data/1.webp", 'change text to "AMEER"'],
179
- ["data/2.webp", "remove the spoon from hand only"],
180
- ["data/3.webp", 'change text to "Make it "'],
181
- ["data/1.jpg", "add joker style only on face"],
182
- ["data/1777043.jpg", "add joker style only on face"],
183
- ["data/2807615.jpg", "add lipstick on lip only"],
184
- ["data/76860.jpg", "add lipstick on lip only"],
185
- ["data/2807615.jpg", "make it happy looking face only"],
186
- ]
187
-
188
- gr.Examples(
189
- examples=examples,
190
- inputs=[image_input, prompt_input],
191
- elem_id="examples-grid"
192
- )
193
-
194
- demo.queue(max_size=50).launch(mcp_server=True, share=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ from fastapi import FastAPI, UploadFile, File, HTTPException, Form, Request
3
+ from fastapi.middleware.cors import CORSMiddleware
4
+ from fastapi.responses import FileResponse, JSONResponse
5
+ import base64
6
  import os
7
  import time
8
+ import jwt
9
+ from pathlib import Path
10
+ from typing import List
11
+ import io
12
+ import razorpay
13
+ from razorpay.errors import SignatureVerificationError
14
+ from supabase import create_client, Client
15
+ from pydantic import BaseModel
16
+ from typing import Optional
17
  from PIL import Image
 
 
 
18
  from io import BytesIO
19
  from google import genai
20
 
21
+ # Configure logging
22
+ logging.basicConfig(level=logging.INFO)
23
+ logger = logging.getLogger(__name__)
24
+
25
+ # Initialize FastAPI app
26
+ app = FastAPI(title="Gemini Image Editing API with Razorpay")
27
+
28
+ # Enable CORS for frontend
29
+ app.add_middleware(
30
+ CORSMiddleware,
31
+ allow_origins=[
32
+ "https://hivili.web.app",
33
+ "http://localhost:3000",
34
+ "https://*.lovable.dev",
35
+ "https://*.sandbox.lovable.dev",
36
+ ],
37
+ allow_origin_regex=r"https://.*\.lovable\.dev|https://.*\.sandbox\.lovable\.dev",
38
+ allow_credentials=True,
39
+ allow_methods=["*"],
40
+ allow_headers=["*"],
41
+ )
42
+
43
+ # ===== API CONFIGURATION =====
44
+ GEMINI_API_KEY = os.getenv("GEMINI_API_KEY", "AIzaSyDL5Rilo7ptJpUOZdY6wy8PJYUcVcnDADs")
45
+ GEMINI_MODEL = "gemini-2.5-flash-image-preview"
46
+
47
+ # ===== RAZORPAY CONFIGURATION =====
48
+ RAZORPAY_KEY_ID = os.getenv("RAZORPAY_KEY_ID")
49
+ RAZORPAY_KEY_SECRET = os.getenv("RAZORPAY_KEY_SECRET")
50
+ razorpay_client = razorpay.Client(auth=(RAZORPAY_KEY_ID, RAZORPAY_KEY_SECRET)) if RAZORPAY_KEY_ID and RAZORPAY_KEY_SECRET else None
51
+
52
+ # ===== SUPABASE CONFIGURATION =====
53
+ SUPABASE_URL = os.getenv("SUPABASE_URL")
54
+ SUPABASE_KEY = os.getenv("SUPABASE_KEY")
55
+ supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY) if SUPABASE_URL and SUPABASE_KEY else None
56
+
57
+ # Pydantic models for JSON input validation
58
+ class CreateOrderRequest(BaseModel):
59
+ amount: int
60
+
61
+ class VerifyPaymentRequest(BaseModel):
62
+ razorpay_order_id: str
63
+ razorpay_payment_id: str
64
+ razorpay_signature: str
65
+ user_id: Optional[str] = None
66
+
67
+ class GenerateImageRequest(BaseModel):
68
+ prompt: str
69
+ user_id: Optional[str] = None
70
+
71
+ # ===== AUTHENTICATION =====
72
+ def generate_jwt_token():
73
+ """Generate JWT token for API authentication"""
74
+ payload = {
75
+ "iss": "gemini-image-editor",
76
+ "exp": int(time.time()) + 1800, # 30 minutes expiration
77
+ "nbf": int(time.time()) - 5 # Not before 5 seconds ago
78
+ }
79
+ return jwt.encode(payload, GEMINI_API_KEY, algorithm="HS256")
80
+
81
+ # ===== IMAGE PROCESSING =====
82
+ def prepare_image_base64(image_content: bytes):
83
+ """Convert image bytes to base64 without prefix"""
84
  try:
85
+ return base64.b64encode(image_content).decode('utf-8')
86
+ except Exception as e:
87
+ logger.error(f"Image processing failed: {str(e)}")
88
+ raise HTTPException(status_code=500, detail=f"Image processing failed: {str(e)}")
89
+
90
+ def validate_image(image_content: bytes):
91
+ """Validate image meets API requirements"""
92
+ try:
93
+ size_mb = len(image_content) / (1024 * 1024)
94
+ if size_mb > 10:
95
+ raise HTTPException(status_code=400, detail="Image too large (max 10MB)")
96
+ img = Image.open(BytesIO(image_content))
97
+ if img.format.lower() not in ['png', 'jpg', 'jpeg', 'webp']:
98
+ raise HTTPException(status_code=400, detail="Unsupported image format. Use PNG, JPG, JPEG, or WEBP")
99
+ return True, ""
100
+ except Exception as e:
101
+ raise HTTPException(status_code=400, detail=f"Image validation error: {str(e)}")
102
+
103
+ def generate_gemini_image(images: List[Image.Image], prompt: str, api_key: str):
104
+ """Generate or edit image using Gemini API"""
105
+ try:
106
+ client = genai.Client(api_key=api_key)
107
+ contents = images + [prompt]
108
 
109
+ response = client.models.generate_content(
110
+ model=GEMINI_MODEL,
111
+ contents=contents,
112
+ )
113
+
114
+ text_response = ""
115
+ image_path = None
116
+
117
+ for part in response.candidates[0].content.parts:
118
+ if part.text is not None:
119
+ text_response += part.text + "\n"
120
+ elif part.inline_data is not None:
121
+ with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp:
122
+ temp_path = tmp.name
123
+ generated_image = Image.open(BytesIO(part.inline_data.data))
124
+ generated_image.save(temp_path)
125
+ image_path = temp_path
126
+ logger.info(f"Generated image saved to: {temp_path} with prompt: {prompt}")
127
+
128
  if image_path:
129
+ return image_path, ""
 
 
 
 
130
  else:
131
+ return None, text_response.strip()
 
132
  except Exception as e:
133
+ logger.error(f"Gemini API error: {str(e)}")
134
+ raise HTTPException(status_code=500, detail=f"Gemini API error: {str(e)}")
135
+
136
+ # ===== RAZORPAY FUNCTIONS =====
137
+ def create_razorpay_order(amount: int):
138
+ """Create a Razorpay order"""
139
+ try:
140
+ if not razorpay_client:
141
+ raise HTTPException(status_code=500, detail="Razorpay configuration missing")
142
+ if amount <= 0:
143
+ raise ValueError("Amount must be a positive integer")
144
+ order_data = {
145
+ "amount": amount * 100, # Convert INR to paise
146
+ "currency": "INR",
147
+ "payment_capture": 1 # Auto-capture payment
148
+ }
149
+ order = razorpay_client.order.create(data=order_data)
150
+ logger.info(f"Razorpay order created successfully: {order['id']}")
151
+ return order
152
+ except Exception as e:
153
+ logger.error(f"Failed to create Razorpay order: {str(e)}")
154
+ raise HTTPException(status_code=500, detail=f"Failed to create order: {str(e)}")
155
+
156
+ def verify_payment_signature(order_id: str, payment_id: str, signature: str):
157
+ """Verify Razorpay payment signature"""
158
+ try:
159
+ if not razorpay_client:
160
+ raise HTTPException(status_code=500, detail="Razorpay configuration missing")
161
+ params_dict = {
162
+ "razorpay_order_id": order_id,
163
+ "razorpay_payment_id": payment_id,
164
+ "razorpay_signature": signature
165
+ }
166
+ razorpay_client.utility.verify_payment_signature(params_dict)
167
+ logger.info(f"Payment signature verified successfully for order: {order_id}")
168
+ return True
169
+ except SignatureVerificationError as e:
170
+ logger.error(f"Payment signature verification failed: {str(e)}")
171
+ return False
172
+ except Exception as e:
173
+ logger.error(f"Error verifying payment signature: {str(e)}")
174
+ raise HTTPException(status_code=500, detail=f"Verification error: {str(e)}")
175
+
176
+ # ===== MAIN PROCESSING =====
177
+ async def generate_image(images: List[bytes], prompt: str, user_id: Optional[str] = None):
178
+ """Handle complete image generation workflow"""
179
+ # Validate images
180
+ for img_content in images:
181
+ if img_content:
182
+ validate_image(img_content)
183
+
184
+ # Convert bytes to PIL Images
185
+ pil_images = []
186
+ for img_content in images:
187
+ try:
188
+ img = Image.open(BytesIO(img_content))
189
+ if img.mode == "RGBA":
190
+ img = img.convert("RGBA")
191
+ pil_images.append(img)
192
+ except Exception as e:
193
+ logger.error(f"Image conversion failed: {str(e)}")
194
+ raise HTTPException(status_code=400, detail=f"Image conversion failed: {str(e)}")
195
+
196
+ if len(pil_images) < 1:
197
+ raise HTTPException(status_code=400, detail="At least one image required")
198
+
199
+ # Check user premium status if Supabase is configured
200
+ if user_id and supabase:
201
+ try:
202
+ user_data = supabase.table("users").select("is_premium").eq("user_id", user_id).execute()
203
+ if not user_data.data or not user_data.data[0].get("is_premium"):
204
+ raise HTTPException(status_code=403, detail="Premium subscription required")
205
+ except Exception as e:
206
+ logger.error(f"Supabase user check failed: {str(e)}")
207
+ raise HTTPException(status_code=500, detail=f"User verification failed: {str(e)}")
208
+
209
+ # Generate image using Gemini API
210
+ image_path, text_response = generate_gemini_image(pil_images, prompt, GEMINI_API_KEY)
211
+
212
+ if image_path:
213
+ return image_path
214
+ else:
215
+ raise HTTPException(status_code=500, detail=f"Image generation failed: {text_response or 'Unknown error'}")
216
+
217
+ # ===== API ENDPOINTS =====
218
+ @app.post("/generate")
219
+ async def generate_image_endpoint(
220
+ prompt: str = Form(...),
221
+ images: List[UploadFile] = File(...),
222
+ user_id: Optional[str] = Form(None)
223
+ ):
224
+ """Endpoint to generate or edit an image using Gemini API"""
225
+ try:
226
+ if len(images) < 1:
227
+ raise HTTPException(status_code=400, detail="At least one image required")
228
+ if len(images) > 4:
229
+ raise HTTPException(status_code=400, detail="Maximum 4 images allowed")
230
+
231
+ image_contents = [await image.read() for image in images]
232
+ output_path = await generate_image(image_contents, prompt, user_id)
233
+
234
+ return FileResponse(
235
+ path=output_path,
236
+ media_type="image/png",
237
+ filename=f"gemini_output_{Path(output_path).stem}.png"
238
+ )
239
+ except HTTPException as e:
240
+ raise
241
+ except Exception as e:
242
+ logger.error(f"Error in /generate: {str(e)}")
243
+ raise HTTPException(status_code=500, detail=str(e))
244
+
245
+ @app.post("/create-razorpay-order")
246
+ async def create_order_endpoint(
247
+ request: Request,
248
+ amount: Optional[int] = Form(None),
249
+ body: Optional[CreateOrderRequest] = None
250
+ ):
251
+ """Create a Razorpay order (supports form-data and JSON)"""
252
+ logger.info("Received create order request")
253
+
254
+ try:
255
+ if not razorpay_client:
256
+ raise HTTPException(status_code=500, detail="Razorpay configuration missing")
257
 
258
+ # Handle JSON body if provided
259
+ if body and body.amount:
260
+ amount = body.amount
261
+ elif not amount:
262
+ try:
263
+ json_body = await request.json()
264
+ amount = json_body.get('amount')
265
+ except:
266
+ pass
267
+
268
+ if not amount or amount <= 0:
269
+ raise HTTPException(status_code=422, detail="Missing or invalid 'amount' parameter")
270
+
271
+ logger.info(f"Creating order with amount: {amount}")
272
+ order = create_razorpay_order(amount)
273
+
274
+ response_data = {
275
+ "id": order["id"],
276
+ "amount": order["amount"],
277
+ "currency": order["currency"],
278
+ "key_id": RAZORPAY_KEY_ID
279
+ }
280
+
281
+ logger.info(f"Order created successfully: {order['id']}")
282
+ return JSONResponse(content=response_data)
283
+
284
+ except HTTPException:
285
+ raise
286
+ except Exception as e:
287
+ logger.error(f"Error creating order: {str(e)}")
288
+ raise HTTPException(status_code=500, detail=f"Failed to create order: {str(e)}")
289
+
290
+ @app.post("/verify-razorpay-payment")
291
+ async def verify_payment_endpoint(
292
+ request: Request,
293
+ razorpay_order_id: Optional[str] = Form(None),
294
+ razorpay_payment_id: Optional[str] = Form(None),
295
+ razorpay_signature: Optional[str] = Form(None),
296
+ user_id: Optional[str] = Form(None),
297
+ body: Optional[VerifyPaymentRequest] = None
298
+ ):
299
+ """Verify Razorpay payment signature (supports form-data and JSON)"""
300
+ logger.info("Received payment verification request")
301
+
302
+ try:
303
+ # Handle JSON body if provided
304
+ if body:
305
+ razorpay_order_id = razorpay_order_id or body.razorpay_order_id
306
+ razorpay_payment_id = razorpay_payment_id or body.razorpay_payment_id
307
+ razorpay_signature = razorpay_signature or body.razorpay_signature
308
+ user_id = user_id or body.user_id
309
+ else:
310
+ try:
311
+ json_body = await request.json()
312
+ razorpay_order_id = razorpay_order_id or json_body.get('razorpay_order_id')
313
+ razorpay_payment_id = razorpay_payment_id or json_body.get('razorpay_payment_id')
314
+ razorpay_signature = razorpay_signature or json_body.get('razorpay_signature')
315
+ user_id = user_id or json_body.get('user_id')
316
+ except:
317
+ pass
318
+
319
+ # Validate required fields
320
+ if not all([razorpay_order_id, razorpay_payment_id, razorpay_signature]):
321
+ missing_fields = []
322
+ if not razorpay_order_id: missing_fields.append("razorpay_order_id")
323
+ if not razorpay_payment_id: missing_fields.append("razorpay_payment_id")
324
+ if not razorpay_signature: missing_fields.append("razorpay_signature")
325
+
326
+ logger.error(f"Missing required fields: {missing_fields}")
327
+ raise HTTPException(
328
+ status_code=422,
329
+ detail=f"Missing required fields: {', '.join(missing_fields)}"
330
  )
331
 
332
+ logger.info(f"Verifying payment for order_id: {razorpay_order_id}")
333
+ is_valid = verify_payment_signature(razorpay_order_id, razorpay_payment_id, razorpay_signature)
334
+
335
+ if is_valid:
336
+ if user_id and supabase:
337
+ logger.info(f"Updating Supabase for user_id: {user_id}")
338
+ try:
339
+ supabase.table("users").update({"is_premium": True}).eq("user_id", user_id).execute()
340
+ logger.info(f"Successfully updated premium status for user: {user_id}")
341
+ except Exception as e:
342
+ logger.error(f"Failed to update Supabase: {str(e)}")
343
+
344
+ return JSONResponse(content={"success": True, "message": "Payment verified successfully"})
345
+ else:
346
+ logger.warning(f"Payment verification failed for order: {razorpay_order_id}")
347
+ return JSONResponse(content={"success": False, "message": "Payment verification failed"}, status_code=400)
348
+
349
+ except HTTPException:
350
+ raise
351
+ except Exception as e:
352
+ logger.error(f"Error verifying payment: {str(e)}")
353
+ raise HTTPException(status_code=500, detail=f"Verification error: {str(e)}")
354
+
355
+ @app.get("/")
356
+ async def index():
357
+ return {
358
+ "status": "Gemini Image Editing API with Razorpay is running",
359
+ "endpoints": {
360
+ "generate": "POST /generate",
361
+ "create_order": "POST /create-razorpay-order",
362
+ "verify_payment": "POST /verify-razorpay-payment"
363
+ }
364
+ }
365
+
366
+ @app.get("/health")
367
+ async def health_check():
368
+ return {
369
+ "status": "healthy",
370
+ "razorpay_configured": bool(RAZORPAY_KEY_ID and RAZORPAY_KEY_SECRET),
371
+ "supabase_configured": bool(SUPABASE_URL and SUPABASE_KEY),
372
+ "gemini_configured": bool(GEMINI_API_KEY)
373
+ }
374
+
375
+ if __name__ == "__main__":
376
+ import uvicorn
377
+ uvicorn.run(app, host="0.0.0.0", port=7860)