video-to-text-python 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,27 @@
1
+ from video_to_text.async_client import AsyncVideoToTextClient
2
+ from video_to_text.client import VideoToTextClient
3
+ from video_to_text.errors import APIError, AuthError, RateLimitError, UploadError, ValidationError
4
+ from video_to_text.types import (
5
+ TranscriptChunk,
6
+ TranscriptTask,
7
+ TranscriptTaskCreateResponse,
8
+ TranscriptWord,
9
+ UploadCompleteResponse,
10
+ UploadCreateResponse,
11
+ )
12
+
13
+ __all__ = [
14
+ "APIError",
15
+ "AsyncVideoToTextClient",
16
+ "AuthError",
17
+ "RateLimitError",
18
+ "TranscriptChunk",
19
+ "TranscriptTask",
20
+ "TranscriptTaskCreateResponse",
21
+ "TranscriptWord",
22
+ "UploadCompleteResponse",
23
+ "UploadCreateResponse",
24
+ "UploadError",
25
+ "ValidationError",
26
+ "VideoToTextClient",
27
+ ]
@@ -0,0 +1,136 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import random
5
+ from typing import Any
6
+
7
+ import httpx
8
+
9
+ from video_to_text.errors import APIError, UploadError, create_api_error
10
+ from video_to_text.resources.tasks import AsyncTasksResource
11
+ from video_to_text.resources.transcriptions import AsyncTranscriptionsResource
12
+ from video_to_text.resources.uploads import AsyncUploadsResource
13
+
14
+
15
+ class AsyncVideoToTextClient:
16
+ def __init__(
17
+ self,
18
+ api_key: str,
19
+ *,
20
+ base_url: str = "https://api.videototext.dev",
21
+ timeout: float = 60,
22
+ max_retries: int = 2,
23
+ poll_interval: float = 1.5,
24
+ transport: httpx.AsyncBaseTransport | None = None,
25
+ ) -> None:
26
+ if not api_key:
27
+ raise ValueError("api_key is required")
28
+
29
+ self._api_key = api_key
30
+ self._base_url = base_url.rstrip("/")
31
+ self._timeout = timeout
32
+ self._max_retries = max_retries
33
+ self._poll_interval = poll_interval
34
+ self._client = httpx.AsyncClient(
35
+ base_url=self._base_url,
36
+ timeout=self._timeout,
37
+ transport=transport,
38
+ )
39
+
40
+ self.uploads = AsyncUploadsResource(self._request, self._put_upload_url)
41
+ self.tasks = AsyncTasksResource(self._request, self._poll_interval)
42
+ self.transcriptions = AsyncTranscriptionsResource(self.uploads, self.tasks)
43
+
44
+ async def aclose(self) -> None:
45
+ await self._client.aclose()
46
+
47
+ async def __aenter__(self) -> AsyncVideoToTextClient:
48
+ return self
49
+
50
+ async def __aexit__(self, *_: object) -> None:
51
+ await self.aclose()
52
+
53
+ async def _request(
54
+ self,
55
+ method: str,
56
+ path: str,
57
+ *,
58
+ json: dict[str, Any] | None = None,
59
+ headers: dict[str, str] | None = None,
60
+ idempotency_key: str | None = None,
61
+ skip_retry: bool = False,
62
+ unwrap_data: bool = True,
63
+ ) -> Any:
64
+ attempt = 0
65
+
66
+ while True:
67
+ try:
68
+ request_headers = {
69
+ "Authorization": f"Bearer {self._api_key}",
70
+ "Accept": "application/json",
71
+ **(headers or {}),
72
+ }
73
+ if idempotency_key:
74
+ request_headers["Idempotency-Key"] = idempotency_key
75
+
76
+ response = await self._client.request(
77
+ method,
78
+ path,
79
+ json=json,
80
+ headers=request_headers,
81
+ )
82
+ payload = self._parse_json_response(response)
83
+
84
+ if response.status_code >= 400:
85
+ raise create_api_error(response.status_code, payload)
86
+
87
+ if not isinstance(payload, dict) or "data" not in payload or "meta" not in payload:
88
+ raise create_api_error(response.status_code, payload)
89
+
90
+ if not unwrap_data:
91
+ return payload
92
+
93
+ return payload["data"]
94
+ except APIError as error:
95
+ if skip_retry:
96
+ raise
97
+
98
+ if not self._should_retry(error.status) or attempt >= self._max_retries:
99
+ raise
100
+
101
+ await self._sleep_with_backoff(attempt)
102
+ attempt += 1
103
+ except (httpx.ConnectError, httpx.ReadTimeout, httpx.WriteTimeout):
104
+ if skip_retry or attempt >= self._max_retries:
105
+ raise
106
+ await self._sleep_with_backoff(attempt)
107
+ attempt += 1
108
+
109
+ async def _put_upload_url(self, upload_url: str, content: bytes, mimetype: str) -> None:
110
+ response = await self._client.request(
111
+ "PUT",
112
+ upload_url,
113
+ content=content,
114
+ headers={"Content-Type": mimetype},
115
+ )
116
+ if response.status_code >= 400:
117
+ raise UploadError(response.status_code, response.text)
118
+
119
+ @staticmethod
120
+ def _parse_json_response(response: httpx.Response) -> Any:
121
+ if not response.content:
122
+ return {}
123
+ try:
124
+ return response.json()
125
+ except ValueError:
126
+ return {}
127
+
128
+ @staticmethod
129
+ def _should_retry(status: int) -> bool:
130
+ return status == 429 or status >= 500
131
+
132
+ @staticmethod
133
+ async def _sleep_with_backoff(attempt: int) -> None:
134
+ base = min(3.0, 0.3 * (2**attempt))
135
+ jitter = random.random() * 0.1
136
+ await asyncio.sleep(base + jitter)
@@ -0,0 +1,137 @@
1
+ from __future__ import annotations
2
+
3
+ import random
4
+ import time
5
+ from typing import Any
6
+
7
+ import httpx
8
+
9
+ from video_to_text.errors import APIError, UploadError, create_api_error
10
+ from video_to_text.resources.tasks import TasksResource
11
+ from video_to_text.resources.transcriptions import TranscriptionsResource
12
+ from video_to_text.resources.uploads import UploadsResource
13
+
14
+
15
+ class VideoToTextClient:
16
+ def __init__(
17
+ self,
18
+ api_key: str,
19
+ *,
20
+ base_url: str = "https://api.videototext.dev",
21
+ timeout: float = 60,
22
+ max_retries: int = 2,
23
+ poll_interval: float = 1.5,
24
+ transport: httpx.BaseTransport | None = None,
25
+ ) -> None:
26
+ if not api_key:
27
+ raise ValueError("api_key is required")
28
+
29
+ self._api_key = api_key
30
+ self._base_url = base_url.rstrip("/")
31
+ self._timeout = timeout
32
+ self._max_retries = max_retries
33
+ self._poll_interval = poll_interval
34
+ self._client = httpx.Client(
35
+ base_url=self._base_url,
36
+ timeout=self._timeout,
37
+ transport=transport,
38
+ )
39
+
40
+ self.uploads = UploadsResource(self._request, self._put_upload_url)
41
+ self.tasks = TasksResource(self._request, self._poll_interval)
42
+ self.transcriptions = TranscriptionsResource(self.uploads, self.tasks)
43
+
44
+ def close(self) -> None:
45
+ self._client.close()
46
+
47
+ def __enter__(self) -> VideoToTextClient:
48
+ return self
49
+
50
+ def __exit__(self, *_: object) -> None:
51
+ self.close()
52
+
53
+ def _request(
54
+ self,
55
+ method: str,
56
+ path: str,
57
+ *,
58
+ json: dict[str, Any] | None = None,
59
+ headers: dict[str, str] | None = None,
60
+ idempotency_key: str | None = None,
61
+ skip_retry: bool = False,
62
+ unwrap_data: bool = True,
63
+ ) -> Any:
64
+ attempt = 0
65
+
66
+ while True:
67
+ try:
68
+ request_headers = {
69
+ "Authorization": f"Bearer {self._api_key}",
70
+ "Accept": "application/json",
71
+ **(headers or {}),
72
+ }
73
+ if idempotency_key:
74
+ request_headers["Idempotency-Key"] = idempotency_key
75
+
76
+ response = self._client.request(
77
+ method,
78
+ path,
79
+ json=json,
80
+ headers=request_headers,
81
+ )
82
+ payload = self._parse_json_response(response)
83
+
84
+ if response.status_code >= 400:
85
+ raise create_api_error(response.status_code, payload)
86
+
87
+ if not isinstance(payload, dict) or "data" not in payload or "meta" not in payload:
88
+ raise create_api_error(response.status_code, payload)
89
+
90
+ if not unwrap_data:
91
+ return payload
92
+
93
+ return payload["data"]
94
+ except APIError as error:
95
+ if skip_retry:
96
+ raise
97
+
98
+ if not self._should_retry(error.status) or attempt >= self._max_retries:
99
+ raise
100
+
101
+ self._sleep_with_backoff(attempt)
102
+ attempt += 1
103
+ except (httpx.ConnectError, httpx.ReadTimeout, httpx.WriteTimeout):
104
+ if skip_retry or attempt >= self._max_retries:
105
+ raise
106
+ self._sleep_with_backoff(attempt)
107
+ attempt += 1
108
+
109
+ def _put_upload_url(self, upload_url: str, content: bytes, mimetype: str) -> None:
110
+ response = self._client.request(
111
+ "PUT",
112
+ upload_url,
113
+ content=content,
114
+ headers={"Content-Type": mimetype},
115
+ )
116
+ if response.status_code >= 400:
117
+ raise UploadError(response.status_code, response.text)
118
+
119
+ @staticmethod
120
+ def _parse_json_response(response: httpx.Response) -> Any:
121
+ if not response.content:
122
+ return {}
123
+ try:
124
+ return response.json()
125
+ except ValueError:
126
+ return {}
127
+
128
+ @staticmethod
129
+ def _should_retry(status: int) -> bool:
130
+ return status == 429 or status >= 500
131
+
132
+ @staticmethod
133
+ def _sleep_with_backoff(attempt: int) -> None:
134
+ # Use exponential backoff with light jitter so clients do not retry in lockstep.
135
+ base = min(3.0, 0.3 * (2**attempt))
136
+ jitter = random.random() * 0.1
137
+ time.sleep(base + jitter)
@@ -0,0 +1,76 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any
5
+
6
+
7
+ @dataclass
8
+ class Problem:
9
+ type: str
10
+ title: str
11
+ status: int
12
+ detail: str
13
+ error_code: str | None = None
14
+
15
+
16
+ class APIError(Exception):
17
+ def __init__(self, problem: Problem):
18
+ super().__init__(problem.detail)
19
+ self.problem = problem
20
+ self.status = problem.status
21
+ self.type = problem.type
22
+ self.error_code = problem.error_code
23
+
24
+
25
+ class AuthError(APIError):
26
+ pass
27
+
28
+
29
+ class ValidationError(APIError):
30
+ pass
31
+
32
+
33
+ class RateLimitError(APIError):
34
+ pass
35
+
36
+
37
+ class UploadError(Exception):
38
+ def __init__(self, status: int, detail: str):
39
+ super().__init__(detail or f"Upload failed with status {status}")
40
+ self.status = status
41
+ self.detail = detail
42
+
43
+
44
+ def parse_problem(status: int, payload: Any) -> Problem:
45
+ if isinstance(payload, dict):
46
+ maybe_type = payload.get("type")
47
+ maybe_title = payload.get("title")
48
+ if isinstance(maybe_type, str) and isinstance(maybe_title, str):
49
+ return Problem(
50
+ type=maybe_type,
51
+ title=maybe_title,
52
+ status=int(payload.get("status") or status),
53
+ detail=str(payload.get("detail") or maybe_title),
54
+ error_code=payload.get("errorCode"),
55
+ )
56
+
57
+ return Problem(
58
+ type="INTERNAL_ERROR",
59
+ title="Internal server error",
60
+ status=status,
61
+ detail="Unexpected API response",
62
+ error_code="SDK_UNEXPECTED_RESPONSE",
63
+ )
64
+
65
+
66
+ def create_api_error(status: int, payload: Any) -> APIError:
67
+ problem = parse_problem(status, payload)
68
+
69
+ if status in (401, 403):
70
+ return AuthError(problem)
71
+ if status in (400, 422):
72
+ return ValidationError(problem)
73
+ if status == 429:
74
+ return RateLimitError(problem)
75
+
76
+ return APIError(problem)
@@ -0,0 +1,15 @@
1
+ from video_to_text.resources.tasks import AsyncTasksResource, TasksResource
2
+ from video_to_text.resources.transcriptions import (
3
+ AsyncTranscriptionsResource,
4
+ TranscriptionsResource,
5
+ )
6
+ from video_to_text.resources.uploads import AsyncUploadsResource, UploadsResource
7
+
8
+ __all__ = [
9
+ "AsyncTasksResource",
10
+ "AsyncTranscriptionsResource",
11
+ "AsyncUploadsResource",
12
+ "TasksResource",
13
+ "TranscriptionsResource",
14
+ "UploadsResource",
15
+ ]
@@ -0,0 +1,125 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import time
5
+ from collections.abc import Callable
6
+ from typing import Any
7
+
8
+ from video_to_text.errors import APIError, Problem
9
+ from video_to_text.types import TranscriptTask, TranscriptTaskCreateResponse
10
+
11
+ TERMINAL_STATUSES = {"SUCCEEDED", "FAILED", "CANCELED"}
12
+
13
+
14
+ class TasksResource:
15
+ def __init__(self, request: Callable[..., Any], poll_interval: float) -> None:
16
+ self._request = request
17
+ self._poll_interval = poll_interval
18
+
19
+ def create(
20
+ self,
21
+ payload: dict[str, Any],
22
+ *,
23
+ idempotency_key: str | None = None,
24
+ ) -> TranscriptTaskCreateResponse:
25
+ key = idempotency_key.strip() if idempotency_key else None
26
+ data = self._request(
27
+ "POST",
28
+ "/v1/tasks",
29
+ json=payload,
30
+ idempotency_key=key,
31
+ skip_retry=key is None,
32
+ )
33
+ return TranscriptTaskCreateResponse.model_validate(data["task"])
34
+
35
+ def get(self, transcript_id: str) -> TranscriptTask:
36
+ data = self._request("GET", f"/v1/tasks/{transcript_id}")
37
+ return TranscriptTask.model_validate(data["task"])
38
+
39
+ def wait(
40
+ self,
41
+ transcript_id: str,
42
+ *,
43
+ timeout: float = 600,
44
+ poll_interval: float | None = None,
45
+ ) -> TranscriptTask:
46
+ interval = poll_interval if poll_interval is not None else self._poll_interval
47
+ started = time.time()
48
+
49
+ # Polling only observes server task state; failed terminal states become SDK errors.
50
+ while True:
51
+ if time.time() - started > timeout:
52
+ raise TimeoutError(f"wait timeout after {timeout} seconds")
53
+
54
+ task = self.get(transcript_id)
55
+ if task.status in TERMINAL_STATUSES:
56
+ if task.status in {"FAILED", "CANCELED"}:
57
+ raise APIError(
58
+ Problem(
59
+ type="TASK_TERMINAL_FAILURE",
60
+ title="Task failed",
61
+ status=422,
62
+ detail=task.show_error or f"Task ended with status {task.status}",
63
+ error_code="TASK_TERMINAL_FAILURE",
64
+ )
65
+ )
66
+ return task
67
+
68
+ time.sleep(interval)
69
+
70
+
71
+ class AsyncTasksResource:
72
+ def __init__(self, request: Callable[..., Any], poll_interval: float) -> None:
73
+ self._request = request
74
+ self._poll_interval = poll_interval
75
+
76
+ async def create(
77
+ self,
78
+ payload: dict[str, Any],
79
+ *,
80
+ idempotency_key: str | None = None,
81
+ ) -> TranscriptTaskCreateResponse:
82
+ key = idempotency_key.strip() if idempotency_key else None
83
+ data = await self._request(
84
+ "POST",
85
+ "/v1/tasks",
86
+ json=payload,
87
+ idempotency_key=key,
88
+ skip_retry=key is None,
89
+ )
90
+ return TranscriptTaskCreateResponse.model_validate(data["task"])
91
+
92
+ async def get(self, transcript_id: str) -> TranscriptTask:
93
+ data = await self._request("GET", f"/v1/tasks/{transcript_id}")
94
+ return TranscriptTask.model_validate(data["task"])
95
+
96
+ async def wait(
97
+ self,
98
+ transcript_id: str,
99
+ *,
100
+ timeout: float = 600,
101
+ poll_interval: float | None = None,
102
+ ) -> TranscriptTask:
103
+ interval = poll_interval if poll_interval is not None else self._poll_interval
104
+ loop = asyncio.get_running_loop()
105
+ started = loop.time()
106
+
107
+ while True:
108
+ if loop.time() - started > timeout:
109
+ raise TimeoutError(f"wait timeout after {timeout} seconds")
110
+
111
+ task = await self.get(transcript_id)
112
+ if task.status in TERMINAL_STATUSES:
113
+ if task.status in {"FAILED", "CANCELED"}:
114
+ raise APIError(
115
+ Problem(
116
+ type="TASK_TERMINAL_FAILURE",
117
+ title="Task failed",
118
+ status=422,
119
+ detail=task.show_error or f"Task ended with status {task.status}",
120
+ error_code="TASK_TERMINAL_FAILURE",
121
+ )
122
+ )
123
+ return task
124
+
125
+ await asyncio.sleep(interval)
@@ -0,0 +1,153 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+ from uuid import uuid4
5
+
6
+ from video_to_text.resources.tasks import AsyncTasksResource, TasksResource
7
+ from video_to_text.resources.uploads import AsyncUploadsResource, UploadInput, UploadsResource
8
+ from video_to_text.types import TranscriptTask, TranscriptTaskCreateResponse
9
+
10
+
11
+ def _build_task_payload(
12
+ asset_id: str,
13
+ *,
14
+ language: str | None,
15
+ timestamp_mode: str | None,
16
+ transcription_mode: str | None,
17
+ ) -> dict[str, Any]:
18
+ payload: dict[str, Any] = {"assetId": asset_id}
19
+ if language is not None:
20
+ payload["language"] = language
21
+ if timestamp_mode is not None:
22
+ payload["timestampMode"] = timestamp_mode
23
+ if transcription_mode is not None:
24
+ payload["transcriptionMode"] = transcription_mode
25
+ return payload
26
+
27
+
28
+ class TranscriptionsResource:
29
+ def __init__(self, uploads: UploadsResource, tasks: TasksResource) -> None:
30
+ self._uploads = uploads
31
+ self._tasks = tasks
32
+
33
+ def create_from_file(
34
+ self,
35
+ content: UploadInput,
36
+ *,
37
+ filename: str | None = None,
38
+ mimetype: str | None = None,
39
+ file_size: int | None = None,
40
+ language: str | None = None,
41
+ timestamp_mode: str | None = None,
42
+ transcription_mode: str | None = None,
43
+ idempotency_key: str | None = None,
44
+ ) -> TranscriptTaskCreateResponse:
45
+ asset = self._uploads.upload(
46
+ content,
47
+ filename=filename,
48
+ mimetype=mimetype,
49
+ file_size=file_size,
50
+ )
51
+ return self._tasks.create(
52
+ _build_task_payload(
53
+ asset.asset_id,
54
+ language=language,
55
+ timestamp_mode=timestamp_mode,
56
+ transcription_mode=transcription_mode,
57
+ ),
58
+ idempotency_key=idempotency_key or str(uuid4()),
59
+ )
60
+
61
+ def transcribe_file(
62
+ self,
63
+ content: UploadInput,
64
+ *,
65
+ filename: str | None = None,
66
+ mimetype: str | None = None,
67
+ file_size: int | None = None,
68
+ language: str | None = None,
69
+ timestamp_mode: str | None = None,
70
+ transcription_mode: str | None = None,
71
+ idempotency_key: str | None = None,
72
+ timeout: float = 600,
73
+ poll_interval: float | None = None,
74
+ ) -> TranscriptTask:
75
+ created = self.create_from_file(
76
+ content,
77
+ filename=filename,
78
+ mimetype=mimetype,
79
+ file_size=file_size,
80
+ language=language,
81
+ timestamp_mode=timestamp_mode,
82
+ transcription_mode=transcription_mode,
83
+ idempotency_key=idempotency_key,
84
+ )
85
+ return self._tasks.wait(
86
+ created.transcript_id,
87
+ timeout=timeout,
88
+ poll_interval=poll_interval,
89
+ )
90
+
91
+
92
+ class AsyncTranscriptionsResource:
93
+ def __init__(self, uploads: AsyncUploadsResource, tasks: AsyncTasksResource) -> None:
94
+ self._uploads = uploads
95
+ self._tasks = tasks
96
+
97
+ async def create_from_file(
98
+ self,
99
+ content: UploadInput,
100
+ *,
101
+ filename: str | None = None,
102
+ mimetype: str | None = None,
103
+ file_size: int | None = None,
104
+ language: str | None = None,
105
+ timestamp_mode: str | None = None,
106
+ transcription_mode: str | None = None,
107
+ idempotency_key: str | None = None,
108
+ ) -> TranscriptTaskCreateResponse:
109
+ asset = await self._uploads.upload(
110
+ content,
111
+ filename=filename,
112
+ mimetype=mimetype,
113
+ file_size=file_size,
114
+ )
115
+ return await self._tasks.create(
116
+ _build_task_payload(
117
+ asset.asset_id,
118
+ language=language,
119
+ timestamp_mode=timestamp_mode,
120
+ transcription_mode=transcription_mode,
121
+ ),
122
+ idempotency_key=idempotency_key or str(uuid4()),
123
+ )
124
+
125
+ async def transcribe_file(
126
+ self,
127
+ content: UploadInput,
128
+ *,
129
+ filename: str | None = None,
130
+ mimetype: str | None = None,
131
+ file_size: int | None = None,
132
+ language: str | None = None,
133
+ timestamp_mode: str | None = None,
134
+ transcription_mode: str | None = None,
135
+ idempotency_key: str | None = None,
136
+ timeout: float = 600,
137
+ poll_interval: float | None = None,
138
+ ) -> TranscriptTask:
139
+ created = await self.create_from_file(
140
+ content,
141
+ filename=filename,
142
+ mimetype=mimetype,
143
+ file_size=file_size,
144
+ language=language,
145
+ timestamp_mode=timestamp_mode,
146
+ transcription_mode=transcription_mode,
147
+ idempotency_key=idempotency_key,
148
+ )
149
+ return await self._tasks.wait(
150
+ created.transcript_id,
151
+ timeout=timeout,
152
+ poll_interval=poll_interval,
153
+ )
@@ -0,0 +1,195 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import mimetypes
5
+ import os
6
+ from collections.abc import Callable
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ from video_to_text.types import UploadCompleteResponse, UploadCreateResponse
11
+
12
+ BytesLike = bytes | bytearray | memoryview
13
+ UploadInput = BytesLike | str | os.PathLike[str]
14
+
15
+
16
+ def _normalize_upload_input(
17
+ content: UploadInput,
18
+ *,
19
+ filename: str | None,
20
+ mimetype: str | None,
21
+ file_size: int | None,
22
+ ) -> tuple[bytes, str, str, int]:
23
+ resolved_filename: str | None
24
+ resolved_mimetype: str | None
25
+ if isinstance(content, str | os.PathLike):
26
+ path = Path(content)
27
+ body = path.read_bytes()
28
+ resolved_filename = filename or path.name
29
+ resolved_mimetype = mimetype or mimetypes.guess_type(str(path))[0]
30
+ resolved_size = file_size if file_size is not None else path.stat().st_size
31
+ else:
32
+ body = content.tobytes() if isinstance(content, memoryview) else bytes(content)
33
+ resolved_filename = filename
34
+ resolved_mimetype = mimetype
35
+ resolved_size = file_size if file_size is not None else len(body)
36
+
37
+ if not resolved_filename:
38
+ raise ValueError("filename is required for bytes-like upload content")
39
+ if not resolved_mimetype:
40
+ raise ValueError("mimetype is required for bytes-like upload content")
41
+ if resolved_size < 0:
42
+ raise ValueError("file_size must be non-negative")
43
+
44
+ return body, resolved_filename, resolved_mimetype, int(resolved_size)
45
+
46
+
47
+ async def _normalize_upload_input_async(
48
+ content: UploadInput,
49
+ *,
50
+ filename: str | None,
51
+ mimetype: str | None,
52
+ file_size: int | None,
53
+ ) -> tuple[bytes, str, str, int]:
54
+ if isinstance(content, str | os.PathLike):
55
+ path = Path(content)
56
+ body = await asyncio.to_thread(path.read_bytes)
57
+ resolved_filename = filename or path.name
58
+ resolved_mimetype = mimetype or mimetypes.guess_type(str(path))[0]
59
+ resolved_size = file_size if file_size is not None else path.stat().st_size
60
+ if not resolved_filename:
61
+ raise ValueError("filename is required for bytes-like upload content")
62
+ if not resolved_mimetype:
63
+ raise ValueError("mimetype is required for bytes-like upload content")
64
+ if resolved_size < 0:
65
+ raise ValueError("file_size must be non-negative")
66
+ return body, resolved_filename, resolved_mimetype, int(resolved_size)
67
+
68
+ return _normalize_upload_input(
69
+ content,
70
+ filename=filename,
71
+ mimetype=mimetype,
72
+ file_size=file_size,
73
+ )
74
+
75
+
76
+ class UploadsResource:
77
+ def __init__(self, request: Callable[..., Any], put_upload_url: Callable[..., Any]) -> None:
78
+ self._request = request
79
+ self._put_upload_url = put_upload_url
80
+
81
+ def create(self, payload: dict[str, Any]) -> UploadCreateResponse:
82
+ data = self._request(
83
+ "POST",
84
+ "/v1/uploads",
85
+ json=payload,
86
+ skip_retry=True,
87
+ )
88
+ return UploadCreateResponse.model_validate(data)
89
+
90
+ def put(self, upload_url: str, content: BytesLike, *, mimetype: str) -> None:
91
+ body = content.tobytes() if isinstance(content, memoryview) else bytes(content)
92
+ self._put_upload_url(upload_url, body, mimetype)
93
+
94
+ def complete(self, upload_id: str, payload: dict[str, Any]) -> UploadCompleteResponse:
95
+ data = self._request(
96
+ "POST",
97
+ f"/v1/uploads/{upload_id}/operations/complete",
98
+ json=payload,
99
+ skip_retry=True,
100
+ )
101
+ return UploadCompleteResponse.model_validate(data)
102
+
103
+ def upload(
104
+ self,
105
+ content: UploadInput,
106
+ *,
107
+ filename: str | None = None,
108
+ mimetype: str | None = None,
109
+ file_size: int | None = None,
110
+ ) -> UploadCompleteResponse:
111
+ body, resolved_filename, resolved_mimetype, resolved_size = _normalize_upload_input(
112
+ content,
113
+ filename=filename,
114
+ mimetype=mimetype,
115
+ file_size=file_size,
116
+ )
117
+ upload = self.create(
118
+ {
119
+ "filename": resolved_filename,
120
+ "mimetype": resolved_mimetype,
121
+ }
122
+ )
123
+ self.put(upload.upload_url, body, mimetype=resolved_mimetype)
124
+ return self.complete(
125
+ upload.upload_id,
126
+ {
127
+ "fileKey": upload.file_key,
128
+ "fileUrl": upload.file_url,
129
+ "filename": resolved_filename,
130
+ "mimetype": resolved_mimetype,
131
+ "fileSize": resolved_size,
132
+ },
133
+ )
134
+
135
+
136
+ class AsyncUploadsResource:
137
+ def __init__(self, request: Callable[..., Any], put_upload_url: Callable[..., Any]) -> None:
138
+ self._request = request
139
+ self._put_upload_url = put_upload_url
140
+
141
+ async def create(self, payload: dict[str, Any]) -> UploadCreateResponse:
142
+ data = await self._request(
143
+ "POST",
144
+ "/v1/uploads",
145
+ json=payload,
146
+ skip_retry=True,
147
+ )
148
+ return UploadCreateResponse.model_validate(data)
149
+
150
+ async def put(self, upload_url: str, content: BytesLike, *, mimetype: str) -> None:
151
+ body = content.tobytes() if isinstance(content, memoryview) else bytes(content)
152
+ await self._put_upload_url(upload_url, body, mimetype)
153
+
154
+ async def complete(self, upload_id: str, payload: dict[str, Any]) -> UploadCompleteResponse:
155
+ data = await self._request(
156
+ "POST",
157
+ f"/v1/uploads/{upload_id}/operations/complete",
158
+ json=payload,
159
+ skip_retry=True,
160
+ )
161
+ return UploadCompleteResponse.model_validate(data)
162
+
163
+ async def upload(
164
+ self,
165
+ content: UploadInput,
166
+ *,
167
+ filename: str | None = None,
168
+ mimetype: str | None = None,
169
+ file_size: int | None = None,
170
+ ) -> UploadCompleteResponse:
171
+ body, resolved_filename, resolved_mimetype, resolved_size = (
172
+ await _normalize_upload_input_async(
173
+ content,
174
+ filename=filename,
175
+ mimetype=mimetype,
176
+ file_size=file_size,
177
+ )
178
+ )
179
+ upload = await self.create(
180
+ {
181
+ "filename": resolved_filename,
182
+ "mimetype": resolved_mimetype,
183
+ }
184
+ )
185
+ await self.put(upload.upload_url, body, mimetype=resolved_mimetype)
186
+ return await self.complete(
187
+ upload.upload_id,
188
+ {
189
+ "fileKey": upload.file_key,
190
+ "fileUrl": upload.file_url,
191
+ "filename": resolved_filename,
192
+ "mimetype": resolved_mimetype,
193
+ "fileSize": resolved_size,
194
+ },
195
+ )
video_to_text/types.py ADDED
@@ -0,0 +1,68 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Literal
4
+
5
+ from pydantic import BaseModel, ConfigDict, Field
6
+
7
+ TaskStatus = Literal["QUEUED", "PROCESSING", "SUCCEEDED", "FAILED", "CANCELED"]
8
+ TimestampMode = Literal["CHUNK", "WORD"]
9
+ TranscriptionMode = Literal["balanced", "precision"]
10
+
11
+
12
+ class ApiModel(BaseModel):
13
+ model_config = ConfigDict(populate_by_name=True)
14
+
15
+
16
+ class UploadCreateResponse(ApiModel):
17
+ upload_url: str = Field(alias="uploadUrl")
18
+ file_key: str = Field(alias="fileKey")
19
+ file_url: str = Field(alias="fileUrl")
20
+ upload_id: str = Field(alias="uploadId")
21
+ expires_at: str = Field(alias="expiresAt")
22
+
23
+
24
+ class UploadCompleteResponse(ApiModel):
25
+ asset_id: str = Field(alias="assetId")
26
+
27
+
28
+ class TranscriptChunk(ApiModel):
29
+ seq: int
30
+ start_ms: int = Field(alias="startMs")
31
+ end_ms: int = Field(alias="endMs")
32
+ text: str
33
+ speaker_key: str | None = Field(default=None, alias="speakerKey")
34
+ speaker_name: str | None = Field(default=None, alias="speakerName")
35
+ word_start_index: int | None = Field(default=None, alias="wordStartIndex")
36
+ word_end_index: int | None = Field(default=None, alias="wordEndIndex")
37
+
38
+
39
+ class TranscriptWord(ApiModel):
40
+ text: str
41
+ start_ms: int = Field(alias="startMs")
42
+ end_ms: int = Field(alias="endMs")
43
+ speaker_key: str | None = Field(default=None, alias="speakerKey")
44
+
45
+
46
+ class TranscriptTaskCreateResponse(ApiModel):
47
+ transcript_id: str = Field(alias="transcriptId")
48
+ status: TaskStatus
49
+ language: str
50
+ timestamp_mode: TimestampMode = Field(alias="timestampMode")
51
+ transcription_mode: TranscriptionMode = Field(alias="transcriptionMode")
52
+ billed_credits: str = Field(alias="billedCredits")
53
+
54
+
55
+ class TranscriptTask(ApiModel):
56
+ transcript_id: str = Field(alias="transcriptId")
57
+ status: TaskStatus
58
+ show_error: str | None = Field(alias="showError")
59
+ full_text: str = Field(alias="fullText")
60
+ chunks: list[TranscriptChunk]
61
+ words: list[TranscriptWord]
62
+ source_duration_ms: int = Field(alias="sourceDurationMs")
63
+ language: str
64
+ result_language: str | None = Field(alias="resultLanguage")
65
+ timestamp_mode: TimestampMode = Field(alias="timestampMode")
66
+ transcription_mode: TranscriptionMode = Field(alias="transcriptionMode")
67
+ billed_credits: str = Field(alias="billedCredits")
68
+ created_at: str = Field(alias="createdAt")
@@ -0,0 +1,46 @@
1
+ Metadata-Version: 2.4
2
+ Name: video-to-text-python
3
+ Version: 1.0.0
4
+ Summary: Official Python client for the Video To Text API
5
+ Author: Video To Text
6
+ License: MIT
7
+ Keywords: AI transcription,api,audio to text,audio transcription,speech to text,transcription,video to text,video transcription
8
+ Requires-Python: >=3.10
9
+ Requires-Dist: httpx<1.0.0,>=0.27.0
10
+ Requires-Dist: pydantic<3.0.0,>=2.9.0
11
+ Provides-Extra: dev
12
+ Requires-Dist: mypy<2.0.0,>=1.13.0; extra == 'dev'
13
+ Requires-Dist: pytest<9.0.0,>=8.3.0; extra == 'dev'
14
+ Requires-Dist: ruff<0.10.0,>=0.8.0; extra == 'dev'
15
+ Description-Content-Type: text/markdown
16
+
17
+ # video-to-text-python
18
+
19
+ Official Python client for the [Video To Text](https://videototext.dev/) API.
20
+
21
+ - API docs: [Video To Text API](https://docs.videototext.dev)
22
+ - Supports Python 3.10+
23
+
24
+ ## Installation
25
+
26
+ ```bash
27
+ pip install video-to-text-python
28
+ ```
29
+
30
+ ## Quick Start
31
+
32
+ ```python
33
+ import os
34
+ from pathlib import Path
35
+
36
+ from video_to_text import VideoToTextClient
37
+
38
+ client = VideoToTextClient(api_key=os.environ["VTT_API_KEY"])
39
+
40
+ task = client.transcriptions.transcribe_file(
41
+ Path("meeting.mp4"),
42
+ transcription_mode="balanced",
43
+ )
44
+
45
+ print(task.status, task.full_text, task.billed_credits)
46
+ ```
@@ -0,0 +1,12 @@
1
+ video_to_text/__init__.py,sha256=aZXMnkhjSUfdt2P3vuY1QX8j6TkyTCPYn-yQLimawaw,725
2
+ video_to_text/async_client.py,sha256=wsn-ZkObMGFocwUwOSukbru9n0yTCq1-voqlvDd5uUo,4435
3
+ video_to_text/client.py,sha256=6CrLnCSdwootbSvettEpH7H_rqsq01FFfjFJ8c2s14I,4385
4
+ video_to_text/errors.py,sha256=gG-Vl8F1A71kFs1wi-UCAkCEC__HEs8v30nKbe0NZYQ,1893
5
+ video_to_text/types.py,sha256=igSI0hAmOFiLdu0JC2LxHiJvD1_1yGQy-8_OGVKp3oM,2363
6
+ video_to_text/resources/__init__.py,sha256=vbN4o2PcfpLgXik_erwv_5BAZ2xBCmRiRZZm-3l2oRw,452
7
+ video_to_text/resources/tasks.py,sha256=FUizcfZdf0HDhT25sGXYinve-IACOgrQOQy4abaggbA,4310
8
+ video_to_text/resources/transcriptions.py,sha256=2Z7Id9VnLY0PvEX804HogNOFUx3n0s4MoAv4HaZE_G8,4893
9
+ video_to_text/resources/uploads.py,sha256=hIfWK3ZEx3W2RuyArif7-j6tPM-9DrY_HyDpQn33d-8,6755
10
+ video_to_text_python-1.0.0.dist-info/METADATA,sha256=ZkPCGtaId_rRPU4Yo81oQQJEQHMhytDIpMq9xlT8qng,1190
11
+ video_to_text_python-1.0.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
12
+ video_to_text_python-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any