Download clocktime.py from lyte-codes/clockface: direct link, hf CLI and curl.
- Browser
- Download file 3.03 kB
-
https://huggingface.co/lyte-codes/clockface/resolve/98f945c8702fd79792ed3cbc1718052d3cb12fd2/clocktime.py
- Command line
-
hf download hf://lyte-codes/clockface@98f945c8702fd79792ed3cbc1718052d3cb12fd2/clocktime.py
-
curl -L -o clocktime.py https://huggingface.co/lyte-codes/clockface/resolve/98f945c8702fd79792ed3cbc1718052d3cb12fd2/clocktime.py
3.03 kB
| """Clock-time parsing and the error metric, in MINUTES. | |
| An analog clock face is a ring of 720 minutes (12 hours). Two times that look | |
| identical on a face are identical here: 3:15 and 15:15 are the same reading. | |
| Error is the shorter way round that ring, so it lives in [0, 360]. | |
| Nothing in this file knows about models, images or torch. It is the definition | |
| of "correct" for the whole project, so it is kept small and tested. | |
| """ | |
| from __future__ import annotations | |
| import re | |
| RING = 720.0 # minutes on a 12-hour face | |
| MAX_ERR = RING / 2 # 360: the two hands are as far apart as they can get | |
| _TIME_RE = re.compile(r"^\s*(\d{1,2})\s*[:.h]\s*(\d{1,2}(?:\.\d+)?)\s*$") | |
| def to_minutes(hour: float, minute: float) -> float: | |
| """Position on the face, in minutes past 12 o'clock, in [0, 720).""" | |
| return ((hour % 12) * 60.0 + minute) % RING | |
| def from_minutes(minutes: float) -> tuple[int, float]: | |
| """Inverse of to_minutes. Returns (hour 1-12, minute).""" | |
| m = minutes % RING | |
| hour = int(m // 60) | |
| return (12 if hour == 0 else hour), m - hour * 60 | |
| def fmt(minutes: float) -> str: | |
| """Human clock string, e.g. 227.0 -> '3:47'. Fractions kept if present.""" | |
| h, m = from_minutes(minutes) | |
| if abs(m - round(m)) < 1e-9: | |
| return f"{h}:{int(round(m)):02d}" | |
| return f"{h}:{m:05.2f}" | |
| def parse(text: str) -> float: | |
| """Parse a human-typed time into minutes-on-the-face. | |
| Accepts '3:47', '03:47', '3.47', '15:47' (same face position as 3:47) and | |
| bare digits '347' / '1215' as typed by the labelling UI. | |
| """ | |
| text = (text or "").strip() | |
| if not text: | |
| raise ValueError("empty time") | |
| m = _TIME_RE.match(text) | |
| if not m: | |
| if text.isdigit() and 3 <= len(text) <= 4: | |
| m = re.match(r"^(\d{1,2})(\d{2})$", text) | |
| if not m: | |
| raise ValueError(f"cannot parse time: {text!r}") | |
| hour = float(m.group(1)) | |
| minute = float(m.group(2)) | |
| if not (0 <= hour <= 23): | |
| raise ValueError(f"hour out of range: {text!r}") | |
| if not (0 <= minute < 60): | |
| raise ValueError(f"minute out of range: {text!r}") | |
| return to_minutes(hour, minute) | |
| def error_minutes(pred: float, label: float) -> float: | |
| """Shorter arc between two face positions, in minutes. In [0, 360].""" | |
| d = abs((pred - label) % RING) | |
| return min(d, RING - d) | |
| def swapped(minutes: float) -> float: | |
| """The reading you get if the hour and minute hands are confused. | |
| The minute hand sits at (minutes % 60) minutes-of-the-hour, i.e. face | |
| position (minutes % 60) * 12. The hour hand sits at face position | |
| `minutes`, which reads as minute-of-hour (minutes / 12). Swapping the two | |
| gives this. Used only as a diagnostic on failures. | |
| """ | |
| minute_hand_pos = (minutes % 60) * 12.0 # where the minute hand is | |
| new_hour = minute_hand_pos // 60 # read it as the hour hand | |
| new_minute = (minutes % RING) / 12.0 # read hour hand as minutes | |
| return to_minutes(new_hour, new_minute) | |