From 1399a1f093b09abf62b148c28c37f8860701839e Mon Sep 17 00:00:00 2001 From: yangxin317 <130425418+yangxin317@users.noreply.github.com> Date: Thu, 7 May 2026 23:34:32 +0800 Subject: [PATCH] feat(media): support HEIC/HEIF images for iPhone uploads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit iPhones default to HEIC (High Efficiency Image Container, the still-image companion to HEVC) since iOS 11, but `load_media` filters them out via the `IMAGE_EXTS` whitelist. The user-visible symptom is the assistant saying "几张 .heic 图片被跳过了" right after upload, which forces users to convert before each session. This change: * Adds `.heic` / `.heif` to every image-extension whitelist in the pipeline: `load_media`, `generate_ai_transition`, `render_video`, `media_handler`, and `sampling_handler` (so MCP samples and renders pick them up too). * Registers the HEIF opener with Pillow on package import, so any code path that goes through `PIL.Image.open()` transparently decodes HEIC without per-call branching. * Adds `pillow-heif==1.3.0` to `requirements.txt`. The opener registration is wrapped in `try/except ImportError` so the change remains forward-compatible with existing environments that haven't reinstalled requirements yet — they keep the previous behavior of filtering HEIC out via the suffix whitelist. ## Test plan - [x] Decoded a 4284×5712 iPhone 15 HEIC via `PIL.Image.open()` after `import open_storyline`. - [x] Verified `load_media.IMAGE_EXTS` now contains both `.heic` and `.heif`. - [x] Confirmed regular `.jpg / .png / .webp` paths unchanged. --- requirements.txt | 1 + src/open_storyline/__init__.py | 14 +++++++++++++- src/open_storyline/mcp/sampling_handler.py | 2 +- .../nodes/core_nodes/generate_ai_transition.py | 2 +- src/open_storyline/nodes/core_nodes/load_media.py | 2 +- .../nodes/core_nodes/render_video.py | 2 +- src/open_storyline/utils/media_handler.py | 2 +- 7 files changed, 19 insertions(+), 6 deletions(-) diff --git a/requirements.txt b/requirements.txt index 8e8313d..5992771 100644 --- a/requirements.txt +++ b/requirements.txt @@ -22,3 +22,4 @@ openai==2.16.0 emoji==2.15.0 funasr==1.3.1 torchaudio==2.11.0 +pillow-heif==1.3.0 diff --git a/src/open_storyline/__init__.py b/src/open_storyline/__init__.py index a68927d..96feb16 100644 --- a/src/open_storyline/__init__.py +++ b/src/open_storyline/__init__.py @@ -1 +1,13 @@ -__version__ = "0.1.0" \ No newline at end of file +__version__ = "0.1.0" + +# Register the HEIF/HEIC opener with Pillow so that downstream nodes that read +# images via PIL.Image.open() (load_media, render_video, sampling_handler, ...) +# transparently support .heic / .heif files produced by iPhone cameras. +try: + from pillow_heif import register_heif_opener as _register_heif_opener + + _register_heif_opener() +except ImportError: + # pillow-heif is an optional dependency; if missing, .heic uploads will + # still be filtered out by the suffix whitelist with a clear log line. + pass diff --git a/src/open_storyline/mcp/sampling_handler.py b/src/open_storyline/mcp/sampling_handler.py index fd2bc6e..57e7c53 100644 --- a/src/open_storyline/mcp/sampling_handler.py +++ b/src/open_storyline/mcp/sampling_handler.py @@ -23,7 +23,7 @@ DEFAULT_FRAMES_PER_SEC = 3.0 GLOBAL_MAX_IMAGE_BLOCKS = 48 # Maximum total images allowed (video frames + images) to prevent payload overflow -IMAGE_EXTS = {".jpg", ".jpeg", ".png", ".webp", ".bmp", ".gif", ".tiff"} +IMAGE_EXTS = {".jpg", ".jpeg", ".png", ".webp", ".bmp", ".gif", ".tiff", ".heic", ".heif"} VIDEO_EXTS = {".mp4", ".mov", ".mkv", ".avi", ".webm", ".m4v"} diff --git a/src/open_storyline/nodes/core_nodes/generate_ai_transition.py b/src/open_storyline/nodes/core_nodes/generate_ai_transition.py index e13e817..d73a602 100644 --- a/src/open_storyline/nodes/core_nodes/generate_ai_transition.py +++ b/src/open_storyline/nodes/core_nodes/generate_ai_transition.py @@ -92,7 +92,7 @@ class GenerateAITransitionNode(BaseNode): ".mp4", ".mov", ".mkv", ".avi" } IMAGE_EXTS = { - ".jpg", ".jpeg", ".png", ".webp", ".bmp" + ".jpg", ".jpeg", ".png", ".webp", ".bmp", ".heic", ".heif" } DEFAULT_TRANSITION_DURATION = 5 diff --git a/src/open_storyline/nodes/core_nodes/load_media.py b/src/open_storyline/nodes/core_nodes/load_media.py index c9dc4d8..0dedf7c 100644 --- a/src/open_storyline/nodes/core_nodes/load_media.py +++ b/src/open_storyline/nodes/core_nodes/load_media.py @@ -17,7 +17,7 @@ ".mp4", ".mov", ".mkv", ".avi" } IMAGE_EXTS = { - ".jpg", ".jpeg", ".png", ".webp", ".bmp" + ".jpg", ".jpeg", ".png", ".webp", ".bmp", ".heic", ".heif" } def _image_metadata_from_path(path: Path) -> dict[str, Any]: diff --git a/src/open_storyline/nodes/core_nodes/render_video.py b/src/open_storyline/nodes/core_nodes/render_video.py index 2212a05..805843b 100644 --- a/src/open_storyline/nodes/core_nodes/render_video.py +++ b/src/open_storyline/nodes/core_nodes/render_video.py @@ -233,7 +233,7 @@ def build_media_id_to_path_map(load_media: Dict[str, Any]) -> Dict[str, str]: def is_image_file(path: str) -> bool: try: - return Path(path).suffix.lower() in {".png", ".jpg", ".jpeg", ".bmp", ".webp", ".tif", ".tiff"} + return Path(path).suffix.lower() in {".png", ".jpg", ".jpeg", ".bmp", ".webp", ".tif", ".tiff", ".heic", ".heif"} except Exception: return False diff --git a/src/open_storyline/utils/media_handler.py b/src/open_storyline/utils/media_handler.py index da837c7..ce35bdf 100644 --- a/src/open_storyline/utils/media_handler.py +++ b/src/open_storyline/utils/media_handler.py @@ -2,7 +2,7 @@ from pathlib import Path from typing import Union -_MEDIA_EXTS_IMG = {".png", ".jpg", ".jpeg", ".gif", ".bmp", ".webp"} +_MEDIA_EXTS_IMG = {".png", ".jpg", ".jpeg", ".gif", ".bmp", ".webp", ".heic", ".heif"} _MEDIA_EXTS_VID = {".mp4", ".mov", ".avi", ".mkv", ".webm", ".m4v"} def scan_media_dir(media_dir: Union[Path, str]) -> dict: