Berzelius255 commited on
Commit
fceef39
·
verified ·
1 Parent(s): eb97a5e

Update app/api/v1/endpoints/tts.py

Browse files
Files changed (1) hide show
  1. app/api/v1/endpoints/tts.py +110 -11
app/api/v1/endpoints/tts.py CHANGED
@@ -1,4 +1,4 @@
1
- from fastapi import APIRouter, HTTPException, Depends
2
  from fastapi.responses import StreamingResponse
3
  from sqlalchemy.orm import Session
4
  from ....services.tts import TTSService
@@ -6,25 +6,114 @@ from ....schemas.tts import TTSRequest, TTSResponse
6
  from app.models.job import ProcessingJob
7
  from app.schemas.job import JobCreate
8
  from app.core.config import get_db
9
- from typing import Optional
 
 
 
10
 
11
  router = APIRouter()
12
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  @router.post("/", response_model=TTSResponse)
14
  async def text_to_speech(
15
- request: TTSRequest,
 
16
  db: Session = Depends(get_db)
17
  ):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  try:
 
 
 
 
19
  service = TTSService()
20
- audio_buffer = service.generate_audio(request.text)
21
 
22
- # Save to database
23
  job = JobCreate(
24
  job_type="tts",
25
  input_file=None,
26
  status="completed",
27
- result=f"Generated {len(request.text)} characters"
 
28
  )
29
 
30
  db_job = ProcessingJob(**job.model_dump())
@@ -37,13 +126,23 @@ async def text_to_speech(
37
  media_type="audio/wav",
38
  headers={
39
  "Content-Disposition": "attachment; filename=speech.wav",
40
- "Text-Length": str(len(request.text)),
41
- "Sample-Rate": str(service.sample_rate)
 
 
 
42
  }
43
  )
44
 
45
- except HTTPException:
46
- raise
 
47
  except Exception as e:
48
  db.rollback()
49
- raise HTTPException(status_code=500, detail=str(e))
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, HTTPException, Depends, Request
2
  from fastapi.responses import StreamingResponse
3
  from sqlalchemy.orm import Session
4
  from ....services.tts import TTSService
 
6
  from app.models.job import ProcessingJob
7
  from app.schemas.job import JobCreate
8
  from app.core.config import get_db
9
+ from typing import Optional, Dict, Tuple
10
+ import time
11
+ from datetime import datetime, timedelta
12
+ from fastapi import status
13
 
14
  router = APIRouter()
15
 
16
+ # Rate limiting storage (use Redis in production)
17
+ _RATE_LIMIT_STORE: Dict[str, Tuple[int, float]] = {}
18
+ _CLEANUP_INTERVAL = 60 # Seconds between cache cleanups
19
+ _LAST_CLEANUP = time.time()
20
+
21
+ # Rate limiting configuration
22
+ RATE_LIMIT_WINDOW = 60 # 60-second window
23
+ MAX_REQUESTS_PER_WINDOW = 10 # Max requests per IP per window
24
+ MIN_REQUEST_INTERVAL = 1.0 # 1 second between requests
25
+
26
+ def _cleanup_rate_limit_store():
27
+ """Remove expired rate limit entries"""
28
+ global _LAST_CLEANUP
29
+ current_time = time.time()
30
+ if current_time - _LAST_CLEANUP < _CLEANUP_INTERVAL:
31
+ return
32
+
33
+ expired = current_time - RATE_LIMIT_WINDOW
34
+ to_delete = [ip for ip, (_, timestamp) in _RATE_LIMIT_STORE.items()
35
+ if timestamp < expired]
36
+
37
+ for ip in to_delete:
38
+ del _RATE_LIMIT_STORE[ip]
39
+
40
+ _LAST_CLEANUP = current_time
41
+
42
+ def _check_rate_limit(client_ip: str) -> Optional[float]:
43
+ """Check if request is allowed, returns retry_after if rate limited"""
44
+ _cleanup_rate_limit_store()
45
+ current_time = time.time()
46
+
47
+ # Check minimum interval
48
+ if client_ip in _RATE_LIMIT_STORE:
49
+ last_request_time = _RATE_LIMIT_STORE[client_ip][1]
50
+ if current_time - last_request_time < MIN_REQUEST_INTERVAL:
51
+ return MIN_REQUEST_INTERVAL - (current_time - last_request_time)
52
+
53
+ # Check request count in window
54
+ request_count, _ = _RATE_LIMIT_STORE.get(client_ip, (0, 0))
55
+ if request_count >= MAX_REQUESTS_PER_WINDOW:
56
+ oldest_entry_time = _RATE_LIMIT_STORE[client_ip][1]
57
+ return RATE_LIMIT_WINDOW - (current_time - oldest_entry_time)
58
+
59
+ return None
60
+
61
+ def _update_rate_limit(client_ip: str):
62
+ """Update rate limit counters for the client"""
63
+ current_time = time.time()
64
+ if client_ip in _RATE_LIMIT_STORE:
65
+ count, _ = _RATE_LIMIT_STORE[client_ip]
66
+ _RATE_LIMIT_STORE[client_ip] = (count + 1, current_time)
67
+ else:
68
+ _RATE_LIMIT_STORE[client_ip] = (1, current_time)
69
+
70
  @router.post("/", response_model=TTSResponse)
71
  async def text_to_speech(
72
+ request: Request,
73
+ tts_request: TTSRequest,
74
  db: Session = Depends(get_db)
75
  ):
76
+ # Get client IP (handles proxies properly)
77
+ client_ip = request.client.host
78
+ if not client_ip:
79
+ client_ip = "unknown"
80
+
81
+ # Check rate limits
82
+ if retry_after := _check_rate_limit(client_ip):
83
+ raise HTTPException(
84
+ status_code=status.HTTP_429_TOO_MANY_REQUESTS,
85
+ detail={
86
+ "message": "Rate limit exceeded",
87
+ "retry_after": retry_after,
88
+ "limits": {
89
+ "requests_per_window": MAX_REQUESTS_PER_WINDOW,
90
+ "window_seconds": RATE_LIMIT_WINDOW,
91
+ "min_request_interval": MIN_REQUEST_INTERVAL
92
+ }
93
+ },
94
+ headers={
95
+ "Retry-After": str(int(retry_after)),
96
+ "X-RateLimit-Limit": str(MAX_REQUESTS_PER_WINDOW),
97
+ "X-RateLimit-Remaining": str(MAX_REQUESTS_PER_WINDOW - _RATE_LIMIT_STORE.get(client_ip, (0, 0))[0]),
98
+ "X-RateLimit-Reset": str(int(time.time() + retry_after))
99
+ }
100
+ )
101
+
102
  try:
103
+ # Update rate limit counters
104
+ _update_rate_limit(client_ip)
105
+
106
+ # Process the request
107
  service = TTSService()
108
+ audio_buffer = service.generate_audio(tts_request.text)
109
 
110
+ # Database logging
111
  job = JobCreate(
112
  job_type="tts",
113
  input_file=None,
114
  status="completed",
115
+ result=f"Generated {len(tts_request.text)} characters",
116
+ client_ip=client_ip # Track which IP made the request
117
  )
118
 
119
  db_job = ProcessingJob(**job.model_dump())
 
126
  media_type="audio/wav",
127
  headers={
128
  "Content-Disposition": "attachment; filename=speech.wav",
129
+ "Text-Length": str(len(tts_request.text)),
130
+ "Sample-Rate": str(service.sample_rate),
131
+ "X-RateLimit-Limit": str(MAX_REQUESTS_PER_WINDOW),
132
+ "X-RateLimit-Remaining": str(MAX_REQUESTS_PER_WINDOW - _RATE_LIMIT_STORE[client_ip][0]),
133
+ "X-RateLimit-Reset": str(int(_RATE_LIMIT_STORE[client_ip][1] + RATE_LIMIT_WINDOW))
134
  }
135
  )
136
 
137
+ except HTTPException as e:
138
+ db.rollback()
139
+ raise e
140
  except Exception as e:
141
  db.rollback()
142
+ # Log failed attempts in rate limiting
143
+ if client_ip in _RATE_LIMIT_STORE:
144
+ _RATE_LIMIT_STORE[client_ip] = (_RATE_LIMIT_STORE[client_ip][0] - 1, _RATE_LIMIT_STORE[client_ip][1])
145
+ raise HTTPException(
146
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
147
+ detail=f"TTS processing failed: {str(e)}"
148
+ )