feat: revue de code, doc CODE_REVIEW, forfaits 2026, traduction LLM, providers avec modèle
Made-with: Cursor
This commit is contained in:
174
services/progress_tracker.py
Normal file
174
services/progress_tracker.py
Normal file
@@ -0,0 +1,174 @@
|
||||
"""
|
||||
Progress Tracker Service (Story 2.11)
|
||||
|
||||
Provides real-time progress tracking for translation jobs.
|
||||
Designed for O(1) updates and < 500ms latency (NFR3).
|
||||
"""
|
||||
|
||||
from typing import Dict, Any, Optional, Callable
|
||||
import threading
|
||||
import time
|
||||
|
||||
|
||||
class ProgressTracker:
|
||||
"""
|
||||
Track translation progress with callback support.
|
||||
|
||||
Designed for high-performance updates with minimal overhead.
|
||||
Uses in-memory storage for MVP (consistent with Story 2.10 pattern).
|
||||
|
||||
Usage:
|
||||
storage = {} # Reference to _translation_jobs dict
|
||||
tracker = ProgressTracker("job_123", storage)
|
||||
tracker.update(50, "Translating sheet 2/4")
|
||||
|
||||
# Or use item-based progress
|
||||
tracker.update_item(3, 10, "Translating slide")
|
||||
"""
|
||||
|
||||
def __init__(self, job_id: str, storage: Dict[str, Any]):
|
||||
"""
|
||||
Initialize progress tracker.
|
||||
|
||||
Args:
|
||||
job_id: The translation job ID
|
||||
storage: Reference to the job storage dict (e.g., _translation_jobs)
|
||||
"""
|
||||
self.job_id = job_id
|
||||
self.storage = storage
|
||||
self._lock = threading.RLock()
|
||||
self._last_update_time = 0
|
||||
self._min_update_interval = 0.05 # 50ms minimum between updates (throttling)
|
||||
|
||||
def update(self, percent: int, step: str) -> None:
|
||||
"""
|
||||
Update progress percentage and current step.
|
||||
|
||||
Thread-safe and throttled to prevent excessive updates.
|
||||
|
||||
Args:
|
||||
percent: Progress percentage (0-100), will be clamped
|
||||
step: Human-readable description of current operation
|
||||
"""
|
||||
with self._lock:
|
||||
current_time = time.time()
|
||||
if current_time - self._last_update_time < self._min_update_interval:
|
||||
if percent < 100:
|
||||
return
|
||||
|
||||
job = self.storage.get(self.job_id)
|
||||
if job:
|
||||
# Never decrease progress — only move forward.
|
||||
new_percent = min(100, max(0, percent))
|
||||
job["progress_percent"] = max(job.get("progress_percent", 0), new_percent)
|
||||
job["current_step"] = step
|
||||
job["processed_items"] = job.get("processed_items", 0)
|
||||
job["total_items"] = job.get("total_items", 0)
|
||||
self._last_update_time = current_time
|
||||
|
||||
def update_item(
|
||||
self, current: int, total: int, item_name: str, max_percent: int = 100
|
||||
) -> None:
|
||||
"""
|
||||
Update progress based on item count (e.g., slides, sheets).
|
||||
|
||||
Calculates percentage from current/total and formats step message.
|
||||
|
||||
Args:
|
||||
current: Current item number (1-based)
|
||||
total: Total number of items
|
||||
item_name: Name of item type (e.g., "Translating slide", "Processing sheet")
|
||||
max_percent: Upper bound for the computed percentage (default 100).
|
||||
Use 95 to reserve the last 5% for file-save + set_completed().
|
||||
"""
|
||||
percent = int((current / total) * 100) if total > 0 else 0
|
||||
percent = min(percent, max_percent)
|
||||
step = f"{item_name} {current}/{total}"
|
||||
|
||||
with self._lock:
|
||||
current_time = time.time()
|
||||
if current_time - self._last_update_time < self._min_update_interval:
|
||||
if percent < 100:
|
||||
return
|
||||
|
||||
job = self.storage.get(self.job_id)
|
||||
if job:
|
||||
# Never decrease progress — only move forward.
|
||||
new_percent = min(100, max(0, percent))
|
||||
job["progress_percent"] = max(job.get("progress_percent", 0), new_percent)
|
||||
job["current_step"] = step
|
||||
job["processed_items"] = current
|
||||
job["total_items"] = total
|
||||
self._last_update_time = current_time
|
||||
|
||||
def set_error(
|
||||
self, error_message: str, step: str = "Error during translation"
|
||||
) -> None:
|
||||
"""
|
||||
Mark job as failed with error message.
|
||||
|
||||
Args:
|
||||
error_message: Description of the error
|
||||
step: Current step description (default: "Error during translation")
|
||||
"""
|
||||
with self._lock:
|
||||
job = self.storage.get(self.job_id)
|
||||
if job:
|
||||
job["status"] = "failed"
|
||||
job["error_message"] = error_message
|
||||
job["current_step"] = step
|
||||
job["failed_at"] = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
||||
|
||||
def set_completed(self, output_path: Optional[str] = None) -> None:
|
||||
"""
|
||||
Mark job as completed.
|
||||
|
||||
Args:
|
||||
output_path: Optional path to the output file
|
||||
"""
|
||||
with self._lock:
|
||||
job = self.storage.get(self.job_id)
|
||||
if job:
|
||||
job["status"] = "completed"
|
||||
job["progress_percent"] = 100
|
||||
job["current_step"] = "Translation complete"
|
||||
job["completed_at"] = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
||||
if output_path:
|
||||
job["output_path"] = str(output_path)
|
||||
|
||||
|
||||
def create_progress_callback(
|
||||
tracker: ProgressTracker, item_name: str, total_items: int
|
||||
) -> Callable[[Dict[str, Any]], None]:
|
||||
"""
|
||||
Create a progress callback function for use with translators.
|
||||
|
||||
Args:
|
||||
tracker: ProgressTracker instance
|
||||
item_name: Name of item being processed (e.g., "Translating slide")
|
||||
total_items: Total number of items
|
||||
|
||||
Returns:
|
||||
Callback function compatible with translator progress_callback parameter
|
||||
"""
|
||||
|
||||
def callback(progress_info: Dict[str, Any]) -> None:
|
||||
"""Progress callback that updates the tracker."""
|
||||
# Extract item number from progress_info dict
|
||||
# Different translators use different keys
|
||||
current = progress_info.get(
|
||||
"slide",
|
||||
progress_info.get(
|
||||
"sheet", progress_info.get("paragraph", progress_info.get("element", 1))
|
||||
),
|
||||
)
|
||||
total = progress_info.get(
|
||||
"total_slides",
|
||||
progress_info.get(
|
||||
"total", progress_info.get("total_paragraphs", total_items)
|
||||
),
|
||||
)
|
||||
|
||||
tracker.update_item(current, total, item_name)
|
||||
|
||||
return callback
|
||||
Reference in New Issue
Block a user