Video attachment caching and handling across platforms
NousResearch/hermes-agent · cluster 4204 · gateway
Data as of
Nothing open on GitHub. Fix merged in #13009. Check it covers your case.
The short answer
- Known?
- Yes. 5 related items grouped here, 2 issues already closed.
- Fix event
- A fix was merged to main in PR #13009. Check it covers your case. No release data is tracked.
- Tracker
- The maintainers' tracker marks this
partially resolved, deciding PR #13009. That is separate from the fix event. - Still open
- Nothing open on GitHub.
These issues and pull requests concern inbound video attachments not being cached or accepted on Discord and Telegram, along with proposed fixes for both platforms including mp4 upload support.
Open issues0
Open PRs0
Fixes on record
-
#13009 fix(telegram): cache inbound videos and accept mp4 uploads
files:
gateway/platforms/base.py,gateway/platforms/telegram.py,scripts/release.py,tests/gateway/test_telegram_documents.pyShow the diff
-
gateway/platforms/base.py@@ -552,6 +552,39 @@ async def cache_audio_from_url(url: str, ext: str = ".ogg", retries: int = 2) -> raise last_exc +# --------------------------------------------------------------------------- +# Video cache utilities +# +# Same pattern as image/audio cache -- videos from platforms are downloaded +# here so the agent can reference them by local file path. +# --------------------------------------------------------------------------- + +VIDEO_CACHE_DIR = get_hermes_dir("cache/videos", "video_cache") + +SUPPORTED_VIDEO_TYPES = { + ".mp4": "video/mp4", + ".mov": "video/quicktime", + ".webm": "video/webm", + ".mkv": "video/x-matroska", + ".avi": "video/x-msvideo", +} + + +def get_video_cache_dir() -> Path: + """Return the video cache directory, creating it if it doesn't exist.""" + VIDEO_CACHE_DIR.mkdir(parents=True, exist_ok=True) + return VIDEO_CACHE_DIR + + +def cache_video_from_bytes(data: bytes, ext: str = ".mp4") -> str: + """Save raw video bytes to the cache and return the absolute file path.""" + cache_dir = get_video_cache_dir() + filename = f"video_{uuid.uuid4().hex[:12]}{ext}" + filepath = cache_dir / filename + filepath.write_bytes(data) + return str(filepath) + + # --------------------------------------------------------------------------- # Document cache utilities # -
gateway/platforms/telegram.py@@ -71,8 +71,10 @@ class _MockContextTypes: SendResult, cache_image_from_bytes, cache_audio_from_bytes, + cache_video_from_bytes, cache_document_from_bytes, resolve_proxy_url, + SUPPORTED_VIDEO_TYPES, SUPPORTED_DOCUMENT_TYPES, utf16_len, _prefix_within_utf16_limit, @@ -2628,6 +2630,23 @@ async def _handle_media_message(self, update: Update, context: ContextTypes.DEFA except Exception as e: logger.warning("[Telegram] Failed to cache audio: %s", e, exc_info=True) + elif msg.video: + try: + file_obj = await msg.video.get_file() + video_bytes = await file_obj.download_as_bytearray() + ext = ".mp4" + if getattr(file_obj, "file_path", None): + for candidate in SUPPORTED_VIDEO_TYPES: + if file_obj.file_path.lower().endswith(candidate): + ext = candidate + break + cached_path = cache_video_from_bytes(bytes(video_bytes), ext=ext) + event.media_urls = [cached_path] + event.media_types = [SUPPORTED_VIDEO_TYPES.get(ext, "video/mp4")] + logger.info("[Telegram] Cached user video at %s", cached_path) + except Exception as e: + logger.warning("[Telegram] Failed to cache video: %s", e, exc_info=True) + # Download document files to cache for agent processing elif msg.document: doc = msg.document @@ -2644,6 +2663,21 @@ async def _handle_media_message(self, update: Update, context: ContextTypes.DEFA mime_to_ext = {v: k for k, v in SUPPORTED_DOCUMENT_TYPES.items()} ext = mime_to_ext.get(doc.mime_type, "") + if not ext and doc.mime_type: + video_mime_to_ext = {v: k for k, v in SUPPORTED_VIDEO_TYPES.items()} + ext = video_mime_to_ext.get(doc.mime_type, "") + + if ext in SUPPORTED_VIDEO_TYPES: + file_obj = await doc.get_file() + video_bytes = await file_obj.download_as_bytearray() + cached_path = cache_video_from_bytes(bytes(video_bytes), ext=ext) + event.media_urls = [cached_path] + event.media_types = [SUPPORTED_VIDEO_TYPES[ext]] + event.message_type = MessageType.VIDEO + logger.info("[Telegram] Cached user video document at %s", cached_path) + await self.handle_message(event) + return + # Check if supported if ext not in SUPPORTED_DOCUMENT_TYPES: supported_list = ", ".join(sorted(SUPPORTED_DOCUMENT_TYPES.keys())) -
scripts/release.py@@ -77,6 +77,7 @@ "[redacted:email]": "yyq4193", "[redacted:email]": "Asunfly", "[redacted:email]": "honghua", + "[redacted:email]": "jplew", "[redacted:email]": "nish3451", "[redacted:email]": "Mibayy", "[redacted:email]": "sgaofen", -
tests/gateway/test_telegram_documents.py@@ -23,6 +23,7 @@ MessageType, SendResult, SUPPORTED_DOCUMENT_TYPES, + SUPPORTED_VIDEO_TYPES, ) @@ -117,6 +118,12 @@ def _make_update(msg): return update +def _make_video(file_obj=None): + video = MagicMock() + video.get_file = AsyncMock(return_value=file_obj or _make_file_obj(b"video-bytes")) + return video + + # --------------------------------------------------------------------------- # Fixtures # --------------------------------------------------------------------------- @@ -132,10 +139,13 @@ def adapter(): @pytest.fixture(autouse=True) def _redirect_cache(tmp_path, monkeypatch): - """Point document cache to tmp_path so tests don't touch ~/.hermes.""" + """Point document/video cache to tmp_path so tests don't touch ~/.hermes.""" monkeypatch.setattr( "gateway.platforms.base.DOCUMENT_CACHE_DIR", tmp_path / "doc_cache" ) + monkeypatch.setattr( + "gateway.platforms.base.VIDEO_CACHE_DIR", tmp_path / "video_cache" + ) # --------------------------------------------------------------------------- @@ -348,6 +358,37 @@ async def test_download_exception_handled(self, adapter): adapter.handle_message.assert_called_once() +class TestVideoDownloadBlock: + @pytest.mark.asyncio + async def test_native_video_is_cached(self, adapter): + file_obj = _make_file_obj(b"fake-mp4") + file_obj.file_path = "videos/clip.mp4" + msg = _make_message() + msg.video = _make_video(file_obj) + update = _make_update(msg) + + await adapter._handle_media_message(update, MagicMock()) + event = adapter.handle_message.call_args[0][0] + assert event.message_type == MessageType.VIDEO + assert len(event.media_urls) == 1 + assert os.path.exists(event.media_urls[0]) + assert event.media_types == [SUPPORTED_VIDEO_TYPES[".mp4"]] + + @pytest.mark.asyncio + async def test_mp4_document_is_treated_as_video(self, adapter): + file_obj = _make_file_obj(b"fake-mp4-doc") + doc = _make_document(file_name="good.mp4", mime_type="video/mp4", file_size=1024, file_obj=file_obj) + msg = _make_message(document=doc) + update = _make_update(msg) + + await adapter._handle_media_message(update, MagicMock()) + event = adapter.handle_message.call_args[0][0] + assert event.message_type == MessageType.VIDEO + assert len(event.media_urls) == 1 + assert os.path.exists(event.media_urls[0]) + assert event.media_types == [SUPPORTED_VIDEO_TYPES[".mp4"]] + + # --------------------------------------------------------------------------- # TestMediaGroups — media group (album) buffering # ---------------------------------------------------------------------------
-
2 PRs closed without merging
From the close comments: done elsewhere 1 · merged via another change 1
Closed issues · 2
How this cluster was grouped
-
Link evidence (
partly structural links): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text. - Members outside that structural core may belong to a different underlying problem.
-
Pairwise link density
0.34: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36.1.00means every pair is linked.
Pick up a Lab task
Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.
Open the LabAt a glance
- Subsystem
- gateway
- Merged PRs
- #13009
- Closed issues
- 2
- Attempts
- 2 PRs closed without merging
- Fix link checked
- 20 Apr 2026. This confirmation is old and may be out of date (a revert or reopen would not have invalidated it here); re-verify.