diff --git a/videodb/__init__.py b/videodb/__init__.py index 62accc2..2fd4d95 100644 --- a/videodb/__init__.py +++ b/videodb/__init__.py @@ -5,7 +5,14 @@ from typing import Optional from videodb._utils._video import play_stream -from videodb._constants import VIDEO_DB_API, MediaType +from videodb._constants import ( + VIDEO_DB_API, + MediaType, + SearchType, + SubtitleAlignment, + SubtitleBorderStyle, + SubtitleStyle, +) from videodb.client import Connection from videodb.exceptions import ( VideodbError, @@ -16,7 +23,7 @@ logger: logging.Logger = logging.getLogger("videodb") -__version__ = "0.0.3" +__version__ = "0.0.4" __author__ = "videodb" __all__ = [ @@ -26,6 +33,10 @@ "SearchError", "play_stream", "MediaType", + "SearchType", + "SubtitleAlignment", + "SubtitleBorderStyle", + "SubtitleStyle", ] diff --git a/videodb/_constants.py b/videodb/_constants.py index 7d4b864..7c2d317 100644 --- a/videodb/_constants.py +++ b/videodb/_constants.py @@ -1,5 +1,6 @@ """Constants used in the videodb package.""" +from dataclasses import dataclass VIDEO_DB_API: str = "https://api.videodb.io" @@ -7,10 +8,12 @@ class MediaType: video = "video" audio = "audio" + image = "image" class SearchType: semantic = "semantic" + keyword = "keyword" class IndexType: @@ -31,6 +34,7 @@ class ApiPath: upload = "upload" video = "video" audio = "audio" + image = "image" stream = "stream" thumbnail = "thumbnail" upload_url = "upload_url" @@ -56,3 +60,46 @@ class HttpClientDefaultValues: class MaxSupported: fade_duration = 5 + + +class SubtitleBorderStyle: + no_border = 1 + opaque_box = 3 + outline = 4 + + +class SubtitleAlignment: + bottom_left = 1 + bottom_center = 2 + bottom_right = 3 + middle_left = 4 + middle_center = 5 + middle_right = 6 + top_left = 7 + top_center = 8 + top_right = 9 + + +@dataclass +class SubtitleStyle: + font_name: str = "Arial" + font_size: float = 18 + primary_colour: str = "&H00FFFFFF" # white + secondary_colour: str = "&H000000FF" # blue + outline_colour: str = "&H00000000" # black + back_colour: str = "&H00000000" # black + bold: bool = False + italic: bool = False + underline: bool = False + strike_out: bool = False + scale_x: float = 1.0 + scale_y: float = 1.0 + spacing: float = 0 + angle: float = 0 + border_style: int = SubtitleBorderStyle.outline + outline: float = 1.0 + shadow: float = 0.0 + alignment: int = SubtitleAlignment.bottom_center + margin_l: int = 10 + margin_r: int = 10 + margin_v: int = 10 diff --git a/videodb/asset.py b/videodb/asset.py index 97c3d31..e64a103 100644 --- a/videodb/asset.py +++ b/videodb/asset.py @@ -85,3 +85,35 @@ def __repr__(self) -> str: f"fade_in_duration={self.fade_in_duration}, " f"fade_out_duration={self.fade_out_duration})" ) + + +class ImageAsset(MediaAsset): + def __init__( + self, + asset_id: str, + width: Union[int, str] = 100, + height: Union[int, str] = 100, + x: Union[int, str] = 80, + y: Union[int, str] = 20, + duration: Optional[int] = None, + ) -> None: + super().__init__(asset_id) + self.width = width + self.height = height + self.x = x + self.y = y + self.duration = duration + + def to_json(self) -> dict: + return copy.deepcopy(self.__dict__) + + def __repr__(self) -> str: + return ( + f"ImageAsset(" + f"asset_id={self.asset_id}, " + f"width={self.width}, " + f"height={self.height}, " + f"x={self.x}, " + f"y={self.y}, " + f"duration={self.duration})" + ) diff --git a/videodb/client.py b/videodb/client.py index 001e586..fd823f9 100644 --- a/videodb/client.py +++ b/videodb/client.py @@ -13,6 +13,7 @@ from videodb._utils._http_client import HttpClient from videodb.video import Video from videodb.audio import Audio +from videodb.image import Image from videodb._upload import ( upload, @@ -46,7 +47,7 @@ def upload( name: Optional[str] = None, description: Optional[str] = None, callback_url: Optional[str] = None, - ) -> Union[Video, Audio, None]: + ) -> Union[Video, Audio, Image, None]: upload_data = upload( self, file_path, @@ -56,7 +57,10 @@ def upload( description, callback_url, ) - if upload_data.get("id").startswith("m-"): - return Video(self, **upload_data) if upload_data else None - elif upload_data.get("id").startswith("a-"): - return Audio(self, **upload_data) if upload_data else None + media_id = upload_data.get("id", "") + if media_id.startswith("m-"): + return Video(self, **upload_data) + elif media_id.startswith("a-"): + return Audio(self, **upload_data) + elif media_id.startswith("img-"): + return Image(self, **upload_data) diff --git a/videodb/collection.py b/videodb/collection.py index c508056..489a56b 100644 --- a/videodb/collection.py +++ b/videodb/collection.py @@ -13,6 +13,7 @@ ) from videodb.video import Video from videodb.audio import Audio +from videodb.image import Image from videodb.search import SearchFactory, SearchResult logger = logging.getLogger(__name__) @@ -54,6 +55,17 @@ def get_audio(self, audio_id: str) -> Audio: def delete_audio(self, audio_id: str) -> None: return self._connection.delete(path=f"{ApiPath.audio}/{audio_id}") + def get_images(self) -> list[Image]: + images_data = self._connection.get(path=f"{ApiPath.image}") + return [Image(self._connection, **image) for image in images_data.get("images")] + + def get_image(self, image_id: str) -> Image: + image_data = self._connection.get(path=f"{ApiPath.image}/{image_id}") + return Image(self._connection, **image_data) + + def delete_image(self, image_id: str) -> None: + return self._connection.delete(path=f"{ApiPath.image}/{image_id}") + def search( self, query: str, @@ -79,7 +91,7 @@ def upload( name: Optional[str] = None, description: Optional[str] = None, callback_url: Optional[str] = None, - ) -> Union[Video, Audio, None]: + ) -> Union[Video, Audio, Image, None]: upload_data = upload( self._connection, file_path, @@ -89,7 +101,10 @@ def upload( description, callback_url, ) - if upload_data.get("id").startswith("m-"): - return Video(self._connection, **upload_data) if upload_data else None - elif upload_data.get("id").startswith("a-"): - return Audio(self._connection, **upload_data) if upload_data else None + media_id = upload_data.get("id", "") + if media_id.startswith("m-"): + return Video(self._connection, **upload_data) + elif media_id.startswith("a-"): + return Audio(self._connection, **upload_data) + elif media_id.startswith("img-"): + return Image(self._connection, **upload_data) diff --git a/videodb/image.py b/videodb/image.py new file mode 100644 index 0000000..69e0ec3 --- /dev/null +++ b/videodb/image.py @@ -0,0 +1,22 @@ +from videodb._constants import ( + ApiPath, +) + + +class Image: + def __init__(self, _connection, id: str, collection_id: str, **kwargs) -> None: + self._connection = _connection + self.id = id + self.collection_id = collection_id + self.name = kwargs.get("name", None) + + def __repr__(self) -> str: + return ( + f"Image(" + f"id={self.id}, " + f"collection_id={self.collection_id}, " + f"name={self.name})" + ) + + def delete(self) -> None: + self._connection.delete(f"{ApiPath.image}/{self.id}") diff --git a/videodb/search.py b/videodb/search.py index f42f645..49db816 100644 --- a/videodb/search.py +++ b/videodb/search.py @@ -116,7 +116,7 @@ def search_inside_video( search_data = self._connection.post( path=f"{ApiPath.video}/{video_id}/{ApiPath.search}", data={ - "type": SearchType.semantic, + "index_type": SearchType.semantic, "query": query, "score_threshold": score_threshold or SemanticSearchDefaultValues.score_threshold, @@ -137,7 +137,7 @@ def search_inside_collection( search_data = self._connection.post( path=f"{ApiPath.collection}/{collection_id}/{ApiPath.search}", data={ - "type": SearchType.semantic, + "index_type": SearchType.semantic, "query": query, "score_threshold": score_threshold or SemanticSearchDefaultValues.score_threshold, @@ -148,7 +148,35 @@ def search_inside_collection( return SearchResult(self._connection, **search_data) -search_type = {SearchType.semantic: SemanticSearch} +class KeywordSearch(Search): + def __init__(self, _connection): + self._connection = _connection + + def search_inside_video( + self, + video_id: str, + query: str, + result_threshold: Optional[int] = None, + score_threshold: Optional[int] = None, + dynamic_score_percentage: Optional[int] = None, + **kwargs, + ): + search_data = self._connection.post( + path=f"{ApiPath.video}/{video_id}/{ApiPath.search}", + data={ + "index_type": SearchType.keyword, + "query": query, + "score_threshold": score_threshold, + "result_threshold": result_threshold, + }, + ) + return SearchResult(self._connection, **search_data) + + def search_inside_collection(**kwargs): + raise NotImplementedError("Keyword search will be implemented in the future") + + +search_type = {SearchType.semantic: SemanticSearch, SearchType.keyword: KeywordSearch} class SearchFactory: diff --git a/videodb/timeline.py b/videodb/timeline.py index c0720cd..96b66bf 100644 --- a/videodb/timeline.py +++ b/videodb/timeline.py @@ -1,7 +1,7 @@ from typing import Union from videodb._constants import ApiPath -from videodb.asset import VideoAsset, AudioAsset +from videodb.asset import VideoAsset, AudioAsset, ImageAsset class Timeline(object): @@ -28,9 +28,9 @@ def add_inline(self, asset: Union[VideoAsset]) -> None: raise ValueError("asset must be of type VideoAsset") self._timeline.append(asset) - def add_overlay(self, start: int, asset: Union[AudioAsset]) -> None: - if not isinstance(asset, AudioAsset): - raise ValueError("asset must be of type AudioAsset") + def add_overlay(self, start: int, asset: Union[AudioAsset, ImageAsset]) -> None: + if not isinstance(asset, AudioAsset) and not isinstance(asset, ImageAsset): + raise ValueError("asset must be of type AudioAsset or ImageAsset") self._timeline.append((start, asset)) def generate_stream(self) -> str: diff --git a/videodb/video.py b/videodb/video.py index 0d48280..6e8f3dc 100644 --- a/videodb/video.py +++ b/videodb/video.py @@ -5,6 +5,7 @@ SearchType, IndexType, Workflows, + SubtitleStyle, ) from videodb.search import SearchFactory, SearchResult from videodb.shot import Shot @@ -129,11 +130,14 @@ def index_spoken_words(self) -> None: }, ) - def add_subtitle(self) -> str: + def add_subtitle(self, style: SubtitleStyle = SubtitleStyle()) -> str: + if not isinstance(style, SubtitleStyle): + raise ValueError("style must be of type SubtitleStyle") subtitle_data = self._connection.post( path=f"{ApiPath.video}/{self.id}/{ApiPath.workflow}", data={ "type": Workflows.add_subtitles, + "subtitle_style": style.__dict__, }, ) return subtitle_data.get("stream_url", None)