From b3aaa948313348a400cbcae09ea8d1bd20c1c5ad Mon Sep 17 00:00:00 2001 From: JB Date: Mon, 1 Dec 2025 05:26:50 -0800 Subject: [PATCH] feat: Add estimated time remaining display and duration formatting to job progress cards --- abogen/web/routes/main.py | 15 +++++++++++++ abogen/web/service.py | 29 +++++++++++++++++-------- abogen/web/static/styles.css | 11 ++++++++++ abogen/web/templates/partials/jobs.html | 7 +++++- 4 files changed, 52 insertions(+), 10 deletions(-) diff --git a/abogen/web/routes/main.py b/abogen/web/routes/main.py index ac63b72..29a25c2 100644 --- a/abogen/web/routes/main.py +++ b/abogen/web/routes/main.py @@ -35,6 +35,21 @@ def datetimeformat(value: float, fmt: str = "%Y-%m-%d %H:%M:%S") -> str: from datetime import datetime return datetime.fromtimestamp(value).strftime(fmt) +@main_bp.app_template_filter("durationformat") +def durationformat(value: Optional[float]) -> str: + if value is None: + return "" + seconds = int(value) + if seconds < 60: + return f"{seconds}s" + minutes = seconds // 60 + seconds = seconds % 60 + if minutes < 60: + return f"{minutes}m {seconds}s" + hours = minutes // 60 + minutes = minutes % 60 + return f"{hours}h {minutes}m" + @main_bp.route("/") def index(): pending_id = request.args.get("pending_id") diff --git a/abogen/web/service.py b/abogen/web/service.py index 69782c4..755775a 100644 --- a/abogen/web/service.py +++ b/abogen/web/service.py @@ -148,13 +148,24 @@ class Job: generate_epub3: bool = False speaker_analysis: Dict[str, Any] = field(default_factory=dict) speaker_analysis_threshold: int = 3 - analysis_requested: bool = False - speaker_voice_languages: List[str] = field(default_factory=list) - applied_speaker_config: Optional[str] = None - entity_summary: Dict[str, Any] = field(default_factory=dict) - manual_overrides: List[Dict[str, Any]] = field(default_factory=list) - pronunciation_overrides: List[Dict[str, Any]] = field(default_factory=list) - normalization_overrides: Dict[str, Any] = field(default_factory=dict) + + @property + def estimated_time_remaining(self) -> Optional[float]: + """ + Returns the estimated seconds remaining based on current progress and elapsed time. + Returns None if the job hasn't started, is finished, or progress is 0. + """ + if self.status != JobStatus.RUNNING or not self.started_at or self.progress <= 0: + return None + + elapsed = time.time() - self.started_at + if elapsed <= 0: + return None + + # Estimate total time based on current progress + total_estimated = elapsed / self.progress + remaining = total_estimated - elapsed + return max(0.0, remaining) def add_log(self, message: str, level: str = "info") -> None: entry = JobLog(timestamp=time.time(), message=message, level=level) @@ -1589,8 +1600,8 @@ def default_storage_root() -> Path: def build_service( runner: Callable[[Job], None], *, - output_root: Optional[Path] = None, - uploads_root: Optional[Path] = None, + output_root: Optional ConversionService: output_root = output_root or default_storage_root() service = ConversionService( diff --git a/abogen/web/static/styles.css b/abogen/web/static/styles.css index 51aec01..e822dcc 100644 --- a/abogen/web/static/styles.css +++ b/abogen/web/static/styles.css @@ -1910,6 +1910,17 @@ button.step-indicator__item:focus-visible { color: var(--muted); } +.job-card__progress-meta { + display: flex; + justify-content: space-between; + align-items: center; +} + +.job-card__eta { + color: var(--muted); + font-variant-numeric: tabular-nums; +} + .job-card__footer { display: flex; flex-wrap: wrap; diff --git a/abogen/web/templates/partials/jobs.html b/abogen/web/templates/partials/jobs.html index 3fe76a4..a24a77c 100644 --- a/abogen/web/templates/partials/jobs.html +++ b/abogen/web/templates/partials/jobs.html @@ -26,7 +26,12 @@
- {{ progress_value }}% · {{ job.processed_characters }} / {{ job.total_characters or '—' }} +
+ {{ progress_value }}% · {{ job.processed_characters }} / {{ job.total_characters or '—' }} + {% if job.estimated_time_remaining %} + ~{{ job.estimated_time_remaining | durationformat }} remaining + {% endif %} +