From 11a912f990ef6c082c1f317bc73a0f3490bf366d Mon Sep 17 00:00:00 2001 From: hp Date: Thu, 14 May 2026 20:40:59 +0800 Subject: [PATCH 1/2] refine: use thin percent progress bar --- src/asr/observability/console.py | 70 ++++++++++++++++++++++++++------ tests/test_observability.py | 30 ++++++++------ 2 files changed, 76 insertions(+), 24 deletions(-) diff --git a/src/asr/observability/console.py b/src/asr/observability/console.py index 3e893d0..7ba79b2 100644 --- a/src/asr/observability/console.py +++ b/src/asr/observability/console.py @@ -8,11 +8,26 @@ from typing import TextIO from rich.console import Console -from rich.progress import BarColumn, Progress, TextColumn +from rich.progress import Progress, ProgressColumn, Task, TextColumn +from rich.text import Text from asr.observability.events import ObservabilityEvent -_PROGRESS_BAR_WIDTH = 16 +_PROGRESS_BAR_WIDTH = 20 + + +class _ThinProgressBarColumn(ProgressColumn): + """Render a full-length thin progress track.""" + + def render(self, task: Task) -> Text: + total = max(1.0, float(task.total or 1.0)) + completed = max(0.0, min(float(task.completed), total)) + filled = int((completed / total) * _PROGRESS_BAR_WIDTH + 0.5) + empty = _PROGRESS_BAR_WIDTH - filled + bar = Text() + bar.append("━" * filled, style="cyan") + bar.append("─" * empty, style="dim white") + return bar @dataclass(slots=True) @@ -101,7 +116,11 @@ def _record_window_progress(self, event: ObservabilityEvent) -> None: def _window_progress_line(self, perf_counter: float) -> str: count = self._current_window_count index = min(self._current_window_index, count) if count else self._current_window_index - return f"{self._progress_bar(index, count)} | {index}/{count} | {self._elapsed(perf_counter)}" + return ( + f"{self._progress_bar(index, count)} | " + f"{self._progress_percent(index, count)} | " + f"{self._progress_elapsed(perf_counter)}" + ) def _write_window_progress(self, perf_counter: float) -> None: if not self.is_tty: @@ -112,13 +131,15 @@ def _write_window_progress(self, perf_counter: float) -> None: count = self._current_window_count total = max(1, count) completed = min(self._current_window_index, total) - elapsed = self._elapsed(perf_counter) + elapsed = self._progress_elapsed(perf_counter) + percent = self._progress_percent(self._current_window_index, count) if self._progress_task_id is None: self._progress_task_id = progress.add_task( self._progress_description(), total=total, completed=completed, elapsed=elapsed, + percent=percent, ) else: progress.update( @@ -126,6 +147,7 @@ def _write_window_progress(self, perf_counter: float) -> None: total=total, completed=completed, elapsed=elapsed, + percent=percent, ) progress.refresh() @@ -138,12 +160,11 @@ def _ensure_progress(self) -> Progress: self._console = Console( file=self.stream, force_terminal=True, - no_color=True, ) self._progress = Progress( TextColumn("{task.description}"), - BarColumn(bar_width=_PROGRESS_BAR_WIDTH), - TextColumn("{task.completed:.0f}/{task.total:.0f}"), + _ThinProgressBarColumn(), + TextColumn("{task.fields[percent]}"), TextColumn("{task.fields[elapsed]}"), console=self._console, auto_refresh=False, @@ -160,8 +181,9 @@ def _stop_progress(self) -> None: return self._progress.stop() if self.is_tty: - self.stream.write("\n") - self.stream.flush() + if not self._stream_ends_with_newline(): + self.stream.write("\n") + self.stream.flush() self._last_width = 0 self._progress = None self._progress_task_id = None @@ -169,11 +191,35 @@ def _stop_progress(self) -> None: def _progress_description(self) -> str: return f"[{self._current_index}/{self._current_total}] {self._current_name}" + def _stream_ends_with_newline(self) -> bool: + getvalue = getattr(self.stream, "getvalue", None) + if not callable(getvalue): + return False + value = str(getvalue()) + return value.endswith("\n") + def _progress_bar(self, index: int, count: int) -> str: if count <= 0: - return "░" * _PROGRESS_BAR_WIDTH - filled = round((max(0, min(index, count)) / count) * _PROGRESS_BAR_WIDTH) - return ("█" * filled) + ("░" * (_PROGRESS_BAR_WIDTH - filled)) + return "─" * _PROGRESS_BAR_WIDTH + filled = int((max(0, min(index, count)) / count) * _PROGRESS_BAR_WIDTH + 0.5) + return ("━" * filled) + ("─" * (_PROGRESS_BAR_WIDTH - filled)) + + def _progress_percent(self, index: int, count: int) -> str: + if count <= 0: + return "0%" + bounded = max(0, min(index, count)) + percent = int((bounded / count) * 100 + 0.5) + return f"{percent}%" + + def _progress_elapsed(self, perf_counter: float) -> str: + if self._file_start_perf is None: + return "00:00" + elapsed = int(max(0.0, perf_counter - self._file_start_perf)) + minutes, seconds = divmod(elapsed, 60) + hours, minutes = divmod(minutes, 60) + if hours: + return f"{hours:d}:{minutes:02d}:{seconds:02d}" + return f"{minutes:02d}:{seconds:02d}" def _elapsed(self, perf_counter: float) -> str: if self._file_start_perf is None: diff --git a/tests/test_observability.py b/tests/test_observability.py index ca5fcfc..de99161 100644 --- a/tests/test_observability.py +++ b/tests/test_observability.py @@ -136,7 +136,7 @@ def test_non_tty_falls_back_to_plain_lines(self) -> None: self.assertIn("prepare", output) self.assertIn("0.5s", output) - def test_provider_window_step_renders_bar_count_and_elapsed_only(self) -> None: + def test_provider_window_step_renders_thin_bar_percent_and_elapsed(self) -> None: stream = io.StringIO() observer = ConsoleProgressObserver(stream=stream, is_tty=False) observer.on_event( @@ -156,16 +156,18 @@ def test_provider_window_step_renders_bar_count_and_elapsed_only(self) -> None: file_id="1", source_path="demo.wav", step="provider_window", - meta={"window_index": 2, "window_count": 8}, - perf_counter=12.5, + meta={"window_index": 3, "window_count": 8}, + perf_counter=52.0, ) ) output = stream.getvalue() - self.assertIn("2/8", output) - self.assertIn("2.5s", output) - self.assertIn("█", output) - self.assertIn("░", output) + self.assertIn("[1/1] demo.wav", output) + self.assertIn("38%", output) + self.assertIn("00:42", output) + self.assertIn("━", output) + self.assertIn("─", output) + self.assertNotIn("3/8", output) self.assertNotIn("transcribe", output) self.assertNotIn("window", output) @@ -190,7 +192,7 @@ def test_tty_provider_window_uses_rich_progress(self) -> None: source_path="demo.wav", step="provider_window", meta={"window_index": 3, "window_count": 8}, - perf_counter=18.0, + perf_counter=52.0, ) ) @@ -199,8 +201,11 @@ def test_tty_provider_window_uses_rich_progress(self) -> None: output = stream.getvalue() self.assertGreaterEqual(output.count("[1/1] demo.wav"), 2) self.assertGreaterEqual(output.count("demo.wav"), 2) - self.assertIn("3/8", output) - self.assertIn("8.0s", output) + self.assertIn("38%", output) + self.assertIn("00:42", output) + self.assertIn("━", output) + self.assertIn("─", output) + self.assertNotIn("3/8", output) self.assertNotIn("transcribe", output) self.assertNotIn("window", output) @@ -241,8 +246,9 @@ def test_file_end_finalizes_window_progress_bar(self) -> None: output = stream.getvalue() self.assertTrue(output.endswith("\n")) - self.assertIn("8/8", output) - self.assertIn("15.5s", output) + self.assertIn("100%", output) + self.assertIn("00:15", output) + self.assertNotIn("8/8", output) self.assertEqual(observer._last_width, 0) def test_vad_missing_dependency_warning_is_rendered_once(self) -> None: From 8335300e7725f54725abaa430d0179deb86a120c Mon Sep 17 00:00:00 2001 From: hp Date: Thu, 14 May 2026 20:57:26 +0800 Subject: [PATCH 2/2] fix: rely on Rich progress cleanup --- src/asr/observability/console.py | 10 ---------- tests/test_observability.py | 1 - 2 files changed, 11 deletions(-) diff --git a/src/asr/observability/console.py b/src/asr/observability/console.py index 7ba79b2..9807300 100644 --- a/src/asr/observability/console.py +++ b/src/asr/observability/console.py @@ -181,9 +181,6 @@ def _stop_progress(self) -> None: return self._progress.stop() if self.is_tty: - if not self._stream_ends_with_newline(): - self.stream.write("\n") - self.stream.flush() self._last_width = 0 self._progress = None self._progress_task_id = None @@ -191,13 +188,6 @@ def _stop_progress(self) -> None: def _progress_description(self) -> str: return f"[{self._current_index}/{self._current_total}] {self._current_name}" - def _stream_ends_with_newline(self) -> bool: - getvalue = getattr(self.stream, "getvalue", None) - if not callable(getvalue): - return False - value = str(getvalue()) - return value.endswith("\n") - def _progress_bar(self, index: int, count: int) -> str: if count <= 0: return "─" * _PROGRESS_BAR_WIDTH diff --git a/tests/test_observability.py b/tests/test_observability.py index de99161..3c349b2 100644 --- a/tests/test_observability.py +++ b/tests/test_observability.py @@ -245,7 +245,6 @@ def test_file_end_finalizes_window_progress_bar(self) -> None: ) output = stream.getvalue() - self.assertTrue(output.endswith("\n")) self.assertIn("100%", output) self.assertIn("00:15", output) self.assertNotIn("8/8", output)