sachin commited on
Commit
de26925
·
1 Parent(s): 99bc65d

rever-proxed

Browse files
Files changed (1) hide show
  1. src/server/main.py +58 -1063
src/server/main.py CHANGED
@@ -1,1079 +1,74 @@
1
- import argparse
2
- import io
3
- from time import time
4
- from typing import List, Optional
5
- from abc import ABC, abstractmethod
6
 
7
- import uvicorn
8
- from fastapi import Depends, FastAPI, File, HTTPException, Query, Request, UploadFile, Header, Form
9
- from fastapi.middleware.cors import CORSMiddleware
10
- from fastapi.responses import JSONResponse, RedirectResponse, StreamingResponse
11
- from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
12
- from pydantic import BaseModel, field_validator, Field
13
- from slowapi import Limiter
14
- from slowapi.util import get_remote_address
15
- import requests
16
- from PIL import Image
17
- import base64
18
- from Crypto.Cipher import AES
19
-
20
- # Import from auth.py
21
- from utils.auth import get_current_user, get_current_user_with_admin, login, refresh_token, register, app_register, TokenResponse, Settings, LoginRequest, RegisterRequest, bearer_scheme
22
-
23
- # Import decryption utility
24
- from utils.crypto import decrypt_data
25
-
26
- # Assuming these are in your project structure
27
- from config.tts_config import SPEED, ResponseFormat, config as tts_config
28
- from config.logging_config import logger
29
-
30
- settings = Settings()
31
-
32
- # FastAPI app setup with enhanced docs
33
  app = FastAPI(
34
- title="Dhwani API",
35
- description="A multilingual AI-powered API supporting Indian languages for chat, text-to-speech, audio processing, and transcription. "
36
- "**Authentication Guide:** \n"
37
- "1. Obtain an access token by sending a POST request to `/v1/token` with `username` and `password`. \n"
38
- "2. Click the 'Authorize' button (top-right), enter your access token (e.g., `your_access_token`) in the 'bearerAuth' field, and click 'Authorize'. \n"
39
- "All protected endpoints require this token for access. \n",
40
  version="1.0.0",
41
  redirect_slashes=False,
42
- openapi_tags=[
43
- {"name": "Chat", "description": "Chat-related endpoints"},
44
- {"name": "Audio", "description": "Audio processing and TTS endpoints"},
45
- {"name": "Translation", "description": "Text translation endpoints"},
46
- {"name": "Authentication", "description": "User authentication and registration"},
47
- {"name": "Utility", "description": "General utility endpoints"},
48
- ],
49
- )
50
-
51
- app.add_middleware(
52
- CORSMiddleware,
53
- allow_origins=["*"],
54
- allow_credentials=False,
55
- allow_methods=["*"],
56
- allow_headers=["*"],
57
  )
58
 
59
- # Rate limiting based on user_id with fallback to IP
60
- async def get_user_id_for_rate_limit(request: Request):
61
- try:
62
- credentials = bearer_scheme(request)
63
- user_id = await get_current_user(credentials)
64
- return user_id
65
- except Exception:
66
- return get_remote_address(request)
67
-
68
- limiter = Limiter(key_func=get_user_id_for_rate_limit)
69
-
70
- # Request/Response Models
71
- class TranscriptionResponse(BaseModel):
72
- text: str = Field(..., description="Transcribed text from the audio")
73
-
74
- class Config:
75
- schema_extra = {"example": {"text": "Hello, how are you?"}}
76
-
77
- class TextGenerationResponse(BaseModel):
78
- text: str = Field(..., description="Generated text response")
79
-
80
- class Config:
81
- schema_extra = {"example": {"text": "Hi there, I'm doing great!"}}
82
-
83
- class AudioProcessingResponse(BaseModel):
84
- result: str = Field(..., description="Processed audio result")
85
-
86
- class Config:
87
- schema_extra = {"example": {"result": "Processed audio output"}}
88
-
89
- class ChatRequest(BaseModel):
90
- prompt: str = Field(..., description="Base64-encoded encrypted prompt (max 1000 characters after decryption)")
91
- src_lang: str = Field(..., description="Base64-encoded encrypted source language code")
92
- tgt_lang: str = Field(..., description="Base64-encoded encrypted target language code")
93
-
94
- @field_validator("prompt", "src_lang", "tgt_lang")
95
- def must_be_valid_base64(cls, v):
96
- try:
97
- base64.b64decode(v)
98
- except Exception:
99
- raise ValueError("Field must be valid base64-encoded data")
100
- return v
101
-
102
- class Config:
103
- schema_extra = {
104
- "example": {
105
- "prompt": "base64_encoded_encrypted_prompt",
106
- "src_lang": "base64_encoded_encrypted_kan_Knda",
107
- "tgt_lang": "base64_encoded_encrypted_kan_Knda"
108
- }
109
- }
110
-
111
- class ChatResponse(BaseModel):
112
- response: str = Field(..., description="Generated chat response")
113
-
114
- class Config:
115
- schema_extra = {"example": {"response": "Hi there, I'm doing great!"}}
116
-
117
- class TranslationRequest(BaseModel):
118
- sentences: List[str] = Field(..., description="List of base64-encoded encrypted sentences")
119
- src_lang: str = Field(..., description="Base64-encoded encrypted source language code")
120
- tgt_lang: str = Field(..., description="Base64-encoded encrypted target language code")
121
-
122
- @field_validator("sentences", "src_lang", "tgt_lang")
123
- def must_be_valid_base64(cls, v):
124
- if isinstance(v, list):
125
- for item in v:
126
- try:
127
- base64.b64decode(item)
128
- except Exception:
129
- raise ValueError("Each sentence must be valid base64-encoded data")
130
- else:
131
- try:
132
- base64.b64decode(v)
133
- except Exception:
134
- raise ValueError("Field must be valid base64-encoded data")
135
- return v
136
-
137
- class Config:
138
- schema_extra = {
139
- "example": {
140
- "sentences": ["base64_encoded_encrypted_hello", "base64_encoded_encrypted_how_are_you"],
141
- "src_lang": "base64_encoded_encrypted_en",
142
- "tgt_lang": "base64_encoded_encrypted_kan_Knda"
143
- }
144
- }
145
-
146
- class TranslationResponse(BaseModel):
147
- translations: List[str] = Field(..., description="Translated sentences")
148
-
149
- class Config:
150
- schema_extra = {"example": {"translations": ["ನಮಸ್ಕಾರ", "ನೀವು ಹೇಗಿದ್ದೀರಿ?"]}}
151
-
152
- class VisualQueryRequest(BaseModel):
153
- query: str = Field(..., description="Base64-encoded encrypted text query")
154
- src_lang: str = Field(..., description="Base64-encoded encrypted source language code")
155
- tgt_lang: str = Field(..., description="Base64-encoded encrypted target language code")
156
-
157
- @field_validator("query", "src_lang", "tgt_lang")
158
- def must_be_valid_base64(cls, v):
159
- try:
160
- base64.b64decode(v)
161
- except Exception:
162
- raise ValueError("Field must be valid base64-encoded data")
163
- return v
164
-
165
- class Config:
166
- schema_extra = {
167
- "example": {
168
- "query": "base64_encoded_encrypted_describe_image",
169
- "src_lang": "base64_encoded_encrypted_kan_Knda",
170
- "tgt_lang": "base64_encoded_encrypted_kan_Knda"
171
- }
172
- }
173
-
174
- class VisualQueryResponse(BaseModel):
175
- answer: str
176
-
177
- # TTS Service Interface
178
- class TTSService(ABC):
179
- @abstractmethod
180
- async def generate_speech(self, payload: dict) -> requests.Response:
181
- pass
182
 
183
- class ExternalTTSService(TTSService):
184
- async def generate_speech(self, payload: dict) -> requests.Response:
185
- try:
186
- return requests.post(
187
- settings.external_tts_url,
188
- json=payload,
189
- headers={"accept": "*/*", "Content-Type": "application/json"},
190
- stream=True,
191
- timeout=60
192
- )
193
- except requests.Timeout:
194
- logger.error("External TTS API timeout")
195
- raise HTTPException(status_code=504, detail="External TTS API timeout")
196
- except requests.RequestException as e:
197
- logger.error(f"External TTS API error: {str(e)}")
198
- raise HTTPException(status_code=502, detail=f"External TTS service error: {str(e)}")
199
-
200
- def get_tts_service() -> TTSService:
201
- return ExternalTTSService()
202
-
203
- # Endpoints with enhanced Swagger docs
204
- @app.get("/v1/health",
205
- summary="Check API Health",
206
- description="Returns the health status of the API and the current model in use.",
207
- tags=["Utility"],
208
- response_model=dict)
209
- async def health_check():
210
- return {"status": "healthy", "model": settings.llm_model_name}
211
-
212
- @app.get("/",
213
- summary="Redirect to Docs",
214
- description="Redirects to the Swagger UI documentation.",
215
- tags=["Utility"])
216
- async def home():
217
- return RedirectResponse(url="/docs")
218
-
219
- @app.post("/v1/token",
220
- response_model=TokenResponse,
221
- summary="User Login",
222
- description="Authenticate a user with encrypted email and device token to obtain an access token and refresh token. Requires X-Session-Key header.",
223
- tags=["Authentication"],
224
- responses={
225
- 200: {"description": "Successful login", "model": TokenResponse},
226
- 400: {"description": "Invalid encrypted data"},
227
- 401: {"description": "Invalid email or device token"}
228
- })
229
- async def token(
230
- login_request: LoginRequest,
231
- x_session_key: str = Header(..., alias="X-Session-Key")
232
- ):
233
- return await login(login_request, x_session_key)
234
-
235
- @app.post("/v1/refresh",
236
- response_model=TokenResponse,
237
- summary="Refresh Access Token",
238
- description="Generate a new access token and refresh token using an existing valid refresh token.",
239
- tags=["Authentication"],
240
- responses={
241
- 200: {"description": "New tokens issued", "model": TokenResponse},
242
- 401: {"description": "Invalid or expired refresh token"}
243
- })
244
- async def refresh(credentials: HTTPAuthorizationCredentials = Depends(bearer_scheme)):
245
- return await refresh_token(credentials)
246
-
247
- @app.post("/v1/register",
248
- response_model=TokenResponse,
249
- summary="Register New User (Admin Only)",
250
- description="Create a new user account in the `users` table. Only admin accounts can register new users (use 'admin' user with password 'admin54321' initially). Non-admin users are forbidden from modifying the users table.",
251
- tags=["Authentication"],
252
- responses={
253
- 200: {"description": "User registered successfully", "model": TokenResponse},
254
- 400: {"description": "Username already exists"},
255
- 401: {"description": "Unauthorized - Valid admin token required"},
256
- 403: {"description": "Forbidden - Admin access required"},
257
- 500: {"description": "Registration failed due to server error"}
258
- })
259
- async def register_user(
260
- register_request: RegisterRequest,
261
- current_user: str = Depends(get_current_user_with_admin)
262
- ):
263
- return await register(register_request, current_user)
264
-
265
- @app.post("/v1/app/register",
266
- response_model=TokenResponse,
267
- summary="Register New App User",
268
- description="Create a new user account for the mobile app in the `app_users` table using an encrypted email and device token. Returns an access token and refresh token. Rate limited to 5 requests per minute per IP. Requires X-Session-Key header.",
269
- tags=["Authentication"],
270
- responses={
271
- 200: {"description": "User registered successfully", "model": TokenResponse},
272
- 400: {"description": "Email already registered or invalid encrypted data"},
273
- 429: {"description": "Rate limit exceeded"}
274
- })
275
- @limiter.limit(settings.speech_rate_limit)
276
- async def app_register_user(
277
- request: Request,
278
- register_request: RegisterRequest,
279
- x_session_key: str = Header(..., alias="X-Session-Key")
280
- ):
281
- logger.info(f"App registration attempt")
282
- return await app_register(register_request, x_session_key)
283
-
284
- @app.post("/v1/audio/speech",
285
- summary="Generate Speech from Text",
286
- description="Convert encrypted text to speech using an external TTS service. Rate limited to 5 requests per minute per user. Requires authentication and X-Session-Key header.",
287
- tags=["Audio"],
288
- responses={
289
- 200: {"description": "Audio stream", "content": {"audio/mp3": {"example": "Binary audio data"}}},
290
- 400: {"description": "Invalid or empty input"},
291
- 401: {"description": "Unauthorized - Token required"},
292
- 429: {"description": "Rate limit exceeded"},
293
- 502: {"description": "External TTS service unavailable"},
294
- 504: {"description": "TTS service timeout"}
295
- })
296
- @limiter.limit(settings.speech_rate_limit)
297
- async def generate_audio(
298
- request: Request,
299
- input: str = Query(..., description="Base64-encoded encrypted text to convert to speech (max 1000 characters after decryption)"),
300
- response_format: str = Query("mp3", description="Audio format (ignored, defaults to mp3 for external API)"),
301
- credentials: HTTPAuthorizationCredentials = Depends(bearer_scheme),
302
- x_session_key: str = Header(..., alias="X-Session-Key"),
303
- tts_service: TTSService = Depends(get_tts_service)
304
- ):
305
- user_id = await get_current_user(credentials)
306
- session_key = base64.b64decode(x_session_key)
307
-
308
- # Decrypt input
309
- try:
310
- encrypted_input = base64.b64decode(input)
311
- decrypted_input = decrypt_data(encrypted_input, session_key).decode("utf-8")
312
- except Exception as e:
313
- logger.error(f"Input decryption failed: {str(e)}")
314
- raise HTTPException(status_code=400, detail="Invalid encrypted input")
315
-
316
- if not decrypted_input.strip():
317
- raise HTTPException(status_code=400, detail="Input cannot be empty")
318
- if len(decrypted_input) > 1000:
319
- raise HTTPException(status_code=400, detail="Decrypted input cannot exceed 1000 characters")
320
 
321
- logger.info("Processing speech request", extra={
322
- "endpoint": "/v1/audio/speech",
323
- "input_length": len(decrypted_input),
324
- "client_ip": get_remote_address(request),
325
- "user_id": user_id
326
- })
327
-
328
- payload = {
329
- "text": decrypted_input
330
- }
331
-
332
- try:
333
- response = await tts_service.generate_speech(payload)
334
- response.raise_for_status()
335
- except requests.HTTPError as e:
336
- logger.error(f"External TTS request failed: {str(e)}")
337
- raise HTTPException(status_code=502, detail=f"External TTS service error: {str(e)}")
338
 
 
339
  headers = {
340
- "Content-Disposition": "inline; filename=\"speech.mp3\"",
341
- "Cache-Control": "no-cache",
342
- "Content-Type": "audio/mp3"
343
  }
344
 
345
- return StreamingResponse(
346
- response.iter_content(chunk_size=8192),
347
- media_type="audio/mp3",
348
- headers=headers
349
- )
350
-
351
- @app.post("/v1/chat",
352
- response_model=ChatResponse,
353
- summary="Chat with AI",
354
- description="Generate a chat response from an encrypted prompt and encrypted language code. Rate limited to 100 requests per minute per user. Requires authentication and X-Session-Key header.",
355
- tags=["Chat"],
356
- responses={
357
- 200: {"description": "Chat response", "model": ChatResponse},
358
- 400: {"description": "Invalid prompt, encrypted data, or language code"},
359
- 401: {"description": "Unauthorized - Token required"},
360
- 429: {"description": "Rate limit exceeded"},
361
- 504: {"description": "Chat service timeout"}
362
- })
363
- @limiter.limit(settings.chat_rate_limit)
364
- async def chat(
365
- request: Request,
366
- chat_request: ChatRequest,
367
- credentials: HTTPAuthorizationCredentials = Depends(bearer_scheme),
368
- x_session_key: str = Header(..., alias="X-Session-Key")
369
- ):
370
- user_id = await get_current_user(credentials)
371
- session_key = base64.b64decode(x_session_key)
372
-
373
- # Decrypt the prompt
374
- try:
375
- encrypted_prompt = base64.b64decode(chat_request.prompt)
376
- decrypted_prompt = decrypt_data(encrypted_prompt, session_key).decode("utf-8")
377
- except Exception as e:
378
- logger.error(f"Prompt decryption failed: {str(e)}")
379
- raise HTTPException(status_code=400, detail="Invalid encrypted prompt")
380
-
381
- # Decrypt the source language
382
- try:
383
- encrypted_src_lang = base64.b64decode(chat_request.src_lang)
384
- decrypted_src_lang = decrypt_data(encrypted_src_lang, session_key).decode("utf-8")
385
- except Exception as e:
386
- logger.error(f"Source language decryption failed: {str(e)}")
387
- raise HTTPException(status_code=400, detail="Invalid encrypted source language")
388
-
389
- # Decrypt the target language
390
- try:
391
- encrypted_tgt_lang = base64.b64decode(chat_request.tgt_lang)
392
- decrypted_tgt_lang = decrypt_data(encrypted_tgt_lang, session_key).decode("utf-8")
393
- except Exception as e:
394
- logger.error(f"Target language decryption failed: {str(e)}")
395
- raise HTTPException(status_code=400, detail="Invalid encrypted target language")
396
-
397
- if not decrypted_prompt:
398
- raise HTTPException(status_code=400, detail="Prompt cannot be empty")
399
- if len(decrypted_prompt) > 1000:
400
- raise HTTPException(status_code=400, detail="Decrypted prompt cannot exceed 1000 characters")
401
-
402
- logger.info(f"Received prompt: {decrypted_prompt}, src_lang: {decrypted_src_lang}, user_id: {user_id}")
403
-
404
- try:
405
- external_url = f"{settings.external_api_base_url}/v1/chat"
406
- payload = {
407
- "prompt": decrypted_prompt,
408
- "src_lang": decrypted_src_lang,
409
- "tgt_lang": decrypted_tgt_lang
410
- }
411
-
412
- response = requests.post(
413
- external_url,
414
- json=payload,
415
- headers={
416
- "accept": "application/json",
417
- "Content-Type": "application/json"
418
- },
419
- timeout=60
420
- )
421
- response.raise_for_status()
422
-
423
- response_data = response.json()
424
- response_text = response_data.get("response", "")
425
- logger.info(f"Generated Chat response from external API: {response_text}")
426
- return ChatResponse(response=response_text)
427
-
428
- except requests.Timeout:
429
- logger.error("External chat API request timed out")
430
- raise HTTPException(status_code=504, detail="Chat service timeout")
431
- except requests.RequestException as e:
432
- logger.error(f"Error calling external chat API: {str(e)}")
433
- raise HTTPException(status_code=500, detail=f"Chat failed: {str(e)}")
434
- except Exception as e:
435
- logger.error(f"Error processing request: {str(e)}")
436
- raise HTTPException(status_code=500, detail=f"An error occurred: {str(e)}")
437
-
438
- @app.post("/v1/process_audio/",
439
- response_model=AudioProcessingResponse,
440
- summary="Process Audio File",
441
- description="Process an uploaded audio file in the specified language. Rate limited to 100 requests per minute per user. Requires authentication.",
442
- tags=["Audio"],
443
- responses={
444
- 200: {"description": "Processed result", "model": AudioProcessingResponse},
445
- 401: {"description": "Unauthorized - Token required"},
446
- 429: {"description": "Rate limit exceeded"},
447
- 504: {"description": "Audio processing timeout"}
448
- })
449
- @limiter.limit(settings.chat_rate_limit)
450
- async def process_audio(
451
- request: Request,
452
- file: UploadFile = File(..., description="Audio file to process"),
453
- language: str = Query(..., description="Base64-encoded encrypted language of the audio (kannada, hindi, tamil after decryption)"),
454
- credentials: HTTPAuthorizationCredentials = Depends(bearer_scheme),
455
- x_session_key: str = Header(..., alias="X-Session-Key")
456
- ):
457
- user_id = await get_current_user(credentials)
458
- session_key = base64.b64decode(x_session_key)
459
-
460
- # Decrypt the language
461
- try:
462
- encrypted_language = base64.b64decode(language)
463
- decrypted_language = decrypt_data(encrypted_language, session_key).decode("utf-8")
464
- except Exception as e:
465
- logger.error(f"Language decryption failed: {str(e)}")
466
- raise HTTPException(status_code=400, detail="Invalid encrypted language")
467
-
468
- # Validate language
469
- allowed_languages = ["kannada", "hindi", "tamil"]
470
- if decrypted_language not in allowed_languages:
471
- raise HTTPException(status_code=400, detail=f"Language must be one of {allowed_languages}")
472
-
473
- logger.info("Processing audio processing request", extra={
474
- "endpoint": "/v1/process_audio",
475
- "filename": file.filename,
476
- "language": decrypted_language,
477
- "client_ip": get_remote_address(request),
478
- "user_id": user_id
479
- })
480
-
481
- start_time = time()
482
- try:
483
- file_content = await file.read()
484
- files = {"file": (file.filename, file_content, file.content_type)}
485
-
486
- external_url = f"{settings.external_api_base_url}/process_audio/?language={decrypted_language}"
487
- response = requests.post(
488
- external_url,
489
- files=files,
490
- headers={"accept": "application/json"},
491
- timeout=60
492
- )
493
- response.raise_for_status()
494
-
495
- processed_result = response.json().get("result", "")
496
- logger.info(f"Audio processing completed in {time() - start_time:.2f} seconds")
497
- return AudioProcessingResponse(result=processed_result)
498
-
499
- except requests.Timeout:
500
- raise HTTPException(status_code=504, detail="Audio processing service timeout")
501
- except requests.RequestException as e:
502
- logger.error(f"Audio processing request failed: {str(e)}")
503
- raise HTTPException(status_code=500, detail=f"Audio processing failed: {str(e)}")
504
-
505
- @app.post("/v1/transcribe/",
506
- response_model=TranscriptionResponse,
507
- summary="Transcribe Audio File",
508
- description="Transcribe an encrypted audio file into text in the specified encrypted language. Requires authentication and X-Session-Key header.",
509
- tags=["Audio"],
510
- responses={
511
- 200: {"description": "Transcription result", "model": TranscriptionResponse},
512
- 400: {"description": "Invalid encrypted audio or language"},
513
- 401: {"description": "Unauthorized - Token required"},
514
- 504: {"description": "Transcription service timeout"}
515
- })
516
- async def transcribe_audio(
517
- file: UploadFile = File(..., description="Encrypted audio file to transcribe"),
518
- language: str = Query(..., description="Base64-encoded encrypted language of the audio (kannada, hindi, tamil after decryption)"),
519
- credentials: HTTPAuthorizationCredentials = Depends(bearer_scheme),
520
- x_session_key: str = Header(..., alias="X-Session-Key")
521
- ):
522
- user_id = await get_current_user(credentials)
523
- session_key = base64.b64decode(x_session_key)
524
-
525
- # Decrypt the language
526
- try:
527
- encrypted_language = base64.b64decode(language)
528
- decrypted_language = decrypt_data(encrypted_language, session_key).decode("utf-8")
529
- except Exception as e:
530
- logger.error(f"Language decryption failed: {str(e)}")
531
- raise HTTPException(status_code=400, detail="Invalid encrypted language")
532
 
533
- # Validate language
534
- allowed_languages = ["kannada", "hindi", "tamil"]
535
- if decrypted_language not in allowed_languages:
536
- raise HTTPException(status_code=400, detail=f"Language must be one of {allowed_languages}")
537
-
538
- start_time = time()
539
- try:
540
- encrypted_content = await file.read()
541
- file_content = decrypt_data(encrypted_content, session_key)
542
- files = {"file": (file.filename, file_content, file.content_type)}
543
-
544
- external_url = f"{settings.external_api_base_url}/v1/transcribe/?language={decrypted_language}"
545
- response = requests.post(
546
- external_url,
547
- files=files,
548
- headers={"accept": "application/json"},
549
- timeout=60
550
- )
551
- response.raise_for_status()
552
-
553
- transcription = response.json().get("text", "")
554
- logger.info(f"Transcription completed in {time() - start_time:.2f} seconds")
555
- return TranscriptionResponse(text=transcription)
556
-
557
- except HTTPException:
558
- raise
559
- except requests.Timeout:
560
- logger.error("Transcription service timed out")
561
- raise HTTPException(status_code=504, detail="Transcription service timeout")
562
- except requests.RequestException as e:
563
- logger.error(f"Transcription request failed: {str(e)}")
564
- raise HTTPException(status_code=500, detail=f"Transcription failed: {str(e)}")
565
-
566
- @app.post("/v1/chat_v2",
567
- response_model=TranscriptionResponse,
568
- summary="Chat with Image (V2)",
569
- description="Generate a response from a text prompt and optional image. Rate limited to 100 requests per minute per user. Requires authentication.",
570
- tags=["Chat"],
571
- responses={
572
- 200: {"description": "Chat response", "model": TranscriptionResponse},
573
- 400: {"description": "Invalid prompt"},
574
- 401: {"description": "Unauthorized - Token required"},
575
- 429: {"description": "Rate limit exceeded"}
576
- })
577
- @limiter.limit(settings.chat_rate_limit)
578
- async def chat_v2(
579
- request: Request,
580
- prompt: str = Form(..., description="Text prompt for chat"),
581
- image: UploadFile = File(default=None, description="Optional image to accompany the prompt"),
582
- credentials: HTTPAuthorizationCredentials = Depends(bearer_scheme)
583
- ):
584
- user_id = await get_current_user(credentials)
585
- if not prompt:
586
- raise HTTPException(status_code=400, detail="Prompt cannot be empty")
587
-
588
- logger.info("Processing chat_v2 request", extra={
589
- "endpoint": "/v1/chat_v2",
590
- "prompt_length": len(prompt),
591
- "has_image": bool(image),
592
- "client_ip": get_remote_address(request),
593
- "user_id": user_id
594
- })
595
-
596
- try:
597
- image_data = Image.open(await image.read()) if image else None
598
- response_text = f"Processed: {prompt}" + (" with image" if image_data else "")
599
- return TranscriptionResponse(text=response_text)
600
- except Exception as e:
601
- logger.error(f"Chat_v2 processing failed: {str(e)}", exc_info=True)
602
- raise HTTPException(status_code=500, detail=f"An error occurred: {str(e)}")
603
-
604
- @app.post("/v1/translate",
605
- response_model=TranslationResponse,
606
- summary="Translate Text",
607
- description="Translate a list of base64-encoded encrypted sentences from an encrypted source to an encrypted target language. Requires authentication and X-Session-Key header.",
608
- tags=["Translation"],
609
- responses={
610
- 200: {"description": "Translation result", "model": TranslationResponse},
611
- 400: {"description": "Invalid encrypted sentences or languages"},
612
- 401: {"description": "Unauthorized - Token required"},
613
- 500: {"description": "Translation service error"},
614
- 504: {"description": "Translation service timeout"}
615
- })
616
- async def translate(
617
- request: TranslationRequest,
618
- credentials: HTTPAuthorizationCredentials = Depends(bearer_scheme),
619
- x_session_key: str = Header(..., alias="X-Session-Key")
620
- ):
621
- user_id = await get_current_user(credentials)
622
- try:
623
- session_key = base64.b64decode(x_session_key)
624
- except Exception as e:
625
- logger.error(f"Invalid X-Session-Key: {str(e)}")
626
- raise HTTPException(status_code=400, detail="Invalid session key")
627
-
628
- # Decrypt sentences
629
- decrypted_sentences = []
630
- for sentence in request.sentences:
631
  try:
632
- encrypted_sentence = base64.b64decode(sentence)
633
- decrypted_sentence = decrypt_data(encrypted_sentence, session_key).decode("utf-8")
634
- if not decrypted_sentence.strip():
635
- raise ValueError("Decrypted sentence is empty")
636
- decrypted_sentences.append(decrypted_sentence)
637
- except Exception as e:
638
- logger.error(f"Sentence decryption failed: {str(e)}")
639
- raise HTTPException(status_code=400, detail=f"Invalid encrypted sentence: {str(e)}")
640
-
641
- # Decrypt source language
642
- try:
643
- encrypted_src_lang = base64.b64decode(request.src_lang)
644
- decrypted_src_lang = decrypt_data(encrypted_src_lang, session_key).decode("utf-8")
645
- if not decrypted_src_lang.strip():
646
- raise ValueError("Decrypted source language is empty")
647
- except Exception as e:
648
- logger.error(f"Source language decryption failed: {str(e)}")
649
- raise HTTPException(status_code=400, detail=f"Invalid encrypted source language: {str(e)}")
650
-
651
- # Decrypt target language
652
- try:
653
- encrypted_tgt_lang = base64.b64decode(request.tgt_lang)
654
- decrypted_tgt_lang = decrypt_data(encrypted_tgt_lang, session_key).decode("utf-8")
655
- if not decrypted_tgt_lang.strip():
656
- raise ValueError("Decrypted target language is empty")
657
- except Exception as e:
658
- logger.error(f"Target language decryption failed: {str(e)}")
659
- raise HTTPException(status_code=400, detail=f"Invalid encrypted target language: {str(e)}")
660
-
661
- # Validate language codes
662
- supported_languages = [
663
- "eng_Latn", "hin_Deva", "kan_Knda", "tam_Taml", "mal_Mlym", "tel_Telu",
664
- "deu_Latn", "fra_Latn", "nld_Latn", "spa_Latn", "ita_Latn", "por_Latn",
665
- "rus_Cyrl", "pol_Latn"
666
- ]
667
- if decrypted_src_lang not in supported_languages or decrypted_tgt_lang not in supported_languages:
668
- logger.error(f"Unsupported language codes: src={decrypted_src_lang}, tgt={decrypted_tgt_lang}")
669
- raise HTTPException(status_code=400, detail=f"Unsupported language codes: src={decrypted_src_lang}, tgt={decrypted_tgt_lang}")
670
-
671
- logger.info(f"Received translation request: {len(decrypted_sentences)} sentences, src_lang: {decrypted_src_lang}, tgt_lang: {decrypted_tgt_lang}, user_id: {user_id}")
672
-
673
- external_url = f"{settings.external_api_base_url}/v1/translate"
674
-
675
- payload = {
676
- "sentences": decrypted_sentences,
677
- "src_lang": decrypted_src_lang,
678
- "tgt_lang": decrypted_tgt_lang
679
- }
680
-
681
- try:
682
- response = requests.post(
683
- external_url,
684
- json=payload,
685
- headers={
686
- "accept": "application/json",
687
- "Content-Type": "application/json"
688
- },
689
- timeout=60
690
- )
691
- response.raise_for_status()
692
-
693
- response_data = response.json()
694
- translations = response_data.get("translations", [])
695
-
696
- if not translations or len(translations) != len(decrypted_sentences):
697
- logger.warning(f"Unexpected response format: {response_data}")
698
- raise HTTPException(status_code=500, detail="Invalid response from translation service")
699
-
700
- logger.info(f"Translation successful: {translations}")
701
- return TranslationResponse(translations=translations)
702
-
703
- except requests.Timeout:
704
- logger.error("Translation request timed out")
705
- raise HTTPException(status_code=504, detail="Translation service timeout")
706
- except requests.RequestException as e:
707
- logger.error(f"Error during translation: {str(e)}")
708
- raise HTTPException(status_code=500, detail=f"Translation failed: {str(e)}")
709
- except ValueError as e:
710
- logger.error(f"Invalid JSON response: {str(e)}")
711
- raise HTTPException(status_code=500, detail="Invalid response format from translation service")
712
-
713
- class PDFTextExtractionResponse(BaseModel):
714
- page_content: str = Field(..., description="Extracted text from the specified PDF page")
715
-
716
- class Config:
717
- schema_extra = {
718
- "example": {
719
- "page_content": "Google Interview Preparation Guide\nCustomer Engineer Specialist\n\nOur hiring process\n..."
720
- }
721
- }
722
-
723
- @app.post("/v1/extract-text",
724
- response_model=PDFTextExtractionResponse,
725
- summary="Extract Text from PDF",
726
- description="Extract text from a specified page of an encrypted PDF file by calling an external API. Rate limited to 100 requests per minute per user. Requires authentication and X-Session-Key header.",
727
- tags=["PDF"],
728
- responses={
729
- 200: {"description": "Extracted text", "model": PDFTextExtractionResponse},
730
- 400: {"description": "Invalid encrypted PDF or page number"},
731
- 401: {"description": "Unauthorized - Token required"},
732
- 429: {"description": "Rate limit exceeded"},
733
- 500: {"description": "External API error"},
734
- 504: {"description": "External API timeout"}
735
- })
736
- @limiter.limit(settings.chat_rate_limit)
737
- async def extract_text(
738
- request: Request,
739
- file: UploadFile = File(..., description="Encrypted PDF file to extract text from"),
740
- page_number: int = Query(1, description="Page number to extract text from (1-based indexing)"),
741
- credentials: HTTPAuthorizationCredentials = Depends(bearer_scheme),
742
- x_session_key: str = Header(..., alias="X-Session-Key")
743
- ):
744
- user_id = await get_current_user(credentials)
745
- session_key = base64.b64decode(x_session_key)
746
-
747
- # Validate page number
748
- if page_number < 1:
749
- raise HTTPException(status_code=400, detail="Page number must be at least 1")
750
-
751
- # Decrypt PDF content
752
- try:
753
- encrypted_content = await file.read()
754
- decrypted_content = decrypt_data(encrypted_content, session_key)
755
- except Exception as e:
756
- logger.error(f"PDF decryption failed: {str(e)}")
757
- raise HTTPException(status_code=400, detail="Invalid encrypted PDF")
758
-
759
- logger.info("Processing PDF text extraction request", extra={
760
- "endpoint": "/v1/extract-text",
761
- "file_name": file.filename,
762
- "page_number": page_number,
763
- "client_ip": get_remote_address(request),
764
- "user_id": user_id
765
- })
766
-
767
- start_time = time()
768
- try:
769
- # Call external API
770
- external_url = f"{settings.external_pdf_api_base_url}/extract-text/?page_number={page_number}"
771
- files = {"file": (file.filename, decrypted_content, file.content_type)}
772
-
773
- response = requests.post(
774
- external_url,
775
- files=files,
776
- headers={"accept": "application/json"},
777
- timeout=60
778
- )
779
- response.raise_for_status()
780
-
781
- response_data = response.json()
782
- extracted_text = response_data.get("page_content", "")
783
- if not extracted_text:
784
- logger.warning("No page_content found in external API response")
785
- extracted_text = ""
786
-
787
- logger.info(f"PDF text extraction completed in {time() - start_time:.2f} seconds")
788
- return PDFTextExtractionResponse(page_content=extracted_text.strip())
789
-
790
- except requests.Timeout:
791
- logger.error("External PDF extraction API timed out")
792
- raise HTTPException(status_code=504, detail="External API timeout")
793
- except requests.RequestException as e:
794
- logger.error(f"External PDF extraction API error: {str(e)}")
795
- raise HTTPException(status_code=500, detail=f"External API error: {str(e)}")
796
- except ValueError as e:
797
- logger.error(f"Invalid JSON response from external API: {str(e)}")
798
- raise HTTPException(status_code=500, detail="Invalid response format from external API")
799
-
800
- @app.post("/v1/visual_query",
801
- response_model=VisualQueryResponse,
802
- summary="Visual Query with Image",
803
- description="Process a visual query with an encrypted text query, encrypted image, and encrypted language codes provided in a JSON body named 'data'. Rate limited to 100 requests per minute per user. Requires authentication and X-Session-Key header.",
804
- tags=["Chat"],
805
- responses={
806
- 200: {"description": "Query response", "model": VisualQueryResponse},
807
- 400: {"description": "Invalid query, encrypted data, or language codes"},
808
- 401: {"description": "Unauthorized - Token required"},
809
- 422: {"description": "Validation error in request body"},
810
- 429: {"description": "Rate limit exceeded"},
811
- 504: {"description": "Visual query service timeout"}
812
- })
813
- @limiter.limit(settings.chat_rate_limit)
814
- async def visual_query(
815
- request: Request,
816
- data: str = Form(..., description="JSON string containing encrypted query, src_lang, and tgt_lang"),
817
- file: UploadFile = File(..., description="Encrypted image file to analyze"),
818
- credentials: HTTPAuthorizationCredentials = Depends(bearer_scheme),
819
- x_session_key: str = Header(..., alias="X-Session-Key")
820
- ):
821
- user_id = await get_current_user(credentials)
822
- session_key = base64.b64decode(x_session_key)
823
-
824
- # Parse and validate JSON data
825
- try:
826
- import json
827
- visual_query_request = VisualQueryRequest.parse_raw(data)
828
- logger.info(f"Received visual query JSON: {data}")
829
- except Exception as e:
830
- logger.error(f"Failed to parse JSON data: {str(e)}")
831
- raise HTTPException(status_code=422, detail=f"Invalid JSON data: {str(e)}")
832
-
833
- # Decrypt query
834
- try:
835
- encrypted_query = base64.b64decode(visual_query_request.query)
836
- decrypted_query = decrypt_data(encrypted_query, session_key).decode("utf-8")
837
- except Exception as e:
838
- logger.error(f"Query decryption failed: {str(e)}")
839
- raise HTTPException(status_code=400, detail="Invalid encrypted query")
840
-
841
- # Decrypt source language
842
- try:
843
- encrypted_src_lang = base64.b64decode(visual_query_request.src_lang)
844
- decrypted_src_lang = decrypt_data(encrypted_src_lang, session_key).decode("utf-8")
845
- except Exception as e:
846
- logger.error(f"Source language decryption failed: {str(e)}")
847
- raise HTTPException(status_code=400, detail="Invalid encrypted source language")
848
-
849
- # Decrypt target language
850
- try:
851
- encrypted_tgt_lang = base64.b64decode(visual_query_request.tgt_lang)
852
- decrypted_tgt_lang = decrypt_data(encrypted_tgt_lang, session_key).decode("utf-8")
853
- except Exception as e:
854
- logger.error(f"Target language decryption failed: {str(e)}")
855
- raise HTTPException(status_code=400, detail="Invalid encrypted target language")
856
-
857
- if not decrypted_query.strip():
858
- raise HTTPException(status_code=400, detail="Query cannot be empty")
859
- if len(decrypted_query) > 1000:
860
- raise HTTPException(status_code=400, detail="Decrypted query cannot exceed 1000 characters")
861
-
862
- # Decrypt image
863
- try:
864
- encrypted_content = await file.read()
865
- decrypted_content = decrypt_data(encrypted_content, session_key)
866
- except Exception as e:
867
- logger.error(f"Image decryption failed: {str(e)}")
868
- raise HTTPException(status_code=400, detail="Invalid encrypted image")
869
-
870
- logger.info("Processing visual query request", extra={
871
- "endpoint": "/v1/visual_query",
872
- "query_length": len(decrypted_query),
873
- "file_name": file.filename,
874
- "client_ip": get_remote_address(request),
875
- "user_id": user_id,
876
- "src_lang": decrypted_src_lang,
877
- "tgt_lang": decrypted_tgt_lang
878
- })
879
-
880
- external_url = f"{settings.external_api_base_url}/v1/visual_query/?src_lang={decrypted_src_lang}&tgt_lang={decrypted_tgt_lang}"
881
-
882
- try:
883
- files = {"file": (file.filename, decrypted_content, file.content_type)}
884
- data = {"query": decrypted_query}
885
-
886
- response = requests.post(
887
- external_url,
888
- files=files,
889
- data=data,
890
- headers={"accept": "application/json"},
891
- timeout=60
892
- )
893
- response.raise_for_status()
894
-
895
- response_data = response.json()
896
- answer = response_data.get("answer", "")
897
-
898
- if not answer:
899
- logger.warning(f"Empty answer received from external API: {response_data}")
900
- raise HTTPException(status_code=500, detail="No answer provided by visual query service")
901
-
902
- logger.info(f"Visual query successful: {answer}")
903
- return VisualQueryResponse(answer=answer)
904
-
905
- except requests.Timeout:
906
- logger.error("Visual query request timed out")
907
- raise HTTPException(status_code=504, detail="Visual query service timeout")
908
- except requests.RequestException as e:
909
- logger.error(f"Error during visual query: {str(e)}")
910
- raise HTTPException(status_code=500, detail=f"Visual query failed: {str(e)}")
911
- except ValueError as e:
912
- logger.error(f"Invalid JSON response: {str(e)}")
913
- raise HTTPException(status_code=500, detail="Invalid response format from visual query service")
914
-
915
- from enum import Enum
916
-
917
- class SupportedLanguage(str, Enum):
918
- kannada = "kannada"
919
- hindi = "hindi"
920
- tamil = "tamil"
921
-
922
- @app.post("/v1/speech_to_speech",
923
- summary="Speech-to-Speech Conversion",
924
- description="Convert input encrypted speech to processed speech in the specified encrypted language by calling an external speech-to-speech API. Rate limited to 5 requests per minute per user. Requires authentication and X-Session-Key header.",
925
- tags=["Audio"],
926
- responses={
927
- 200: {"description": "Audio stream", "content": {"audio/mp3": {"example": "Binary audio data"}}},
928
- 400: {"description": "Invalid input, encrypted audio, or language"},
929
- 401: {"description": "Unauthorized - Token required"},
930
- 429: {"description": "Rate limit exceeded"},
931
- 504: {"description": "External API timeout"},
932
- 500: {"description": "External API error"}
933
- })
934
- @limiter.limit(settings.speech_rate_limit)
935
- async def speech_to_speech(
936
- request: Request,
937
- file: UploadFile = File(..., description="Encrypted audio file to process"),
938
- language: str = Query(..., description="Base64-encoded encrypted language of the audio (kannada, hindi, tamil after decryption)"),
939
- credentials: HTTPAuthorizationCredentials = Depends(bearer_scheme),
940
- x_session_key: str = Header(..., alias="X-Session-Key")
941
- ) -> StreamingResponse:
942
- user_id = await get_current_user(credentials)
943
- session_key = base64.b64decode(x_session_key)
944
-
945
- # Decrypt the language
946
- try:
947
- encrypted_language = base64.b64decode(language)
948
- decrypted_language = decrypt_data(encrypted_language, session_key).decode("utf-8")
949
- except Exception as e:
950
- logger.error(f"Language decryption failed: {str(e)}")
951
- raise HTTPException(status_code=400, detail="Invalid encrypted language")
952
-
953
- # Validate language
954
- allowed_languages = [lang.value for lang in SupportedLanguage]
955
- if decrypted_language not in allowed_languages:
956
- raise HTTPException(status_code=400, detail=f"Language must be one of {allowed_languages}")
957
-
958
- logger.info("Processing speech-to-speech request", extra={
959
- "endpoint": "/v1/speech_to_speech",
960
- "audio_filename": file.filename,
961
- "language": decrypted_language,
962
- "client_ip": get_remote_address(request),
963
- "user_id": user_id
964
- })
965
-
966
- try:
967
- encrypted_content = await file.read()
968
- file_content = decrypt_data(encrypted_content, session_key)
969
- files = {"file": (file.filename, file_content, file.content_type)}
970
- external_url = f"{settings.external_api_base_url}/v1/speech_to_speech?language={decrypted_language}"
971
-
972
- response = requests.post(
973
- external_url,
974
- files=files,
975
- headers={"accept": "application/json"},
976
- stream=True,
977
- timeout=60
978
- )
979
- response.raise_for_status()
980
-
981
- headers = {
982
- "Content-Disposition": f"inline; filename=\"speech.mp3\"",
983
- "Cache-Control": "no-cache",
984
- "Content-Type": "audio/mp3"
985
- }
986
-
987
- return StreamingResponse(
988
- response.iter_content(chunk_size=8192),
989
- media_type="audio/mp3",
990
- headers=headers
991
- )
992
-
993
- except requests.Timeout:
994
- logger.error("External speech-to-speech API timed out", extra={"user_id": user_id})
995
- raise HTTPException(status_code=504, detail="External API timeout")
996
- except requests.RequestException as e:
997
- logger.error(f"External speech-to-speech API error: {str(e)}", extra={"user_id": user_id})
998
- raise HTTPException(status_code=500, detail=f"External API error: {str(e)}")
999
-
1000
-
1001
- @app.post("/v1/speech_to_speech_v2",
1002
- summary="Speech-to-Speech Conversion",
1003
- description="Convert input encrypted speech to processed speech in the specified encrypted language by calling an external speech-to-speech API. Rate limited to 5 requests per minute per user. Requires authentication and X-Session-Key header.",
1004
- tags=["Audio"],
1005
- responses={
1006
- 200: {"description": "Audio stream", "content": {"audio/mp3": {"example": "Binary audio data"}}},
1007
- 400: {"description": "Invalid input, encrypted audio, or language"},
1008
- 401: {"description": "Unauthorized - Token required"},
1009
- 429: {"description": "Rate limit exceeded"},
1010
- 504: {"description": "External API timeout"},
1011
- 500: {"description": "External API error"}
1012
- })
1013
- async def speech_to_speech_v2(
1014
- request: Request,
1015
- file: UploadFile = File(..., description="Encrypted audio file to process"),
1016
- language: str = Query(..., description="Base64-encoded encrypted language of the audio (kannada, hindi, tamil after decryption)"),
1017
- ) -> StreamingResponse:
1018
-
1019
- # Decrypt the language
1020
- try:
1021
- encrypted_language = language
1022
- decrypted_language = encrypted_language
1023
- except Exception as e:
1024
- logger.error(f"Language decryption failed: {str(e)}")
1025
- raise HTTPException(status_code=400, detail="Invalid encrypted language")
1026
-
1027
- # Validate language
1028
- allowed_languages = [lang.value for lang in SupportedLanguage]
1029
- if decrypted_language not in allowed_languages:
1030
- raise HTTPException(status_code=400, detail=f"Language must be one of {allowed_languages}")
1031
-
1032
- logger.info("Processing speech-to-speech request", extra={
1033
- "endpoint": "/v1/speech_to_speech",
1034
- "audio_filename": file.filename,
1035
- "language": decrypted_language,
1036
- "client_ip": get_remote_address(request),
1037
- })
1038
-
1039
- try:
1040
- encrypted_content = await file.read()
1041
- file_content = encrypted_content
1042
- files = {"file": (file.filename, file_content, file.content_type)}
1043
- external_url = f"{settings.external_api_base_url}/v1/speech_to_speech?language={decrypted_language}"
1044
-
1045
- response = requests.post(
1046
- external_url,
1047
- files=files,
1048
- headers={"accept": "application/json"},
1049
- stream=True,
1050
- timeout=60
1051
- )
1052
- response.raise_for_status()
1053
-
1054
- headers = {
1055
- "Content-Disposition": f"inline; filename=\"speech.mp3\"",
1056
- "Cache-Control": "no-cache",
1057
- "Content-Type": "audio/mp3"
1058
- }
1059
-
1060
- return StreamingResponse(
1061
- response.iter_content(chunk_size=8192),
1062
- media_type="audio/mp3",
1063
- headers=headers
1064
- )
1065
-
1066
- except requests.Timeout:
1067
- logger.error("External speech-to-speech API timed out")
1068
- raise HTTPException(status_code=504, detail="External API timeout")
1069
- except requests.RequestException as e:
1070
- logger.error(f"External speech-to-speech API error: {str(e)}")
1071
- raise HTTPException(status_code=500, detail=f"External API error: {str(e)}")
1072
-
1073
 
1074
  if __name__ == "__main__":
1075
- parser = argparse.ArgumentParser(description="Run the FastAPI server.")
1076
- parser.add_argument("--port", type=int, default=settings.port, help="Port to run the server on.")
1077
- parser.add_argument("--host", type=str, default=settings.host, help="Host to run the server on.")
1078
- args = parser.parse_args()
1079
- uvicorn.run(app, host=args.host, port=args.port)
 
1
+ from fastapi import FastAPI, Request, HTTPException
2
+ from fastapi.responses import StreamingResponse
3
+ import httpx
 
 
4
 
5
+ # FastAPI app setup
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  app = FastAPI(
7
+ title="Dhwani API Proxy",
8
+ description="A proxy that forwards all requests to a target server.",
 
 
 
 
9
  version="1.0.0",
10
  redirect_slashes=False,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  )
12
 
13
+ # Target server to forward requests to
14
+ TARGET_SERVER = "https://dwani-dwani-server.hf.space" # Replace with the actual target server IP and port
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
+ # Catch-all route to forward all requests
17
+ @app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"])
18
+ async def proxy(request: Request, path: str):
19
+ # Construct the target URL
20
+ target_url = f"{TARGET_SERVER}/{path}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
 
22
+ # Prepare query parameters
23
+ query_params = dict(request.query_params)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
 
25
+ # Prepare headers, excluding FastAPI-specific headers
26
  headers = {
27
+ key: value for key, value in request.headers.items()
28
+ if key.lower() not in ("host", "connection", "accept-encoding")
 
29
  }
30
 
31
+ # Get the request body, if any
32
+ body = await request.body()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
 
34
+ # Create an HTTPX client for making the request
35
+ async with httpx.AsyncClient(timeout=60) as client:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  try:
37
+ # Forward the request to the target server
38
+ response = await client.request(
39
+ method=request.method,
40
+ url=target_url,
41
+ params=query_params,
42
+ headers=headers,
43
+ content=body,
44
+ follow_redirects=False
45
+ )
46
+
47
+ # Handle streaming responses (e.g., for audio endpoints)
48
+ if response.is_streamed:
49
+ return StreamingResponse(
50
+ response.aiter_raw(),
51
+ status_code=response.status_code,
52
+ headers=dict(response.headers),
53
+ media_type=response.headers.get("content-type", "application/octet-stream")
54
+ )
55
+
56
+ # Handle non-streaming responses
57
+ content = response.content
58
+ return StreamingResponse(
59
+ content=iter([content]),
60
+ status_code=response.status_code,
61
+ headers=dict(response.headers),
62
+ media_type=response.headers.get("content-type", "application/json")
63
+ )
64
+
65
+ except httpx.TimeoutException:
66
+ raise HTTPException(status_code=504, detail="Target server timeout")
67
+ except httpx.HTTPStatusError as e:
68
+ raise HTTPException(status_code=e.response.status_code, detail=str(e))
69
+ except httpx.RequestError as e:
70
+ raise HTTPException(status_code=500, detail=f"Failed to forward request: {str(e)}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
 
72
  if __name__ == "__main__":
73
+ import uvicorn
74
+ uvicorn.run(app, host="0.0.0.0", port=7860) # Run the proxy server