|
14 | 14 | from src.utils.file_utils import get_unique_output_path, generate_tts_for_translation |
15 | 15 | from src.core.llm import OpenRouterProvider |
16 | 16 | from src.core.llm.exceptions import RateLimitError |
| 17 | +from src.config import AUTO_PAUSE_ON_RATE_LIMIT, RATE_LIMIT_AUTO_RESUME_DELAY |
17 | 18 | from src.core.adapters import translate_file |
18 | 19 | from src.tts.tts_config import TTSConfig |
19 | 20 | from .websocket import emit_update |
@@ -440,44 +441,82 @@ def _openrouter_cost_callback(cost_data): |
440 | 441 | }, namespace='/') |
441 | 442 |
|
442 | 443 | except RateLimitError as e: |
443 | | - # Auto-pause on rate limit — save checkpoint so user can resume later |
| 444 | + auto_pause = config.get('auto_pause_on_rate_limit', AUTO_PAUSE_ON_RATE_LIMIT) |
444 | 445 | retry_msg = f" Retry suggested after ~{e.retry_after}s." if e.retry_after else "" |
445 | 446 | provider_name = e.provider or config.get('llm_provider', 'API') |
446 | | - pause_msg = f"⏸️ Rate limited by {provider_name}.{retry_msg} Translation auto-paused — you can resume when ready." |
447 | 447 |
|
448 | | - _log_message_callback("rate_limit_auto_pause", pause_msg) |
449 | | - |
450 | | - if state_manager.exists(translation_id): |
451 | | - state_manager.set_translation_field(translation_id, 'status', 'rate_limited') |
452 | | - state_manager.set_translation_field(translation_id, 'interrupted', True) |
453 | | - |
454 | | - # Mark checkpoint as interrupted so it appears in resumable jobs |
455 | | - checkpoint_manager.mark_interrupted(translation_id) |
| 448 | + if not state_manager.exists(translation_id): |
| 449 | + return |
456 | 450 |
|
457 | | - stats = state_manager.get_translation_field(translation_id, 'stats') or {} |
458 | | - elapsed_time = time.time() - stats.get('start_time', time.time()) |
459 | | - _update_translation_stats_callback({'elapsed_time': elapsed_time}) |
| 451 | + # Auto-resume mode keeps the job running: wait, then re-enter from the checkpoint. |
| 452 | + if not auto_pause: |
| 453 | + wait_seconds = e.retry_after or RATE_LIMIT_AUTO_RESUME_DELAY |
| 454 | + wait_msg = (f"⏳ Rate limited by {provider_name}.{retry_msg} " |
| 455 | + f"Auto-resume in {wait_seconds}s (auto-pause disabled).") |
| 456 | + _log_message_callback("rate_limit_auto_resume", wait_msg) |
460 | 457 |
|
461 | | - # Emit rate_limited status + checkpoint_created for UI |
| 458 | + # Surface 'rate_limited' transiently so the UI shows what's happening. |
| 459 | + state_manager.set_translation_field(translation_id, 'status', 'rate_limited') |
462 | 460 | emit_update(socketio, translation_id, { |
463 | 461 | 'status': 'rate_limited', |
464 | | - 'log': pause_msg, |
465 | | - 'result': state_manager.get_translation_field(translation_id, 'result') or f"Translation paused (rate limited)" |
| 462 | + 'log': wait_msg |
466 | 463 | }, state_manager) |
467 | 464 |
|
468 | | - socketio.emit('checkpoint_created', { |
469 | | - 'translation_id': translation_id, |
470 | | - 'status': 'rate_limited', |
471 | | - 'message': pause_msg |
472 | | - }, namespace='/') |
| 465 | + await asyncio.sleep(wait_seconds) |
473 | 466 |
|
474 | | - # Trigger file list refresh for partial output |
475 | | - output_filepath = state_manager.get_translation_field(translation_id, 'output_filepath') |
476 | | - if output_filepath and os.path.exists(output_filepath): |
477 | | - socketio.emit('file_list_changed', { |
478 | | - 'reason': 'rate_limited', |
479 | | - 'filename': config.get('output_filename', 'unknown') |
480 | | - }, namespace='/') |
| 467 | + # Honor an interrupt that arrived during the wait by falling through to pause. |
| 468 | + if state_manager.get_translation_field(translation_id, 'interrupted'): |
| 469 | + _log_message_callback("rate_limit_auto_resume_cancelled", |
| 470 | + "🛑 Auto-resume cancelled by user, pausing instead.") |
| 471 | + else: |
| 472 | + cp_data = checkpoint_manager.load_checkpoint(translation_id) |
| 473 | + if cp_data: |
| 474 | + new_config = dict(config) |
| 475 | + new_config['is_resume'] = True |
| 476 | + new_config['resume_from_index'] = cp_data['resume_from_index'] |
| 477 | + checkpoint_manager.mark_running(translation_id) |
| 478 | + state_manager.set_translation_field(translation_id, 'status', 'running') |
| 479 | + emit_update(socketio, translation_id, { |
| 480 | + 'status': 'running', |
| 481 | + 'log': f"▶️ Auto-resuming from chunk {cp_data['resume_from_index']}..." |
| 482 | + }, state_manager) |
| 483 | + await perform_actual_translation( |
| 484 | + translation_id, new_config, state_manager, output_dir, socketio |
| 485 | + ) |
| 486 | + return |
| 487 | + # No checkpoint available, fall through to the pause path below. |
| 488 | + _log_message_callback("rate_limit_no_checkpoint", |
| 489 | + "⚠️ Auto-resume requested but no checkpoint found, falling back to pause.") |
| 490 | + |
| 491 | + pause_msg = f"⏸️ Rate limited by {provider_name}.{retry_msg} Translation auto-paused, you can resume when ready." |
| 492 | + _log_message_callback("rate_limit_auto_pause", pause_msg) |
| 493 | + |
| 494 | + state_manager.set_translation_field(translation_id, 'status', 'rate_limited') |
| 495 | + state_manager.set_translation_field(translation_id, 'interrupted', True) |
| 496 | + checkpoint_manager.mark_interrupted(translation_id) |
| 497 | + |
| 498 | + stats = state_manager.get_translation_field(translation_id, 'stats') or {} |
| 499 | + elapsed_time = time.time() - stats.get('start_time', time.time()) |
| 500 | + _update_translation_stats_callback({'elapsed_time': elapsed_time}) |
| 501 | + |
| 502 | + emit_update(socketio, translation_id, { |
| 503 | + 'status': 'rate_limited', |
| 504 | + 'log': pause_msg, |
| 505 | + 'result': state_manager.get_translation_field(translation_id, 'result') or f"Translation paused (rate limited)" |
| 506 | + }, state_manager) |
| 507 | + |
| 508 | + socketio.emit('checkpoint_created', { |
| 509 | + 'translation_id': translation_id, |
| 510 | + 'status': 'rate_limited', |
| 511 | + 'message': pause_msg |
| 512 | + }, namespace='/') |
| 513 | + |
| 514 | + output_filepath = state_manager.get_translation_field(translation_id, 'output_filepath') |
| 515 | + if output_filepath and os.path.exists(output_filepath): |
| 516 | + socketio.emit('file_list_changed', { |
| 517 | + 'reason': 'rate_limited', |
| 518 | + 'filename': config.get('output_filename', 'unknown') |
| 519 | + }, namespace='/') |
481 | 520 |
|
482 | 521 | except Exception as e: |
483 | 522 | critical_error_msg = f"Critical error during translation task ({translation_id}): {str(e)}" |
|
0 commit comments