mirror of
https://github.com/gradio-app/gradio.git
synced 2024-11-21 01:01:05 +08:00
Adds ability to watermark videos via a watermark
parameter in Video component (#8900)
* First pass at adding watermark to Video component * add changeset * Update video.py Space. * Adding demo files * linting * Automatically generated notebook generator * lint * Changing parentheses in check on whether conversion is needed * Unimportant comment/spacing changes * video * Update gradio/components/video.py Co-authored-by: Abubakar Abid <abubakar@huggingface.co> * Update gradio/components/video.py Co-authored-by: Abubakar Abid <abubakar@huggingface.co> * Update video.py Adding more details on watermark_file. * Update gradio/components/video.py Change in and/or statement. Co-authored-by: Abubakar Abid <abubakar@huggingface.co> * Updating handling of WASM. * Add paren. * Minor lint change: if instead of elif * lint change: spacing * tweaks * fixes * demo * demos * notebooks * notebooks * remove comments * video --------- Co-authored-by: gradio-pr-bot <gradio-pr-bot@users.noreply.github.com> Co-authored-by: Abubakar Abid <abubakar@huggingface.co>
This commit is contained in:
parent
7928c6edaa
commit
de997e67c9
5
.changeset/tricky-humans-poke.md
Normal file
5
.changeset/tricky-humans-poke.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"gradio": minor
|
||||
---
|
||||
|
||||
feat:Adds ability to watermark videos via a `watermark` parameter in Video component
|
BIN
demo/video_watermark/files/a.mp4
Normal file
BIN
demo/video_watermark/files/a.mp4
Normal file
Binary file not shown.
BIN
demo/video_watermark/files/b.mp4
Normal file
BIN
demo/video_watermark/files/b.mp4
Normal file
Binary file not shown.
BIN
demo/video_watermark/files/w1.jpg
Normal file
BIN
demo/video_watermark/files/w1.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 20 KiB |
BIN
demo/video_watermark/files/w2.png
Normal file
BIN
demo/video_watermark/files/w2.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.9 KiB |
1
demo/video_watermark/run.ipynb
Normal file
1
demo/video_watermark/run.ipynb
Normal file
@ -0,0 +1 @@
|
||||
{"cells": [{"cell_type": "markdown", "id": "302934307671667531413257853548643485645", "metadata": {}, "source": ["# Gradio Demo: video_watermark"]}, {"cell_type": "code", "execution_count": null, "id": "272996653310673477252411125948039410165", "metadata": {}, "outputs": [], "source": ["!pip install -q gradio "]}, {"cell_type": "code", "execution_count": null, "id": "288918539441861185822528903084949547379", "metadata": {}, "outputs": [], "source": ["# Downloading files from the demo repo\n", "import os\n", "os.mkdir('files')\n", "!wget -q -O files/a.mp4 https://github.com/gradio-app/gradio/raw/main/demo/video_watermark/files/a.mp4\n", "!wget -q -O files/b.mp4 https://github.com/gradio-app/gradio/raw/main/demo/video_watermark/files/b.mp4\n", "!wget -q -O files/w1.jpg https://github.com/gradio-app/gradio/raw/main/demo/video_watermark/files/w1.jpg\n", "!wget -q -O files/w2.png https://github.com/gradio-app/gradio/raw/main/demo/video_watermark/files/w2.png"]}, {"cell_type": "code", "execution_count": null, "id": "44380577570523278879349135829904343037", "metadata": {}, "outputs": [], "source": ["import gradio as gr\n", "import os\n", "\n", "a = os.path.join(os.path.abspath(''), \"files/a.mp4\")\n", "b = os.path.join(os.path.abspath(''), \"files/b.mp4\")\n", "w1 = os.path.join(os.path.abspath(''), \"files/w1.jpg\")\n", "w2 = os.path.join(os.path.abspath(''), \"files/w2.png\")\n", "\n", "def generate_video(original_video, watermark):\n", " return gr.Video(original_video, watermark=watermark)\n", "\n", "\n", "demo = gr.Interface(generate_video, [gr.Video(), gr.File()], gr.Video(),\n", " examples=[[a, w1], [b, w2]])\n", "\n", "if __name__ == \"__main__\":\n", " demo.launch()\n"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5}
|
17
demo/video_watermark/run.py
Normal file
17
demo/video_watermark/run.py
Normal file
@ -0,0 +1,17 @@
|
||||
import gradio as gr
|
||||
import os
|
||||
|
||||
a = os.path.join(os.path.dirname(__file__), "files/a.mp4")
|
||||
b = os.path.join(os.path.dirname(__file__), "files/b.mp4")
|
||||
w1 = os.path.join(os.path.dirname(__file__), "files/w1.jpg")
|
||||
w2 = os.path.join(os.path.dirname(__file__), "files/w2.png")
|
||||
|
||||
def generate_video(original_video, watermark):
|
||||
return gr.Video(original_video, watermark=watermark)
|
||||
|
||||
|
||||
demo = gr.Interface(generate_video, [gr.Video(), gr.File()], gr.Video(),
|
||||
examples=[[a, w1], [b, w2]])
|
||||
|
||||
if __name__ == "__main__":
|
||||
demo.launch()
|
@ -91,12 +91,13 @@ class Video(Component):
|
||||
min_length: int | None = None,
|
||||
max_length: int | None = None,
|
||||
loop: bool = False,
|
||||
watermark: str | Path | None = None,
|
||||
):
|
||||
"""
|
||||
Parameters:
|
||||
value: A path or URL for the default value that Video component is going to take. Can also be a tuple consisting of (video filepath, subtitle filepath). If a subtitle file is provided, it should be of type .srt or .vtt. Or can be callable, in which case the function will be called whenever the app loads to set the initial value of the component.
|
||||
format: Format of video format to be returned by component, such as 'avi' or 'mp4'. Use 'mp4' to ensure browser playability. If set to None, video will keep uploaded format.
|
||||
sources: A list of sources permitted for video. "upload" creates a box where user can drop an video file, "webcam" allows user to record a video from their webcam. If None, defaults to ["upload, "webcam"].
|
||||
sources: A list of sources permitted for video. "upload" creates a box where user can drop a video file, "webcam" allows user to record a video from their webcam. If None, defaults to ["upload, "webcam"].
|
||||
height: The height of the displayed video, specified in pixels if a number is passed, or in CSS units if a string is passed.
|
||||
width: The width of the displayed video, specified in pixels if a number is passed, or in CSS units if a string is passed.
|
||||
label: The label for this component. Appears above the component and is also used as the header if there are a table of examples for this component. If None and used in a `gr.Interface`, the label will be the name of the parameter this component is assigned to.
|
||||
@ -120,6 +121,7 @@ class Video(Component):
|
||||
min_length: The minimum length of video (in seconds) that the user can pass into the prediction function. If None, there is no minimum length.
|
||||
max_length: The maximum length of video (in seconds) that the user can pass into the prediction function. If None, there is no maximum length.
|
||||
loop: If True, the video will loop when it reaches the end and continue playing from the beginning.
|
||||
watermark: An image file to be included as a watermark on the video. The image is not scaled and is displayed on the bottom right of the video. Valid formats for the image are: jpeg, png.
|
||||
"""
|
||||
valid_sources: list[Literal["upload", "webcam"]] = ["upload", "webcam"]
|
||||
if sources is None:
|
||||
@ -154,6 +156,7 @@ class Video(Component):
|
||||
self.show_download_button = show_download_button
|
||||
self.min_length = min_length
|
||||
self.max_length = max_length
|
||||
self.watermark = watermark
|
||||
super().__init__(
|
||||
label=label,
|
||||
every=every,
|
||||
@ -199,7 +202,20 @@ class Video(Component):
|
||||
raise gr.Error(
|
||||
f"Video is too long, and must be at most {self.max_length} seconds"
|
||||
)
|
||||
|
||||
# TODO: Check other image extensions to see if they work.
|
||||
valid_watermark_extensions = [".png", ".jpg", ".jpeg"]
|
||||
if self.watermark is not None:
|
||||
if not isinstance(self.watermark, (str, Path)):
|
||||
raise ValueError(
|
||||
f"Provided watermark file not an expected file type. "
|
||||
f"Received: {self.watermark}"
|
||||
)
|
||||
if Path(self.watermark).suffix not in valid_watermark_extensions:
|
||||
raise ValueError(
|
||||
f"Watermark file does not have a supported extension. "
|
||||
f"Expected one of {','.join(valid_watermark_extensions)}. "
|
||||
f"Received: {Path(self.watermark).suffix}."
|
||||
)
|
||||
if needs_formatting or flip:
|
||||
format = f".{self.format if needs_formatting else uploaded_format}"
|
||||
output_options = ["-vf", "hflip", "-c:a", "copy"] if flip else []
|
||||
@ -279,7 +295,8 @@ class Video(Component):
|
||||
|
||||
def _format_video(self, video: str | Path | None) -> FileData | None:
|
||||
"""
|
||||
Processes a video to ensure that it is in the correct format.
|
||||
Processes a video to ensure that it is in the correct format
|
||||
and adds a watermark if requested.
|
||||
"""
|
||||
if video is None:
|
||||
return None
|
||||
@ -292,11 +309,13 @@ class Video(Component):
|
||||
|
||||
is_url = client_utils.is_http_url_like(video)
|
||||
|
||||
# For cases where the video is a URL and does not need to be converted to another format, we can just return the URL
|
||||
if is_url and not (conversion_needed):
|
||||
# For cases where the video is a URL and does not need to be converted
|
||||
# to another format and have a watermark added, we can just return the URL
|
||||
if not self.watermark and (is_url and not conversion_needed):
|
||||
return FileData(path=video)
|
||||
|
||||
# For cases where the video needs to be converted to another format
|
||||
# or have a watermark added.
|
||||
if is_url:
|
||||
video = processing_utils.save_url_to_cache(
|
||||
video, cache_dir=self.GRADIO_CACHE
|
||||
@ -306,21 +325,38 @@ class Video(Component):
|
||||
and not processing_utils.video_is_playable(video)
|
||||
):
|
||||
warnings.warn(
|
||||
"Video does not have browser-compatible container or codec. Converting to mp4"
|
||||
"Video does not have browser-compatible container or codec. Converting to mp4."
|
||||
)
|
||||
video = processing_utils.convert_video_to_playable_mp4(video)
|
||||
# Recalculate the format in case convert_video_to_playable_mp4 already made it the selected format
|
||||
returned_format = utils.get_extension_from_file_path_or_url(video).lower()
|
||||
if self.format is not None and returned_format != self.format:
|
||||
if (
|
||||
self.format is not None and returned_format != self.format
|
||||
) or self.watermark:
|
||||
if wasm_utils.IS_WASM:
|
||||
raise wasm_utils.WasmUnsupportedError(
|
||||
"Returning a video in a different format is not supported in the Wasm mode."
|
||||
"Modifying a video is not supported in the Wasm mode."
|
||||
)
|
||||
global_option_list = ["-y"]
|
||||
inputs_dict = {video: None}
|
||||
output_file_name = video[0 : video.rindex(".") + 1]
|
||||
if self.format is not None:
|
||||
output_file_name += self.format
|
||||
else:
|
||||
output_file_name += returned_format
|
||||
if self.watermark:
|
||||
inputs_dict[str(self.watermark)] = None
|
||||
watermark_cmd = "overlay=W-w-5:H-h-5"
|
||||
global_option_list += ["-filter_complex", watermark_cmd]
|
||||
output_file_name = (
|
||||
Path(output_file_name).stem
|
||||
+ "_watermarked"
|
||||
+ Path(output_file_name).suffix
|
||||
)
|
||||
output_file_name = video[0 : video.rindex(".") + 1] + self.format
|
||||
ff = FFmpeg( # type: ignore
|
||||
inputs={video: None},
|
||||
inputs=inputs_dict,
|
||||
outputs={output_file_name: None},
|
||||
global_options="-y",
|
||||
global_options=global_option_list,
|
||||
)
|
||||
ff.run()
|
||||
video = output_file_name
|
||||
|
@ -371,6 +371,7 @@ class PlayableVideo(components.Video):
|
||||
min_length: int | None = None,
|
||||
max_length: int | None = None,
|
||||
loop: bool = False,
|
||||
watermark: str | Path | None = None,
|
||||
):
|
||||
sources = ["upload"]
|
||||
super().__init__(
|
||||
@ -400,6 +401,7 @@ class PlayableVideo(components.Video):
|
||||
min_length=min_length,
|
||||
max_length=max_length,
|
||||
loop=loop,
|
||||
watermark=watermark,
|
||||
)
|
||||
|
||||
|
||||
|
@ -68,6 +68,7 @@ class TestVideo:
|
||||
"_selectable": False,
|
||||
"key": None,
|
||||
"loop": False,
|
||||
"watermark": None,
|
||||
}
|
||||
assert video_input.preprocess(None) is None
|
||||
video_input = gr.Video(format="avi")
|
||||
|
Loading…
Reference in New Issue
Block a user