Spaces:
Running
Running
Download lumen_typing_pace.py from SilverElixir/Lumen: direct link, hf CLI and curl.
- Browser
- Download file 4.04 kB
-
https://huggingface.co/spaces/SilverElixir/Lumen/resolve/main/lumen_typing_pace.py
- Command line
-
hf download hf://spaces/SilverElixir/Lumen/lumen_typing_pace.py
-
curl -L -o lumen_typing_pace.py https://huggingface.co/spaces/SilverElixir/Lumen/resolve/main/lumen_typing_pace.py
4.04 kB
| """ | |
| lumen_typing_pace.py — самокалибрующийся темп "печати" при стриминге в Telegram. | |
| Скорость — свойство бэкенда под слагом, а не модели, поэтому замер по факту + EMA. Единица — символы/с. EMA только в памяти. | |
| """ | |
| from __future__ import annotations | |
| # ── границы скорости печати (символов/сек) ── | |
| # Подобраны под ощущение живого набора, владелец может менять прямо здесь. | |
| DEFAULT_CHARS_PER_SEC = 90.0 | |
| MIN_CHARS_PER_SEC = 40.0 | |
| MAX_CHARS_PER_SEC = 260.0 | |
| # Меньше альфа — стабильнее оценка, но медленнее подстройка под смену бэкенда под тем же слагом. | |
| _EMA_ALPHA = 0.3 | |
| # Выше, чем у EMA всего ответа: темп прихода меняется внутри одного стрима. | |
| _ARRIVAL_ALPHA = 0.5 | |
| _speed_ema: dict[str, float] = {} | |
| def speed_key(provider: str, model_id: str) -> str: | |
| """Единый ключ пары (provider, model_id), как в GLOBAL_QUOTA.""" | |
| return f"{provider}:{model_id}" | |
| def get_typing_speed(key: str) -> float: | |
| """Оценка для модели или дефолт без замеров. Результат всегда в границах скорости.""" | |
| return max(MIN_CHARS_PER_SEC, min(MAX_CHARS_PER_SEC, _speed_ema.get(key, DEFAULT_CHARS_PER_SEC))) | |
| def record_observed_speed(key: str, elapsed_sec: float, chars_len: int) -> None: | |
| """Учесть один завершённый стрим, вызывать раз в конце. elapsed_sec — без фазы довывода, иначе EMA поверит своим же задержкам. Наблюдение зажимается до усреднения, иначе один выброс утащит EMA.""" | |
| if elapsed_sec <= 0 or chars_len <= 0: | |
| return | |
| observed = max(MIN_CHARS_PER_SEC, min(MAX_CHARS_PER_SEC, chars_len / elapsed_sec)) | |
| prev = _speed_ema.get(key) | |
| _speed_ema[key] = observed if prev is None else (_EMA_ALPHA * observed + (1 - _EMA_ALPHA) * prev) | |
| def catchup_reveal_steps(remaining_len: int, chars_per_sec: float, tick_interval_sec: float, max_ticks: int) -> list[int]: | |
| """Довывод остатка шагами; хвост добирается форсированно, задержка не больше max_ticks × tick. | |
| Без обращения к часам: в тестах ожидание подменено no-op, цикл на monotonic завис бы.""" | |
| if remaining_len <= 0: | |
| return [] | |
| chars_per_tick = max(1, int(chars_per_sec * tick_interval_sec)) | |
| steps: list[int] = [] | |
| revealed = 0 | |
| while revealed < remaining_len and len(steps) < max_ticks: | |
| revealed = min(remaining_len, revealed + chars_per_tick) | |
| steps.append(revealed) | |
| if steps and steps[-1] < remaining_len: | |
| steps[-1] = remaining_len | |
| return steps | |
| def blend_arrival_speed(prev_ewma: float | None, piece_len: int, dt_sec: float) -> float | None: | |
| """Подмешивает кусок в темп прихода. Нулевая дельта не наблюдение, возвращаем прежнее.""" | |
| if piece_len <= 0 or dt_sec <= 0: | |
| return prev_ewma | |
| inst = max(MIN_CHARS_PER_SEC, min(MAX_CHARS_PER_SEC, piece_len / dt_sec)) | |
| return inst if prev_ewma is None else _ARRIVAL_ALPHA * inst + (1 - _ARRIVAL_ALPHA) * prev_ewma | |
| def display_speed_for(arrival_ewma: float | None, pace_key: str) -> float: | |
| """Скорость показа: измеренный темп прихода, пока он есть; иначе глобальная EMA модели (затравка на первые куски).""" | |
| if arrival_ewma is not None: | |
| return max(MIN_CHARS_PER_SEC, min(MAX_CHARS_PER_SEC, arrival_ewma)) | |
| return get_typing_speed(pace_key) | |