Coco-18 commited on
Commit
e13b2ed
Β·
verified Β·
1 Parent(s): 4df2148

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +379 -37
app.py CHANGED
@@ -1,18 +1,34 @@
1
- # app.py - Main application file
2
 
3
  import os
4
  import sys
5
  import logging
6
  import traceback
7
-
8
- # Configure logging
 
 
 
 
 
 
9
  logging.basicConfig(
10
  level=logging.INFO,
11
- format='%(asctime)s - %(levelname)s - %(message)s',
12
  datefmt='%Y-%m-%d %H:%M:%S'
13
  )
14
  logger = logging.getLogger("speech_api")
15
 
 
 
 
 
 
 
 
 
 
 
16
  # Set all cache directories to locations within /tmp
17
  cache_dirs = {
18
  "HF_HOME": "/tmp/hf_home",
@@ -59,10 +75,15 @@ except ImportError as e:
59
  logger.critical(f"❌ Failed to import necessary libraries: {str(e)}")
60
  sys.exit(1)
61
 
62
- # Check CUDA availability
63
  if torch.cuda.is_available():
64
  logger.info(f"πŸš€ CUDA available: {torch.cuda.get_device_name(0)}")
65
  device = "cuda"
 
 
 
 
 
66
  else:
67
  logger.info("⚠️ CUDA not available, using CPU")
68
  device = "cpu"
@@ -71,6 +92,12 @@ else:
71
  SAMPLE_RATE = 16000
72
  OUTPUT_DIR = "/tmp/audio_outputs"
73
  REFERENCE_AUDIO_DIR = "./reference_audios"
 
 
 
 
 
 
74
 
75
  try:
76
  os.makedirs(OUTPUT_DIR, exist_ok=True)
@@ -78,62 +105,311 @@ try:
78
  except Exception as e:
79
  logger.error(f"❌ Failed to create output directory: {str(e)}")
80
 
 
 
 
 
 
 
 
 
 
 
81
  # Initialize Flask app
82
  app = Flask(__name__)
83
  CORS(app)
 
84
 
85
  # Load models
86
  init_models(device)
87
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
 
89
  # Define routes
90
  @app.route("/", methods=["GET"])
91
  def home():
92
- return jsonify({"message": "Speech API is running", "status": "active"})
93
-
 
 
 
 
94
 
95
  @app.route("/health", methods=["GET"])
96
  def health_check():
97
  health_status = check_model_status()
98
  health_status["api_status"] = "online"
99
  health_status["device"] = device
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
  return jsonify(health_status)
101
 
102
-
103
  @app.route("/asr", methods=["POST"])
 
104
  def transcribe_audio():
105
- return handle_asr_request(request, OUTPUT_DIR, SAMPLE_RATE)
106
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
 
108
  @app.route("/tts", methods=["POST"])
 
109
  def generate_tts():
110
- return handle_tts_request(request, OUTPUT_DIR)
111
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
 
113
  @app.route("/translate", methods=["POST"])
 
114
  def translate_text():
115
- return handle_translation_request(request)
116
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
 
118
  @app.route("/download/<filename>", methods=["GET"])
119
  def download_audio(filename):
 
 
 
 
 
 
 
 
120
  file_path = os.path.join(OUTPUT_DIR, filename)
121
  if os.path.exists(file_path):
122
  logger.info(f"πŸ“€ Serving audio file: {file_path}")
123
  return send_file(file_path, mimetype="audio/wav", as_attachment=True)
124
-
125
- logger.warning(f"⚠️ Requested file not found: {file_path}")
 
 
 
 
 
 
 
126
  return jsonify({"error": "File not found"}), 404
127
 
128
-
129
  @app.route("/evaluate", methods=["POST"])
 
130
  def evaluate_pronunciation():
131
- return handle_evaluation_request(request, REFERENCE_AUDIO_DIR, OUTPUT_DIR, SAMPLE_RATE)
132
-
 
133
 
134
  @app.route("/check_references", methods=["GET"])
135
  def check_references():
136
- """Endpoint to check if reference files exist and are accessible"""
137
  ref_patterns = ["mayap_a_abak", "mayap_a_ugtu", "mayap_a_gatpanapun", "mayap_a_bengi",
138
  "komusta_ka", "malaus_ko_pu", "malaus_kayu", "agaganaka_da_ka",
139
  "pagdulapan_da_ka", "kaluguran_da_ka", "dakal_a_salamat", "panapaya_mu_ku",
@@ -148,8 +424,41 @@ def check_references():
148
  "pisan", "dara", "achi", "apu", "ima", "tatang", "pengari", "koya", "kapatad", "wali",
149
  "pasbul", "awang", "dagis", "bale", "ulas", "sambra", "sulu", "pitudturan", "luklukan", "ulnan"
150
  ]
151
- results = {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
152
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
153
  for pattern in ref_patterns:
154
  pattern_dir = os.path.join(REFERENCE_AUDIO_DIR, pattern)
155
  if os.path.exists(pattern_dir):
@@ -168,36 +477,68 @@ def check_references():
168
 
169
  return jsonify({
170
  "reference_audio_dir": REFERENCE_AUDIO_DIR,
171
- "directory_exists": os.path.exists(REFERENCE_AUDIO_DIR),
172
  "patterns": results
173
  })
174
 
175
-
176
  @app.route("/upload_reference", methods=["POST"])
 
177
  def upload_reference_audio():
178
  return handle_upload_reference(request, REFERENCE_AUDIO_DIR, SAMPLE_RATE)
179
 
180
-
181
- @app.before_request
182
- def before_request():
183
- global REFERENCE_AUDIO_DIR # Remove this line
184
- if not hasattr(g, 'initialized'):
185
- # This might return an updated path if the original fails
186
- updated_ref_dir = init_reference_audio(REFERENCE_AUDIO_DIR, OUTPUT_DIR)
187
- if updated_ref_dir and updated_ref_dir != REFERENCE_AUDIO_DIR:
188
- REFERENCE_AUDIO_DIR = updated_ref_dir
189
- logger.info(f"πŸ“ Updated reference audio directory to: {REFERENCE_AUDIO_DIR}")
190
- g.initialized = True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
191
 
192
  if __name__ == "__main__":
193
-
194
  # This might return an updated path if the original fails
195
  updated_ref_dir = init_reference_audio(REFERENCE_AUDIO_DIR, OUTPUT_DIR)
196
  if updated_ref_dir and updated_ref_dir != REFERENCE_AUDIO_DIR:
197
  REFERENCE_AUDIO_DIR = updated_ref_dir
198
  logger.info(f"πŸ“ Updated reference audio directory to: {REFERENCE_AUDIO_DIR}")
199
 
200
- logger.info("πŸš€ Starting Speech API server")
201
 
202
  # Get the status for logging
203
  status = check_model_status()
@@ -205,4 +546,5 @@ if __name__ == "__main__":
205
  for lang, model_status in status['tts_models'].items():
206
  logger.info(f"πŸ“Š TTS model {lang}: {'βœ…' if model_status == 'loaded' else '❌'}")
207
 
208
- app.run(host="0.0.0.0", port=7860, debug=True)
 
 
1
+ # app.py - Main application file (OPTIMIZED FOR HUGGING FACE SPACES)
2
 
3
  import os
4
  import sys
5
  import logging
6
  import traceback
7
+ import time
8
+ import uuid
9
+ import threading
10
+ from functools import lru_cache
11
+ import concurrent.futures
12
+ from collections import defaultdict, deque
13
+
14
+ # Configure logging - keeping it simple for Hugging Face Spaces
15
  logging.basicConfig(
16
  level=logging.INFO,
17
+ format='%(asctime)s - %(levelname)s - [%(thread)d] %(message)s',
18
  datefmt='%Y-%m-%d %H:%M:%S'
19
  )
20
  logger = logging.getLogger("speech_api")
21
 
22
+ # Simple in-memory rate limiting
23
+ REQUEST_HISTORY = defaultdict(deque)
24
+ RATE_LIMIT_WINDOW = 60 # seconds
25
+ MAX_REQUESTS_PER_WINDOW = 15 # More conservative for HF
26
+ rate_limit_lock = threading.Lock()
27
+
28
+ # Small thread pool suitable for HF Spaces
29
+ MAX_WORKERS = 3 # Conservative number for HF Spaces
30
+ worker_pool = concurrent.futures.ThreadPoolExecutor(max_workers=MAX_WORKERS)
31
+
32
  # Set all cache directories to locations within /tmp
33
  cache_dirs = {
34
  "HF_HOME": "/tmp/hf_home",
 
75
  logger.critical(f"❌ Failed to import necessary libraries: {str(e)}")
76
  sys.exit(1)
77
 
78
+ # Check CUDA availability and optimize memory usage
79
  if torch.cuda.is_available():
80
  logger.info(f"πŸš€ CUDA available: {torch.cuda.get_device_name(0)}")
81
  device = "cuda"
82
+ # Optimize CUDA memory usage for HF Spaces
83
+ torch.cuda.empty_cache()
84
+ # Conservative memory settings for HF Spaces
85
+ torch.cuda.set_per_process_memory_fraction(0.7) # Don't use all GPU memory
86
+ torch.backends.cudnn.benchmark = True # Speed up operations
87
  else:
88
  logger.info("⚠️ CUDA not available, using CPU")
89
  device = "cpu"
 
92
  SAMPLE_RATE = 16000
93
  OUTPUT_DIR = "/tmp/audio_outputs"
94
  REFERENCE_AUDIO_DIR = "./reference_audios"
95
+ MAX_CACHE_SIZE = 50 # Smaller cache for HF Spaces
96
+
97
+ # In-memory caches
98
+ asr_cache = {}
99
+ tts_cache = {}
100
+ translation_cache = {}
101
 
102
  try:
103
  os.makedirs(OUTPUT_DIR, exist_ok=True)
 
105
  except Exception as e:
106
  logger.error(f"❌ Failed to create output directory: {str(e)}")
107
 
108
+ # Create user-specific directories to prevent conflicts
109
+ def get_user_output_dir(user_id=None):
110
+ """Create and return a user-specific output directory"""
111
+ if user_id is None:
112
+ user_id = str(uuid.uuid4())[:8]
113
+
114
+ user_dir = os.path.join(OUTPUT_DIR, user_id)
115
+ os.makedirs(user_dir, exist_ok=True)
116
+ return user_dir
117
+
118
  # Initialize Flask app
119
  app = Flask(__name__)
120
  CORS(app)
121
+ app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16MB max upload for HF
122
 
123
  # Load models
124
  init_models(device)
125
 
126
+ # Rate limit decorator - simple in-memory implementation
127
+ def rate_limit(f):
128
+ def decorated_function(*args, **kwargs):
129
+ client_ip = request.remote_addr or request.headers.get('X-Forwarded-For', 'unknown')
130
+
131
+ with rate_limit_lock:
132
+ current_time = time.time()
133
+
134
+ # Add current request timestamp
135
+ if client_ip not in REQUEST_HISTORY:
136
+ REQUEST_HISTORY[client_ip] = deque(maxlen=MAX_REQUESTS_PER_WINDOW)
137
+
138
+ # Clean old requests (older than window)
139
+ while REQUEST_HISTORY[client_ip] and current_time - REQUEST_HISTORY[client_ip][0] > RATE_LIMIT_WINDOW:
140
+ REQUEST_HISTORY[client_ip].popleft()
141
+
142
+ # Check if rate limit is exceeded
143
+ if len(REQUEST_HISTORY[client_ip]) >= MAX_REQUESTS_PER_WINDOW:
144
+ logger.warning(f"⚠️ Rate limit exceeded for {client_ip}")
145
+ return jsonify({
146
+ "error": "Rate limit exceeded",
147
+ "message": "Too many requests, please try again later"
148
+ }), 429
149
+
150
+ # Add this request
151
+ REQUEST_HISTORY[client_ip].append(current_time)
152
+
153
+ return f(*args, **kwargs)
154
+
155
+ return decorated_function
156
+
157
+ # Caching helpers
158
+ def compute_hash(data):
159
+ """Compute a hash for caching purposes"""
160
+ import hashlib
161
+ if isinstance(data, str):
162
+ return hashlib.md5(data.encode('utf-8')).hexdigest()
163
+ return hashlib.md5(str(data).encode('utf-8')).hexdigest()
164
+
165
+ # Cache decorator for responses
166
+ def cache_response(cache_dict, key_fn, max_size=MAX_CACHE_SIZE):
167
+ def decorator(f):
168
+ def wrapper(*args, **kwargs):
169
+ key = key_fn(*args, **kwargs)
170
+
171
+ # Check cache
172
+ if key in cache_dict:
173
+ logger.info(f"βœ… Cache hit for {f.__name__}")
174
+ return cache_dict[key]
175
+
176
+ # Get actual response
177
+ response = f(*args, **kwargs)
178
+
179
+ # Store in cache if it's a successful response
180
+ if isinstance(response, tuple):
181
+ result, status_code = response
182
+ if status_code < 400: # Only cache successful responses
183
+ cache_dict[key] = response
184
+ else:
185
+ cache_dict[key] = response
186
+
187
+ # Limit cache size
188
+ if len(cache_dict) > max_size:
189
+ # Remove random item (simple approach for HF Spaces)
190
+ cache_dict.pop(next(iter(cache_dict)))
191
+
192
+ return response
193
+ return wrapper
194
+ return decorator
195
+
196
+ # Request tracking middleware
197
+ @app.before_request
198
+ def before_request():
199
+ g.request_id = str(uuid.uuid4())[:8]
200
+ g.start_time = time.time()
201
+
202
+ # Initialize reference directory if needed
203
+ if not hasattr(g, 'initialized'):
204
+ global REFERENCE_AUDIO_DIR
205
+ # This might return an updated path if the original fails
206
+ updated_ref_dir = init_reference_audio(REFERENCE_AUDIO_DIR, OUTPUT_DIR)
207
+ if updated_ref_dir and updated_ref_dir != REFERENCE_AUDIO_DIR:
208
+ REFERENCE_AUDIO_DIR = updated_ref_dir
209
+ logger.info(f"πŸ“ Updated reference audio directory to: {REFERENCE_AUDIO_DIR}")
210
+ g.initialized = True
211
+
212
+ # Create user-specific directory
213
+ user_id = request.headers.get('X-User-ID', str(uuid.uuid4())[:8])
214
+ g.user_output_dir = get_user_output_dir(user_id)
215
+
216
+ logger.info(f"[{g.request_id}] πŸ”„ {request.method} {request.path} started")
217
+
218
+ @app.after_request
219
+ def after_request(response):
220
+ if hasattr(g, 'request_id') and hasattr(g, 'start_time'):
221
+ duration = time.time() - g.start_time
222
+ logger.info(f"[{g.request_id}] βœ… Completed in {duration:.2f}s with status {response.status_code}")
223
+
224
+ # Set cache headers
225
+ if request.endpoint == 'download_audio':
226
+ response.headers['Cache-Control'] = 'public, max-age=86400' # Cache audio for a day
227
+ else:
228
+ response.headers['Cache-Control'] = 'no-store' # No caching for API responses
229
+
230
+ return response
231
+
232
+ # Global error handler
233
+ @app.errorhandler(Exception)
234
+ def handle_exception(e):
235
+ logger.error(f"❌ Unhandled exception: {str(e)}")
236
+ logger.debug(traceback.format_exc())
237
+
238
+ return jsonify({
239
+ "error": "Internal server error",
240
+ "message": str(e)
241
+ }), 500
242
 
243
  # Define routes
244
  @app.route("/", methods=["GET"])
245
  def home():
246
+ return jsonify({
247
+ "message": "Speech API is running",
248
+ "status": "active",
249
+ "version": "1.1",
250
+ "environment": "Hugging Face Spaces"
251
+ })
252
 
253
  @app.route("/health", methods=["GET"])
254
  def health_check():
255
  health_status = check_model_status()
256
  health_status["api_status"] = "online"
257
  health_status["device"] = device
258
+
259
+ # Add memory usage info
260
+ if torch.cuda.is_available():
261
+ health_status["memory"] = {
262
+ "cuda_allocated_mb": round(torch.cuda.memory_allocated() / (1024 * 1024), 2),
263
+ "cuda_reserved_mb": round(torch.cuda.memory_reserved() / (1024 * 1024), 2)
264
+ }
265
+
266
+ # Add cache stats
267
+ health_status["cache_stats"] = {
268
+ "asr_cache_size": len(asr_cache),
269
+ "tts_cache_size": len(tts_cache),
270
+ "translation_cache_size": len(translation_cache)
271
+ }
272
+
273
  return jsonify(health_status)
274
 
275
+ # ASR with optimizations
276
  @app.route("/asr", methods=["POST"])
277
+ @rate_limit
278
  def transcribe_audio():
279
+ # Get user-specific output directory
280
+ user_output_dir = g.user_output_dir if hasattr(g, 'user_output_dir') else OUTPUT_DIR
281
+
282
+ # Check cache first (simple caching logic)
283
+ if 'audio' in request.files:
284
+ audio_file = request.files['audio']
285
+ language = request.form.get("language", "english").lower()
286
+
287
+ # Create a simple cache key
288
+ audio_content = audio_file.read()
289
+ audio_file.seek(0) # Reset file pointer
290
+
291
+ cache_key = f"asr_{compute_hash(audio_content)}_{language}"
292
+
293
+ if cache_key in asr_cache:
294
+ logger.info(f"[{g.request_id}] βœ… Using cached ASR result")
295
+ return asr_cache[cache_key]
296
+
297
+ # Process the request normally
298
+ result = handle_asr_request(request, user_output_dir, SAMPLE_RATE)
299
+
300
+ # Cache successful responses
301
+ if isinstance(result, tuple):
302
+ response, status_code = result
303
+ if status_code == 200:
304
+ asr_cache[cache_key] = result
305
+
306
+ # Limit cache size
307
+ if len(asr_cache) > MAX_CACHE_SIZE:
308
+ asr_cache.pop(next(iter(asr_cache)))
309
+
310
+ return result
311
 
312
  @app.route("/tts", methods=["POST"])
313
+ @rate_limit
314
  def generate_tts():
315
+ # Get user-specific output directory
316
+ user_output_dir = g.user_output_dir if hasattr(g, 'user_output_dir') else OUTPUT_DIR
317
+
318
+ # Check cache first
319
+ if request.is_json:
320
+ data = request.get_json()
321
+ if data:
322
+ text = data.get("text", "").strip()
323
+ language = data.get("language", "kapampangan").lower()
324
+
325
+ cache_key = f"tts_{compute_hash(text)}_{language}"
326
+
327
+ if cache_key in tts_cache:
328
+ logger.info(f"[{g.request_id}] βœ… Using cached TTS result")
329
+ return tts_cache[cache_key]
330
+
331
+ # Process the request normally
332
+ result = handle_tts_request(request, user_output_dir)
333
+
334
+ # Cache successful responses
335
+ if isinstance(result, tuple):
336
+ response, status_code = result
337
+ if status_code == 200 and request.is_json:
338
+ tts_cache[cache_key] = result
339
+
340
+ # Limit cache size
341
+ if len(tts_cache) > MAX_CACHE_SIZE:
342
+ tts_cache.pop(next(iter(tts_cache)))
343
+
344
+ return result
345
 
346
  @app.route("/translate", methods=["POST"])
347
+ @rate_limit
348
  def translate_text():
349
+ # Check cache first
350
+ if request.is_json:
351
+ data = request.get_json()
352
+ if data:
353
+ text = data.get("text", "").strip()
354
+ source_language = data.get("source_language", "").lower()
355
+ target_language = data.get("target_language", "").lower()
356
+
357
+ cache_key = f"translate_{compute_hash(text)}_{source_language}_{target_language}"
358
+
359
+ if cache_key in translation_cache:
360
+ logger.info(f"[{g.request_id}] βœ… Using cached translation result")
361
+ return translation_cache[cache_key]
362
+
363
+ # Process the request normally
364
+ result = handle_translation_request(request)
365
+
366
+ # Cache successful responses
367
+ if isinstance(result, tuple):
368
+ response, status_code = result
369
+ if status_code == 200 and request.is_json:
370
+ translation_cache[cache_key] = result
371
+
372
+ # Limit cache size
373
+ if len(translation_cache) > MAX_CACHE_SIZE:
374
+ translation_cache.pop(next(iter(translation_cache)))
375
+
376
+ return result
377
 
378
  @app.route("/download/<filename>", methods=["GET"])
379
  def download_audio(filename):
380
+ # First try user-specific directory if available
381
+ if hasattr(g, 'user_output_dir'):
382
+ file_path = os.path.join(g.user_output_dir, filename)
383
+ if os.path.exists(file_path):
384
+ logger.info(f"πŸ“€ Serving user audio file: {file_path}")
385
+ return send_file(file_path, mimetype="audio/wav", as_attachment=True)
386
+
387
+ # Then try main output directory
388
  file_path = os.path.join(OUTPUT_DIR, filename)
389
  if os.path.exists(file_path):
390
  logger.info(f"πŸ“€ Serving audio file: {file_path}")
391
  return send_file(file_path, mimetype="audio/wav", as_attachment=True)
392
+
393
+ # Check for any subdirectories (simplified approach)
394
+ for root, dirs, files in os.walk(OUTPUT_DIR):
395
+ if filename in files:
396
+ full_path = os.path.join(root, filename)
397
+ logger.info(f"πŸ“€ Serving found audio file: {full_path}")
398
+ return send_file(full_path, mimetype="audio/wav", as_attachment=True)
399
+
400
+ logger.warning(f"⚠️ Requested file not found: {filename}")
401
  return jsonify({"error": "File not found"}), 404
402
 
 
403
  @app.route("/evaluate", methods=["POST"])
404
+ @rate_limit
405
  def evaluate_pronunciation():
406
+ # Get user-specific output directory
407
+ user_output_dir = g.user_output_dir if hasattr(g, 'user_output_dir') else OUTPUT_DIR
408
+ return handle_evaluation_request(request, REFERENCE_AUDIO_DIR, user_output_dir, SAMPLE_RATE)
409
 
410
  @app.route("/check_references", methods=["GET"])
411
  def check_references():
412
+ """Optimized endpoint to check if reference files exist"""
413
  ref_patterns = ["mayap_a_abak", "mayap_a_ugtu", "mayap_a_gatpanapun", "mayap_a_bengi",
414
  "komusta_ka", "malaus_ko_pu", "malaus_kayu", "agaganaka_da_ka",
415
  "pagdulapan_da_ka", "kaluguran_da_ka", "dakal_a_salamat", "panapaya_mu_ku",
 
424
  "pisan", "dara", "achi", "apu", "ima", "tatang", "pengari", "koya", "kapatad", "wali",
425
  "pasbul", "awang", "dagis", "bale", "ulas", "sambra", "sulu", "pitudturan", "luklukan", "ulnan"
426
  ]
427
+
428
+ # Get a summary instead of details to reduce response size
429
+ summary = {
430
+ "reference_audio_dir": REFERENCE_AUDIO_DIR,
431
+ "directory_exists": os.path.exists(REFERENCE_AUDIO_DIR),
432
+ "total_patterns": len(ref_patterns),
433
+ "existing_patterns": 0,
434
+ "total_files": 0
435
+ }
436
+
437
+ for pattern in ref_patterns:
438
+ pattern_dir = os.path.join(REFERENCE_AUDIO_DIR, pattern)
439
+ if os.path.exists(pattern_dir):
440
+ wav_files = glob.glob(os.path.join(pattern_dir, "*.wav"))
441
+ if wav_files:
442
+ summary["existing_patterns"] += 1
443
+ summary["total_files"] += len(wav_files)
444
+
445
+ return jsonify(summary)
446
 
447
+ # Add detailed reference check as a separate endpoint
448
+ @app.route("/check_references/detailed", methods=["GET"])
449
+ def check_references_detailed():
450
+ """Get detailed information for specific reference patterns"""
451
+ patterns = request.args.get('patterns', '').split(',')
452
+
453
+ # If no patterns specified, return the first 10 (avoid heavy response)
454
+ if not patterns or patterns == ['']:
455
+ ref_patterns = ["mayap_a_abak", "mayap_a_ugtu", "mayap_a_gatpanapun", "mayap_a_bengi",
456
+ "komusta_ka", "malaus_ko_pu", "malaus_kayu", "agaganaka_da_ka",
457
+ "pagdulapan_da_ka", "kaluguran_da_ka"]
458
+ else:
459
+ ref_patterns = [p.strip() for p in patterns if p.strip()]
460
+
461
+ results = {}
462
  for pattern in ref_patterns:
463
  pattern_dir = os.path.join(REFERENCE_AUDIO_DIR, pattern)
464
  if os.path.exists(pattern_dir):
 
477
 
478
  return jsonify({
479
  "reference_audio_dir": REFERENCE_AUDIO_DIR,
 
480
  "patterns": results
481
  })
482
 
 
483
  @app.route("/upload_reference", methods=["POST"])
484
+ @rate_limit
485
  def upload_reference_audio():
486
  return handle_upload_reference(request, REFERENCE_AUDIO_DIR, SAMPLE_RATE)
487
 
488
+ # Add a cleanup endpoint
489
+ @app.route("/cleanup", methods=["POST"])
490
+ def cleanup_files():
491
+ """Clean up old files to free space (important for HF Spaces)"""
492
+ try:
493
+ # Only allow from local or with API key
494
+ if not (request.remote_addr == '127.0.0.1' or
495
+ request.headers.get('X-Cleanup-Key') == os.environ.get('CLEANUP_KEY', 'cleanup-secret')):
496
+ return jsonify({"error": "Unauthorized"}), 403
497
+
498
+ # Delete files older than 2 hours
499
+ cutoff_time = time.time() - 7200 # 2 hours in seconds
500
+ deleted_count = 0
501
+
502
+ for root, dirs, files in os.walk(OUTPUT_DIR):
503
+ for file in files:
504
+ try:
505
+ file_path = os.path.join(root, file)
506
+ if os.path.getmtime(file_path) < cutoff_time:
507
+ os.remove(file_path)
508
+ deleted_count += 1
509
+ except Exception as e:
510
+ logger.warning(f"⚠️ Failed to delete {file}: {e}")
511
+
512
+ # Clear empty directories
513
+ for root, dirs, files in os.walk(OUTPUT_DIR, topdown=False):
514
+ for dir_name in dirs:
515
+ try:
516
+ dir_path = os.path.join(root, dir_name)
517
+ if not os.listdir(dir_path):
518
+ os.rmdir(dir_path)
519
+ except Exception as e:
520
+ logger.warning(f"⚠️ Failed to remove empty dir {dir_name}: {e}")
521
+
522
+ # Clear torch cache
523
+ if torch.cuda.is_available():
524
+ torch.cuda.empty_cache()
525
+
526
+ return jsonify({
527
+ "message": "Cleanup completed",
528
+ "files_deleted": deleted_count
529
+ })
530
+ except Exception as e:
531
+ logger.error(f"❌ Cleanup error: {str(e)}")
532
+ return jsonify({"error": str(e)}), 500
533
 
534
  if __name__ == "__main__":
 
535
  # This might return an updated path if the original fails
536
  updated_ref_dir = init_reference_audio(REFERENCE_AUDIO_DIR, OUTPUT_DIR)
537
  if updated_ref_dir and updated_ref_dir != REFERENCE_AUDIO_DIR:
538
  REFERENCE_AUDIO_DIR = updated_ref_dir
539
  logger.info(f"πŸ“ Updated reference audio directory to: {REFERENCE_AUDIO_DIR}")
540
 
541
+ logger.info("πŸš€ Starting Speech API server optimized for Hugging Face Spaces")
542
 
543
  # Get the status for logging
544
  status = check_model_status()
 
546
  for lang, model_status in status['tts_models'].items():
547
  logger.info(f"πŸ“Š TTS model {lang}: {'βœ…' if model_status == 'loaded' else '❌'}")
548
 
549
+ # Use threaded=True for better performance
550
+ app.run(host="0.0.0.0", port=7860, debug=False, threaded=True)