GitHub Actions commited on
Commit
d462da9
·
1 Parent(s): 3da69a8

Sync from GitHub repo

Browse files
Files changed (3) hide show
  1. .gitignore +3 -1
  2. app.py +247 -22
  3. 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
- # Create temp directory for audio files
 
 
 
 
 
 
 
 
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
- harvard_sentences = f.readlines()
279
- random.shuffle(harvard_sentences)
 
 
280
 
281
  @app.route("/")
282
  def arena():
283
- return render_template("arena.html", harvard_sentences=json.dumps(harvard_sentences))
 
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
- # Get two random TTS models
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 process_model(model):
389
- # Call TTS service
390
- audio_path = predict_tts(text, model.id)
 
 
 
391
 
392
- # Copy to temp dir with unique name
393
- file_uuid = str(uuid.uuid4())
394
- dest_path = os.path.join(TEMP_AUDIO_DIR, f"{file_uuid}.wav")
395
- shutil.copy(audio_path, dest_path)
 
 
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(process_model, selected_models))
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, # 30 minutes in seconds
 
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
- // Random text options
1030
- const randomTexts = {{ harvard_sentences|safe }};
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1031
 
1032
  // Check URL hash for direct tab access
1033
  function checkHashAndSetTab() {
@@ -1284,9 +1310,23 @@
1284
  }
1285
 
1286
  function handleRandom() {
1287
- // Select a random text from the array
1288
- const randomText = randomTexts[Math.floor(Math.random() * randomTexts.length)];
1289
- textInput.value = randomText;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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