Spaces:
Running
Running
SilverElixir commited on
Commit ·
87dd95b
1
Parent(s): 6b895d8
Perf: download album extras in parallel, keep message order
Browse files- lumen_message_core.py +5 -1
- tests/test_bot_state.py +48 -0
lumen_message_core.py
CHANGED
|
@@ -59,6 +59,7 @@ async def _process_media_group_buffers(mgid: str) -> None:
|
|
| 59 |
MAX_ALBUM_EXTRA = 9 # первое уходит как основное, до +9 дополнительных (итого 10 — как лимит TikTok-слайдшоу)
|
| 60 |
album_state = bot.get_state(main_msg.chat.id)
|
| 61 |
album_user_id = main_msg.from_user.id if main_msg.from_user else None
|
|
|
|
| 62 |
for m in messages[1:1 + MAX_ALBUM_EXTRA]:
|
| 63 |
src = _msg_media_source(m)
|
| 64 |
if not src:
|
|
@@ -66,7 +67,10 @@ async def _process_media_group_buffers(mgid: str) -> None:
|
|
| 66 |
fid, mime, _ = _media_file_id_and_mime(src)
|
| 67 |
if not fid:
|
| 68 |
continue
|
| 69 |
-
|
|
|
|
|
|
|
|
|
|
| 70 |
if fetched:
|
| 71 |
extra_media.append(fetched)
|
| 72 |
# Регрессия: файлы альбома пишем в recent_media_ids, иначе "что на втором фото" не найдёт их.
|
|
|
|
| 59 |
MAX_ALBUM_EXTRA = 9 # первое уходит как основное, до +9 дополнительных (итого 10 — как лимит TikTok-слайдшоу)
|
| 60 |
album_state = bot.get_state(main_msg.chat.id)
|
| 61 |
album_user_id = main_msg.from_user.id if main_msg.from_user else None
|
| 62 |
+
targets: list[tuple[str, str, Any]] = []
|
| 63 |
for m in messages[1:1 + MAX_ALBUM_EXTRA]:
|
| 64 |
src = _msg_media_source(m)
|
| 65 |
if not src:
|
|
|
|
| 67 |
fid, mime, _ = _media_file_id_and_mime(src)
|
| 68 |
if not fid:
|
| 69 |
continue
|
| 70 |
+
targets.append((fid, mime, src))
|
| 71 |
+
# Качаем параллельно, а не по очереди: альбом из 9 фото иначе ждал бы до ~10-20с последовательных скачиваний.
|
| 72 |
+
fetched_list = await asyncio.gather(*(bot._fetch_media(fid, mime) for fid, mime, _ in targets))
|
| 73 |
+
for (fid, mime, src), fetched in zip(targets, fetched_list):
|
| 74 |
if fetched:
|
| 75 |
extra_media.append(fetched)
|
| 76 |
# Регрессия: файлы альбома пишем в recent_media_ids, иначе "что на втором фото" не найдёт их.
|
tests/test_bot_state.py
CHANGED
|
@@ -484,6 +484,54 @@ def test_process_media_group_buffers_records_extra_photos_to_recent_media():
|
|
| 484 |
bot.chat_state.pop(chat_id, None)
|
| 485 |
|
| 486 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 487 |
def test_media_question_without_file_gets_no_file_notice(rate_guard_setup, monkeypatch):
|
| 488 |
# Прод-кейс 17.09.2026: "что на фото?" без файла — модель выдумала описание
|
| 489 |
# несуществующего скриншота. Раз резолвинг ничего не нашёл, этот факт едет
|
|
|
|
| 484 |
bot.chat_state.pop(chat_id, None)
|
| 485 |
|
| 486 |
|
| 487 |
+
def test_process_media_group_buffers_fetches_extras_in_parallel_keeping_order():
|
| 488 |
+
# Альбом качается параллельно: медленное первое фото не должно задерживать остальные,
|
| 489 |
+
# а порядок вложений обязан совпасть с порядком сообщений.
|
| 490 |
+
chat_id = 999961
|
| 491 |
+
user = SimpleNamespace(id=778)
|
| 492 |
+
|
| 493 |
+
def _photo_msg(file_id):
|
| 494 |
+
photo = SimpleNamespace(file_id=file_id, mime_type=None, file_name=None)
|
| 495 |
+
return SimpleNamespace(
|
| 496 |
+
chat=SimpleNamespace(id=chat_id, type=bot.ChatType.PRIVATE), from_user=user,
|
| 497 |
+
media_group_id="mg2", text=None, caption=None, reply_to_message=None,
|
| 498 |
+
photo=[photo], video=None, animation=None, video_note=None,
|
| 499 |
+
voice=None, audio=None, document=None, sticker=None,
|
| 500 |
+
)
|
| 501 |
+
|
| 502 |
+
msgs = [_photo_msg(f"file_{i}") for i in range(5)]
|
| 503 |
+
captured = {}
|
| 504 |
+
concurrent = 0
|
| 505 |
+
max_concurrent = 0
|
| 506 |
+
|
| 507 |
+
async def fake_fetch_media(file_id, mime):
|
| 508 |
+
nonlocal concurrent, max_concurrent
|
| 509 |
+
concurrent += 1
|
| 510 |
+
max_concurrent = max(max_concurrent, concurrent)
|
| 511 |
+
try:
|
| 512 |
+
await asyncio.sleep(0.01)
|
| 513 |
+
return (file_id.encode(), "image/jpeg")
|
| 514 |
+
finally:
|
| 515 |
+
concurrent -= 1
|
| 516 |
+
|
| 517 |
+
async def fake_handle_core(message, extra_media=None):
|
| 518 |
+
captured["extra"] = extra_media
|
| 519 |
+
|
| 520 |
+
original_fetch, original_core = bot._fetch_media, bot._handle_message_core
|
| 521 |
+
bot._fetch_media = fake_fetch_media
|
| 522 |
+
bot._handle_message_core = fake_handle_core
|
| 523 |
+
bot._mg_buffers["mg2"] = msgs
|
| 524 |
+
try:
|
| 525 |
+
asyncio.run(bot._process_media_group_buffers("mg2"))
|
| 526 |
+
# Все 4 доп. фото качались одновременно, а не по очереди.
|
| 527 |
+
assert max_concurrent == 4
|
| 528 |
+
assert [b for b, _ in captured["extra"]] == [b"file_1", b"file_2", b"file_3", b"file_4"]
|
| 529 |
+
finally:
|
| 530 |
+
bot._fetch_media = original_fetch
|
| 531 |
+
bot._handle_message_core = original_core
|
| 532 |
+
bot.chat_state.pop(chat_id, None)
|
| 533 |
+
|
| 534 |
+
|
| 535 |
def test_media_question_without_file_gets_no_file_notice(rate_guard_setup, monkeypatch):
|
| 536 |
# Прод-кейс 17.09.2026: "что на фото?" без файла — модель выдумала описание
|
| 537 |
# несуществующего скриншота. Раз резолвинг ничего не нашёл, этот факт едет
|