chore(deps): remove OpenCV python (#415)

Closes #413
This commit is contained in:
Jérome Eertmans
2024-04-16 17:35:23 +02:00
committed by GitHub
parent 8a3bf87db8
commit 59dd365291
4 changed files with 965 additions and 932 deletions

View File

@ -6,13 +6,14 @@ import sys
import tempfile import tempfile
import webbrowser import webbrowser
from base64 import b64encode from base64 import b64encode
from collections import deque
from enum import Enum from enum import Enum
from importlib import resources from importlib import resources
from pathlib import Path from pathlib import Path
from typing import Any, Callable, Dict, List, Optional, Type, Union from typing import Any, Callable, Dict, List, Optional, Type, Union
import av
import click import click
import cv2
import pptx import pptx
from click import Context, Parameter from click import Context, Parameter
from jinja2 import Template from jinja2 import Template
@ -79,11 +80,23 @@ def file_to_data_uri(file: Path) -> str:
def get_duration_ms(file: Path) -> float: def get_duration_ms(file: Path) -> float:
"""Read a video and return its duration in milliseconds.""" """Read a video and return its duration in milliseconds."""
cap = cv2.VideoCapture(str(file)) with av.open(str(file)) as container:
fps: int = cap.get(cv2.CAP_PROP_FPS) video = container.streams.video[0]
frame_count: int = cap.get(cv2.CAP_PROP_FRAME_COUNT)
return 1000 * frame_count / fps return float(1000 * video.duration * video.time_base)
def read_image_from_video_file(file: Path, frame_index: "FrameIndex") -> Image:
"""Read a image from a video file at a given index."""
with av.open(str(file)) as container:
frames = container.decode(video=0)
if frame_index == FrameIndex.last:
(frame,) = deque(frames, 1)
else:
frame = next(frames)
return frame.to_image()
class Converter(BaseModel): # type: ignore class Converter(BaseModel): # type: ignore
@ -438,23 +451,6 @@ class PDF(Converter):
def convert_to(self, dest: Path) -> None: def convert_to(self, dest: Path) -> None:
"""Convert this configuration into a PDF presentation, saved to DEST.""" """Convert this configuration into a PDF presentation, saved to DEST."""
def read_image_from_video_file(file: Path, frame_index: FrameIndex) -> Image:
cap = cv2.VideoCapture(str(file))
if frame_index == FrameIndex.last:
index = cap.get(cv2.CAP_PROP_FRAME_COUNT)
cap.set(cv2.CAP_PROP_POS_FRAMES, index - 1)
ret, frame = cap.read()
cap.release()
if ret:
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
return Image.fromarray(frame)
else:
raise ValueError("Failed to read {image_index} image from video file")
images = [] images = []
for i, presentation_config in enumerate(self.presentation_configs): for i, presentation_config in enumerate(self.presentation_configs):
@ -490,7 +486,7 @@ class PowerPoint(Converter):
def open(self, file: Path) -> None: def open(self, file: Path) -> None:
return open_with_default(file) return open_with_default(file)
def convert_to(self, dest: Path) -> None: # noqa: C901 def convert_to(self, dest: Path) -> None:
"""Convert this configuration into a PowerPoint presentation, saved to DEST.""" """Convert this configuration into a PowerPoint presentation, saved to DEST."""
prs = pptx.Presentation() prs = pptx.Presentation()
prs.slide_width = self.width * 9525 prs.slide_width = self.width * 9525
@ -519,53 +515,48 @@ class PowerPoint(Converter):
nsmap = {"p": "http://schemas.openxmlformats.org/presentationml/2006/main"} nsmap = {"p": "http://schemas.openxmlformats.org/presentationml/2006/main"}
return etree.ElementBase.xpath(el, query, namespaces=nsmap) return etree.ElementBase.xpath(el, query, namespaces=nsmap)
def save_first_image_from_video_file(file: Path) -> Optional[str]: with tempfile.TemporaryDirectory() as directory_name:
cap = cv2.VideoCapture(file.as_posix()) directory = Path(directory_name)
ret, frame = cap.read() frame_number = 0
cap.release() for i, presentation_config in enumerate(self.presentation_configs):
for slide_config in tqdm(
presentation_config.slides,
desc=f"Generating video slides for config {i + 1}",
leave=False,
):
file = slide_config.file
if ret: mime_type = mimetypes.guess_type(file)[0]
f = tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".png")
cv2.imwrite(f.name, frame)
f.close()
return f.name
else:
logger.warn("Failed to read first image from video file")
return None
for i, presentation_config in enumerate(self.presentation_configs): if self.poster_frame_image is None:
for slide_config in tqdm( poster_frame_image = str(directory / f"{frame_number}.png")
presentation_config.slides, image = read_image_from_video_file(
desc=f"Generating video slides for config {i + 1}", file, frame_index=FrameIndex.first
leave=False, )
): image.save(poster_frame_image)
file = slide_config.file
mime_type = mimetypes.guess_type(file)[0] frame_number += 1
else:
poster_frame_image = str(self.poster_frame_image)
if self.poster_frame_image is None: slide = prs.slides.add_slide(layout)
poster_frame_image = save_first_image_from_video_file(file) movie = slide.shapes.add_movie(
else: str(file),
poster_frame_image = str(self.poster_frame_image) self.left,
self.top,
self.width * 9525,
self.height * 9525,
poster_frame_image=poster_frame_image,
mime_type=mime_type,
)
if slide_config.notes != "":
slide.notes_slide.notes_text_frame.text = slide_config.notes
slide = prs.slides.add_slide(layout) if self.auto_play_media:
movie = slide.shapes.add_movie( auto_play_media(movie, loop=slide_config.loop)
str(file),
self.left,
self.top,
self.width * 9525,
self.height * 9525,
poster_frame_image=poster_frame_image,
mime_type=mime_type,
)
if slide_config.notes != "":
slide.notes_slide.notes_text_frame.text = slide_config.notes
if self.auto_play_media: dest.parent.mkdir(parents=True, exist_ok=True)
auto_play_media(movie, loop=slide_config.loop) prs.save(dest)
dest.parent.mkdir(parents=True, exist_ok=True)
prs.save(dest)
def show_config_options(function: Callable[..., Any]) -> Callable[..., Any]: def show_config_options(function: Callable[..., Any]) -> Callable[..., Any]:

1770
pdm.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -23,7 +23,6 @@ dependencies = [
"jinja2>=3.1.2", "jinja2>=3.1.2",
"lxml>=4.9.2", "lxml>=4.9.2",
"numpy>=1.19", "numpy>=1.19",
"opencv-python>=4.6.0.66",
"pillow>=9.5.0", "pillow>=9.5.0",
"pydantic>=2.0.1", "pydantic>=2.0.1",
"pydantic-extra-types>=2.0.0", "pydantic-extra-types>=2.0.0",

View File

@ -153,11 +153,14 @@ class TestConverter:
file_contents = Path(out_file).read_text() file_contents = Path(out_file).read_text()
assert "manim" in file_contents.casefold() assert "manim" in file_contents.casefold()
@pytest.mark.parametrize("frame_index", ("first", "last"))
def test_pdf_converter( def test_pdf_converter(
self, tmp_path: Path, presentation_config: PresentationConfig self, frame_index: str, tmp_path: Path, presentation_config: PresentationConfig
) -> None: ) -> None:
out_file = tmp_path / "slides.pdf" out_file = tmp_path / "slides.pdf"
PDF(presentation_configs=[presentation_config]).convert_to(out_file) PDF(
presentation_configs=[presentation_config], frame_index=frame_index
).convert_to(out_file)
assert out_file.exists() assert out_file.exists()
def test_converter_no_presentation_config(self) -> None: def test_converter_no_presentation_config(self) -> None: