{"record":{"id":"dc0d428dfc2aea20","repo":"HKUDS/DeepTutor","slug":"rendered-suffix-artifact-not-found","errorCode":null,"errorMessage":"Rendered {suffix} artifact not found.","messagePattern":"Rendered (.+?) artifact not found\\.","errorType":"exception","errorClass":"ManimRenderError","httpStatus":null,"severity":"error","filePath":"deeptutor/agents/math_animator/renderer.py","lineNumber":219,"sourceCode":"            )\n\n    async def _emit_progress(self, message: str, raw: bool = False) -> None:\n        if self.progress_callback is None:\n            return\n        await self.progress_callback(message, raw)\n\n    def _find_rendered_file(self, suffix: str) -> Path:\n        # Manim stores many transient chunks under ``partial_movie_files``.\n        # We only want the final exported artifact for the scene.\n        matches = [\n            path\n            for path in self.media_dir.rglob(f\"*{suffix}\")\n            if \"partial_movie_files\" not in path.parts\n        ]\n        if not matches:\n            matches = list(self.media_dir.rglob(f\"*{suffix}\"))\n        if not matches:\n            raise ManimRenderError(f\"Rendered {suffix} artifact not found.\")\n        return max(matches, key=lambda path: path.stat().st_mtime)\n\n    @staticmethod\n    def _extract_scene_name(code: str) -> str:\n        match = SCENE_PATTERN.search(code)\n        if not match:\n            raise ManimRenderError(\"Generated code does not define a renderable Manim Scene class.\")\n        return match.group(1)\n\n    def _build_artifact(\n        self,\n        artifact_path: Path,\n        artifact_type: str,\n        content_type: str,\n        label: str,\n    ) -> RenderedArtifact:\n        rel_path = artifact_path.resolve().relative_to(self.path_service.user_data_dir.resolve())\n        return RenderedArtifact(","sourceCodeStart":201,"sourceCodeEnd":237,"githubUrl":"https://github.com/HKUDS/DeepTutor/blob/3e82f130422a813cdd73c10b21a44e9325f5821a/deeptutor/agents/math_animator/renderer.py#L201-L237","documentation":"Raised by _validate_epub_archive as DocumentTooLargeError when a non-directory member count exceeds _EPUB_MAX_MEMBERS. It is a zip-bomb / resource-exhaustion guard applied before any member is read.","triggerScenarios":"Ingesting an EPUB with tens of thousands of archive entries; a maliciously crafted EPUB designed to exhaust file handles or memory during member iteration.","commonSituations":"User-created EPUBs from tools that emit one file per HTML fragment; adversarial uploads to a public ingestion endpoint.","solutions":["Rebuild/normalize the EPUB (ebook-convert) to reduce member count","Reject the file upstream with a clear size/complexity limit in the upload UI","Catch DocumentTooLargeError and skip with a warning during batch ingest"],"exampleFix":"// before\ntext = extract_text_from_bytes(data, filename=\"huge.epub\")  # raises\n\n// after\nsubprocess.run([\"ebook-convert\",\"huge.epub\",\"huge2.epub\"], check=True)  # normalizes archive\ntext = extract_text_from_bytes(Path(\"huge2.epub\").read_bytes(), filename=\"huge2.epub\")","handlingStrategy":"validation","validationCode":"import zipfile, io\nzf = zipfile.ZipFile(io.BytesIO(data))\nmembers = [i for i in zf.infolist() if not i.is_dir()]\nif len(members) > 10_000:  # mirror _EPUB_MAX_MEMBERS\n    reject(fn, \"EPUB too complex\")","typeGuard":null,"tryCatchPattern":"from deeptutor.utils.document_extractor import DocumentTooLargeError\nexcept DocumentTooLargeError as e:\n    if \"too many archive members\" in str(e):\n        skip_file(fn, reason=e)","preventionTips":["Apply archive-complexity limits at the upload boundary","Normalize EPUBs through Calibre before ingestion"],"tags":["epub","zip-bomb","resource-limits","document-extraction"],"backgroundTag":"archive-too-many-entries","analyzedSha":"3e82f130422a813cdd73c10b21a44e9325f5821a","analyzedAt":"2026-08-27T06:57:25.364Z","schemaVersion":2},"datasetVersion":"2026-08-27T08:17:20.692Z"}