ColabWan / shared /gradio /video_preview.py
1ripon1's picture
Upload folder using huggingface_hub
7344bef verified
Raw
History Blame Contribute Delete
13.7 kB
from __future__ import annotations
import hashlib
import json
import mimetypes
import os
import subprocess
import tempfile
import threading
from pathlib import Path
from urllib.parse import quote
import gradio as gr
from gradio.data_classes import FileData
from shared.utils.video_decode import probe_video_stream_metadata, resolve_media_binary
_BROWSER_VIDEO_EXTENSIONS = {".mp4", ".webm", ".ogg", ".ogv"}
_BROWSER_PLAYABLE_VIDEO_CODECS = {
(".mp4", "h264"),
(".mp4", "h265"),
(".mp4", "hevc"),
(".mkv", "h265"),
(".mkv", "hevc"),
(".mov", "h265"),
(".mov", "hevc"),
(".webm", "vp9"),
(".ogg", "theora"),
(".ogv", "theora"),
}
_PREVIEW_SECONDS = 20
_PREVIEW_MAX_WIDTH = 1280
_PREVIEW_CRF = "24"
_PREVIEW_PRESET = "veryfast"
_PREVIEW_CACHE_VERSION = 5
_CODEC_DISPLAY_NAMES = {
"av1": "AV1",
"dnxhd": "DNxHD",
"ffv1": "FFV1",
"h264": "H.264",
"h265": "HEVC",
"hevc": "HEVC",
"prores": "ProRes",
"theora": "Theora",
"vp8": "VP8",
"vp9": "VP9",
}
_lock = threading.Lock()
_installed = False
_original_video_format_video = None
_original_video_postprocess = None
_original_video_preprocess = None
_original_file_process_single_file = None
_original_upload_button_process_single_file = None
_original_create_app = None
_original_gallery_postprocess = None
_original_gallery_preprocess = None
_preview_to_source: dict[str, str] = {}
def _is_http_url_like(value):
return isinstance(value, str) and value.lower().startswith(("http://", "https://"))
def _abs_path(path):
return str(Path(os.fspath(path)).resolve())
def _cache_dir(cache_dir=None):
base = cache_dir or getattr(gr.Video(), "GRADIO_CACHE", None) or tempfile.gettempdir()
directory = Path(base) / "wangp_video_previews"
directory.mkdir(parents=True, exist_ok=True)
return directory
def _source_signature(source_path):
stat = source_path.stat()
data = f"{_PREVIEW_CACHE_VERSION}|{source_path.resolve()}|{stat.st_size}|{stat.st_mtime_ns}"
return hashlib.sha1(data.encode("utf-8")).hexdigest()[:16]
def _preview_path_for(source_path, cache_dir=None):
suffix = _source_signature(source_path)
stem = source_path.stem[:80] or "video"
return _cache_dir(cache_dir) / f"{stem}_{suffix}.mp4"
def _sidecar_path(preview_path):
return preview_path.with_suffix(preview_path.suffix + ".source.json")
def _remember_preview(source_path, preview_path):
source = _abs_path(source_path)
preview = _abs_path(preview_path)
_preview_to_source[preview] = source
try:
_sidecar_path(Path(preview)).write_text(json.dumps({"source": source}), encoding="utf-8")
except OSError:
pass
def _restore_preview_path(path):
if not isinstance(path, (str, os.PathLike)):
return path
if not os.fspath(path) or _is_http_url_like(path):
return path
preview = _abs_path(path)
if preview in _preview_to_source:
return _preview_to_source[preview]
sidecar = _sidecar_path(Path(preview))
if not sidecar.is_file():
return path
try:
source = json.loads(sidecar.read_text(encoding="utf-8")).get("source")
except (OSError, json.JSONDecodeError):
return path
if source:
_preview_to_source[preview] = source
return source
return path
def _is_browser_playable(path):
metadata = probe_video_stream_metadata(str(path)) or {}
codec_name = str(metadata.get("codec_name") or "").strip().lower()
if len(codec_name) > 0:
return (path.suffix.lower(), codec_name) in _BROWSER_PLAYABLE_VIDEO_CODECS
return path.suffix.lower() in _BROWSER_VIDEO_EXTENSIONS
def _is_video_path(path):
mime_type = mimetypes.guess_type(str(path))[0]
return bool(mime_type and mime_type.startswith("video/"))
def needs_fast_video_preview(video_path):
if not video_path or _is_http_url_like(video_path):
return False
path = Path(os.fspath(video_path))
if not path.is_file():
return False
if not _is_video_path(path):
return False
return not _is_browser_playable(path)
def _format_codec_label(metadata):
codec_name = str((metadata or {}).get("codec_name") or "").strip().lower()
codec_profile = str((metadata or {}).get("codec_profile") or "").strip()
if codec_name == "dnxhd" and codec_profile.upper().startswith("DNXHR"):
return codec_profile.upper().replace("DNXHR", "DNxHR", 1)
if codec_name in _CODEC_DISPLAY_NAMES:
return _CODEC_DISPLAY_NAMES[codec_name]
return codec_name.upper() if len(codec_name) > 0 else "Unknown Codec"
def _preview_video_filter(source_path):
metadata = probe_video_stream_metadata(str(source_path)) or {}
width = int(metadata.get("display_width") or metadata.get("width") or 0)
if width > _PREVIEW_MAX_WIDTH:
scale_filter = f"scale={_PREVIEW_MAX_WIDTH}:-2:flags=fast_bilinear"
else:
scale_filter = "scale=trunc(iw/2)*2:trunc(ih/2)*2:flags=fast_bilinear"
text = f"Codec {_format_codec_label(metadata)} Not Supported, Low Res Preview"
text = text.replace("\\", "\\\\").replace(":", "\\:").replace("'", "\\'")
label_filter = f"drawtext=text='{text}':fontcolor=white@0.9:fontsize=max(11\\,h/44):box=1:boxcolor=black@0.45:boxborderw=6:x=w-tw-12:y=12"
return f"{scale_filter},{label_filter}"
def ensure_fast_video_preview(video_path, cache_dir=None):
if not needs_fast_video_preview(video_path):
return None
ffmpeg = resolve_media_binary("ffmpeg")
if ffmpeg is None:
return None
source_path = Path(os.fspath(video_path)).resolve()
preview_path = _preview_path_for(source_path, cache_dir)
if preview_path.is_file():
_remember_preview(source_path, preview_path)
return str(preview_path)
tmp_path = preview_path.with_name(preview_path.stem + ".tmp" + preview_path.suffix)
command = [
ffmpeg,
"-y",
"-v",
"error",
"-nostdin",
"-i",
str(source_path),
"-map",
"0:v:0",
"-map",
"0:a:0?",
"-t",
str(_PREVIEW_SECONDS),
"-vf",
_preview_video_filter(source_path),
"-c:v",
"libx264",
"-preset",
_PREVIEW_PRESET,
"-crf",
_PREVIEW_CRF,
"-pix_fmt",
"yuv420p",
"-c:a",
"aac",
"-b:a",
"96k",
"-ac",
"2",
"-movflags",
"+faststart",
"-shortest",
str(tmp_path),
]
with _lock:
if preview_path.is_file():
_remember_preview(source_path, preview_path)
return str(preview_path)
try:
result = subprocess.run(command, capture_output=True, text=True, encoding="utf-8", errors="ignore", check=False)
except OSError:
result = None
if result is None or result.returncode != 0 or not tmp_path.is_file():
try:
tmp_path.unlink(missing_ok=True)
except OSError:
pass
return None
os.replace(tmp_path, preview_path)
_remember_preview(source_path, preview_path)
return str(preview_path)
def _gradio_file_url(path):
return "/gradio_api/file=" + quote(_abs_path(path), safe="")
def _attach_preview(file_data, cache_dir=None):
if file_data is None or not getattr(file_data, "path", None):
return file_data
source_path = file_data.path
preview_path = ensure_fast_video_preview(file_data.path, cache_dir)
if preview_path is None:
return file_data
file_data.path = preview_path
file_data.url = _gradio_file_url(preview_path)
file_data.orig_name = file_data.orig_name or Path(source_path).name
file_data.mime_type = "video/mp4"
return file_data
def _replace_upload_paths_with_previews(paths):
if not isinstance(paths, list):
return paths
replaced_paths = []
for path in paths:
preview_path = ensure_fast_video_preview(path)
replaced_paths.append(preview_path or path)
return replaced_paths
def _patched_video_format_video(self, video):
if video is None or _is_http_url_like(video) or self.format is not None or self.watermark:
return _original_video_format_video(self, video)
video_path = Path(os.fspath(video))
preview_path = ensure_fast_video_preview(video_path, self.GRADIO_CACHE)
if preview_path is None:
return _original_video_format_video(self, video)
return FileData(path=preview_path, url=_gradio_file_url(preview_path), orig_name=video_path.name, mime_type="video/mp4")
def _patched_video_postprocess(self, value):
data = _original_video_postprocess(self, value)
if data is not None and getattr(data, "video", None) is not None:
_attach_preview(data.video, self.GRADIO_CACHE)
return data
def _patched_video_preprocess(self, payload):
if payload is not None and getattr(payload, "video", None) is not None and getattr(payload.video, "path", None):
payload.video.path = _restore_preview_path(payload.video.path)
result = _original_video_preprocess(self, payload)
return _restore_preview_path(result)
def _patched_gallery_postprocess(self, value):
data = _original_gallery_postprocess(self, value)
for item in getattr(data, "root", []):
video = getattr(item, "video", None)
if video is not None:
_attach_preview(video, self.GRADIO_CACHE)
return data
def _patched_gallery_preprocess(self, payload):
if payload is not None:
for item in getattr(payload, "root", []) or []:
video = getattr(item, "video", None)
if video is not None and getattr(video, "path", None):
video.path = _restore_preview_path(video.path)
data = _original_gallery_preprocess(self, payload)
if data is None:
return data
return [(_restore_preview_path(media), caption) for media, caption in data]
def _patched_file_process_single_file(self, file_data):
if file_data is not None and getattr(file_data, "path", None):
file_data.path = _restore_preview_path(file_data.path)
return _original_file_process_single_file(self, file_data)
def _patched_upload_button_process_single_file(self, file_data):
if file_data is not None and getattr(file_data, "path", None):
file_data.path = _restore_preview_path(file_data.path)
return _original_upload_button_process_single_file(self, file_data)
def _install_upload_preview_middleware(app):
if getattr(app, "_wangp_video_preview_upload_middleware", False):
return
@app.middleware("http")
async def _wangp_video_preview_upload_middleware(request, call_next):
response = await call_next(request)
if request.method != "POST" or not request.url.path.endswith("/upload") or response.status_code != 200:
return response
body = b""
async for chunk in response.body_iterator:
body += chunk
try:
paths = json.loads(body)
except json.JSONDecodeError:
from fastapi import Response
return Response(content=body, status_code=response.status_code, headers=dict(response.headers), media_type=response.media_type, background=response.background)
replaced_paths = _replace_upload_paths_with_previews(paths)
if replaced_paths == paths:
from fastapi import Response
return Response(content=body, status_code=response.status_code, headers=dict(response.headers), media_type=response.media_type, background=response.background)
from fastapi.responses import JSONResponse
headers = {key: value for key, value in response.headers.items() if key.lower() not in {"content-length", "content-type"}}
return JSONResponse(replaced_paths, status_code=response.status_code, headers=headers, background=response.background)
app._wangp_video_preview_upload_middleware = True
def _patched_create_app(*args, **kwargs):
app = _original_create_app(*args, **kwargs)
_install_upload_preview_middleware(app)
return app
def install():
global _installed, _original_video_format_video, _original_video_postprocess, _original_video_preprocess, _original_file_process_single_file, _original_upload_button_process_single_file, _original_create_app, _original_gallery_postprocess, _original_gallery_preprocess
if _installed:
return
mimetypes.add_type("video/quicktime", ".mov")
mimetypes.add_type("video/x-matroska", ".mkv")
_original_video_format_video = gr.Video._format_video
_original_video_postprocess = gr.Video.postprocess
_original_video_preprocess = gr.Video.preprocess
_original_file_process_single_file = gr.File._process_single_file
_original_upload_button_process_single_file = gr.UploadButton._process_single_file
_original_gallery_postprocess = gr.Gallery.postprocess
_original_gallery_preprocess = gr.Gallery.preprocess
from gradio.routes import App
_original_create_app = App.create_app
gr.Video._format_video = _patched_video_format_video
gr.Video.postprocess = _patched_video_postprocess
gr.Video.preprocess = _patched_video_preprocess
gr.File._process_single_file = _patched_file_process_single_file
gr.UploadButton._process_single_file = _patched_upload_button_process_single_file
gr.Gallery.postprocess = _patched_gallery_postprocess
gr.Gallery.preprocess = _patched_gallery_preprocess
App.create_app = staticmethod(_patched_create_app)
_installed = True
install()
__all__ = ["ensure_fast_video_preview", "install", "needs_fast_video_preview"]