From d2d57491af67eed882918ddbf39dca311f1268ad Mon Sep 17 00:00:00 2001 From: ashish-spext Date: Thu, 29 May 2025 17:57:44 +0530 Subject: [PATCH 01/28] Add clip support --- videodb/video.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/videodb/video.py b/videodb/video.py index eb4da7b..ab8ae1a 100644 --- a/videodb/video.py +++ b/videodb/video.py @@ -562,6 +562,31 @@ def add_subtitle(self, style: SubtitleStyle = SubtitleStyle()) -> str: ) return subtitle_data.get("stream_url", None) + def clip( + self, + query: str, + content_type: str, + model_name: str, + ) -> str: + """Generate a clip from the video using a prompt. + + :param str query: Prompt to generate the clip + :param str content_type: Content type for the clip + :param str model_name: Model name for generation + :return: The stream url of the generated clip + :rtype: str + """ + + clip_data = self._connection.post( + path=f"{ApiPath.video}/{self.id}/{ApiPath.clip}", + data={ + "prompt": query, + "content_type": content_type, + "model_name": model_name, + }, + ) + return clip_data.get("stream_url") + def insert_video(self, video, timestamp: float) -> str: """Insert a video into another video From 39fadd0c841add83c220cfc2b945fd9d24593172 Mon Sep 17 00:00:00 2001 From: ashish-spext Date: Thu, 29 May 2025 18:03:25 +0530 Subject: [PATCH 02/28] Fix param of video clip to prompt from query --- videodb/video.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/videodb/video.py b/videodb/video.py index ab8ae1a..0a19d82 100644 --- a/videodb/video.py +++ b/videodb/video.py @@ -564,13 +564,13 @@ def add_subtitle(self, style: SubtitleStyle = SubtitleStyle()) -> str: def clip( self, - query: str, + prompt: str, content_type: str, model_name: str, ) -> str: """Generate a clip from the video using a prompt. - :param str query: Prompt to generate the clip + :param str prompt: Prompt to generate the clip :param str content_type: Content type for the clip :param str model_name: Model name for generation :return: The stream url of the generated clip @@ -580,7 +580,7 @@ def clip( clip_data = self._connection.post( path=f"{ApiPath.video}/{self.id}/{ApiPath.clip}", data={ - "prompt": query, + "prompt": prompt, "content_type": content_type, "model_name": model_name, }, From c787a0127b191ed49f36a5ea98ba7d51252e30e5 Mon Sep 17 00:00:00 2001 From: ashish-spext Date: Thu, 29 May 2025 18:06:23 +0530 Subject: [PATCH 03/28] Add api path for clip --- videodb/_constants.py | 1 + 1 file changed, 1 insertion(+) diff --git a/videodb/_constants.py b/videodb/_constants.py index 0e80158..7d0904a 100644 --- a/videodb/_constants.py +++ b/videodb/_constants.py @@ -58,6 +58,7 @@ class ApiPath: index = "index" search = "search" compile = "compile" + clip = "clip" workflow = "workflow" timeline = "timeline" delete = "delete" From 60d7019848d5d2b70ae4814abddb7e32d645793a Mon Sep 17 00:00:00 2001 From: ashish-spext Date: Fri, 30 May 2025 11:29:10 +0530 Subject: [PATCH 04/28] Add rerank support in semantic search for collections and videos --- videodb/_constants.py | 1 + videodb/collection.py | 4 ++++ videodb/search.py | 4 ++++ videodb/video.py | 4 ++++ 4 files changed, 13 insertions(+) diff --git a/videodb/_constants.py b/videodb/_constants.py index 7d0904a..97d0260 100644 --- a/videodb/_constants.py +++ b/videodb/_constants.py @@ -36,6 +36,7 @@ class Workflows: class SemanticSearchDefaultValues: result_threshold = 5 score_threshold = 0.2 + rerank = False class Segmenter: diff --git a/videodb/collection.py b/videodb/collection.py index e941cf4..a46888e 100644 --- a/videodb/collection.py +++ b/videodb/collection.py @@ -8,6 +8,7 @@ ApiPath, IndexType, SearchType, + SemanticSearchDefaultValues, ) from videodb.video import Video from videodb.audio import Audio @@ -387,6 +388,7 @@ def search( result_threshold: Optional[int] = None, score_threshold: Optional[float] = None, dynamic_score_percentage: Optional[float] = None, + rerank: bool = SemanticSearchDefaultValues.rerank, filter: List[Dict[str, Any]] = [], ) -> SearchResult: """Search for a query in the collection. @@ -397,6 +399,7 @@ def search( :param int result_threshold: Number of results to return (optional) :param float score_threshold: Threshold score for the search (optional) :param float dynamic_score_percentage: Percentage of dynamic score to consider (optional) + :param bool rerank: Rerank search results (optional) :raise SearchError: If the search fails :return: :class:`SearchResult ` object :rtype: :class:`videodb.search.SearchResult` @@ -410,6 +413,7 @@ def search( result_threshold=result_threshold, score_threshold=score_threshold, dynamic_score_percentage=dynamic_score_percentage, + rerank=rerank, filter=filter, ) diff --git a/videodb/search.py b/videodb/search.py index ba557be..ac46442 100644 --- a/videodb/search.py +++ b/videodb/search.py @@ -123,6 +123,7 @@ def search_inside_video( result_threshold: Optional[int] = None, score_threshold: Optional[float] = None, dynamic_score_percentage: Optional[float] = None, + rerank: bool = SemanticSearchDefaultValues.rerank, **kwargs, ): search_data = self._connection.post( @@ -136,6 +137,7 @@ def search_inside_video( "result_threshold": result_threshold or SemanticSearchDefaultValues.result_threshold, "dynamic_score_percentage": dynamic_score_percentage, + "rerank": rerank, **kwargs, }, ) @@ -150,6 +152,7 @@ def search_inside_collection( result_threshold: Optional[int] = None, score_threshold: Optional[float] = None, dynamic_score_percentage: Optional[float] = None, + rerank: bool = SemanticSearchDefaultValues.rerank, **kwargs, ): search_data = self._connection.post( @@ -163,6 +166,7 @@ def search_inside_collection( "result_threshold": result_threshold or SemanticSearchDefaultValues.result_threshold, "dynamic_score_percentage": dynamic_score_percentage, + "rerank": rerank, **kwargs, }, ) diff --git a/videodb/video.py b/videodb/video.py index 0a19d82..7ed9a2e 100644 --- a/videodb/video.py +++ b/videodb/video.py @@ -5,6 +5,7 @@ IndexType, SceneExtractionType, SearchType, + SemanticSearchDefaultValues, Segmenter, SubtitleStyle, Workflows, @@ -69,6 +70,7 @@ def search( result_threshold: Optional[int] = None, score_threshold: Optional[float] = None, dynamic_score_percentage: Optional[float] = None, + rerank: bool = SemanticSearchDefaultValues.rerank, filter: List[Dict[str, Any]] = [], **kwargs, ) -> SearchResult: @@ -80,6 +82,7 @@ def search( :param int result_threshold: (optional) Number of results to return :param float score_threshold: (optional) Threshold score for the search :param float dynamic_score_percentage: (optional) Percentage of dynamic score to consider + :param bool rerank: (optional) Rerank search results :raise SearchError: If the search fails :return: :class:`SearchResult ` object :rtype: :class:`videodb.search.SearchResult` @@ -93,6 +96,7 @@ def search( result_threshold=result_threshold, score_threshold=score_threshold, dynamic_score_percentage=dynamic_score_percentage, + rerank=rerank, filter=filter, **kwargs, ) From 710ec9acfcb11c54267a478a648cf5696c131003 Mon Sep 17 00:00:00 2001 From: ashish-spext Date: Fri, 30 May 2025 12:58:39 +0530 Subject: [PATCH 05/28] Add rerank_params in semantic search --- videodb/_constants.py | 1 + videodb/collection.py | 3 +++ videodb/search.py | 4 ++++ videodb/video.py | 3 +++ 4 files changed, 11 insertions(+) diff --git a/videodb/_constants.py b/videodb/_constants.py index 97d0260..1c0c63f 100644 --- a/videodb/_constants.py +++ b/videodb/_constants.py @@ -37,6 +37,7 @@ class SemanticSearchDefaultValues: result_threshold = 5 score_threshold = 0.2 rerank = False + rerank_param = {} class Segmenter: diff --git a/videodb/collection.py b/videodb/collection.py index a46888e..d49e9cf 100644 --- a/videodb/collection.py +++ b/videodb/collection.py @@ -389,6 +389,7 @@ def search( score_threshold: Optional[float] = None, dynamic_score_percentage: Optional[float] = None, rerank: bool = SemanticSearchDefaultValues.rerank, + rerank_param: dict = SemanticSearchDefaultValues.rerank_param, filter: List[Dict[str, Any]] = [], ) -> SearchResult: """Search for a query in the collection. @@ -400,6 +401,7 @@ def search( :param float score_threshold: Threshold score for the search (optional) :param float dynamic_score_percentage: Percentage of dynamic score to consider (optional) :param bool rerank: Rerank search results (optional) + :param dict rerank_param: Parameters for reranking (optional) :raise SearchError: If the search fails :return: :class:`SearchResult ` object :rtype: :class:`videodb.search.SearchResult` @@ -414,6 +416,7 @@ def search( score_threshold=score_threshold, dynamic_score_percentage=dynamic_score_percentage, rerank=rerank, + rerank_param=rerank_param, filter=filter, ) diff --git a/videodb/search.py b/videodb/search.py index ac46442..9528aec 100644 --- a/videodb/search.py +++ b/videodb/search.py @@ -124,6 +124,7 @@ def search_inside_video( score_threshold: Optional[float] = None, dynamic_score_percentage: Optional[float] = None, rerank: bool = SemanticSearchDefaultValues.rerank, + rerank_param: dict = SemanticSearchDefaultValues.rerank_param, **kwargs, ): search_data = self._connection.post( @@ -138,6 +139,7 @@ def search_inside_video( or SemanticSearchDefaultValues.result_threshold, "dynamic_score_percentage": dynamic_score_percentage, "rerank": rerank, + "rerank_param": rerank_param, **kwargs, }, ) @@ -153,6 +155,7 @@ def search_inside_collection( score_threshold: Optional[float] = None, dynamic_score_percentage: Optional[float] = None, rerank: bool = SemanticSearchDefaultValues.rerank, + rerank_param: dict = SemanticSearchDefaultValues.rerank_param, **kwargs, ): search_data = self._connection.post( @@ -167,6 +170,7 @@ def search_inside_collection( or SemanticSearchDefaultValues.result_threshold, "dynamic_score_percentage": dynamic_score_percentage, "rerank": rerank, + "rerank_param": rerank_param, **kwargs, }, ) diff --git a/videodb/video.py b/videodb/video.py index 7ed9a2e..08ca277 100644 --- a/videodb/video.py +++ b/videodb/video.py @@ -71,6 +71,7 @@ def search( score_threshold: Optional[float] = None, dynamic_score_percentage: Optional[float] = None, rerank: bool = SemanticSearchDefaultValues.rerank, + rerank_param: dict = SemanticSearchDefaultValues.rerank_param, filter: List[Dict[str, Any]] = [], **kwargs, ) -> SearchResult: @@ -83,6 +84,7 @@ def search( :param float score_threshold: (optional) Threshold score for the search :param float dynamic_score_percentage: (optional) Percentage of dynamic score to consider :param bool rerank: (optional) Rerank search results + :param dict rerank_param: (optional) Parameters for reranking :raise SearchError: If the search fails :return: :class:`SearchResult ` object :rtype: :class:`videodb.search.SearchResult` @@ -97,6 +99,7 @@ def search( score_threshold=score_threshold, dynamic_score_percentage=dynamic_score_percentage, rerank=rerank, + rerank_param=rerank_param, filter=filter, **kwargs, ) From 52c6c5e5249bb81dc885f614b84ca92cccf20c54 Mon Sep 17 00:00:00 2001 From: ashish-spext Date: Fri, 30 May 2025 13:02:52 +0530 Subject: [PATCH 06/28] Fix rrank param name to rerank_params --- videodb/_constants.py | 2 +- videodb/collection.py | 6 +++--- videodb/search.py | 8 ++++---- videodb/video.py | 6 +++--- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/videodb/_constants.py b/videodb/_constants.py index 1c0c63f..0bb1036 100644 --- a/videodb/_constants.py +++ b/videodb/_constants.py @@ -37,7 +37,7 @@ class SemanticSearchDefaultValues: result_threshold = 5 score_threshold = 0.2 rerank = False - rerank_param = {} + rerank_params = {} class Segmenter: diff --git a/videodb/collection.py b/videodb/collection.py index d49e9cf..e857ece 100644 --- a/videodb/collection.py +++ b/videodb/collection.py @@ -389,7 +389,7 @@ def search( score_threshold: Optional[float] = None, dynamic_score_percentage: Optional[float] = None, rerank: bool = SemanticSearchDefaultValues.rerank, - rerank_param: dict = SemanticSearchDefaultValues.rerank_param, + rerank_params: dict = SemanticSearchDefaultValues.rerank_params, filter: List[Dict[str, Any]] = [], ) -> SearchResult: """Search for a query in the collection. @@ -401,7 +401,7 @@ def search( :param float score_threshold: Threshold score for the search (optional) :param float dynamic_score_percentage: Percentage of dynamic score to consider (optional) :param bool rerank: Rerank search results (optional) - :param dict rerank_param: Parameters for reranking (optional) + :param dict rerank_params: Parameters for reranking (optional) :raise SearchError: If the search fails :return: :class:`SearchResult ` object :rtype: :class:`videodb.search.SearchResult` @@ -416,7 +416,7 @@ def search( score_threshold=score_threshold, dynamic_score_percentage=dynamic_score_percentage, rerank=rerank, - rerank_param=rerank_param, + rerank_params=rerank_params, filter=filter, ) diff --git a/videodb/search.py b/videodb/search.py index 9528aec..b27f469 100644 --- a/videodb/search.py +++ b/videodb/search.py @@ -124,7 +124,7 @@ def search_inside_video( score_threshold: Optional[float] = None, dynamic_score_percentage: Optional[float] = None, rerank: bool = SemanticSearchDefaultValues.rerank, - rerank_param: dict = SemanticSearchDefaultValues.rerank_param, + rerank_params: dict = SemanticSearchDefaultValues.rerank_params, **kwargs, ): search_data = self._connection.post( @@ -139,7 +139,7 @@ def search_inside_video( or SemanticSearchDefaultValues.result_threshold, "dynamic_score_percentage": dynamic_score_percentage, "rerank": rerank, - "rerank_param": rerank_param, + "rerank_params": rerank_params, **kwargs, }, ) @@ -155,7 +155,7 @@ def search_inside_collection( score_threshold: Optional[float] = None, dynamic_score_percentage: Optional[float] = None, rerank: bool = SemanticSearchDefaultValues.rerank, - rerank_param: dict = SemanticSearchDefaultValues.rerank_param, + rerank_params: dict = SemanticSearchDefaultValues.rerank_params, **kwargs, ): search_data = self._connection.post( @@ -170,7 +170,7 @@ def search_inside_collection( or SemanticSearchDefaultValues.result_threshold, "dynamic_score_percentage": dynamic_score_percentage, "rerank": rerank, - "rerank_param": rerank_param, + "rerank_params": rerank_params, **kwargs, }, ) diff --git a/videodb/video.py b/videodb/video.py index 08ca277..3a2c26a 100644 --- a/videodb/video.py +++ b/videodb/video.py @@ -71,7 +71,7 @@ def search( score_threshold: Optional[float] = None, dynamic_score_percentage: Optional[float] = None, rerank: bool = SemanticSearchDefaultValues.rerank, - rerank_param: dict = SemanticSearchDefaultValues.rerank_param, + rerank_params: dict = SemanticSearchDefaultValues.rerank_params, filter: List[Dict[str, Any]] = [], **kwargs, ) -> SearchResult: @@ -84,7 +84,7 @@ def search( :param float score_threshold: (optional) Threshold score for the search :param float dynamic_score_percentage: (optional) Percentage of dynamic score to consider :param bool rerank: (optional) Rerank search results - :param dict rerank_param: (optional) Parameters for reranking + :param dict rerank_params: (optional) Parameters for reranking :raise SearchError: If the search fails :return: :class:`SearchResult ` object :rtype: :class:`videodb.search.SearchResult` @@ -99,7 +99,7 @@ def search( score_threshold=score_threshold, dynamic_score_percentage=dynamic_score_percentage, rerank=rerank, - rerank_param=rerank_param, + rerank_params=rerank_params, filter=filter, **kwargs, ) From 6378ca61fa5cdc465b8e2db5e5e6b8c6dea5131e Mon Sep 17 00:00:00 2001 From: ashish-spext Date: Fri, 30 May 2025 16:10:05 +0530 Subject: [PATCH 07/28] Add segmenation type in spoken index --- videodb/video.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/videodb/video.py b/videodb/video.py index 3a2c26a..0ec11ac 100644 --- a/videodb/video.py +++ b/videodb/video.py @@ -284,12 +284,14 @@ def translate_transcript( def index_spoken_words( self, language_code: Optional[str] = None, + segmentation_type: Optional[str] = None, force: bool = False, callback_url: str = None, ) -> None: """Semantic indexing of spoken words in the video. :param str language_code: (optional) Language code of the video + :param str segmentation_type: (optional) Segmentation type used for indexing :param bool force: (optional) Force to index the video :param str callback_url: (optional) URL to receive the callback :raises InvalidRequestError: If the video is already indexed @@ -301,6 +303,7 @@ def index_spoken_words( data={ "index_type": IndexType.spoken_word, "language_code": language_code, + "segmentation_type": segmentation_type, "force": force, "callback_url": callback_url, }, From 60fdc5e984509bf38ef60fd0e3f121ba7f452292 Mon Sep 17 00:00:00 2001 From: ashish-spext Date: Mon, 2 Jun 2025 15:10:19 +0530 Subject: [PATCH 08/28] Allow 0 as score threshold and result threshold --- videodb/search.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/videodb/search.py b/videodb/search.py index b27f469..94efcd1 100644 --- a/videodb/search.py +++ b/videodb/search.py @@ -134,9 +134,11 @@ def search_inside_video( "index_type": index_type, "query": query, "score_threshold": score_threshold - or SemanticSearchDefaultValues.score_threshold, + if score_threshold is not None + else SemanticSearchDefaultValues.score_threshold, "result_threshold": result_threshold - or SemanticSearchDefaultValues.result_threshold, + if result_threshold is not None + else SemanticSearchDefaultValues.result_threshold, "dynamic_score_percentage": dynamic_score_percentage, "rerank": rerank, "rerank_params": rerank_params, @@ -165,9 +167,11 @@ def search_inside_collection( "index_type": index_type, "query": query, "score_threshold": score_threshold - or SemanticSearchDefaultValues.score_threshold, + if score_threshold is not None + else SemanticSearchDefaultValues.score_threshold, "result_threshold": result_threshold - or SemanticSearchDefaultValues.result_threshold, + if result_threshold is not None + else SemanticSearchDefaultValues.result_threshold, "dynamic_score_percentage": dynamic_score_percentage, "rerank": rerank, "rerank_params": rerank_params, From 723fa12e419b3405fa7f8eea0649d1d43397ce24 Mon Sep 17 00:00:00 2001 From: ashish-spext Date: Tue, 10 Jun 2025 16:42:51 +0530 Subject: [PATCH 09/28] Add hybrid search option --- videodb/_constants.py | 1 + 1 file changed, 1 insertion(+) diff --git a/videodb/_constants.py b/videodb/_constants.py index 0bb1036..43e9fde 100644 --- a/videodb/_constants.py +++ b/videodb/_constants.py @@ -22,6 +22,7 @@ class SearchType: class IndexType: spoken_word = "spoken_word" scene = "scene" + hybrid = "hybrid" class SceneExtractionType: From 163921e02b59342996c2b797c1619f8219ce2440 Mon Sep 17 00:00:00 2001 From: ashish-spext Date: Wed, 11 Jun 2025 16:20:36 +0530 Subject: [PATCH 10/28] Add scene index id and name in the shot of search response --- videodb/search.py | 4 ++++ videodb/shot.py | 22 +++++++++++++++++++--- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/videodb/search.py b/videodb/search.py index 94efcd1..a536d65 100644 --- a/videodb/search.py +++ b/videodb/search.py @@ -34,6 +34,8 @@ def __init__(self, _connection, **kwargs): def _format_results(self): for result in self._results: self.collection_id = result.get("collection_id") + scene_index_id = result.get("scene_index_id") + scene_index_name = result.get("scene_index_name") for doc in result.get("docs"): self.shots.append( Shot( @@ -45,6 +47,8 @@ def _format_results(self): doc.get("end"), doc.get("text"), doc.get("score"), + scene_index_id=scene_index_id, + scene_index_name=scene_index_name, ) ) diff --git a/videodb/shot.py b/videodb/shot.py index c2fadcb..484ff6b 100644 --- a/videodb/shot.py +++ b/videodb/shot.py @@ -19,6 +19,8 @@ class Shot: :ivar int search_score: Search relevance score :ivar str stream_url: URL to stream the shot :ivar str player_url: URL to play the shot in a player + :ivar Optional[str] scene_index_id: ID of the scene index for scene search results + :ivar Optional[str] scene_index_name: Name of the scene index for scene search results """ def __init__( @@ -31,6 +33,8 @@ def __init__( end: float, text: Optional[str] = None, search_score: Optional[int] = None, + scene_index_id: Optional[str] = None, + scene_index_name: Optional[str] = None, ) -> None: self._connection = _connection self.video_id = video_id @@ -40,21 +44,33 @@ def __init__( self.end = end self.text = text self.search_score = search_score + self.scene_index_id = scene_index_id + self.scene_index_name = scene_index_name self.stream_url = None self.player_url = None def __repr__(self) -> str: - return ( + repr_str = ( f"Shot(" f"video_id={self.video_id}, " f"video_title={self.video_title}, " f"start={self.start}, " f"end={self.end}, " f"text={self.text}, " - f"search_score={self.search_score}, " - f"stream_url={self.stream_url}, " + f"search_score={self.search_score}" + ) + if self.scene_index_id: + repr_str += f", scene_index_id={self.scene_index_id}" + + if self.scene_index_name: + repr_str += f", scene_index_name={self.scene_index_name}" + + repr_str += ( + f", stream_url={self.stream_url}, " f"player_url={self.player_url})" ) + return repr_str + def __getitem__(self, key): """Get an item from the shot object""" From bf02b6a65a953ea17c3176b9e80f9445e7da68d7 Mon Sep 17 00:00:00 2001 From: ashish-spext Date: Wed, 11 Jun 2025 18:10:18 +0530 Subject: [PATCH 11/28] Put scene index id and name of shots in search result at doc level and introduce metadata --- videodb/search.py | 7 +++---- videodb/shot.py | 7 +++++++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/videodb/search.py b/videodb/search.py index a536d65..a5e53ff 100644 --- a/videodb/search.py +++ b/videodb/search.py @@ -34,8 +34,6 @@ def __init__(self, _connection, **kwargs): def _format_results(self): for result in self._results: self.collection_id = result.get("collection_id") - scene_index_id = result.get("scene_index_id") - scene_index_name = result.get("scene_index_name") for doc in result.get("docs"): self.shots.append( Shot( @@ -47,8 +45,9 @@ def _format_results(self): doc.get("end"), doc.get("text"), doc.get("score"), - scene_index_id=scene_index_id, - scene_index_name=scene_index_name, + scene_index_id=doc.get("scene_index_id"), + scene_index_name=doc.get("scene_index_name"), + metadata=doc.get("metadata"), ) ) diff --git a/videodb/shot.py b/videodb/shot.py index 484ff6b..2fee6d4 100644 --- a/videodb/shot.py +++ b/videodb/shot.py @@ -21,6 +21,7 @@ class Shot: :ivar str player_url: URL to play the shot in a player :ivar Optional[str] scene_index_id: ID of the scene index for scene search results :ivar Optional[str] scene_index_name: Name of the scene index for scene search results + :ivar Optional[dict] metadata: Additional metadata for the shot """ def __init__( @@ -35,6 +36,8 @@ def __init__( search_score: Optional[int] = None, scene_index_id: Optional[str] = None, scene_index_name: Optional[str] = None, + metadata: Optional[dict] = None, + ) -> None: self._connection = _connection self.video_id = video_id @@ -46,6 +49,7 @@ def __init__( self.search_score = search_score self.scene_index_id = scene_index_id self.scene_index_name = scene_index_name + self.metadata = metadata self.stream_url = None self.player_url = None @@ -65,6 +69,9 @@ def __repr__(self) -> str: if self.scene_index_name: repr_str += f", scene_index_name={self.scene_index_name}" + if self.metadata: + repr_str += f", metadata={self.metadata}" + repr_str += ( f", stream_url={self.stream_url}, " f"player_url={self.player_url})" From c431d8034455240d22b08ace9ccc74c7eb366f30 Mon Sep 17 00:00:00 2001 From: ashish-spext Date: Wed, 11 Jun 2025 18:19:00 +0530 Subject: [PATCH 12/28] Give support for sort_docs_on param in collection search --- videodb/_constants.py | 1 + videodb/collection.py | 3 +++ videodb/search.py | 2 ++ 3 files changed, 6 insertions(+) diff --git a/videodb/_constants.py b/videodb/_constants.py index 43e9fde..2479ba9 100644 --- a/videodb/_constants.py +++ b/videodb/_constants.py @@ -39,6 +39,7 @@ class SemanticSearchDefaultValues: score_threshold = 0.2 rerank = False rerank_params = {} + sort_docs_on = None class Segmenter: diff --git a/videodb/collection.py b/videodb/collection.py index e857ece..4e2dcdb 100644 --- a/videodb/collection.py +++ b/videodb/collection.py @@ -390,6 +390,7 @@ def search( dynamic_score_percentage: Optional[float] = None, rerank: bool = SemanticSearchDefaultValues.rerank, rerank_params: dict = SemanticSearchDefaultValues.rerank_params, + sort_docs_on: str = SemanticSearchDefaultValues.sort_docs_on, filter: List[Dict[str, Any]] = [], ) -> SearchResult: """Search for a query in the collection. @@ -402,6 +403,7 @@ def search( :param float dynamic_score_percentage: Percentage of dynamic score to consider (optional) :param bool rerank: Rerank search results (optional) :param dict rerank_params: Parameters for reranking (optional) + :param str sort_docs_on: Parameter to specify what metric to sort the docs of video on :raise SearchError: If the search fails :return: :class:`SearchResult ` object :rtype: :class:`videodb.search.SearchResult` @@ -418,6 +420,7 @@ def search( rerank=rerank, rerank_params=rerank_params, filter=filter, + sort_docs_on=sort_docs_on ) def search_title(self, query) -> List[Video]: diff --git a/videodb/search.py b/videodb/search.py index a5e53ff..7a6eea2 100644 --- a/videodb/search.py +++ b/videodb/search.py @@ -161,6 +161,7 @@ def search_inside_collection( dynamic_score_percentage: Optional[float] = None, rerank: bool = SemanticSearchDefaultValues.rerank, rerank_params: dict = SemanticSearchDefaultValues.rerank_params, + sort_docs_on: str = SemanticSearchDefaultValues.sort_docs_on, **kwargs, ): search_data = self._connection.post( @@ -178,6 +179,7 @@ def search_inside_collection( "dynamic_score_percentage": dynamic_score_percentage, "rerank": rerank, "rerank_params": rerank_params, + "sort_docs_on": sort_docs_on, **kwargs, }, ) From 72c07ae9fd609f161e10d715089e7d35ea553f89 Mon Sep 17 00:00:00 2001 From: ashish-spext Date: Wed, 11 Jun 2025 19:19:30 +0530 Subject: [PATCH 13/28] Add param to access result search metadata --- videodb/search.py | 1 + 1 file changed, 1 insertion(+) diff --git a/videodb/search.py b/videodb/search.py index 7a6eea2..407e1f4 100644 --- a/videodb/search.py +++ b/videodb/search.py @@ -29,6 +29,7 @@ def __init__(self, _connection, **kwargs): self.player_url = None self.collection_id = "default" self._results = kwargs.get("results", []) + self.metadata = kwargs.get("meta", None) self._format_results() def _format_results(self): From 6e65feb472e73eaf511ac0473b09786b37e7f4f6 Mon Sep 17 00:00:00 2001 From: ashish-spext Date: Wed, 11 Jun 2025 19:33:20 +0530 Subject: [PATCH 14/28] Give custom search type support --- videodb/_constants.py | 1 + videodb/search.py | 1 + 2 files changed, 2 insertions(+) diff --git a/videodb/_constants.py b/videodb/_constants.py index 2479ba9..c8d920d 100644 --- a/videodb/_constants.py +++ b/videodb/_constants.py @@ -17,6 +17,7 @@ class SearchType: keyword = "keyword" scene = "scene" llm = "llm" + custom = "custom" class IndexType: diff --git a/videodb/search.py b/videodb/search.py index 407e1f4..7750a57 100644 --- a/videodb/search.py +++ b/videodb/search.py @@ -256,6 +256,7 @@ def search_inside_collection(self, **kwargs): SearchType.semantic: SemanticSearch, SearchType.keyword: KeywordSearch, SearchType.scene: SceneSearch, + SearchType.custom: SemanticSearch, } From e22fe8a5c65819e1d73d78db250f6daad560e294 Mon Sep 17 00:00:00 2001 From: Rohit Garg Date: Mon, 16 Jun 2025 19:39:57 +0530 Subject: [PATCH 15/28] SearchResult res in video.clip method --- videodb/video.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/videodb/video.py b/videodb/video.py index 0ec11ac..edb7a1e 100644 --- a/videodb/video.py +++ b/videodb/video.py @@ -595,7 +595,7 @@ def clip( "model_name": model_name, }, ) - return clip_data.get("stream_url") + return SearchResult(self._connection, **clip_data) def insert_video(self, video, timestamp: float) -> str: """Insert a video into another video From 8e79b159c9fee8e51ffe15a0f97745a1a95727f9 Mon Sep 17 00:00:00 2001 From: Ankit raj <113342181+ankit-v2-3@users.noreply.github.com> Date: Mon, 23 Jun 2025 12:54:46 +0530 Subject: [PATCH 16/28] feat: add timelinev2 --- videodb/timeline_v2.py | 250 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 250 insertions(+) create mode 100644 videodb/timeline_v2.py diff --git a/videodb/timeline_v2.py b/videodb/timeline_v2.py new file mode 100644 index 0000000..2f4732c --- /dev/null +++ b/videodb/timeline_v2.py @@ -0,0 +1,250 @@ +from typing import List, Optional, Union +from enum import Enum + + +class AssetType(str, Enum): + video = "video" + image = "image" + + +class Fit(str, Enum): + crop = "crop" + cover = "cover" + contain = "contain" + none = "none" + + +class Position(str, Enum): + top = "top" + bottom = "bottom" + left = "left" + right = "right" + center = "center" + top_left = "top-left" + top_right = "top-right" + bottom_left = "bottom-left" + bottom_right = "bottom-right" + + +class Filter(str, Enum): + """A filter effect to apply to the Clip.""" + + blur = "blur" + boost = "boost" + contrast = "contrast" + darken = "darken" + greyscale = "greyscale" + lighten = "lighten" + muted = "muted" + negative = "negative" + + +class Offset: + def __init__(self, x: float = 0, y: float = 0): + self.x = x + self.y = y + + def to_json(self): + return { + "x": self.x, + "y": self.y, + } + + +class Crop: + def __init__(self, top: int = 0, right: int = 0, bottom: int = 0, left: int = 0): + self.top = top + self.right = right + self.bottom = bottom + self.left = left + + def to_json(self): + return { + "top": self.top, + "right": self.right, + "bottom": self.bottom, + "left": self.left, + } + + +class Transition: + def __init__(self, in_: str = None, out: str = None): + self.in_ = in_ + self.out = out + + def to_json(self): + return { + "in": self.in_, + "out": self.out, + } + + +class BaseAsset: + """The type of asset to display for the duration of the Clip.""" + + type: AssetType + + +class VideoAsset(BaseAsset): + """The VideoAsset is used to create video sequences from video files. The src must be a publicly accessible URL to a video resource""" + + type = AssetType.video + + def __init__( + self, + id: str, + trim: int = 0, + volume: float = 1, + crop: Optional[Crop] = None, + ): + if trim < 0: + raise ValueError("trim must be non-negative") + if not (0 <= volume <= 2): + raise ValueError("volume must be between 0 and 2") + + self.id = id + self.trim = trim + self.volume = volume + self.crop = crop if crop is not None else Crop() + + def to_json(self): + return { + "type": self.type, + "id": self.id, + "trim": self.trim, + "volume": self.volume, + "crop": self.crop.to_json(), + } + + +class ImageAsset(BaseAsset): + """The ImageAsset is used to create video from images to compose an image. The src must be a publicly accessible URL to an image resource such as a jpg or png file.""" + + type = AssetType.image + + def __init__(self, id: str, trim: int = 0, crop: Optional[Crop] = None): + if trim < 0: + raise ValueError("trim must be non-negative") + + self.id = id + self.trim = trim + self.crop = crop if crop is not None else Crop() + + def to_json(self): + return { + "type": self.type, + "id": self.id, + "trim": self.trim, + "crop": self.crop.to_json(), + } + + +AnyAsset = Union[VideoAsset, ImageAsset] + + +class Clip: + """A clip is a container for a specific type of asset, i.e. a title, image, video, audio or html. You use a Clip to define when an asset will display on the timeline, how long it will play for and transitions, filters and effects to apply to it.""" + + def __init__( + self, + asset: AnyAsset, + start: Union[float, int], + length: Union[float, int], + transition: Optional[Transition] = None, + effect: Optional[str] = None, + filter: Optional[Filter] = None, + scale: float = 1, + opacity: float = 1, + fit: Optional[Fit] = Fit.crop, + position: Position = Position.center, + offset: Optional[Offset] = None, + ): + if start < 0: + raise ValueError("start must be non-negative") + if length <= 0: + raise ValueError("length must be positive") + if not (0 <= scale <= 10): + raise ValueError("scale must be between 0 and 10") + if not (0 <= opacity <= 1): + raise ValueError("opacity must be between 0 and 1") + + self.asset = asset + self.start = start + self.length = length + self.transition = transition + self.effect = effect + self.filter = filter + self.scale = scale + self.opacity = opacity + self.fit = fit + self.position = position + self.offset = offset if offset is not None else Offset() + + def to_json(self): + json = { + "asset": self.asset.to_json(), + "start": self.start, + "length": self.length, + "effect": self.effect, + "scale": self.scale, + "opacity": self.opacity, + "fit": self.fit, + "position": self.position, + "offset": self.offset.to_json(), + } + + if self.transition: + json["transition"] = self.transition.to_json() + if self.filter: + json["filter"] = self.filter.value + + return json + + +class Track: + clips: List[Clip] + + def __init__(self, clips: List[Clip] = []): + self.clips = clips + + def add_clip(self, clip: Clip): + self.clips.append(clip) + + def to_json(self): + return { + "clips": [clip.to_json() for clip in self.clips], + } + + +class TimelineV2: + def __init__(self, connection): + self.connection = connection + self.background: str = "#000000" + self.resolution: str = "1280x720" + self.tracks: List[Track] = [] + self.stream_url = None + self.player_url = None + + def add_track(self, track: Track): + self.tracks.append(track) + + def add_clip(self, track_index: int, clip: Clip): + self.tracks[track_index].clips.append(clip) + + def to_json(self): + return { + "timeline": { + "background": self.background, + "resolution": self.resolution, + "tracks": [track.to_json() for track in self.tracks], + } + } + + def generate_stream(self): + stream_data = self.connection.post( + path="timeline_v2", + data=self.to_json(), + ) + self.stream_url = stream_data.get("stream_url") + self.player_url = stream_data.get("player_url") + return stream_data.get("stream_url", None) From a8fdf4ee943f4d977851e130189730a729a6459e Mon Sep 17 00:00:00 2001 From: Ankit raj <113342181+ankit-v2-3@users.noreply.github.com> Date: Mon, 23 Jun 2025 14:28:13 +0530 Subject: [PATCH 17/28] fix: fit --- videodb/timeline_v2.py | 1 - 1 file changed, 1 deletion(-) diff --git a/videodb/timeline_v2.py b/videodb/timeline_v2.py index 2f4732c..8e973d0 100644 --- a/videodb/timeline_v2.py +++ b/videodb/timeline_v2.py @@ -11,7 +11,6 @@ class Fit(str, Enum): crop = "crop" cover = "cover" contain = "contain" - none = "none" class Position(str, Enum): From 1e4a5f61259e3979853fbef4c605e03923a6e30f Mon Sep 17 00:00:00 2001 From: Ankit raj <113342181+ankit-v2-3@users.noreply.github.com> Date: Mon, 23 Jun 2025 15:24:41 +0530 Subject: [PATCH 18/28] build: update v --- videodb/__about__.py | 2 +- videodb/timeline_v2.py | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/videodb/__about__.py b/videodb/__about__.py index 3cc2806..fe85f3d 100644 --- a/videodb/__about__.py +++ b/videodb/__about__.py @@ -2,7 +2,7 @@ -__version__ = "0.2.15" +__version__ = "0.2.16" __title__ = "videodb" __author__ = "videodb" __email__ = "contact@videodb.io" diff --git a/videodb/timeline_v2.py b/videodb/timeline_v2.py index 8e973d0..c6b82ba 100644 --- a/videodb/timeline_v2.py +++ b/videodb/timeline_v2.py @@ -201,8 +201,6 @@ def to_json(self): class Track: - clips: List[Clip] - def __init__(self, clips: List[Clip] = []): self.clips = clips From 2b51bfc22c2a13009ef4aa11bd9d301f972d4e98 Mon Sep 17 00:00:00 2001 From: Ankit raj <113342181+ankit-v2-3@users.noreply.github.com> Date: Thu, 26 Jun 2025 10:12:58 +0530 Subject: [PATCH 19/28] fix: image asset --- videodb/timeline_v2.py | 1 - 1 file changed, 1 deletion(-) diff --git a/videodb/timeline_v2.py b/videodb/timeline_v2.py index c6b82ba..8c98e65 100644 --- a/videodb/timeline_v2.py +++ b/videodb/timeline_v2.py @@ -133,7 +133,6 @@ def to_json(self): return { "type": self.type, "id": self.id, - "trim": self.trim, "crop": self.crop.to_json(), } From 2940995c6b67985c538ea5123f0aefc7f07d0713 Mon Sep 17 00:00:00 2001 From: Ankit raj <113342181+ankit-v2-3@users.noreply.github.com> Date: Wed, 2 Jul 2025 18:30:39 +0530 Subject: [PATCH 20/28] feat: add audio asset --- videodb/__about__.py | 2 +- videodb/timeline_v2.py | 21 ++++++++++++++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/videodb/__about__.py b/videodb/__about__.py index fe85f3d..1a52acf 100644 --- a/videodb/__about__.py +++ b/videodb/__about__.py @@ -2,7 +2,7 @@ -__version__ = "0.2.16" +__version__ = "0.2.17" __title__ = "videodb" __author__ = "videodb" __email__ = "contact@videodb.io" diff --git a/videodb/timeline_v2.py b/videodb/timeline_v2.py index 8c98e65..b323893 100644 --- a/videodb/timeline_v2.py +++ b/videodb/timeline_v2.py @@ -137,7 +137,26 @@ def to_json(self): } -AnyAsset = Union[VideoAsset, ImageAsset] +class AudioAsset(BaseAsset): + """The AudioAsset is used to create audio sequences from audio files. The src must be a publicly accessible URL to an audio resource""" + + type = AssetType.audio + + def __init__(self, id: str, trim: int = 0, volume: float = 1): + self.id = id + self.trim = trim + self.volume = volume + + def to_json(self): + return { + "type": self.type, + "id": self.id, + "trim": self.trim, + "volume": self.volume, + } + + +AnyAsset = Union[VideoAsset, ImageAsset, AudioAsset] class Clip: From 2672b729ab597b09986e73459b02d864e022b8dc Mon Sep 17 00:00:00 2001 From: Ankit raj <113342181+ankit-v2-3@users.noreply.github.com> Date: Wed, 2 Jul 2025 18:35:43 +0530 Subject: [PATCH 21/28] fix: asset enum --- videodb/timeline_v2.py | 1 + 1 file changed, 1 insertion(+) diff --git a/videodb/timeline_v2.py b/videodb/timeline_v2.py index b323893..5482cad 100644 --- a/videodb/timeline_v2.py +++ b/videodb/timeline_v2.py @@ -5,6 +5,7 @@ class AssetType(str, Enum): video = "video" image = "image" + audio = "audio" class Fit(str, Enum): From 3537e396d00e4210229883275cc5cb642e561fec Mon Sep 17 00:00:00 2001 From: Ankit raj <113342181+ankit-v2-3@users.noreply.github.com> Date: Tue, 8 Jul 2025 13:07:45 +0530 Subject: [PATCH 22/28] feat: add text asset --- videodb/timeline_v2.py | 223 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 222 insertions(+), 1 deletion(-) diff --git a/videodb/timeline_v2.py b/videodb/timeline_v2.py index 5482cad..f2e01c0 100644 --- a/videodb/timeline_v2.py +++ b/videodb/timeline_v2.py @@ -6,6 +6,7 @@ class AssetType(str, Enum): video = "video" image = "image" audio = "audio" + text = "text" class Fit(str, Enum): @@ -39,6 +40,36 @@ class Filter(str, Enum): negative = "negative" +class TextAlignment(str, Enum): + """Place the text in one of nine predefined positions of the background.""" + + top = "top" + top_right = "top_right" + right = "right" + bottom_right = "bottom_right" + bottom = "bottom" + bottom_left = "bottom_left" + left = "left" + top_left = "top_left" + center = "center" + + +class HorizontalAlignment(str, Enum): + """Horizontal text alignment options.""" + + left = "left" + center = "center" + right = "right" + + +class VerticalAlignment(str, Enum): + """Vertical text alignment options.""" + + top = "top" + center = "center" + bottom = "bottom" + + class Offset: def __init__(self, x: float = 0, y: float = 0): self.x = x @@ -157,7 +188,197 @@ def to_json(self): } -AnyAsset = Union[VideoAsset, ImageAsset, AudioAsset] +class Font: + """Font styling properties for text assets.""" + + def __init__( + self, + family: str = "Clear Sans", + size: int = 48, + color: str = "#FFFFFF", + opacity: float = 1.0, + weight: Optional[int] = None, + ): + if size < 1: + raise ValueError("size must be at least 1") + if not (0.0 <= opacity <= 1.0): + raise ValueError("opacity must be between 0.0 and 1.0") + if weight is not None and not (100 <= weight <= 900): + raise ValueError("weight must be between 100 and 900") + + self.family = family + self.size = size + self.color = color + self.opacity = opacity + self.weight = weight + + def to_json(self): + data = { + "family": self.family, + "size": self.size, + "color": self.color, + "opacity": self.opacity, + } + if self.weight is not None: + data["weight"] = self.weight + return data + + +class Border: + """Text border properties.""" + + def __init__(self, color: str = "#000000", width: float = 0.0): + if width < 0.0: + raise ValueError("width must be non-negative") + self.color = color + self.width = width + + def to_json(self): + return { + "color": self.color, + "width": self.width, + } + + +class Shadow: + """Text shadow properties.""" + + def __init__(self, color: str = "#000000", x: float = 0.0, y: float = 0.0): + if x < 0.0: + raise ValueError("x must be non-negative") + if y < 0.0: + raise ValueError("y must be non-negative") + self.color = color + self.x = x + self.y = y + + def to_json(self): + return { + "color": self.color, + "x": self.x, + "y": self.y, + } + + +class Background: + """Text background styling properties.""" + + def __init__( + self, + width: float = 0.0, + height: float = 0.0, + color: str = "#000000", + border_width: float = 0.0, + opacity: float = 1.0, + text_alignment: TextAlignment = TextAlignment.center, + ): + if width < 0.0: + raise ValueError("width must be non-negative") + if height < 0.0: + raise ValueError("height must be non-negative") + if border_width < 0.0: + raise ValueError("border_width must be non-negative") + if not (0.0 <= opacity <= 1.0): + raise ValueError("opacity must be between 0.0 and 1.0") + + self.width = width + self.height = height + self.color = color + self.border_width = border_width + self.opacity = opacity + self.text_alignment = text_alignment + + def to_json(self): + return { + "width": self.width, + "height": self.height, + "color": self.color, + "border_width": self.border_width, + "opacity": self.opacity, + "text_alignment": self.text_alignment.value, + } + + +class Alignment: + """Text alignment properties.""" + + def __init__( + self, + horizontal: HorizontalAlignment = HorizontalAlignment.center, + vertical: VerticalAlignment = VerticalAlignment.center, + ): + self.horizontal = horizontal + self.vertical = vertical + + def to_json(self): + return { + "horizontal": self.horizontal.value, + "vertical": self.vertical.value, + } + + +class TextAsset(BaseAsset): + """The TextAsset is used to create text sequences from text strings with full control over the text styling and positioning.""" + + type = AssetType.text + + def __init__( + self, + text: str, + font: Optional[Font] = None, + border: Optional[Border] = None, + shadow: Optional[Shadow] = None, + background: Optional[Background] = None, + alignment: Optional[Alignment] = None, + tabsize: int = 4, + line_spacing: float = 0, + width: Optional[int] = None, + height: Optional[int] = None, + ): + if tabsize < 1: + raise ValueError("tabsize must be at least 1") + if line_spacing < 0.0: + raise ValueError("line_spacing must be non-negative") + if width is not None and width < 1: + raise ValueError("width must be at least 1") + if height is not None and height < 1: + raise ValueError("height must be at least 1") + + self.text = text + self.font = font if font is not None else Font() + self.border = border + self.shadow = shadow + self.background = background + self.alignment = alignment if alignment is not None else Alignment() + self.tabsize = tabsize + self.line_spacing = line_spacing + self.width = width + self.height = height + + def to_json(self): + data = { + "type": self.type, + "text": self.text, + "font": self.font.to_json(), + "alignment": self.alignment.to_json(), + "tabsize": self.tabsize, + "line_spacing": self.line_spacing, + } + if self.border: + data["border"] = self.border.to_json() + if self.shadow: + data["shadow"] = self.shadow.to_json() + if self.background: + data["background"] = self.background.to_json() + if self.width is not None: + data["width"] = self.width + if self.height is not None: + data["height"] = self.height + + return data + + +AnyAsset = Union[VideoAsset, ImageAsset, AudioAsset, TextAsset] class Clip: From ac78d552712e66e9e361dc01ac27378c3aed3551 Mon Sep 17 00:00:00 2001 From: Ankit raj <113342181+ankit-v2-3@users.noreply.github.com> Date: Thu, 17 Jul 2025 11:53:45 +0530 Subject: [PATCH 23/28] fix: volume range --- videodb/timeline_v2.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/videodb/timeline_v2.py b/videodb/timeline_v2.py index f2e01c0..b74bc62 100644 --- a/videodb/timeline_v2.py +++ b/videodb/timeline_v2.py @@ -130,8 +130,8 @@ def __init__( ): if trim < 0: raise ValueError("trim must be non-negative") - if not (0 <= volume <= 2): - raise ValueError("volume must be between 0 and 2") + if not (0 <= volume <= 5): + raise ValueError("volume must be between 0 and 5") self.id = id self.trim = trim From 20cc3ad0f83a3e044c7ac091e32cb9dc19dcee34 Mon Sep 17 00:00:00 2001 From: ashish-spext Date: Mon, 4 Aug 2025 21:17:28 +0530 Subject: [PATCH 24/28] Add prompt id optional arg for collsection search --- videodb/collection.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/videodb/collection.py b/videodb/collection.py index 4e2dcdb..9451e57 100644 --- a/videodb/collection.py +++ b/videodb/collection.py @@ -392,6 +392,7 @@ def search( rerank_params: dict = SemanticSearchDefaultValues.rerank_params, sort_docs_on: str = SemanticSearchDefaultValues.sort_docs_on, filter: List[Dict[str, Any]] = [], + prompt_id: Optional[str] = None, ) -> SearchResult: """Search for a query in the collection. @@ -404,6 +405,7 @@ def search( :param bool rerank: Rerank search results (optional) :param dict rerank_params: Parameters for reranking (optional) :param str sort_docs_on: Parameter to specify what metric to sort the docs of video on + :param str prompt_id: ID of the prompt to use for search (optional) :raise SearchError: If the search fails :return: :class:`SearchResult ` object :rtype: :class:`videodb.search.SearchResult` @@ -420,7 +422,8 @@ def search( rerank=rerank, rerank_params=rerank_params, filter=filter, - sort_docs_on=sort_docs_on + sort_docs_on=sort_docs_on, + prompt_id=prompt_id, ) def search_title(self, query) -> List[Video]: From fcf4796ed6cfd18ba96209bddeffcb7686f9edc2 Mon Sep 17 00:00:00 2001 From: Ankit raj <113342181+ankit-v2-3@users.noreply.github.com> Date: Tue, 5 Aug 2025 11:09:52 +0530 Subject: [PATCH 25/28] feat: add caption asset --- videodb/timeline_v2.py | 168 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 167 insertions(+), 1 deletion(-) diff --git a/videodb/timeline_v2.py b/videodb/timeline_v2.py index b74bc62..27c2903 100644 --- a/videodb/timeline_v2.py +++ b/videodb/timeline_v2.py @@ -7,6 +7,7 @@ class AssetType(str, Enum): image = "image" audio = "audio" text = "text" + caption = "caption" class Fit(str, Enum): @@ -62,6 +63,40 @@ class HorizontalAlignment(str, Enum): right = "right" +class CaptionBorderStyle(int, Enum): + """Border style properties for caption assets.""" + + no_border = 1 + opaque_box = 3 + outline = 4 + + +class CaptionAlignment(int, Enum): + """Caption alignment properties for caption assets.""" + + bottom_left = 1 + bottom_center = 2 + bottom_right = 3 + middle_left = 9 + middle_center = 10 + middle_right = 11 + top_left = 5 + top_center = 6 + top_right = 7 + + +class CaptionAnimation(str, Enum): + """Caption animation properties for caption assets.""" + + float_in_bottom = "float_in_bottom" + box_highlight = "box_highlight" + color_highlight = "color_highlight" + reveal = "reveal" + karioke = "karioke" + impact = "impact" + supersize = "supersize" + + class VerticalAlignment(str, Enum): """Vertical text alignment options.""" @@ -378,7 +413,138 @@ def to_json(self): return data -AnyAsset = Union[VideoAsset, ImageAsset, AudioAsset, TextAsset] +class FontStyling: + """Font styling properties for caption assets.""" + + def __init__( + self, + name: str = "Clear Sans", + size: int = 30, + bold: bool = False, + italic: bool = False, + underline: bool = False, + strikeout: bool = False, + scale_x: float = 1.0, + scale_y: float = 1.0, + spacing: float = 0.0, + angle: float = 0.0, + ): + self.name = name + self.size = size + self.bold = bold + self.italic = italic + self.underline = underline + self.strikeout = strikeout + self.scale_x = scale_x + self.scale_y = scale_y + self.spacing = spacing + self.angle = angle + + def to_json(self): + return { + "font_name": self.name, + "font_size": self.size, + "bold": self.bold, + "italic": self.italic, + "underline": self.underline, + "strikeout": self.strikeout, + "scale_x": self.scale_x, + "scale_y": self.scale_y, + "spacing": self.spacing, + "angle": self.angle, + } + + +class BorderAndShadow: + """Border and shadow properties for caption assets.""" + + def __init__( + self, + style: CaptionBorderStyle = CaptionBorderStyle.no_border, + outline: int = 1, + outline_color: str = "&H00000000", + shadow: int = 0, + ): + self.style = style + self.outline = outline + self.outline_color = outline_color + self.shadow = shadow + + def to_json(self): + return { + "style": self.style.value, + "outline": self.outline, + "outline_color": self.outline_color, + "shadow": self.shadow, + } + + +class Positioning: + """Positioning properties for caption assets.""" + + def __init__( + self, + alignment: CaptionAlignment = CaptionAlignment.bottom_center, + margin_l: int = 30, + margin_r: int = 30, + margin_v: int = 30, + ): + self.alignment = alignment + self.margin_l = margin_l + self.margin_r = margin_r + self.margin_v = margin_v + + def to_json(self): + return { + "alignment": self.alignment.value, + "margin_l": self.margin_l, + "margin_r": self.margin_r, + "margin_v": self.margin_v, + } + + +class CaptionAsset(BaseAsset): + """The CaptionAsset is used to create captions from text strings with full styling and ass support.""" + + type = AssetType.caption + + def __init__( + self, + src: str = "auto", + font: Optional[FontStyling] = None, + primary_color: str = "&H00FFFFFF", + secondary_color: str = "&H000000FF", + back_color: str = "&H00000000", + border: Optional[BorderAndShadow] = None, + position: Optional[Positioning] = None, + animation: Optional[CaptionAnimation] = None, + ): + self.src = src + self.font = font if font is not None else FontStyling() + self.primary_color = primary_color + self.secondary_color = secondary_color + self.back_color = back_color + self.border = border if border is not None else BorderAndShadow() + self.position = position if position is not None else Positioning() + self.animation = animation + + def to_json(self): + data = { + "type": self.type, + "src": self.src, + "font": self.font.to_json(), + "primary_color": self.primary_color, + "secondary_color": self.secondary_color, + "back_color": self.back_color, + "border": self.border.to_json(), + "position": self.position.to_json(), + } + if self.animation: + data["animation"] = self.animation.value + return data + + +AnyAsset = Union[VideoAsset, ImageAsset, AudioAsset, TextAsset, CaptionAsset] class Clip: From 9d5a6b0853485ca14270299cbf5cee9284747746 Mon Sep 17 00:00:00 2001 From: Ankit raj <113342181+ankit-v2-3@users.noreply.github.com> Date: Tue, 5 Aug 2025 12:41:09 +0530 Subject: [PATCH 26/28] fix: caption animation --- videodb/timeline_v2.py | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/videodb/timeline_v2.py b/videodb/timeline_v2.py index 27c2903..925d755 100644 --- a/videodb/timeline_v2.py +++ b/videodb/timeline_v2.py @@ -63,32 +63,32 @@ class HorizontalAlignment(str, Enum): right = "right" -class CaptionBorderStyle(int, Enum): +class CaptionBorderStyle(str, Enum): """Border style properties for caption assets.""" - no_border = 1 - opaque_box = 3 - outline = 4 + no_border = "no_border" + opaque_box = "opaque_box" + outline = "outline" -class CaptionAlignment(int, Enum): +class CaptionAlignment(str, Enum): """Caption alignment properties for caption assets.""" - bottom_left = 1 - bottom_center = 2 - bottom_right = 3 - middle_left = 9 - middle_center = 10 - middle_right = 11 - top_left = 5 - top_center = 6 - top_right = 7 + bottom_left = "bottom_left" + bottom_center = "bottom_center" + bottom_right = "bottom_right" + middle_left = "middle_left" + middle_center = "middle_center" + middle_right = "middle_right" + top_left = "top_left" + top_center = "top_center" + top_right = "top_right" class CaptionAnimation(str, Enum): """Caption animation properties for caption assets.""" - float_in_bottom = "float_in_bottom" + # float_in_bottom = "float_in_bottom" box_highlight = "box_highlight" color_highlight = "color_highlight" reveal = "reveal" @@ -424,8 +424,8 @@ def __init__( italic: bool = False, underline: bool = False, strikeout: bool = False, - scale_x: float = 1.0, - scale_y: float = 1.0, + scale_x: float = 100, + scale_y: float = 100, spacing: float = 0.0, angle: float = 0.0, ): From 92dc137f82cb1969b1c76fa31bf6142ab4291dcf Mon Sep 17 00:00:00 2001 From: Ankit raj <113342181+ankit-v2-3@users.noreply.github.com> Date: Tue, 5 Aug 2025 12:47:11 +0530 Subject: [PATCH 27/28] fix: border style --- videodb/timeline_v2.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/videodb/timeline_v2.py b/videodb/timeline_v2.py index 925d755..efd560d 100644 --- a/videodb/timeline_v2.py +++ b/videodb/timeline_v2.py @@ -66,8 +66,7 @@ class HorizontalAlignment(str, Enum): class CaptionBorderStyle(str, Enum): """Border style properties for caption assets.""" - no_border = "no_border" - opaque_box = "opaque_box" + outline_and_shadow = "outline_and_shadow" outline = "outline" @@ -460,7 +459,7 @@ class BorderAndShadow: def __init__( self, - style: CaptionBorderStyle = CaptionBorderStyle.no_border, + style: CaptionBorderStyle = CaptionBorderStyle.outline_and_shadow, outline: int = 1, outline_color: str = "&H00000000", shadow: int = 0, From 339e3a0b9c251b1fb081bcdce703b7826cc3f12b Mon Sep 17 00:00:00 2001 From: Ankit raj <113342181+ankit-v2-3@users.noreply.github.com> Date: Mon, 18 Aug 2025 17:43:23 +0530 Subject: [PATCH 28/28] feat: add timeline download --- videodb/timeline_v2.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/videodb/timeline_v2.py b/videodb/timeline_v2.py index efd560d..01a50f9 100644 --- a/videodb/timeline_v2.py +++ b/videodb/timeline_v2.py @@ -650,3 +650,8 @@ def generate_stream(self): self.stream_url = stream_data.get("stream_url") self.player_url = stream_data.get("player_url") return stream_data.get("stream_url", None) + + def download_stream(self, stream_url: str): + return self.connection.post( + path="timeline_v2/download", data={"stream_url": stream_url} + )