diff --git a/README.md b/README.md index 00b7f95..be59222 100644 --- a/README.md +++ b/README.md @@ -610,9 +610,6 @@ response = coll.generate_text( model_name="pro", # basic, pro, or ultra response_type="text" # text or json ) - -# Large prompts are uploaded automatically with a unique filename and -# sent as prompt_url instead of inline JSON to avoid request payload limits. ``` ### Video Dubbing and Translation diff --git a/openapi.yaml b/openapi.yaml index c378110..417b87b 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -1915,14 +1915,6 @@ paths: aspect_ratio: type: string example: "16:9" - model_name: - type: string - description: Use `flux` for FLUX self-inference image generation. - example: "flux" - config: - type: object - description: Model configuration. For FLUX supports size, num_inference_steps, guidance_scale, negative_prompt, and seed. - additionalProperties: true callback_url: type: string example: "https://webhook.example.com/callback" @@ -1991,32 +1983,16 @@ paths: schema: type: object required: + - prompt - audio_type properties: prompt: type: string - description: Prompt for music or sound effect generation. example: "Generate upbeat background music" - text: - type: string - description: Text to convert to speech when `audio_type` is `voice`. - example: "Hello, welcome to VideoDB." audio_type: type: string - enum: [voice, sound_effect, music] - example: "voice" - voice_name: - type: string - description: Voice name for hosted text-to-speech. - example: "Default" - model_name: - type: string - description: Use `omnivoice` for OmniVoice self-inference text-to-speech. - example: "omnivoice" - config: - type: object - description: Voice configuration. For OmniVoice supports instructions, ref_audio, ref_text, response_format, speed, language, and max_new_tokens. - additionalProperties: true + enum: [speech, sound_effect, music] + example: "music" callback_url: type: string example: "https://webhook.example.com/callback" @@ -2028,39 +2004,6 @@ paths: schema: $ref: '#/components/schemas/AsyncResponse' - /job/{job_id}: - get: - summary: Get self-inference generation job status - security: - - ApiKeyAuth: [] - parameters: - - name: job_id - in: path - required: true - schema: - type: string - example: "550e8400-e29b-41d4-a716-446655440000" - responses: - '200': - description: Generation job status or final generated asset - content: - application/json: - schema: - type: object - properties: - success: - type: boolean - status: - type: string - enum: [processing, done, failed] - data: - type: object - additionalProperties: true - message: - type: string - '404': - description: Job not found - /collection/{collection_id}/generate/text/: post: summary: Generate text using AI @@ -2079,45 +2022,27 @@ paths: application/json: schema: type: object - description: Provide either `prompt` for inline text or `prompt_url` for a previously uploaded prompt file. + required: + - prompt properties: prompt: type: string - description: Inline prompt text. Use for smaller prompts. example: "Summarize the content of this video" - prompt_url: - type: string - description: URL to a previously uploaded plain-text prompt file. Use for larger prompts. - example: "https://storage.googleapis.com/.../generate_text_prompt_123abc.txt" video_id: type: string example: "m-12345" model_name: type: string - description: Hosted tier (`mini`, `basic`, `pro`, `ultra`) or self-hosted sandbox model name. - example: "Qwen/Qwen3.5-27B" - sandbox_id: - type: string - description: Sandbox ID to use when `model_name` is a self-hosted model. - example: "bx-123abc" + example: "gpt-4" max_tokens: type: integer example: 500 temperature: type: number example: 0.7 - model_config: - type: object - description: Additional self-hosted model configuration. - additionalProperties: true callback_url: type: string example: "https://webhook.example.com/callback" - oneOf: - - required: - - prompt - - required: - - prompt_url responses: '200': description: Text generation started or completed @@ -5677,4 +5602,4 @@ tags: - name: Transcode description: Media transcoding operations - name: Assets - description: Cross-collection asset listing + description: Cross-collection asset listing \ No newline at end of file diff --git a/videodb/__init__.py b/videodb/__init__.py index 2282292..b736c88 100644 --- a/videodb/__init__.py +++ b/videodb/__init__.py @@ -25,16 +25,10 @@ ReframeMode, SegmentationType, RTStreamChannelType, - SandboxTier, - SandboxStatus, ) from videodb.client import Connection from videodb.search import AskResponse, SearchResponse, SearchResult from videodb.understanding import Understanding, UnderstandingAnalyzer -from videodb.job import GenerationJob -from videodb.sandbox import Sandbox -from videodb.sandbox_models import SandboxModel -from videodb.voice_clone import VoiceClone from videodb.capture_session import CaptureSession from videodb.websocket_client import WebSocketConnection from videodb.capture import CaptureClient, Channel, AudioChannel, VideoChannel, Channels, ChannelList @@ -52,10 +46,6 @@ __all__ = [ "connect", "CaptureSession", - "GenerationJob", - "Sandbox", - "SandboxModel", - "VoiceClone", "WebSocketConnection", "CaptureClient", "Channel", @@ -92,8 +82,6 @@ "ReframeMode", "SegmentationType", "RTStreamChannelType", - "SandboxTier", - "SandboxStatus", ] diff --git a/videodb/_constants.py b/videodb/_constants.py index b13a889..c16eab6 100644 --- a/videodb/_constants.py +++ b/videodb/_constants.py @@ -140,36 +140,11 @@ class ApiPath: token = "token" websocket = "websocket" export = "export" - job = "job" - sandbox = "sandbox" - voice_clone = "voice_clone" - understand = "understand" - indexes = "indexes" - identities = "identities" - merge = "merge" - split = "split" - async_response = "async-response" - class Status: processing = "processing" in_progress = "in progress" - complete = "complete" - - -class SandboxTier: - small = "small" - medium = "medium" - - -class SandboxStatus: - provisioning = "provisioning" - active = "active" - stopping = "stopping" - stopped = "stopped" - failed = "failed" - alert = "alert" class MeetingStatus: @@ -184,8 +159,6 @@ class HttpClientDefaultValues: timeout = 30 backoff_factor = 0.1 status_forcelist = [502, 503, 504] - max_poll_time = 500 - poll_interval = 5 class MaxSupported: diff --git a/videodb/_upload.py b/videodb/_upload.py index 0a25e5d..0291abe 100644 --- a/videodb/_upload.py +++ b/videodb/_upload.py @@ -1,9 +1,9 @@ -import os -from typing import Optional, Union -from urllib.parse import urlparse - import requests + +from typing import Optional +from urllib.parse import urlparse from requests import HTTPError +import os from videodb._constants import ( @@ -20,31 +20,6 @@ def _is_url(path: str) -> bool: return all([parsed.scheme in ("http", "https"), parsed.netloc]) -def upload_bytes( - _connection, - content: Union[str, bytes], - name: str, - content_type: str = "application/octet-stream", - collection_id: Optional[str] = None, -) -> str: - """Upload in-memory content using a presigned upload URL and return the object URL.""" - collection_id = collection_id or _connection.collection_id - upload_url_data = _connection.get( - path=f"{ApiPath.collection}/{collection_id}/{ApiPath.upload_url}", - params={"name": name}, - ) - upload_url = upload_url_data.get("upload_url") - - try: - files = {"file": (name, content, content_type)} - response = requests.post(upload_url, files=files) - response.raise_for_status() - except requests.exceptions.RequestException as e: - raise VideodbError("Error while uploading content", cause=e) - - return upload_url - - def upload( _connection, source: Optional[str] = None, @@ -96,13 +71,16 @@ def upload( if file_path: try: name = os.path.splitext(os.path.basename(file_path))[0] if not name else name + upload_url_data = _connection.get( + path=f"{ApiPath.collection}/{collection_id}/{ApiPath.upload_url}", + params={"name": name}, + ) + upload_url = upload_url_data.get("upload_url") with open(file_path, "rb") as file: - url = upload_bytes( - _connection=_connection, - content=file, - name=name, - collection_id=collection_id, - ) + files = {"file": (name, file)} + response = requests.post(upload_url, files=files) + response.raise_for_status() + url = upload_url except FileNotFoundError as e: raise VideodbError("File not found", cause=e) diff --git a/videodb/_utils/_http_client.py b/videodb/_utils/_http_client.py index 6507e38..ab571f6 100644 --- a/videodb/_utils/_http_client.py +++ b/videodb/_utils/_http_client.py @@ -1,8 +1,8 @@ """Http client Module.""" import logging -import time import requests +import backoff from tqdm import tqdm from typing import ( @@ -65,8 +65,6 @@ def __init__( self.base_url = base_url self.show_progress = False self.progress_bar = None - self.max_poll_time = HttpClientDefaultValues.max_poll_time - self.poll_interval = HttpClientDefaultValues.poll_interval logger.debug(f"Initialized http client with base url: {self.base_url}") def _make_request( @@ -75,7 +73,6 @@ def _make_request( path: str, base_url: Optional[str] = None, headers: Optional[dict] = None, - wait: bool = True, **kwargs, ): """Make a request to the api @@ -84,7 +81,6 @@ def _make_request( :param str path: The path to make the request to :param str base_url: (optional) The base url to use for the request :param dict headers: (optional) The headers to use for the request - :param bool wait: If False, return raw data without polling (default True) :param kwargs: The keyword arguments to pass to the request method :return: json response from the request """ @@ -94,11 +90,6 @@ def _make_request( request_headers = {**self.session.headers, **(headers or {})} response = method(url, headers=request_headers, timeout=timeout, **kwargs) response.raise_for_status() - if not wait: - data = response.json().get("data") - if data is not None: - return data - return response.json() return self._parse_response(response) except requests.exceptions.RequestException as e: @@ -142,31 +133,23 @@ def _handle_request_error(self, e: requests.exceptions.RequestException) -> None f"Invalid request: {str(e)}", e.response ) from None + @backoff.on_exception( + backoff.constant, Exception, max_time=500, interval=5, logger=None, jitter=None + ) def _get_output(self, url: str): - """Poll an output URL until the job completes or times out.""" - start = time.monotonic() - while True: - response_json = self.session.get(url).json() - status = response_json.get("status") - if status not in (Status.in_progress, Status.processing): - break - + """Get the output from an async request""" + response_json = self.session.get(url).json() + if ( + response_json.get("status") == Status.in_progress + or response_json.get("status") == Status.processing + ): percentage = response_json.get("data", {}).get("percentage") if percentage and self.show_progress and self.progress_bar: self.progress_bar.n = int(percentage) self.progress_bar.update(0) - elapsed = time.monotonic() - start - if elapsed >= self.max_poll_time: - raise RequestTimeoutError( - f"Polling timed out after {int(elapsed)}s " - f"(max_poll_time={self.max_poll_time})", - None, - ) - - logger.debug("Waiting for processing to complete (%.0fs elapsed)", elapsed) - time.sleep(self.poll_interval) - + logger.debug("Waiting for processing to complete") + raise Exception("Stuck on processing status") from None if self.show_progress and self.progress_bar: self.progress_bar.n = 100 self.progress_bar.update(0) @@ -226,31 +209,19 @@ def _format_headers(self, headers: dict): formatted_headers[f"x-{key}"] = value return formatted_headers - def _apply_poll_overrides(self, kwargs): - """Extract and apply per-call poll overrides from kwargs.""" - self.max_poll_time = kwargs.pop( - "max_poll_time", HttpClientDefaultValues.max_poll_time - ) - self.poll_interval = kwargs.pop( - "poll_interval", HttpClientDefaultValues.poll_interval - ) - def get( - self, path: str, show_progress: Optional[bool] = False, wait: bool = True, **kwargs + self, path: str, show_progress: Optional[bool] = False, **kwargs ) -> requests.Response: """Make a get request""" self.show_progress = show_progress - self._apply_poll_overrides(kwargs) - return self._make_request(method=self.session.get, path=path, wait=wait, **kwargs) + return self._make_request(method=self.session.get, path=path, **kwargs) def post( - self, path: str, data=None, show_progress: Optional[bool] = False, - wait: bool = True, **kwargs, + self, path: str, data=None, show_progress: Optional[bool] = False, **kwargs ) -> requests.Response: """Make a post request""" self.show_progress = show_progress - self._apply_poll_overrides(kwargs) - return self._make_request(self.session.post, path, json=data, wait=wait, **kwargs) + return self._make_request(self.session.post, path, json=data, **kwargs) def put(self, path: str, data=None, **kwargs) -> requests.Response: """Make a put request""" diff --git a/videodb/client.py b/videodb/client.py index 28f1d4d..7eac7bb 100644 --- a/videodb/client.py +++ b/videodb/client.py @@ -1,5 +1,4 @@ import logging -import requests from typing import ( Optional, @@ -12,7 +11,6 @@ TranscodeMode, VideoConfig, AudioConfig, - HttpClientDefaultValues, ) from videodb.collection import Collection @@ -21,8 +19,6 @@ from videodb.audio import Audio from videodb.image import Image from videodb.meeting import Meeting -from videodb.sandbox import Sandbox -from videodb.voice_clone import VoiceClone from videodb.capture_session import CaptureSession from videodb.websocket_client import WebSocketConnection @@ -268,165 +264,6 @@ def get_transcode_details(self, job_id: str) -> dict: """ return self.get(path=f"{ApiPath.transcode}/{job_id}") - def get_job_status(self, job_id: str) -> dict: - """Get status/details for a self-inference generation job. - - This preserves and normalizes the job response wrapper instead of using - the standard SDK response parser, because callers need the top-level - job status while polling. - - :param str job_id: ID of the generation job - :return: Normalized job response with success, status, data, message - :rtype: dict - """ - try: - url = f"{self.base_url}/{ApiPath.job}/{job_id}" - response = self.session.get(url, timeout=HttpClientDefaultValues.timeout) - response.raise_for_status() - response_json = response.json() - except requests.exceptions.RequestException as e: - self._handle_request_error(e) - except ValueError: - from videodb.exceptions import InvalidRequestError - - raise InvalidRequestError("Invalid request: Unable to parse job response") from None - - data = response_json.get("data") or {} - status = response_json.get("status") or data.get("status") - success = response_json.get("success", False) - - if not status: - status = "done" if success else "failed" - - return { - "success": success, - "status": status, - "data": data, - "message": response_json.get("message"), - } - - def wait_for_job( - self, - job_id: str, - timeout: int = 600, - interval: int = 5, - result_type: str = None, - ): - """Poll a self-inference generation job until completion. - - :param str job_id: ID of the generation job - :param int timeout: Maximum seconds to wait - :param int interval: Seconds between polls - :param str result_type: Optional expected result type, "audio" or "image" - :return: Generated SDK asset or final job data - """ - from videodb.job import GenerationJob - - return GenerationJob( - self, job_id=job_id, result_type=result_type - ).wait(timeout=timeout, interval=interval) - - def create_sandbox( - self, - tier: Optional[str] = None, - name: Optional[str] = None, - callback_url: Optional[str] = None, - ) -> "Sandbox": - """Create a new sandbox (GPU compute pool). - - :param str tier: Sandbox tier — "small" or "medium" (default: server decides) - :param str name: Human-readable name (auto-generated if not provided) - :param str callback_url: URL to receive sandbox lifecycle webhooks - :return: :class:`Sandbox ` object in provisioning state - :rtype: :class:`videodb.sandbox.Sandbox` - """ - data = self.post( - path=ApiPath.sandbox, - data={"tier": tier, "name": name, "callback_url": callback_url}, - ) - return Sandbox(self, **(data or {})) - - def get_sandbox(self, sandbox_id: str) -> "Sandbox": - """Get a sandbox by ID. - - :param str sandbox_id: The sandbox ID - :return: :class:`Sandbox ` object - :rtype: :class:`videodb.sandbox.Sandbox` - """ - data = self.get(path=f"{ApiPath.sandbox}/{sandbox_id}") - return Sandbox(self, **(data or {})) - - def list_sandboxes(self, status: Optional[str] = None) -> List["Sandbox"]: - """List all sandboxes, optionally filtered by status. - - :param str status: Filter by sandbox status (optional) - :return: List of :class:`Sandbox ` objects - :rtype: list[:class:`videodb.sandbox.Sandbox`] - """ - params = {} - if status: - params["status"] = status - data = self.get(path=ApiPath.sandbox, params=params) - sandboxes_data = (data or {}).get("sandboxes", []) - return [Sandbox(self, **s) for s in sandboxes_data] - - def create_voice_clone( - self, - ref_audio_id: str, - name: Optional[str] = None, - description: Optional[str] = None, - ref_text: Optional[str] = None, - language: Optional[str] = None, - collection_id: Optional[str] = None, - ) -> "VoiceClone": - """Create a reusable voice clone from an existing audio asset. - - :param str ref_audio_id: Source audio ID to use as the voice reference. - :param str name: Human-readable name (optional). - :param str description: Description (optional). - :param str ref_text: Text spoken in the reference audio (optional). - :param str language: Language code, e.g. ``"en"`` (optional). - :param str collection_id: Collection associated with the source audio (optional). - :return: :class:`VoiceClone ` object. - :rtype: :class:`videodb.voice_clone.VoiceClone` - """ - data = self.post( - path=ApiPath.voice_clone, - data={ - "ref_audio_id": ref_audio_id, - "name": name, - "description": description, - "ref_text": ref_text, - "language": language, - "collection_id": collection_id, - }, - ) - return VoiceClone(self, **(data or {})) - - def get_voice_clone(self, voice_clone_id: str) -> "VoiceClone": - """Get a voice clone by ID.""" - data = self.get(path=f"{ApiPath.voice_clone}/{voice_clone_id}") - return VoiceClone(self, **(data or {})) - - def list_voice_clones( - self, - page: int = 1, - page_size: int = 20, - language: Optional[str] = None, - ) -> List["VoiceClone"]: - """List voice clones for the current user.""" - params = {"page": page, "page_size": page_size} - if language: - params["language"] = language - data = self.get(path=ApiPath.voice_clone, params=params) - voice_clones = (data or {}).get("voice_clones", []) - return [VoiceClone(self, **v) for v in voice_clones] - - def delete_voice_clone(self, voice_clone_id: str) -> None: - """Delete a voice clone by ID.""" - self.delete(path=f"{ApiPath.voice_clone}/{voice_clone_id}") - return None - def upload( self, source: Optional[str] = None, @@ -657,12 +494,3 @@ def generate_client_token(self, expires_in: int = 86400) -> str: data={"expires_in": expires_in}, ) return response.get("token") - - def get_async_response(self, id: str) -> dict: - """Get the details of an async response. - - :param str id: ID of the async response - :return: Details of the async response - :rtype: dict - """ - return self.get(path=f"{ApiPath.async_response}/{id}", wait=False) diff --git a/videodb/collection.py b/videodb/collection.py index c8e1ea7..6751cf6 100644 --- a/videodb/collection.py +++ b/videodb/collection.py @@ -1,11 +1,8 @@ -import json import logging -import uuid from typing import Optional, Union, List, Dict, Any, Literal, Tuple from videodb._upload import ( upload, - upload_bytes, ) from videodb._constants import ( ApiPath, @@ -17,8 +14,6 @@ from videodb.video import Video from videodb.audio import Audio from videodb.image import Image -from videodb.job import GenerationJob -from videodb.voice_clone import VoiceClone from videodb.meeting import Meeting from videodb.capture_session import CaptureSession from videodb.rtstream import RTStream, RTStreamSearchResult, RTStreamShot @@ -34,8 +29,6 @@ logger = logging.getLogger(__name__) -MAX_GENERATE_TEXT_PAYLOAD_SIZE = 250 * 1024 - class Collection: """Collection class to interact with the Collection. @@ -148,54 +141,6 @@ def delete_audio(self, audio_id: str) -> None: path=f"{ApiPath.audio}/{audio_id}", params={"collection_id": self.id} ) - def create_voice_clone( - self, - ref_audio_id: str, - name: Optional[str] = None, - description: Optional[str] = None, - ref_text: Optional[str] = None, - language: Optional[str] = None, - ) -> VoiceClone: - """Create a reusable voice clone from an audio in this collection. - - :param str ref_audio_id: Source audio ID to use as the voice reference. - :param str name: Human-readable name (optional). - :param str description: Description (optional). - :param str ref_text: Text spoken in the reference audio (optional). - :param str language: Language code, e.g. ``"en"`` (optional). - :return: :class:`VoiceClone ` object. - :rtype: :class:`videodb.voice_clone.VoiceClone` - """ - return self._connection.create_voice_clone( - ref_audio_id=ref_audio_id, - name=name, - description=description, - ref_text=ref_text, - language=language, - collection_id=self.id, - ) - - def get_voice_clone(self, voice_clone_id: str) -> VoiceClone: - """Get a voice clone by ID.""" - return self._connection.get_voice_clone(voice_clone_id) - - def list_voice_clones( - self, - page: int = 1, - page_size: int = 20, - language: Optional[str] = None, - ) -> List[VoiceClone]: - """List user voice clones, optionally filtered by language.""" - return self._connection.list_voice_clones( - page=page, - page_size=page_size, - language=language, - ) - - def delete_voice_clone(self, voice_clone_id: str) -> None: - """Delete a voice clone by ID.""" - return self._connection.delete_voice_clone(voice_clone_id) - def get_images(self) -> List[Image]: """Get all the images in the collection. @@ -337,51 +282,25 @@ def generate_image( prompt: str, aspect_ratio: Optional[Literal["1:1", "9:16", "16:9", "4:3", "3:4"]] = "1:1", callback_url: Optional[str] = None, - model_name: Optional[str] = None, - config: Optional[dict] = None, - sandbox_id: Optional[str] = None, - wait: bool = False, - poll_interval: int = 5, - timeout: int = 600, - ) -> Union[Image, GenerationJob]: + ) -> Image: """Generate an image from a prompt. :param str prompt: Prompt for the image generation - :param str aspect_ratio: Aspect ratio of the image (optional, hosted models) + :param str aspect_ratio: Aspect ratio of the image (optional) :param str callback_url: URL to receive the callback (optional) - :param str model_name: Model name. Use ``"black-forest-labs/FLUX.1-dev"`` for FLUX self-inference. - :param dict config: Model configuration. Used by FLUX. - :param str sandbox_id: ID of the sandbox to route the self-inference job to (optional). - :param bool wait: If True, wait for self-inference jobs and return Image. - :param int poll_interval: Seconds between job polls when wait=True. - :param int timeout: Maximum seconds to wait when wait=True. - :return: :class:`Image ` or :class:`GenerationJob ` - :rtype: Union[:class:`videodb.image.Image`, :class:`videodb.job.GenerationJob`] + :return: :class:`Image ` object + :rtype: :class:`videodb.image.Image` """ - payload = { - "prompt": prompt, - "aspect_ratio": aspect_ratio, - "callback_url": callback_url, - } - if model_name: - payload["model_name"] = model_name - if config is not None: - payload["config"] = config - if sandbox_id: - payload["sandbox_id"] = sandbox_id - image_data = self._connection.post( path=f"{ApiPath.collection}/{self.id}/{ApiPath.generate}/{ApiPath.image}", - data=payload, + data={ + "prompt": prompt, + "aspect_ratio": aspect_ratio, + "callback_url": callback_url, + }, ) - if not image_data: - return None - if image_data.get("job_id"): - job = GenerationJob.from_data( - self._connection, image_data, result_type="image" - ) - return job.wait(timeout=timeout, interval=poll_interval) if wait else job - return Image(self._connection, **image_data) + if image_data: + return Image(self._connection, **image_data) def generate_music( self, prompt: str, duration: int = 5, callback_url: Optional[str] = None @@ -441,55 +360,28 @@ def generate_voice( voice_name: str = "Default", config: dict = {}, callback_url: Optional[str] = None, - model_name: str = "elevenlabs", - sandbox_id: Optional[str] = None, - voice_clone_id: Optional[str] = None, - clone_voice_id: Optional[str] = None, - wait: bool = False, - poll_interval: int = 5, - timeout: int = 600, - ) -> Union[Audio, GenerationJob]: + ) -> Audio: """Generate voice from text. :param str text: Text to convert to voice :param str voice_name: Name of the voice to use :param dict config: Configuration for the voice generation :param str callback_url: URL to receive the callback (optional) - :param str model_name: Model name. Use ``"k2-fsa/OmniVoice"`` for OmniVoice. - :param str sandbox_id: ID of the sandbox to route the self-inference job to (optional). - :param str voice_clone_id: ID of a reusable voice clone to use for OmniVoice (optional). - :param str clone_voice_id: Alias for ``voice_clone_id`` (optional). - :param bool wait: If True, wait for self-inference jobs and return Audio. - :param int poll_interval: Seconds between job polls when wait=True. - :param int timeout: Maximum seconds to wait when wait=True. - :return: :class:`Audio