From ab17fc54156a8638cf933e3fd526cbbe4f35b78e Mon Sep 17 00:00:00 2001 From: agent_coder Date: Mon, 6 Jul 2026 18:25:45 +0300 Subject: [PATCH] =?UTF-8?q?refactor(render):=20=D1=8D=D1=82=D0=B0=D0=BF=20?= =?UTF-8?q?5a=20=E2=80=94=20=D1=82=D0=B0=D0=B1=D0=BB=D0=B8=D1=86=D0=B0=20?= =?UTF-8?q?=D0=BC=D0=B5=D0=B4=D0=B8=D0=B0-=D1=82=D0=B8=D0=BF=D0=BE=D0=B2?= =?UTF-8?q?=20(=D0=B1=D0=B0=D0=B9=D1=82-=D0=B2-=D0=B1=D0=B0=D0=B9=D1=82)?= =?UTF-8?q?=20(#32)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Три параллельные ручно-синхронизируемые «лестницы» выбора media-объекта (_get_file_unique_id dict-map, _save_media_file_ids if/elif, _generate_html_media elif-каскад) сведены в одну точку правды: MEDIA_SOURCES (селектор → (media_obj, render_kind), 12 записей) + RenderCtx + RENDERERS (7 рендереров подняты дословно из старых веток). Все три консьюмера теперь ходят через таблицу; uid единообразно через getattr(file_unique_id). Байт-в-байт: fragment-oracle (media_fragment_replay + media_fragments.json, 24 кейса) без изменений, feed-golden без изменений. Единственная унификация — _save_media_file_ids теперь ключуется на message.media (как две другие лестницы), что заодно чинит латентную рассинхронизацию (в корпусе не встречается → оракул 0). test_stage5_media_table.py (48 тестов). Полный сюит 413 passed, PYTHONHASHSEED 1/42. Co-Authored-By: Claude Opus 4.8 (1M context) --- post_parser.py | 297 ++++++++++++++++----------- tests/media_fragment_replay.py | 181 ++++++++++++++++ tests/test_data/media_fragments.json | 218 ++++++++++++++++++++ tests/test_stage5_media_table.py | 233 +++++++++++++++++++++ 4 files changed, 813 insertions(+), 116 deletions(-) create mode 100644 tests/media_fragment_replay.py create mode 100644 tests/test_data/media_fragments.json create mode 100644 tests/test_stage5_media_table.py diff --git a/post_parser.py b/post_parser.py index bc08055..5d8bb6a 100644 --- a/post_parser.py +++ b/post_parser.py @@ -14,8 +14,9 @@ import re import os import html import inspect +from dataclasses import dataclass from datetime import datetime -from typing import Union, Dict, Any, List, Optional +from typing import Union, Dict, Any, List, Optional, Callable, Tuple from pyrogram.types import Message from pyrogram.enums import MessageMediaType from sanitizer import sanitize_html @@ -107,6 +108,138 @@ def _story_media_object(message): return None, None +# --------------------------------------------------------------------------- # +# Single source of truth for media handling (render-pipeline refactor, stage 5). +# +# MEDIA_SOURCES maps a media type to a selector returning (media object, render +# kind). It replaces the three hand-synced ladders that used to live in +# _get_file_unique_id (dict map), _save_media_file_ids (if/elif) and +# _generate_html_media (elif cascade). All three now go through this table. +# +# kind=None means "selector-only entry": the object participates in file-id +# extraction/collection but is NOT rendered by the dispatcher. The ONLY kind=None +# entry is WEB_PAGE (its preview is rendered by _format_webpage). PAID_MEDIA has +# NO entry at all: its info block is a dedicated branch in _generate_html_media +# and it is deliberately never collected/downloaded (paid content). +# --------------------------------------------------------------------------- # +def _select_document(message): + """DOCUMENT selector: PDF documents render as a t.me link ('pdf'), everything + else as an image ('img_400'). The selected object is always message.document.""" + document = message.document + if (document is not None and hasattr(document, 'mime_type') + and document.mime_type == 'application/pdf'): + return document, 'pdf' + return document, 'img_400' + + +def _select_sticker(message): + """STICKER selector: video stickers loop as