From 30e4a740f8f0b77bb2768c182745ac448f8b7666 Mon Sep 17 00:00:00 2001 From: Ankit raj <113342181+ankit-v2-3@users.noreply.github.com> Date: Fri, 22 Dec 2023 10:31:18 +0530 Subject: [PATCH 1/7] refactor: stream, thumbnail method, add player --- README.md | 10 ++++----- videodb/search.py | 38 +++++++++++++++++++++++++------- videodb/shot.py | 33 +++++++++++++++++++--------- videodb/video.py | 56 +++++++++++++++++++++++++++++++---------------- 4 files changed, 95 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index f7100fb..84fba48 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ video = conn.upload(url="https://www.youtube.com/") video = conn.upload(file_path="path/to/video.mp4") # get the stream url for the video -stream_url = video.get_stream() +stream_url = video.generate_stream() ``` @@ -139,7 +139,7 @@ stream_url = result.compile() # get shots of the result returns a list of Shot objects shots = result.get_shots() # get stream url of the shot -short_stream_url = shots[0].get_stream() +short_stream_url = shots[0].generate_stream() ``` @@ -155,10 +155,10 @@ video = collection.get_video("video_id") # get the stream url of the dynamically curated video based on the given timeline sequence # optional parameters: # - timeline: Optional[list[tuple[int, int]] to specify the start and end time of the video -stream_url = video.get_stream(timeline=[(0, 10), (30, 40)]) +stream_url = video.generate_stream(timeline=[(0, 10), (30, 40)]) -# get thumbnail of the video -thumbnail = video.get_thumbnail() +# get thumbnail url of the video +thumbnail_url = video.generate_thumbnail() # get transcript of the video # optional parameters: diff --git a/videodb/search.py b/videodb/search.py index 6543793..8dc839c 100644 --- a/videodb/search.py +++ b/videodb/search.py @@ -1,3 +1,5 @@ +import webbrowser as web + from abc import ABC, abstractmethod from videodb._constants import ( SearchType, @@ -15,8 +17,8 @@ class SearchResult: def __init__(self, _connection, **kwargs): self._connection = _connection self.shots = [] - self.text_summary = None - self.stream = None + self.stream_url = None + self.player_url = None self.collection_id = "default" self._results = kwargs.get("results", []) self._format_results() @@ -38,18 +40,27 @@ def _format_results(self): ) ) + def __repr__(self) -> str: + return ( + f"SearchResult(" + f"collection_id={self.collection_id}, " + f"stream_url={self.stream_url}, " + f"player_url={self.player_url}, " + f"shots={self.shots})" + ) + def get_shots(self) -> List[Shot]: return self.shots def compile(self) -> str: - """Compile the search result shots into a stream link + """Compile the search result shots into a stream url :raises SearchError: If no shots are found in the search results - :return: The stream link + :return: The stream url :rtype: str """ - if self.stream: - return self.stream + if self.stream_url: + return self.stream_url elif self.shots: compile_data = self._connection.post( path=f"{ApiPath.compile}", @@ -62,12 +73,23 @@ def compile(self) -> str: for shot in self.shots ], ) - self.stream = compile_data.get("stream_link") - return self.stream + self.stream_url = compile_data.get("stream_link") + self.player_url = compile_data.get("player_link") + return self.stream_url else: raise SearchError("No shots found in search results to compile") + def play(self) -> str: + """Generate a stream url for the shot and open it in the default browser + + :return: The stream url + :rtype: str + """ + self.compile() + web.open(self.player_url) + return self.player_url + class Search(ABC): """Search interface inside video or collection""" diff --git a/videodb/shot.py b/videodb/shot.py index f592616..43f885e 100644 --- a/videodb/shot.py +++ b/videodb/shot.py @@ -1,6 +1,6 @@ """This module contains the shot class""" - +import webbrowser as web from typing import Optional from videodb._constants import ( ApiPath, @@ -29,7 +29,8 @@ def __init__( self.end = end self.text = text self.search_score = search_score - self.stream = None + self.stream_url = None + self.player_url = None def __repr__(self) -> str: return ( @@ -40,22 +41,23 @@ def __repr__(self) -> str: f"end={self.end}, " f"text={self.text}, " f"search_score={self.search_score}, " - f"stream={self.stream})" + f"stream_url={self.stream_url}, " + f"player_url={self.player_url})" ) def __getitem__(self, key): """Get an item from the shot object""" return self.__dict__[key] - def get_stream(self) -> str: - """Get the shot into a stream link + def generate_stream(self) -> str: + """Generate a stream url for the shot - :return: The stream link + :return: The stream url :rtype: str """ - if self.stream: - return self.stream + if self.stream_url: + return self.stream_url else: stream_data = self._connection.post( path=f"{ApiPath.video}/{self.video_id}/{ApiPath.stream}", @@ -64,5 +66,16 @@ def get_stream(self) -> str: "length": self.video_length, }, ) - self.stream = stream_data.get("stream_link") - return self.stream + self.stream_url = stream_data.get("stream_link") + self.player_url = stream_data.get("player_link") + return self.stream_url + + def play(self) -> str: + """Generate a stream url for the shot and open it in the default browser + + :return: The stream url + :rtype: str + """ + self.generate_stream() + web.open(self.player_url) + return self.player_url diff --git a/videodb/video.py b/videodb/video.py index d639795..27fdf89 100644 --- a/videodb/video.py +++ b/videodb/video.py @@ -1,3 +1,4 @@ +import webbrowser as web from videodb._constants import ( ApiPath, SearchType, @@ -14,10 +15,11 @@ def __init__(self, _connection, id: str, collection_id: str, **kwargs) -> None: self._connection = _connection self.id = id self.collection_id = collection_id - self.stream_link = kwargs.get("stream_link", None) + self.stream_url = kwargs.get("stream_link", None) + self.player_url = kwargs.get("player_link", None) self.name = kwargs.get("name", None) self.description = kwargs.get("description", None) - self.thumbnail = kwargs.get("thumbnail", None) + self.thumbnail_url = kwargs.get("thumbnail", None) self.length = float(kwargs.get("length", 0.0)) self.transcript = kwargs.get("transcript", None) self.transcript_text = kwargs.get("transcript_text", None) @@ -27,10 +29,11 @@ def __repr__(self) -> str: f"Video(" f"id={self.id}, " f"collection_id={self.collection_id}, " - f"stream_link={self.stream_link}, " + f"stream_url={self.stream_url}, " + f"player_url={self.player_url}, " f"name={self.name}, " f"description={self.description}, " - f"thumbnail={self.thumbnail}, " + f"thumbnail_url={self.thumbnail_url}, " f"length={self.length})" ) @@ -63,16 +66,16 @@ def delete(self) -> None: """ self._connection.delete(path=f"{ApiPath.video}/{self.id}") - def get_stream(self, timeline: Optional[list[tuple[int, int]]] = None) -> str: - """Get the stream link of the video + def generate_stream(self, timeline: Optional[list[tuple[int, int]]] = None) -> str: + """Generate the stream url of the video :param list timeline: The timeline of the video to be streamed. Defaults to None. :raises InvalidRequestError: If the get_stream fails - :return: The stream link of the video + :return: The stream url of the video :rtype: str """ - if not timeline and self.stream_link: - return self.stream_link + if not timeline and self.stream_url: + return self.stream_url stream_data = self._connection.post( path=f"{ApiPath.video}/{self.id}/{ApiPath.stream}", @@ -81,16 +84,18 @@ def get_stream(self, timeline: Optional[list[tuple[int, int]]] = None) -> str: "length": self.length, }, ) - return stream_data.get("stream_link") + self.stream_url = stream_data.get("stream_link") + self.player_url = stream_data.get("player_link") + return self.stream_url - def get_thumbnail(self): - if self.thumbnail: - return self.thumbnail + def generate_thumbnail(self): + if self.thumbnail_url: + return self.thumbnail_url thumbnail_data = self._connection.get( path=f"{ApiPath.video}/{self.id}/{ApiPath.thumbnail}" ) - self.thumbnail = thumbnail_data.get("thumbnail") - return self.thumbnail + self.thumbnail_url = thumbnail_data.get("thumbnail") + return self.thumbnail_url def _fetch_transcript(self, force: bool = False) -> None: if self.transcript and not force: @@ -132,7 +137,9 @@ def add_subtitle(self) -> str: "type": Workflows.add_subtitles, }, ) - return subtitle_data.get("stream_link") + self.stream_url = subtitle_data.get("stream_link") + self.player_url = subtitle_data.get("player_link") + return self.stream_url def insert_video(self, video, timestamp: float) -> str: """Insert a video into another video @@ -140,7 +147,7 @@ def insert_video(self, video, timestamp: float) -> str: :param Video video: The video to be inserted :param float timestamp: The timestamp where the video should be inserted :raises InvalidRequestError: If the insert fails - :return: The stream link of the inserted video + :return: The stream url of the inserted video :rtype: str """ if timestamp > float(self.length): @@ -171,5 +178,16 @@ def insert_video(self, video, timestamp: float) -> str: for shot in all_shots ], ) - stream_link = compile_data.get("stream_link") - return stream_link + self.stream_url = compile_data.get("stream_link") + self.player_url = compile_data.get("player_link") + return self.stream_url + + def play(self) -> str: + """Generate a stream url for the shot and open it in the default browser + + :return: The stream url + :rtype: str + """ + self.generate_stream() + web.open(self.player_url) + return self.player_url From 4f4b7facbd09d0aeaae7903bd3e65a9f7b77fda7 Mon Sep 17 00:00:00 2001 From: Ankit raj <113342181+ankit-v2-3@users.noreply.github.com> Date: Wed, 27 Dec 2023 18:56:28 +0530 Subject: [PATCH 2/7] feat: add url player. --- videodb/_utils/_http_client.py | 2 +- videodb/_utils/video.py | 15 +++++++++++++++ videodb/search.py | 9 ++++----- videodb/shot.py | 9 +++++---- videodb/video.py | 29 ++++++++++++++--------------- 5 files changed, 39 insertions(+), 25 deletions(-) create mode 100644 videodb/_utils/video.py diff --git a/videodb/_utils/_http_client.py b/videodb/_utils/_http_client.py index 0c30c66..07a19c1 100644 --- a/videodb/_utils/_http_client.py +++ b/videodb/_utils/_http_client.py @@ -120,7 +120,7 @@ def _handle_request_error(self, e: requests.exceptions.RequestException) -> None f"Invalid request: {str(e)}", e.response ) from None - @backoff.on_exception(backoff.expo, Exception, max_time=500) + @backoff.on_exception(backoff.expo, Exception, max_time=500, logger=None) def _get_output(self, url: str): """Get the output from an async request""" response_json = self.session.get(url).json() diff --git a/videodb/_utils/video.py b/videodb/_utils/video.py new file mode 100644 index 0000000..6038bc3 --- /dev/null +++ b/videodb/_utils/video.py @@ -0,0 +1,15 @@ +import webbrowser as web + + +def play_url(url: str) -> bool: + opend = web.open(url) + if not opend: + try: + from IPython.display import IFrame + + player_width = 800 + player_height = 400 + IFrame(url, player_width, player_height) + except ImportError: + return False + return True diff --git a/videodb/search.py b/videodb/search.py index 8dc839c..b7d36de 100644 --- a/videodb/search.py +++ b/videodb/search.py @@ -1,6 +1,5 @@ -import webbrowser as web - from abc import ABC, abstractmethod +from videodb._utils.video import play_url from videodb._constants import ( SearchType, ApiPath, @@ -73,8 +72,8 @@ def compile(self) -> str: for shot in self.shots ], ) - self.stream_url = compile_data.get("stream_link") - self.player_url = compile_data.get("player_link") + self.stream_url = compile_data.get("stream_url") + self.player_url = compile_data.get("player_url") return self.stream_url else: @@ -87,7 +86,7 @@ def play(self) -> str: :rtype: str """ self.compile() - web.open(self.player_url) + play_url(self.player_url) return self.player_url diff --git a/videodb/shot.py b/videodb/shot.py index 43f885e..7eb387d 100644 --- a/videodb/shot.py +++ b/videodb/shot.py @@ -1,7 +1,8 @@ """This module contains the shot class""" -import webbrowser as web + from typing import Optional +from videodb._utils.video import play_url from videodb._constants import ( ApiPath, ) @@ -66,8 +67,8 @@ def generate_stream(self) -> str: "length": self.video_length, }, ) - self.stream_url = stream_data.get("stream_link") - self.player_url = stream_data.get("player_link") + self.stream_url = stream_data.get("stream_url") + self.player_url = stream_data.get("player_url") return self.stream_url def play(self) -> str: @@ -77,5 +78,5 @@ def play(self) -> str: :rtype: str """ self.generate_stream() - web.open(self.player_url) + play_url(self.player_url) return self.player_url diff --git a/videodb/video.py b/videodb/video.py index 27fdf89..1d8d4e4 100644 --- a/videodb/video.py +++ b/videodb/video.py @@ -1,4 +1,5 @@ -import webbrowser as web +from typing import Optional +from videodb._utils.video import play_url from videodb._constants import ( ApiPath, SearchType, @@ -7,7 +8,6 @@ ) from videodb.search import SearchFactory, SearchResult from videodb.shot import Shot -from typing import Optional class Video: @@ -15,11 +15,11 @@ def __init__(self, _connection, id: str, collection_id: str, **kwargs) -> None: self._connection = _connection self.id = id self.collection_id = collection_id - self.stream_url = kwargs.get("stream_link", None) - self.player_url = kwargs.get("player_link", None) + self.stream_url = kwargs.get("stream_url", None) + self.player_url = kwargs.get("player_url", None) self.name = kwargs.get("name", None) self.description = kwargs.get("description", None) - self.thumbnail_url = kwargs.get("thumbnail", None) + self.thumbnail_url = kwargs.get("thumbnail_url", None) self.length = float(kwargs.get("length", 0.0)) self.transcript = kwargs.get("transcript", None) self.transcript_text = kwargs.get("transcript_text", None) @@ -84,8 +84,8 @@ def generate_stream(self, timeline: Optional[list[tuple[int, int]]] = None) -> s "length": self.length, }, ) - self.stream_url = stream_data.get("stream_link") - self.player_url = stream_data.get("player_link") + self.stream_url = stream_data.get("stream_url") + self.player_url = stream_data.get("player_url") return self.stream_url def generate_thumbnail(self): @@ -94,7 +94,7 @@ def generate_thumbnail(self): thumbnail_data = self._connection.get( path=f"{ApiPath.video}/{self.id}/{ApiPath.thumbnail}" ) - self.thumbnail_url = thumbnail_data.get("thumbnail") + self.thumbnail_url = thumbnail_data.get("thumbnail_url") return self.thumbnail_url def _fetch_transcript(self, force: bool = False) -> None: @@ -137,8 +137,8 @@ def add_subtitle(self) -> str: "type": Workflows.add_subtitles, }, ) - self.stream_url = subtitle_data.get("stream_link") - self.player_url = subtitle_data.get("player_link") + self.stream_url = subtitle_data.get("stream_url") + self.player_url = subtitle_data.get("player_url") return self.stream_url def insert_video(self, video, timestamp: float) -> str: @@ -178,16 +178,15 @@ def insert_video(self, video, timestamp: float) -> str: for shot in all_shots ], ) - self.stream_url = compile_data.get("stream_link") - self.player_url = compile_data.get("player_link") + self.stream_url = compile_data.get("stream_url") + self.player_url = compile_data.get("player_url") return self.stream_url def play(self) -> str: - """Generate a stream url for the shot and open it in the default browser + """Open the player url in the browser/iframe and return the stream url :return: The stream url :rtype: str """ - self.generate_stream() - web.open(self.player_url) + play_url(self.player_url) return self.player_url From 0944f0d84e7290b677cecfebaed8277b762ec64d Mon Sep 17 00:00:00 2001 From: Ankit raj <113342181+ankit-v2-3@users.noreply.github.com> Date: Thu, 28 Dec 2023 11:29:03 +0530 Subject: [PATCH 3/7] feat: add play_hls utility --- videodb/__init__.py | 2 ++ videodb/_constants.py | 1 + videodb/_utils/video.py | 18 +++++++++++++----- videodb/search.py | 5 ++--- videodb/shot.py | 5 ++--- videodb/video.py | 17 +++++------------ 6 files changed, 25 insertions(+), 23 deletions(-) diff --git a/videodb/__init__.py b/videodb/__init__.py index 02c7fa5..222f2fd 100644 --- a/videodb/__init__.py +++ b/videodb/__init__.py @@ -4,6 +4,7 @@ import logging from typing import Optional +from videodb._utils.video import play_hls from videodb._constants import VIDEO_DB_API from videodb.client import Connection from videodb.exceptions import ( @@ -23,6 +24,7 @@ "AuthenticationError", "InvalidRequestError", "SearchError", + "play_hls", ] diff --git a/videodb/_constants.py b/videodb/_constants.py index fca23af..2ad2d8a 100644 --- a/videodb/_constants.py +++ b/videodb/_constants.py @@ -2,6 +2,7 @@ VIDEO_DB_API: str = "https://api.videodb.io" +PLAYER_URL: str = "https://console.videodb.io/player" class SearchType: diff --git a/videodb/_utils/video.py b/videodb/_utils/video.py index 6038bc3..3805fd2 100644 --- a/videodb/_utils/video.py +++ b/videodb/_utils/video.py @@ -1,15 +1,23 @@ import webbrowser as web +from videodb._constants import PLAYER_URL -def play_url(url: str) -> bool: - opend = web.open(url) + +def play_hls(url: str): + """Play a hls stream url in the browser/ notebook + + :param str url: The url of the hls stream + :return: The player url if the stream is opened in the browser or the iframe if the stream is opened in the notebook + """ + player = f"{PLAYER_URL}?url={url}" + opend = web.open(player) if not opend: try: from IPython.display import IFrame player_width = 800 player_height = 400 - IFrame(url, player_width, player_height) + return IFrame(player, player_width, player_height) except ImportError: - return False - return True + return player + return player diff --git a/videodb/search.py b/videodb/search.py index b7d36de..0b9a98c 100644 --- a/videodb/search.py +++ b/videodb/search.py @@ -1,5 +1,5 @@ from abc import ABC, abstractmethod -from videodb._utils.video import play_url +from videodb._utils.video import play_hls from videodb._constants import ( SearchType, ApiPath, @@ -86,8 +86,7 @@ def play(self) -> str: :rtype: str """ self.compile() - play_url(self.player_url) - return self.player_url + return play_hls(self.stream_url) class Search(ABC): diff --git a/videodb/shot.py b/videodb/shot.py index 7eb387d..6be387c 100644 --- a/videodb/shot.py +++ b/videodb/shot.py @@ -2,7 +2,7 @@ from typing import Optional -from videodb._utils.video import play_url +from videodb._utils.video import play_hls from videodb._constants import ( ApiPath, ) @@ -78,5 +78,4 @@ def play(self) -> str: :rtype: str """ self.generate_stream() - play_url(self.player_url) - return self.player_url + return play_hls(self.stream_url) diff --git a/videodb/video.py b/videodb/video.py index 1d8d4e4..3004709 100644 --- a/videodb/video.py +++ b/videodb/video.py @@ -1,5 +1,5 @@ from typing import Optional -from videodb._utils.video import play_url +from videodb._utils.video import play_hls from videodb._constants import ( ApiPath, SearchType, @@ -84,9 +84,7 @@ def generate_stream(self, timeline: Optional[list[tuple[int, int]]] = None) -> s "length": self.length, }, ) - self.stream_url = stream_data.get("stream_url") - self.player_url = stream_data.get("player_url") - return self.stream_url + return stream_data.get("stream_url", None) def generate_thumbnail(self): if self.thumbnail_url: @@ -137,9 +135,7 @@ def add_subtitle(self) -> str: "type": Workflows.add_subtitles, }, ) - self.stream_url = subtitle_data.get("stream_url") - self.player_url = subtitle_data.get("player_url") - return self.stream_url + return subtitle_data.get("stream_url", None) def insert_video(self, video, timestamp: float) -> str: """Insert a video into another video @@ -178,9 +174,7 @@ def insert_video(self, video, timestamp: float) -> str: for shot in all_shots ], ) - self.stream_url = compile_data.get("stream_url") - self.player_url = compile_data.get("player_url") - return self.stream_url + return compile_data.get("stream_url", None) def play(self) -> str: """Open the player url in the browser/iframe and return the stream url @@ -188,5 +182,4 @@ def play(self) -> str: :return: The stream url :rtype: str """ - play_url(self.player_url) - return self.player_url + return play_hls(self.player_url) From c860321e55a04c2bd81ba369d36753adcf36f0d7 Mon Sep 17 00:00:00 2001 From: Ankit raj <113342181+ankit-v2-3@users.noreply.github.com> Date: Thu, 28 Dec 2023 12:20:26 +0530 Subject: [PATCH 4/7] refactor: fix typos --- README.md | 2 +- videodb/__init__.py | 2 +- videodb/_utils/{video.py => _video.py} | 0 videodb/search.py | 2 +- videodb/shot.py | 4 ++-- videodb/video.py | 6 +++--- 6 files changed, 8 insertions(+), 8 deletions(-) rename videodb/_utils/{video.py => _video.py} (100%) diff --git a/README.md b/README.md index 84fba48..7b84481 100644 --- a/README.md +++ b/README.md @@ -113,7 +113,7 @@ collection = conn.get_collection() # get the video from the collection video = collection.get_video("video_id") -# index the video for symantic search +# index the video for semantic search video.index_spoken_words() # search relevant moment in video and stream resultant video clip instantly. diff --git a/videodb/__init__.py b/videodb/__init__.py index 222f2fd..664b391 100644 --- a/videodb/__init__.py +++ b/videodb/__init__.py @@ -4,7 +4,7 @@ import logging from typing import Optional -from videodb._utils.video import play_hls +from videodb._utils._video import play_hls from videodb._constants import VIDEO_DB_API from videodb.client import Connection from videodb.exceptions import ( diff --git a/videodb/_utils/video.py b/videodb/_utils/_video.py similarity index 100% rename from videodb/_utils/video.py rename to videodb/_utils/_video.py diff --git a/videodb/search.py b/videodb/search.py index 0b9a98c..6a3b344 100644 --- a/videodb/search.py +++ b/videodb/search.py @@ -1,5 +1,5 @@ from abc import ABC, abstractmethod -from videodb._utils.video import play_hls +from videodb._utils._video import play_hls from videodb._constants import ( SearchType, ApiPath, diff --git a/videodb/shot.py b/videodb/shot.py index 6be387c..bc435fd 100644 --- a/videodb/shot.py +++ b/videodb/shot.py @@ -2,7 +2,7 @@ from typing import Optional -from videodb._utils.video import play_hls +from videodb._utils._video import play_hls from videodb._constants import ( ApiPath, ) @@ -72,7 +72,7 @@ def generate_stream(self) -> str: return self.stream_url def play(self) -> str: - """Generate a stream url for the shot and open it in the default browser + """Generate a stream url for the shot and open it in the default browser/ notebook :return: The stream url :rtype: str diff --git a/videodb/video.py b/videodb/video.py index 3004709..55d926f 100644 --- a/videodb/video.py +++ b/videodb/video.py @@ -1,5 +1,5 @@ from typing import Optional -from videodb._utils.video import play_hls +from videodb._utils._video import play_hls from videodb._constants import ( ApiPath, SearchType, @@ -114,7 +114,7 @@ def get_transcript_text(self, force: bool = False) -> str: return self.transcript_text def index_spoken_words(self) -> None: - """Symantic indexing of spoken words in the video + """Semantic indexing of spoken words in the video :raises InvalidRequestError: If the video is already indexed :return: None if the indexing is successful @@ -182,4 +182,4 @@ def play(self) -> str: :return: The stream url :rtype: str """ - return play_hls(self.player_url) + return play_hls(self.stream_url) From faaac63e48c5cb4e781085549e174374b40800d4 Mon Sep 17 00:00:00 2001 From: Ankit raj <113342181+ankit-v2-3@users.noreply.github.com> Date: Thu, 28 Dec 2023 14:43:14 +0530 Subject: [PATCH 5/7] refactor: rename play_hls -> play_stream --- .gitignore | 1 + videodb/__init__.py | 4 ++-- videodb/_utils/_video.py | 6 +++--- videodb/search.py | 4 ++-- videodb/shot.py | 4 ++-- videodb/video.py | 4 ++-- 6 files changed, 12 insertions(+), 11 deletions(-) diff --git a/.gitignore b/.gitignore index 84e109c..8ae2cb6 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ dist/* venv/ .vscode/* example.ipynb +example.py diff --git a/videodb/__init__.py b/videodb/__init__.py index 664b391..2318106 100644 --- a/videodb/__init__.py +++ b/videodb/__init__.py @@ -4,7 +4,7 @@ import logging from typing import Optional -from videodb._utils._video import play_hls +from videodb._utils._video import play_stream from videodb._constants import VIDEO_DB_API from videodb.client import Connection from videodb.exceptions import ( @@ -24,7 +24,7 @@ "AuthenticationError", "InvalidRequestError", "SearchError", - "play_hls", + "play_stream", ] diff --git a/videodb/_utils/_video.py b/videodb/_utils/_video.py index 3805fd2..b5b0bf9 100644 --- a/videodb/_utils/_video.py +++ b/videodb/_utils/_video.py @@ -3,10 +3,10 @@ from videodb._constants import PLAYER_URL -def play_hls(url: str): - """Play a hls stream url in the browser/ notebook +def play_stream(url: str): + """Play a stream url in the browser/ notebook - :param str url: The url of the hls stream + :param str url: The url of the stream :return: The player url if the stream is opened in the browser or the iframe if the stream is opened in the notebook """ player = f"{PLAYER_URL}?url={url}" diff --git a/videodb/search.py b/videodb/search.py index 6a3b344..f42f645 100644 --- a/videodb/search.py +++ b/videodb/search.py @@ -1,5 +1,5 @@ from abc import ABC, abstractmethod -from videodb._utils._video import play_hls +from videodb._utils._video import play_stream from videodb._constants import ( SearchType, ApiPath, @@ -86,7 +86,7 @@ def play(self) -> str: :rtype: str """ self.compile() - return play_hls(self.stream_url) + return play_stream(self.stream_url) class Search(ABC): diff --git a/videodb/shot.py b/videodb/shot.py index bc435fd..d715de0 100644 --- a/videodb/shot.py +++ b/videodb/shot.py @@ -2,7 +2,7 @@ from typing import Optional -from videodb._utils._video import play_hls +from videodb._utils._video import play_stream from videodb._constants import ( ApiPath, ) @@ -78,4 +78,4 @@ def play(self) -> str: :rtype: str """ self.generate_stream() - return play_hls(self.stream_url) + return play_stream(self.stream_url) diff --git a/videodb/video.py b/videodb/video.py index 55d926f..0a17113 100644 --- a/videodb/video.py +++ b/videodb/video.py @@ -1,5 +1,5 @@ from typing import Optional -from videodb._utils._video import play_hls +from videodb._utils._video import play_stream from videodb._constants import ( ApiPath, SearchType, @@ -182,4 +182,4 @@ def play(self) -> str: :return: The stream url :rtype: str """ - return play_hls(self.stream_url) + return play_stream(self.stream_url) From 140e9f04946ef2519701f5dbcf71798e34a0dcd6 Mon Sep 17 00:00:00 2001 From: codeashu Date: Thu, 28 Dec 2023 19:36:08 +0530 Subject: [PATCH 6/7] updated readme and templates --- .github/ISSUE_TEMPLATE/bug_report.yml | 79 ++++++++ .github/ISSUE_TEMPLATE/feature_request.yml | 48 +++++ .github/pull_request_template.md | 24 +++ README.md | 208 ++++++++++++--------- 4 files changed, 266 insertions(+), 93 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml create mode 100644 .github/pull_request_template.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..76a9a02 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,79 @@ +name: Bug report +description: Create a report to help us improve +labels: ['bug'] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this bug report! + - type: checkboxes + attributes: + label: Confirm this is a new bug report + description: > + Select the checkboxes that apply to this bug report. If you're not sure about any of these, don't worry! We'll help you figure it out. + options: + - label: Possible new bug in VideoDB Python Client + required: false + - label: Potential new bug in VideoDB API + required: false + - label: I've checked the current issues, and there's no record of this bug + required: true + - type: textarea + attributes: + label: Current Behavior + description: > + A clear and concise description of what the bug is. + placeholder: > + I intended to perform action X, but unexpectedly encountered outcome Y. + validations: + required: true + - type: textarea + attributes: + label: Expected Behavior + description: > + A clear and concise description of what you expected to happen. + placeholder: > + I expected outcome Y to occur. + validations: + required: true + - type: textarea + attributes: + label: Steps to Reproduce + description: > + Steps to reproduce the behavior: + placeholder: | + 1. Fetch a '...' + 2. Update the '....' + 3. See error + validations: + required: true + - type: textarea + attributes: + label: Relevant Logs and/or Screenshots + description: > + If applicable, add logs and/or screenshots to help explain your problem. + validations: + required: false + - type: textarea + attributes: + label: Environment + description: | + Please complete the following information: + eg: + - OS: Ubuntu 20.04 + - Python: 3.9.1 + - Videodb: 0.0.1 + value: | + - OS: + - Python: + - Videodb: + validations: + required: false + - type: textarea + attributes: + label: Additional Context + description: > + Add any other context about the problem here. + validations: + required: false + \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..3a217cc --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,48 @@ +name: Feature +description: Submit a proposal/request for a new feature +labels: ['enhancement'] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this feature request! + - type: checkboxes + attributes: + label: Confirm this is a new feature request + description: > + Select the checkboxes that apply to this feature request. If you're not sure about any of these, don't worry! We'll help you figure it out. + options: + - label: Possible new feature in VideoDB Python Client + required: false + - label: Potential new feature in VideoDB API + required: false + - label: I've checked the current issues, and there's no record of this feature request + required: true + - type: textarea + attributes: + label: Describe the feature + description: > + A clear and concise description of what the feature is and why it's needed. + validations: + required: true + - type: textarea + attributes: + label: Describe the solution you'd like + description: | + A clear and concise description of what you want to happen. + validations: + required: true + - type: textarea + attributes: + label: Describe alternatives you've considered + description: > + A clear and concise description of any alternative solutions or features you've considered. + validations: + required: false + - type: textarea + attributes: + label: Additional Context + description: > + Add any other context about the feature request here. + validations: + required: false diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..a3e99db --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,24 @@ +## Pull Request + +**Description:** +Describe the purpose of this pull request. + +**Changes:** +- [ ] Feature A +- [ ] Bugfix B + +**Related Issues:** +- Closes #123 +- Addresses #456 + +**Testing:** +Describe any testing steps that have been taken or are necessary. +Make sure to take in account any existing code change that require some feature to be re-tested. + + +**Checklist:** +- [ ] Code follows project coding standards +- [ ] Tests have been added or updated +- [ ] Code Review +- [ ] Manual test after Merge +- [ ] All checks passed \ No newline at end of file diff --git a/README.md b/README.md index 7b84481..c4720cf 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ Logo -

VideoDB Python Client

+

VideoDB Python SDK

Video Database for your AI Applications @@ -34,8 +34,8 @@

-# VideoDB Python Client -The VideoDB Python client is a python package that allows you to interact with the VideoDB which is a serverless database that lets you manage video as intelligent data, not files. It is secure, scalable & optimized for AI- applications and LLM integrations. +# VideoDB Python SDK +VideoDB Python SDK allows you to interact with the VideoDB serverless database. Manage videos as intelligent data, not files. It's scalable, cost efficient & optimized for AI applications and LLM integration. ## Quick Start ### Creating a Connection -To create a new connection you need to get API key from [VideoDB console](https://console.videodb.io). You can directly upload from youtube, any public url, S3 bucket or local file path. A default collection is created when you create a new connection. +Get API key from [VideoDB console](https://console.videodb.io). Free for first 50 uploads. _(No credit card required)_ ```python import videodb - -# create a new connection to the VideoDB conn = videodb.connect(api_key="YOUR_API_KEY") +``` +## Working with a single Video -# upload to the default collection using the video url returns a Video object -video = conn.upload(url="https://www.youtube.com/") +--- -# upload to the default collection using the local file path returns a Video object -video = conn.upload(file_path="path/to/video.mp4") +⬆️ **Uploading a Video** -# get the stream url for the video -stream_url = video.generate_stream() +Now that you have established a connection to VideoDB, you can upload your videos using `conn.upload()` +You can directly upload from `youtube`, `any public url`, `S3 bucket` or `local file path`. A default collection is created when you create a new connection. -``` - -### Getting a Collection -To get a collection, use the `get_collection` method on the established database connection object. This method returns a `Collection` object. +`upload` method returns a `Video` object. ```python -import videodb +# Upload a video by url +video = conn.upload(url="https://www.youtube.com/watch?v=WDv4AWk0J3U") -# create a connection to the VideoDB -conn = videodb.connect(api_key="YOUR_API_KEY") +# Upload a video from file system +video_f = conn.upload(file_path="./my_video.mp4") + +``` -# get the default collection -collection = conn.get_collection() +### πŸ“Ί Viewing your video -# Upload a video to the collection returns a Video object -video = collection.upload(url="https://www.youtube.com/") +Your video is instantly available for viewing 720p resolution ⚑️ -# async upload -collection.upload(url="https://www.youtube.com/", callback_url="https://yourdomain.com/callback") +* Generate a streamable url for video using video.generate_stream() +* Preview the video using video.play(). This will open the video in your default browser/notebook -# get all the videos in the collection returns a list of Video objects -videos = collection.get_videos() +```python +video.generate_stream() +video.play() +``` -# get a video from the collection returns a Video object -video = collection.get_video("video_id") +### ⛓️ Stream Sections of videos -# delete the video from the collection -collection.delete_video("video_id") +You can easily clip specific sections of a video by passing timeline of start and end sections. +It accepts seconds. For example, Here’s we are streaming only first `10 seconds` and then `120` to `140 second` of a video +```python +stream_link = video.generate_stream(timeline=[[0,10], [120,140]]) +play_stream(stream_link) ``` -### Multi Modal Indexing +### πŸ” Searching inside a video -#### Spoken words indexing +To search bits inside a video β€” you have to index the video first. This can be done by a simple command. +_Indexing may take some time for longer videos._ ```python -import videodb +video.index_spoken_words() +result = video.search("Morning Sunlight") +result.play() +video.get_transcript() +``` +`Videodb` is launching more indexes in upcoming versions. +Currently it offers semantic index - Index by spoken words. -# create a connection to the VideoDB and get the default collection -conn = videodb.connect(api_key="YOUR_API_KEY") -collection = conn.get_collection() +In future you can also index videos using: +1. **Scene** - Visual concepts and events. +2. **Faces**. +3. **Specific domain Index** like Football, Baseball, Drone footage, Cricket etc. -# get the video from the collection -video = collection.get_video("video_id") +### Viewing Search Results : -# index the video for semantic search -video.index_spoken_words() +`video.search()` will return a `SearchResults` object, which contains the sections/shots of videos which semantically match your search query -# search relevant moment in video and stream resultant video clip instantly. -# returns a SearchResults object -# for searching the video, the video must be indexed please use index_spoken_words() before searching -# optional parameters: -# - type: Optional[str] to specify the type of search. default is "semantic" -# - result_threshold: Optional[int] to specify the number of results to return. default is 5 -# - score_threshold: Optional[float] to specify the score threshold for the results. default is 0.2 -result = video.search("what is videodb?") -# get stream url of the result -stream_url = result.compile() -# get shots of the result returns a list of Shot objects -shots = result.get_shots() -# get stream url of the shot -short_stream_url = shots[0].compile() - -# search relevant moment in collections and stream resultant video clip instantly. -# returns a SearchResults object -result = collection.search("what is videodb?") -# get stream url of the result -stream_url = result.compile() -# get shots of the result returns a list of Shot objects -shots = result.get_shots() -# get stream url of the shot -short_stream_url = shots[0].generate_stream() +* `result.get_shots()` Returns a list of Shot that matched search query +* `result.play()` Returns a playable url for video (similar to video.play() you can open this link in browser, or embed it into your website using iframe) -``` +## RAG: Search inside Multiple Videos + +--- + +`VideoDB` can store and search inside multiple videos with ease. By default, videos are uploaded to your default collection. + +### πŸ”„ Using Collection to upload multiple Videos -### Video Object Methods ```python -import videodb +# Get the default collection +coll = conn.get_collection() -# create a connection to the VideoDB, get the default collection and get a video -conn = videodb.connect(api_key="YOUR_API_KEY") -collection = conn.get_collection() -video = collection.get_video("video_id") +# Upload Videos to a collection +coll.upload(url="https://www.youtube.com/watch?v=lsODSDmY4CY") +coll.upload(url="https://www.youtube.com/watch?v=vZ4kOr38JhY") +coll.upload(url="https://www.youtube.com/watch?v=uak_dXHh6s4") +``` +* `conn.get_collection()` : Returns Collection object, the default collection +* `coll.get_videos()` : Returns list of Video, all videos in collections +* `coll.get_video(video_id)`: Returns Video, respective video object from given `video_id` +* `coll.delete_video(video_id)`: Deletes the video from Collection -# get the stream url of the dynamically curated video based on the given timeline sequence -# optional parameters: -# - timeline: Optional[list[tuple[int, int]] to specify the start and end time of the video -stream_url = video.generate_stream(timeline=[(0, 10), (30, 40)]) +### πŸ“‚ Search inside multiple videos in a collection -# get thumbnail url of the video -thumbnail_url = video.generate_thumbnail() +You can simply Index all the videos in a collection and use +search method on collection to find relevant results. +Here we are indexing spoken content of a +collection and performing semantic search. +```python +# Index all videos in collection +for video in coll.get_videos(): + video.index_spoken_words() + +# search in the collection of videos +results = coll.search(query = "What is Dopamine?") +results.play() +``` +The result here has all the matching bits in a single stream from your collection. You can use these results in your application right away. -# get transcript of the video -# optional parameters: -# - force: Optional[bool] to force get the transcript. default is False -transcript = video.get_transcript() +## 🌟 Explore more with Video object -# get transcript text of the video -# optional parameters: -# - force: Optional[bool] to force get the transcript text. default is False -transcript_text = video.get_transcript_text() +There are multiple methods available on a Video Object, that can be helpful for your use-case. -# add subtitle to the video and get the stream url of the video with subtitle -stream_url = video.add_subtitle() +### Access Transcript +```python +# words with timestamps +text_json = video.get_transcript() +text = video.get_transcript_text() +print(text) +``` -# delete the video from the collection -video.delete() +### Add Subtitle to a video +It returns a new stream instantly with subtitle added into the video. +```python +new_stream = video.add_subtitle() +play_stream(new_stream) ``` +**Get Thumbnail of Video:** - -## Roadmap +`video.get_thumbnail()`: Returns a thumbnail image of video. + +**Delete a video:** -See the [open issues](https://github.com/video-db/videodb-python/issues) for a list of proposed features (and known issues). +`video.delete()`: Delete a video. +Checkout more examples and tutorials πŸ‘‰ [Build with VideoDB](https://docs.videodb.io/build-with-videodb-35) to explore what you can +build with `VideoDB` +--- + +## Roadmap +- Adding More Indexes : `Face`, `Scene`, `Security`, `Events`, and `Sports` +- Give prompt support to generate thumbnails using GenAI. +- Give prompt support to access content. +- Give prompt support to edit videos. +- See the [open issues](https://github.com/video-db/videodb-python/issues) for a list of proposed features (and known issues). + +--- ## Contributing @@ -195,6 +215,8 @@ Contributions are what make the open source community such an amazing place to b 4. Push to the Branch (`git push origin feature/AmazingFeature`) 5. Open a Pull Request +--- + ## License From 33f9224f96fe5269397353d73ef985be9579e731 Mon Sep 17 00:00:00 2001 From: codeashu Date: Thu, 28 Dec 2023 20:06:22 +0530 Subject: [PATCH 7/7] modified setup and readme --- .github/pull_request_template.md | 2 +- README.md | 16 +++++----------- setup.py | 10 +++++++--- videodb/__init__.py | 2 +- videodb/_constants.py | 2 +- videodb/_utils/_video.py | 3 +-- 6 files changed, 16 insertions(+), 19 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index a3e99db..f913b20 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -20,5 +20,5 @@ Make sure to take in account any existing code change that require some feature - [ ] Code follows project coding standards - [ ] Tests have been added or updated - [ ] Code Review -- [ ] Manual test after Merge +- [ ] Manual test after merge - [ ] All checks passed \ No newline at end of file diff --git a/README.md b/README.md index c4720cf..574b97b 100644 --- a/README.md +++ b/README.md @@ -147,7 +147,7 @@ coll.upload(url="https://www.youtube.com/watch?v=uak_dXHh6s4") * `coll.get_video(video_id)`: Returns Video, respective video object from given `video_id` * `coll.delete_video(video_id)`: Deletes the video from Collection -### πŸ“‚ Search inside multiple videos in a collection +### πŸ“‚ Search inside collection You can simply Index all the videos in a collection and use search method on collection to find relevant results. @@ -164,11 +164,11 @@ results.play() ``` The result here has all the matching bits in a single stream from your collection. You can use these results in your application right away. -## 🌟 Explore more with Video object +### 🌟 Explore the Video object There are multiple methods available on a Video Object, that can be helpful for your use-case. -### Access Transcript +**Access Transcript** ```python # words with timestamps text_json = video.get_transcript() @@ -176,7 +176,7 @@ text = video.get_transcript_text() print(text) ``` -### Add Subtitle to a video +**Add Subtitle to a video** It returns a new stream instantly with subtitle added into the video. ```python @@ -185,7 +185,7 @@ play_stream(new_stream) ``` **Get Thumbnail of Video:** -`video.get_thumbnail()`: Returns a thumbnail image of video. +`video.generate_thumbnail()`: Returns a thumbnail image of video. **Delete a video:** @@ -217,12 +217,6 @@ Contributions are what make the open source community such an amazing place to b --- - -## License - -Distributed under the MIT License. See `LICENSE` for more information. - - [pypi-shield]: https://img.shields.io/pypi/v/videodb?style=for-the-badge diff --git a/setup.py b/setup.py index 6756694..86ef2b4 100644 --- a/setup.py +++ b/setup.py @@ -21,9 +21,9 @@ def get_version(): setup( name="videodb", version=get_version(), - author="Videodb", + author="videodb", author_email="contact@videodb.io", - description="Videodb Python client", + description="VideoDB Python SDK", long_description=long_description, long_description_content_type="text/markdown", url="https://github.com/video-db/videodb-python", @@ -33,5 +33,9 @@ def get_version(): "requests>=2.25.1", "backoff>=2.2.1", ], - + classifiers=[ + "Intended Audience :: Developers", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.9", + ], ) diff --git a/videodb/__init__.py b/videodb/__init__.py index 2318106..b5f18f4 100644 --- a/videodb/__init__.py +++ b/videodb/__init__.py @@ -16,7 +16,7 @@ logger: logging.Logger = logging.getLogger("videodb") -__version__ = "0.0.1" +__version__ = "0.0.2" __author__ = "videodb" __all__ = [ diff --git a/videodb/_constants.py b/videodb/_constants.py index 2ad2d8a..9911271 100644 --- a/videodb/_constants.py +++ b/videodb/_constants.py @@ -2,7 +2,7 @@ VIDEO_DB_API: str = "https://api.videodb.io" -PLAYER_URL: str = "https://console.videodb.io/player" + class SearchType: diff --git a/videodb/_utils/_video.py b/videodb/_utils/_video.py index b5b0bf9..9cdb012 100644 --- a/videodb/_utils/_video.py +++ b/videodb/_utils/_video.py @@ -1,6 +1,5 @@ import webbrowser as web - -from videodb._constants import PLAYER_URL +PLAYER_URL: str = "https://console.videodb.io/player" def play_stream(url: str):