Spaces:
Running
on
CPU Upgrade
Running
on
CPU Upgrade
GitHub Actions
commited on
Commit
·
d462da9
1
Parent(s):
3da69a8
Sync from GitHub repo
Browse files- .gitignore +3 -1
- app.py +247 -22
- templates/arena.html +52 -9
.gitignore
CHANGED
@@ -47,4 +47,6 @@ instance/
|
|
47 |
Thumbs.db
|
48 |
|
49 |
# Uploads
|
50 |
-
static/temp_audio
|
|
|
|
|
|
47 |
Thumbs.db
|
48 |
|
49 |
# Uploads
|
50 |
+
static/temp_audio
|
51 |
+
|
52 |
+
votes/
|
app.py
CHANGED
@@ -3,6 +3,7 @@ from huggingface_hub import HfApi, hf_hub_download
|
|
3 |
from apscheduler.schedulers.background import BackgroundScheduler
|
4 |
from concurrent.futures import ThreadPoolExecutor
|
5 |
from datetime import datetime
|
|
|
6 |
|
7 |
year = datetime.now().year
|
8 |
month = datetime.now().month
|
@@ -111,9 +112,20 @@ limiter = Limiter(
|
|
111 |
storage_uri="memory://",
|
112 |
)
|
113 |
|
114 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
115 |
TEMP_AUDIO_DIR = os.path.join(tempfile.gettempdir(), "tts_arena_audio")
|
|
|
116 |
os.makedirs(TEMP_AUDIO_DIR, exist_ok=True)
|
|
|
|
|
117 |
|
118 |
# Store active TTS sessions
|
119 |
app.tts_sessions = {}
|
@@ -275,12 +287,15 @@ def verify_turnstile():
|
|
275 |
return redirect(url_for("turnstile_page", redirect_url=redirect_url))
|
276 |
|
277 |
with open("harvard_sentences.txt", "r") as f:
|
278 |
-
|
279 |
-
|
|
|
|
|
280 |
|
281 |
@app.route("/")
|
282 |
def arena():
|
283 |
-
|
|
|
284 |
|
285 |
|
286 |
@app.route("/leaderboard")
|
@@ -357,20 +372,188 @@ def about():
|
|
357 |
return render_template("about.html")
|
358 |
|
359 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
360 |
@app.route("/api/tts/generate", methods=["POST"])
|
361 |
-
@limiter.limit("10 per minute")
|
362 |
def generate_tts():
|
363 |
# If verification not setup, handle it first
|
364 |
if app.config["TURNSTILE_ENABLED"] and not session.get("turnstile_verified"):
|
365 |
return jsonify({"error": "Turnstile verification required"}), 403
|
366 |
|
367 |
data = request.json
|
368 |
-
text = data.get("text")
|
369 |
|
370 |
if not text or len(text) > 1000:
|
371 |
return jsonify({"error": "Invalid or too long text"}), 400
|
372 |
|
373 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
374 |
available_models = Model.query.filter_by(
|
375 |
model_type=ModelType.TTS, is_active=True
|
376 |
).all()
|
@@ -380,25 +563,28 @@ def generate_tts():
|
|
380 |
selected_models = random.sample(available_models, 2)
|
381 |
|
382 |
try:
|
383 |
-
# Generate TTS for both models concurrently
|
384 |
audio_files = []
|
385 |
model_ids = []
|
386 |
|
387 |
-
# Function to process a single model
|
388 |
-
def
|
389 |
-
|
390 |
-
|
|
|
|
|
|
|
391 |
|
392 |
-
|
393 |
-
|
394 |
-
|
395 |
-
|
|
|
|
|
396 |
|
397 |
-
return {"model_id": model.id, "audio_path": dest_path}
|
398 |
|
399 |
# Use ThreadPoolExecutor to process models concurrently
|
400 |
with ThreadPoolExecutor(max_workers=2) as executor:
|
401 |
-
results = list(executor.map(
|
402 |
|
403 |
# Extract results
|
404 |
for result in results:
|
@@ -410,7 +596,7 @@ def generate_tts():
|
|
410 |
app.tts_sessions[session_id] = {
|
411 |
"model_a": model_ids[0],
|
412 |
"model_b": model_ids[1],
|
413 |
-
"audio_a": audio_files[0],
|
414 |
"audio_b": audio_files[1],
|
415 |
"text": text,
|
416 |
"created_at": datetime.utcnow(),
|
@@ -424,13 +610,23 @@ def generate_tts():
|
|
424 |
"session_id": session_id,
|
425 |
"audio_a": f"/api/tts/audio/{session_id}/a",
|
426 |
"audio_b": f"/api/tts/audio/{session_id}/b",
|
427 |
-
"expires_in": 1800,
|
|
|
428 |
}
|
429 |
)
|
430 |
|
431 |
except Exception as e:
|
432 |
-
app.logger.error(f"TTS generation error: {str(e)}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
433 |
return jsonify({"error": "Failed to generate TTS"}), 500
|
|
|
434 |
|
435 |
|
436 |
@app.route("/api/tts/audio/<session_id>/<model_key>")
|
@@ -865,9 +1061,12 @@ def setup_cleanup():
|
|
865 |
cleanup_conversational_session(sid)
|
866 |
app.logger.info(f"Cleaned up {len(expired_tts_sessions)} TTS and {len(expired_conv_sessions)} conversational sessions.")
|
867 |
|
|
|
|
|
|
|
868 |
|
869 |
# Run cleanup every 15 minutes
|
870 |
-
scheduler = BackgroundScheduler()
|
871 |
scheduler.add_job(cleanup_expired_sessions, "interval", minutes=15)
|
872 |
scheduler.start()
|
873 |
print("Cleanup scheduler started") # Use print for startup messages
|
@@ -1000,11 +1199,36 @@ def toggle_leaderboard_visibility():
|
|
1000 |
})
|
1001 |
|
1002 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1003 |
if __name__ == "__main__":
|
1004 |
with app.app_context():
|
1005 |
# Ensure ./instance and ./votes directories exist
|
1006 |
os.makedirs("instance", exist_ok=True)
|
1007 |
os.makedirs("./votes", exist_ok=True) # Create votes directory if it doesn't exist
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1008 |
|
1009 |
# Download database if it doesn't exist (only on initial space start)
|
1010 |
if IS_SPACES and not os.path.exists(app.config["SQLALCHEMY_DATABASE_URI"].replace("sqlite:///", "")):
|
@@ -1025,6 +1249,7 @@ if __name__ == "__main__":
|
|
1025 |
db.create_all() # Create tables if they don't exist
|
1026 |
insert_initial_models()
|
1027 |
# Setup background tasks
|
|
|
1028 |
setup_cleanup()
|
1029 |
setup_periodic_tasks() # Renamed function call
|
1030 |
|
|
|
3 |
from apscheduler.schedulers.background import BackgroundScheduler
|
4 |
from concurrent.futures import ThreadPoolExecutor
|
5 |
from datetime import datetime
|
6 |
+
import threading # Added for locking
|
7 |
|
8 |
year = datetime.now().year
|
9 |
month = datetime.now().month
|
|
|
112 |
storage_uri="memory://",
|
113 |
)
|
114 |
|
115 |
+
# TTS Cache Configuration - Read from environment
|
116 |
+
TTS_CACHE_SIZE = int(os.getenv("TTS_CACHE_SIZE", "10"))
|
117 |
+
CACHE_AUDIO_SUBDIR = "cache"
|
118 |
+
tts_cache = {} # sentence -> {model_a, model_b, audio_a, audio_b, created_at}
|
119 |
+
tts_cache_lock = threading.Lock()
|
120 |
+
cache_executor = ThreadPoolExecutor(max_workers=2, thread_name_prefix='CacheReplacer')
|
121 |
+
all_harvard_sentences = [] # Keep the full list available
|
122 |
+
|
123 |
+
# Create temp directories
|
124 |
TEMP_AUDIO_DIR = os.path.join(tempfile.gettempdir(), "tts_arena_audio")
|
125 |
+
CACHE_AUDIO_DIR = os.path.join(TEMP_AUDIO_DIR, CACHE_AUDIO_SUBDIR)
|
126 |
os.makedirs(TEMP_AUDIO_DIR, exist_ok=True)
|
127 |
+
os.makedirs(CACHE_AUDIO_DIR, exist_ok=True) # Ensure cache subdir exists
|
128 |
+
|
129 |
|
130 |
# Store active TTS sessions
|
131 |
app.tts_sessions = {}
|
|
|
287 |
return redirect(url_for("turnstile_page", redirect_url=redirect_url))
|
288 |
|
289 |
with open("harvard_sentences.txt", "r") as f:
|
290 |
+
# Store all sentences and clean them up
|
291 |
+
all_harvard_sentences = [line.strip() for line in f.readlines() if line.strip()]
|
292 |
+
# Shuffle for initial random selection if needed, but main list remains ordered
|
293 |
+
initial_sentences = random.sample(all_harvard_sentences, min(len(all_harvard_sentences), 500)) # Limit initial pass for template
|
294 |
|
295 |
@app.route("/")
|
296 |
def arena():
|
297 |
+
# Pass a subset of sentences for the random button fallback
|
298 |
+
return render_template("arena.html", harvard_sentences=json.dumps(initial_sentences))
|
299 |
|
300 |
|
301 |
@app.route("/leaderboard")
|
|
|
372 |
return render_template("about.html")
|
373 |
|
374 |
|
375 |
+
# --- TTS Caching Functions ---
|
376 |
+
|
377 |
+
def generate_and_save_tts(text, model_id, output_dir):
|
378 |
+
"""Generates TTS and saves it to a specific directory, returning the full path."""
|
379 |
+
temp_audio_path = None # Initialize to None
|
380 |
+
try:
|
381 |
+
app.logger.debug(f"[TTS Gen {model_id}] Starting generation for: '{text[:30]}...'")
|
382 |
+
# If predict_tts saves file itself and returns path:
|
383 |
+
temp_audio_path = predict_tts(text, model_id)
|
384 |
+
app.logger.debug(f"[TTS Gen {model_id}] predict_tts returned: {temp_audio_path}")
|
385 |
+
|
386 |
+
if not temp_audio_path or not os.path.exists(temp_audio_path):
|
387 |
+
app.logger.warning(f"[TTS Gen {model_id}] predict_tts failed or returned invalid path: {temp_audio_path}")
|
388 |
+
raise ValueError("predict_tts did not return a valid path or file does not exist")
|
389 |
+
|
390 |
+
file_uuid = str(uuid.uuid4())
|
391 |
+
dest_path = os.path.join(output_dir, f"{file_uuid}.wav")
|
392 |
+
app.logger.debug(f"[TTS Gen {model_id}] Moving {temp_audio_path} to {dest_path}")
|
393 |
+
# Move the file generated by predict_tts to the target cache directory
|
394 |
+
shutil.move(temp_audio_path, dest_path)
|
395 |
+
app.logger.debug(f"[TTS Gen {model_id}] Move successful. Returning {dest_path}")
|
396 |
+
return dest_path
|
397 |
+
|
398 |
+
except Exception as e:
|
399 |
+
app.logger.error(f"Error generating/saving TTS for model {model_id} and text '{text[:30]}...': {str(e)}")
|
400 |
+
# Ensure temporary file from predict_tts (if any) is cleaned up
|
401 |
+
if temp_audio_path and os.path.exists(temp_audio_path):
|
402 |
+
try:
|
403 |
+
app.logger.debug(f"[TTS Gen {model_id}] Cleaning up temporary file {temp_audio_path} after error.")
|
404 |
+
os.remove(temp_audio_path)
|
405 |
+
except OSError:
|
406 |
+
pass # Ignore error if file couldn't be removed
|
407 |
+
return None
|
408 |
+
|
409 |
+
|
410 |
+
def _generate_cache_entry_task(sentence):
|
411 |
+
"""Task function to generate audio for a sentence and add to cache."""
|
412 |
+
# Wrap the entire task in an application context
|
413 |
+
with app.app_context():
|
414 |
+
if not sentence:
|
415 |
+
# Select a new sentence if not provided (for replacement)
|
416 |
+
with tts_cache_lock:
|
417 |
+
cached_keys = set(tts_cache.keys())
|
418 |
+
available_sentences = [s for s in all_harvard_sentences if s not in cached_keys]
|
419 |
+
if not available_sentences:
|
420 |
+
app.logger.warning("No more unique Harvard sentences available for caching.")
|
421 |
+
return
|
422 |
+
sentence = random.choice(available_sentences)
|
423 |
+
|
424 |
+
# app.logger.info removed duplicate log
|
425 |
+
print(f"[Cache Task] Querying models for: '{sentence[:50]}...'")
|
426 |
+
available_models = Model.query.filter_by(
|
427 |
+
model_type=ModelType.TTS, is_active=True
|
428 |
+
).all()
|
429 |
+
|
430 |
+
if len(available_models) < 2:
|
431 |
+
app.logger.error("Not enough active TTS models to generate cache entry.")
|
432 |
+
return
|
433 |
+
|
434 |
+
try:
|
435 |
+
models = random.sample(available_models, 2)
|
436 |
+
model_a_id = models[0].id
|
437 |
+
model_b_id = models[1].id
|
438 |
+
|
439 |
+
# Generate audio concurrently using a local executor for clarity within the task
|
440 |
+
with ThreadPoolExecutor(max_workers=2, thread_name_prefix='AudioGen') as audio_executor:
|
441 |
+
future_a = audio_executor.submit(generate_and_save_tts, sentence, model_a_id, CACHE_AUDIO_DIR)
|
442 |
+
future_b = audio_executor.submit(generate_and_save_tts, sentence, model_b_id, CACHE_AUDIO_DIR)
|
443 |
+
|
444 |
+
timeout_seconds = 120
|
445 |
+
audio_a_path = future_a.result(timeout=timeout_seconds)
|
446 |
+
audio_b_path = future_b.result(timeout=timeout_seconds)
|
447 |
+
|
448 |
+
if audio_a_path and audio_b_path:
|
449 |
+
with tts_cache_lock:
|
450 |
+
# Only add if the sentence isn't already back in the cache
|
451 |
+
# And ensure cache size doesn't exceed limit
|
452 |
+
if sentence not in tts_cache and len(tts_cache) < TTS_CACHE_SIZE:
|
453 |
+
tts_cache[sentence] = {
|
454 |
+
"model_a": model_a_id,
|
455 |
+
"model_b": model_b_id,
|
456 |
+
"audio_a": audio_a_path,
|
457 |
+
"audio_b": audio_b_path,
|
458 |
+
"created_at": datetime.utcnow(),
|
459 |
+
}
|
460 |
+
app.logger.info(f"Successfully cached entry for: '{sentence[:50]}...'")
|
461 |
+
elif sentence in tts_cache:
|
462 |
+
app.logger.warning(f"Sentence '{sentence[:50]}...' already re-cached. Discarding new generation.")
|
463 |
+
# Clean up the newly generated files if not added
|
464 |
+
if os.path.exists(audio_a_path): os.remove(audio_a_path)
|
465 |
+
if os.path.exists(audio_b_path): os.remove(audio_b_path)
|
466 |
+
else: # Cache is full
|
467 |
+
app.logger.warning(f"Cache is full ({len(tts_cache)} entries). Discarding new generation for '{sentence[:50]}...'.")
|
468 |
+
# Clean up the newly generated files if not added
|
469 |
+
if os.path.exists(audio_a_path): os.remove(audio_a_path)
|
470 |
+
if os.path.exists(audio_b_path): os.remove(audio_b_path)
|
471 |
+
|
472 |
+
else:
|
473 |
+
app.logger.error(f"Failed to generate one or both audio files for cache: '{sentence[:50]}...'")
|
474 |
+
# Clean up whichever file might have been created
|
475 |
+
if audio_a_path and os.path.exists(audio_a_path): os.remove(audio_a_path)
|
476 |
+
if audio_b_path and os.path.exists(audio_b_path): os.remove(audio_b_path)
|
477 |
+
|
478 |
+
except Exception as e:
|
479 |
+
# Log the exception within the app context
|
480 |
+
app.logger.error(f"Exception in _generate_cache_entry_task for '{sentence[:50]}...': {str(e)}", exc_info=True)
|
481 |
+
|
482 |
+
|
483 |
+
def initialize_tts_cache():
|
484 |
+
print("Initializing TTS cache")
|
485 |
+
"""Selects initial sentences and starts generation tasks."""
|
486 |
+
with app.app_context(): # Ensure access to models
|
487 |
+
if not all_harvard_sentences:
|
488 |
+
app.logger.error("Harvard sentences not loaded. Cannot initialize cache.")
|
489 |
+
return
|
490 |
+
|
491 |
+
initial_selection = random.sample(all_harvard_sentences, min(len(all_harvard_sentences), TTS_CACHE_SIZE))
|
492 |
+
app.logger.info(f"Initializing TTS cache with {len(initial_selection)} sentences...")
|
493 |
+
|
494 |
+
for sentence in initial_selection:
|
495 |
+
# Use the main cache_executor for initial population too
|
496 |
+
cache_executor.submit(_generate_cache_entry_task, sentence)
|
497 |
+
app.logger.info("Submitted initial cache generation tasks.")
|
498 |
+
|
499 |
+
# --- End TTS Caching Functions ---
|
500 |
+
|
501 |
+
|
502 |
@app.route("/api/tts/generate", methods=["POST"])
|
503 |
+
@limiter.limit("10 per minute") # Keep limit, cached responses are still requests
|
504 |
def generate_tts():
|
505 |
# If verification not setup, handle it first
|
506 |
if app.config["TURNSTILE_ENABLED"] and not session.get("turnstile_verified"):
|
507 |
return jsonify({"error": "Turnstile verification required"}), 403
|
508 |
|
509 |
data = request.json
|
510 |
+
text = data.get("text", "").strip() # Ensure text is stripped
|
511 |
|
512 |
if not text or len(text) > 1000:
|
513 |
return jsonify({"error": "Invalid or too long text"}), 400
|
514 |
|
515 |
+
# --- Cache Check ---
|
516 |
+
cache_hit = False
|
517 |
+
session_data_from_cache = None
|
518 |
+
with tts_cache_lock:
|
519 |
+
if text in tts_cache:
|
520 |
+
cache_hit = True
|
521 |
+
cached_entry = tts_cache.pop(text) # Remove from cache immediately
|
522 |
+
app.logger.info(f"TTS Cache HIT for: '{text[:50]}...'")
|
523 |
+
|
524 |
+
# Prepare session data using cached info
|
525 |
+
session_id = str(uuid.uuid4())
|
526 |
+
session_data_from_cache = {
|
527 |
+
"model_a": cached_entry["model_a"],
|
528 |
+
"model_b": cached_entry["model_b"],
|
529 |
+
"audio_a": cached_entry["audio_a"], # Paths are now from cache_dir
|
530 |
+
"audio_b": cached_entry["audio_b"],
|
531 |
+
"text": text,
|
532 |
+
"created_at": datetime.utcnow(),
|
533 |
+
"expires_at": datetime.utcnow() + timedelta(minutes=30),
|
534 |
+
"voted": False,
|
535 |
+
}
|
536 |
+
app.tts_sessions[session_id] = session_data_from_cache
|
537 |
+
|
538 |
+
# Trigger background task to replace the used cache entry
|
539 |
+
cache_executor.submit(_generate_cache_entry_task, None) # Pass None to signal replacement
|
540 |
+
|
541 |
+
if cache_hit and session_data_from_cache:
|
542 |
+
# Return response using cached data
|
543 |
+
# Note: The files are now managed by the session lifecycle (cleanup_session)
|
544 |
+
return jsonify(
|
545 |
+
{
|
546 |
+
"session_id": session_id,
|
547 |
+
"audio_a": f"/api/tts/audio/{session_id}/a",
|
548 |
+
"audio_b": f"/api/tts/audio/{session_id}/b",
|
549 |
+
"expires_in": 1800, # 30 minutes in seconds
|
550 |
+
"cache_hit": True,
|
551 |
+
}
|
552 |
+
)
|
553 |
+
# --- End Cache Check ---
|
554 |
+
|
555 |
+
# --- Cache Miss: Generate on the fly ---
|
556 |
+
app.logger.info(f"TTS Cache MISS for: '{text[:50]}...'. Generating on the fly.")
|
557 |
available_models = Model.query.filter_by(
|
558 |
model_type=ModelType.TTS, is_active=True
|
559 |
).all()
|
|
|
563 |
selected_models = random.sample(available_models, 2)
|
564 |
|
565 |
try:
|
|
|
566 |
audio_files = []
|
567 |
model_ids = []
|
568 |
|
569 |
+
# Function to process a single model (generate directly to TEMP_AUDIO_DIR, not cache subdir)
|
570 |
+
def process_model_on_the_fly(model):
|
571 |
+
# Generate and save directly to the main temp dir
|
572 |
+
# Assume predict_tts handles saving temporary files
|
573 |
+
temp_audio_path = predict_tts(text, model.id)
|
574 |
+
if not temp_audio_path or not os.path.exists(temp_audio_path):
|
575 |
+
raise ValueError(f"predict_tts failed for model {model.id}")
|
576 |
|
577 |
+
# Create a unique name in the main TEMP_AUDIO_DIR for the session
|
578 |
+
file_uuid = str(uuid.uuid4())
|
579 |
+
dest_path = os.path.join(TEMP_AUDIO_DIR, f"{file_uuid}.wav")
|
580 |
+
shutil.move(temp_audio_path, dest_path) # Move from predict_tts's temp location
|
581 |
+
|
582 |
+
return {"model_id": model.id, "audio_path": dest_path}
|
583 |
|
|
|
584 |
|
585 |
# Use ThreadPoolExecutor to process models concurrently
|
586 |
with ThreadPoolExecutor(max_workers=2) as executor:
|
587 |
+
results = list(executor.map(process_model_on_the_fly, selected_models))
|
588 |
|
589 |
# Extract results
|
590 |
for result in results:
|
|
|
596 |
app.tts_sessions[session_id] = {
|
597 |
"model_a": model_ids[0],
|
598 |
"model_b": model_ids[1],
|
599 |
+
"audio_a": audio_files[0], # Paths are now from TEMP_AUDIO_DIR directly
|
600 |
"audio_b": audio_files[1],
|
601 |
"text": text,
|
602 |
"created_at": datetime.utcnow(),
|
|
|
610 |
"session_id": session_id,
|
611 |
"audio_a": f"/api/tts/audio/{session_id}/a",
|
612 |
"audio_b": f"/api/tts/audio/{session_id}/b",
|
613 |
+
"expires_in": 1800,
|
614 |
+
"cache_hit": False,
|
615 |
}
|
616 |
)
|
617 |
|
618 |
except Exception as e:
|
619 |
+
app.logger.error(f"TTS on-the-fly generation error: {str(e)}", exc_info=True)
|
620 |
+
# Cleanup any files potentially created during the failed attempt
|
621 |
+
if 'results' in locals():
|
622 |
+
for res in results:
|
623 |
+
if 'audio_path' in res and os.path.exists(res['audio_path']):
|
624 |
+
try:
|
625 |
+
os.remove(res['audio_path'])
|
626 |
+
except OSError:
|
627 |
+
pass
|
628 |
return jsonify({"error": "Failed to generate TTS"}), 500
|
629 |
+
# --- End Cache Miss ---
|
630 |
|
631 |
|
632 |
@app.route("/api/tts/audio/<session_id>/<model_key>")
|
|
|
1061 |
cleanup_conversational_session(sid)
|
1062 |
app.logger.info(f"Cleaned up {len(expired_tts_sessions)} TTS and {len(expired_conv_sessions)} conversational sessions.")
|
1063 |
|
1064 |
+
# Also cleanup potentially expired cache entries (e.g., > 1 hour old)
|
1065 |
+
# This prevents stale cache entries if generation is slow or failing
|
1066 |
+
# cleanup_stale_cache_entries()
|
1067 |
|
1068 |
# Run cleanup every 15 minutes
|
1069 |
+
scheduler = BackgroundScheduler(daemon=True) # Run scheduler as daemon thread
|
1070 |
scheduler.add_job(cleanup_expired_sessions, "interval", minutes=15)
|
1071 |
scheduler.start()
|
1072 |
print("Cleanup scheduler started") # Use print for startup messages
|
|
|
1199 |
})
|
1200 |
|
1201 |
|
1202 |
+
@app.route("/api/tts/cached-sentences")
|
1203 |
+
def get_cached_sentences():
|
1204 |
+
"""Returns a list of sentences currently available in the TTS cache."""
|
1205 |
+
with tts_cache_lock:
|
1206 |
+
cached_keys = list(tts_cache.keys())
|
1207 |
+
return jsonify(cached_keys)
|
1208 |
+
|
1209 |
+
|
1210 |
if __name__ == "__main__":
|
1211 |
with app.app_context():
|
1212 |
# Ensure ./instance and ./votes directories exist
|
1213 |
os.makedirs("instance", exist_ok=True)
|
1214 |
os.makedirs("./votes", exist_ok=True) # Create votes directory if it doesn't exist
|
1215 |
+
os.makedirs(CACHE_AUDIO_DIR, exist_ok=True) # Ensure cache audio dir exists
|
1216 |
+
|
1217 |
+
# Clean up old cache audio files on startup
|
1218 |
+
try:
|
1219 |
+
app.logger.info(f"Clearing old cache audio files from {CACHE_AUDIO_DIR}")
|
1220 |
+
for filename in os.listdir(CACHE_AUDIO_DIR):
|
1221 |
+
file_path = os.path.join(CACHE_AUDIO_DIR, filename)
|
1222 |
+
try:
|
1223 |
+
if os.path.isfile(file_path) or os.path.islink(file_path):
|
1224 |
+
os.unlink(file_path)
|
1225 |
+
elif os.path.isdir(file_path):
|
1226 |
+
shutil.rmtree(file_path)
|
1227 |
+
except Exception as e:
|
1228 |
+
app.logger.error(f'Failed to delete {file_path}. Reason: {e}')
|
1229 |
+
except Exception as e:
|
1230 |
+
app.logger.error(f"Error clearing cache directory {CACHE_AUDIO_DIR}: {e}")
|
1231 |
+
|
1232 |
|
1233 |
# Download database if it doesn't exist (only on initial space start)
|
1234 |
if IS_SPACES and not os.path.exists(app.config["SQLALCHEMY_DATABASE_URI"].replace("sqlite:///", "")):
|
|
|
1249 |
db.create_all() # Create tables if they don't exist
|
1250 |
insert_initial_models()
|
1251 |
# Setup background tasks
|
1252 |
+
initialize_tts_cache() # Start populating the cache
|
1253 |
setup_cleanup()
|
1254 |
setup_periodic_tasks() # Renamed function call
|
1255 |
|
templates/arena.html
CHANGED
@@ -188,6 +188,10 @@
|
|
188 |
</div>
|
189 |
</div>
|
190 |
</div>
|
|
|
|
|
|
|
|
|
191 |
{% endblock %}
|
192 |
|
193 |
{% block extra_head %}
|
@@ -915,10 +919,6 @@
|
|
915 |
border-color: var(--border-color);
|
916 |
}
|
917 |
|
918 |
-
.random-script-btn:hover {
|
919 |
-
background-color: rgba(255, 255, 255, 0.1);
|
920 |
-
}
|
921 |
-
|
922 |
.line-input {
|
923 |
background-color: var(--light-gray);
|
924 |
color: var(--text-color);
|
@@ -1015,6 +1015,7 @@
|
|
1015 |
let currentSessionId = null;
|
1016 |
let modelNames = { a: '', b: '' };
|
1017 |
let wavePlayers = { a: null, b: null };
|
|
|
1018 |
|
1019 |
// Initialize WavePlayers with mobile settings
|
1020 |
wavePlayerContainers.forEach(container => {
|
@@ -1026,8 +1027,33 @@
|
|
1026 |
});
|
1027 |
});
|
1028 |
|
1029 |
-
//
|
1030 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1031 |
|
1032 |
// Check URL hash for direct tab access
|
1033 |
function checkHashAndSetTab() {
|
@@ -1284,9 +1310,23 @@
|
|
1284 |
}
|
1285 |
|
1286 |
function handleRandom() {
|
1287 |
-
|
1288 |
-
|
1289 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1290 |
textInput.focus();
|
1291 |
}
|
1292 |
|
@@ -1366,6 +1406,9 @@
|
|
1366 |
|
1367 |
// Add event listener for next round button
|
1368 |
nextRoundBtn.addEventListener('click', resetToInitialState);
|
|
|
|
|
|
|
1369 |
});
|
1370 |
</script>
|
1371 |
|
|
|
188 |
</div>
|
189 |
</div>
|
190 |
</div>
|
191 |
+
|
192 |
+
<!-- Hidden element to store fallback sentences data -->
|
193 |
+
<div id="fallback-sentences-data" data-sentences="{{ harvard_sentences | tojson | safe }}" style="display: none;"></div>
|
194 |
+
|
195 |
{% endblock %}
|
196 |
|
197 |
{% block extra_head %}
|
|
|
919 |
border-color: var(--border-color);
|
920 |
}
|
921 |
|
|
|
|
|
|
|
|
|
922 |
.line-input {
|
923 |
background-color: var(--light-gray);
|
924 |
color: var(--text-color);
|
|
|
1015 |
let currentSessionId = null;
|
1016 |
let modelNames = { a: '', b: '' };
|
1017 |
let wavePlayers = { a: null, b: null };
|
1018 |
+
let cachedSentences = []; // To store sentences available in cache
|
1019 |
|
1020 |
// Initialize WavePlayers with mobile settings
|
1021 |
wavePlayerContainers.forEach(container => {
|
|
|
1027 |
});
|
1028 |
});
|
1029 |
|
1030 |
+
// Fallback random text options (if cache is unavailable)
|
1031 |
+
let fallbackRandomTexts = [];
|
1032 |
+
try {
|
1033 |
+
const dataElement = document.getElementById('fallback-sentences-data');
|
1034 |
+
if (dataElement && dataElement.dataset.sentences) {
|
1035 |
+
fallbackRandomTexts = JSON.parse(dataElement.dataset.sentences);
|
1036 |
+
} else {
|
1037 |
+
console.error("Fallback sentences data element not found or empty.");
|
1038 |
+
}
|
1039 |
+
} catch (e) {
|
1040 |
+
console.error("Error parsing fallback sentences from data attribute:", e);
|
1041 |
+
// fallbackRandomTexts remains an empty array if parsing fails
|
1042 |
+
}
|
1043 |
+
|
1044 |
+
// Fetch cached sentences on load
|
1045 |
+
function fetchCachedSentences() {
|
1046 |
+
fetch('/api/tts/cached-sentences')
|
1047 |
+
.then(response => response.ok ? response.json() : Promise.reject('Failed to fetch cached sentences'))
|
1048 |
+
.then(data => {
|
1049 |
+
cachedSentences = data;
|
1050 |
+
console.log(`Fetched ${cachedSentences.length} cached sentences.`);
|
1051 |
+
})
|
1052 |
+
.catch(error => {
|
1053 |
+
console.error('Error fetching cached sentences:', error);
|
1054 |
+
// Keep cachedSentences as empty array, fallback will be used
|
1055 |
+
});
|
1056 |
+
}
|
1057 |
|
1058 |
// Check URL hash for direct tab access
|
1059 |
function checkHashAndSetTab() {
|
|
|
1310 |
}
|
1311 |
|
1312 |
function handleRandom() {
|
1313 |
+
let selectedText = '';
|
1314 |
+
if (cachedSentences && cachedSentences.length > 0) {
|
1315 |
+
// Select a random text from the cache
|
1316 |
+
selectedText = cachedSentences[Math.floor(Math.random() * cachedSentences.length)];
|
1317 |
+
console.log("Using random sentence from cache.");
|
1318 |
+
} else {
|
1319 |
+
// Fallback to the initial list if cache is empty or failed to load
|
1320 |
+
console.log("Cache empty or unavailable, using random sentence from fallback list.");
|
1321 |
+
if (fallbackRandomTexts && fallbackRandomTexts.length > 0) {
|
1322 |
+
selectedText = fallbackRandomTexts[Math.floor(Math.random() * fallbackRandomTexts.length)];
|
1323 |
+
} else {
|
1324 |
+
// Absolute fallback if both cache and initial list fail
|
1325 |
+
openToast("No random sentences available.", "warning");
|
1326 |
+
return;
|
1327 |
+
}
|
1328 |
+
}
|
1329 |
+
textInput.value = selectedText;
|
1330 |
textInput.focus();
|
1331 |
}
|
1332 |
|
|
|
1406 |
|
1407 |
// Add event listener for next round button
|
1408 |
nextRoundBtn.addEventListener('click', resetToInitialState);
|
1409 |
+
|
1410 |
+
// Fetch cached sentences when the DOM is ready
|
1411 |
+
fetchCachedSentences();
|
1412 |
});
|
1413 |
</script>
|
1414 |
|