Skip to content

Commit fe5a1ec

Browse files
authored
chore: add animated emoji progress indicators to progress tracker (#273)
* chore: add animated emoji progress indicators to progress tracker Add fun visual feedback during dataset generation with emoji that evolve based on completion percentage. Randomly selects from moon phases, weather, or hatching styles at tracker initialization. * always log 100% * refactor: move progress emoji logic into RandomEmoji class Add progress() method to RandomEmoji that returns an emoji based on completion percentage. This centralizes the progress style logic that was previously duplicated in ProgressTracker. * add some tests
1 parent 0d51539 commit fe5a1ec

3 files changed

Lines changed: 74 additions & 2 deletions

File tree

packages/data-designer-config/src/data_designer/logging.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,14 @@ def debug(cls):
5050
class RandomEmoji:
5151
"""A generator for various themed emoji collections."""
5252

53+
def __init__(self) -> None:
54+
self._progress_style = random.choice(_PROGRESS_STYLES)
55+
56+
def progress(self, percent: float) -> str:
57+
"""Get a progress emoji based on completion percentage (0-100)."""
58+
phase_idx = min(int(percent / 25), len(self._progress_style) - 1)
59+
return self._progress_style[phase_idx]
60+
5361
@staticmethod
5462
def cooking() -> str:
5563
"""Get a random cooking or food preparation emoji."""
@@ -163,3 +171,10 @@ def _make_stream_formatter() -> logging.Formatter:
163171

164172

165173
_DEFAULT_NOISY_LOGGERS = ["httpx", "matplotlib"]
174+
175+
176+
_PROGRESS_STYLES: list[list[str]] = [
177+
["🌑", "🌘", "🌗", "🌖", "🌕"], # Moon phases
178+
["🌧️", "🌦️", "⛅", "🌤️", "☀️"], # Weather (storm to sun)
179+
["🥚", "🐣", "🐥", "🐤", "🐔"], # Hatching (egg to chicken)
180+
]

packages/data-designer-config/tests/test_logging.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,3 +208,54 @@ def test_random_emoji_randomness():
208208
emojis = [RandomEmoji.magic() for _ in range(100)]
209209
# If we get 100 samples, we should get at least 2 different emojis
210210
assert len(set(emojis)) > 1
211+
212+
213+
def test_random_emoji_progress_returns_valid_emoji() -> None:
214+
emoji_gen = RandomEmoji()
215+
emoji = emoji_gen.progress(50.0)
216+
assert emoji is not None
217+
assert len(emoji) > 0
218+
219+
220+
def test_random_emoji_progress_is_deterministic() -> None:
221+
emoji_gen = RandomEmoji()
222+
# Same percentage should always return the same emoji for a given instance
223+
assert emoji_gen.progress(0.0) == emoji_gen.progress(0.0)
224+
assert emoji_gen.progress(50.0) == emoji_gen.progress(50.0)
225+
assert emoji_gen.progress(100.0) == emoji_gen.progress(100.0)
226+
227+
228+
def test_random_emoji_progress_phases_are_distinct() -> None:
229+
emoji_gen = RandomEmoji()
230+
# Each 25% phase should return a different emoji
231+
phase_emojis = [
232+
emoji_gen.progress(0.0), # phase 0
233+
emoji_gen.progress(25.0), # phase 1
234+
emoji_gen.progress(50.0), # phase 2
235+
emoji_gen.progress(75.0), # phase 3
236+
emoji_gen.progress(100.0), # phase 4
237+
]
238+
# All 5 phases should have distinct emojis
239+
assert len(set(phase_emojis)) == 5
240+
241+
242+
def test_random_emoji_progress_phase_boundaries() -> None:
243+
emoji_gen = RandomEmoji()
244+
# Values within the same phase should return the same emoji
245+
assert emoji_gen.progress(0.0) == emoji_gen.progress(24.9)
246+
assert emoji_gen.progress(25.0) == emoji_gen.progress(49.9)
247+
assert emoji_gen.progress(50.0) == emoji_gen.progress(74.9)
248+
assert emoji_gen.progress(75.0) == emoji_gen.progress(99.9)
249+
# Phase transitions should return different emojis
250+
assert emoji_gen.progress(24.9) != emoji_gen.progress(25.0)
251+
assert emoji_gen.progress(49.9) != emoji_gen.progress(50.0)
252+
assert emoji_gen.progress(74.9) != emoji_gen.progress(75.0)
253+
assert emoji_gen.progress(99.9) != emoji_gen.progress(100.0)
254+
255+
256+
def test_random_emoji_progress_clamps_over_100() -> None:
257+
emoji_gen = RandomEmoji()
258+
emoji_100 = emoji_gen.progress(100.0)
259+
emoji_over = emoji_gen.progress(150.0)
260+
# Both should return the same final emoji
261+
assert emoji_100 == emoji_over

packages/data-designer-engine/src/data_designer/engine/dataset_builders/utils/progress_tracker.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
import time
88
from threading import Lock
99

10+
from data_designer.logging import RandomEmoji
11+
1012
logger = logging.getLogger(__name__)
1113

1214

@@ -51,6 +53,7 @@ def __init__(self, total_records: int, label: str, log_interval_percent: int = 1
5153

5254
self.start_time = time.perf_counter()
5355
self.lock = Lock()
56+
self._random_emoji = RandomEmoji()
5457

5558
def log_start(self, max_workers: int) -> None:
5659
"""Log the start of processing with worker count and interval information."""
@@ -76,7 +79,9 @@ def record_failure(self) -> None:
7679
def log_final(self) -> None:
7780
"""Log final progress if not already logged at completion."""
7881
with self.lock:
79-
if self.total_records > 0 and self.completed < self.total_records:
82+
if self.completed > 0 and self.completed == self.total_records:
83+
self._log_progress_unlocked() # always log 100%
84+
elif self.total_records > 0 and self.completed < self.total_records:
8085
self._log_progress_unlocked()
8186
elif self.completed > 0 and self.completed >= self.next_log_at - self.log_interval:
8287
pass # Already logged at the last interval
@@ -110,7 +115,8 @@ def _log_progress_unlocked(self) -> None:
110115
percent = (self.completed / self.total_records) * 100 if self.total_records else 100.0
111116

112117
logger.info(
113-
" |-- 📈 %s progress: %d/%d (%.0f%%) complete, %d ok, %d failed, %.2f rec/s, eta %s",
118+
" |-- %s %s progress: %d/%d (%.0f%%) complete, %d ok, %d failed, %.2f rec/s, eta %s",
119+
self._random_emoji.progress(percent),
114120
self.label,
115121
self.completed,
116122
self.total_records,

0 commit comments

Comments
 (0)