"""影片轉檔共用邏輯(目前僅 GIF)。 依賴:pip 套件 `imageio-ffmpeg`(內含可攜式 ffmpeg 執行檔,不需要系統安裝 ffmpeg 或設定 PATH)。在新機器上,若下面的 import 失敗,代表這台機器還沒裝, 執行「pip install imageio-ffmpeg」後再重跑即可;裝好後留在該機器的 Python 環境裡,之後不用重裝、也不用每次確認。 """ import subprocess from pathlib import Path try: import imageio_ffmpeg except ImportError as e: raise ImportError( "缺少 imageio-ffmpeg,執行「pip install imageio-ffmpeg」後再重跑。" ) from e MAX_WIDTH = 720 def unique_output_path(path: Path) -> Path: if not path.exists(): return path stem, suffix = path.stem, path.suffix n = 2 while True: candidate = path.with_name(f"{stem}_{n}{suffix}") if not candidate.exists(): return candidate n += 1 def convert_to_gif( path: Path, output_dir: Path, fps: int = 15, max_width: int = MAX_WIDTH ) -> Path: """把影片轉成 GIF(palette 兩階段轉換,畫質較好),寬度超過 max_width 才縮小。輸出到 output_dir,不動原始影片。失敗直接 raise,呼叫端決定 怎麼處理。 """ ffmpeg = imageio_ffmpeg.get_ffmpeg_exe() output_dir.mkdir(parents=True, exist_ok=True) output = unique_output_path(output_dir / path.with_suffix(".gif").name) palette = output_dir / f"_palette_{path.stem}.png" common_filter = f"fps={fps},scale=w='min(iw,{max_width})':h=-1:flags=lanczos" try: subprocess.run( [ ffmpeg, "-y", "-i", str(path), "-vf", f"{common_filter},palettegen=stats_mode=diff", str(palette), ], check=True, capture_output=True, ) subprocess.run( [ ffmpeg, "-y", "-i", str(path), "-i", str(palette), "-lavfi", f"{common_filter}[x];[x][1:v]paletteuse=dither=bayer:bayer_scale=3", str(output), ], check=True, capture_output=True, ) finally: palette.unlink(missing_ok=True) return output