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.
- video_to_text/__init__.py +27 -0
- video_to_text/async_client.py +136 -0
- video_to_text/client.py +137 -0
- video_to_text/errors.py +76 -0
- video_to_text/resources/__init__.py +15 -0
- video_to_text/resources/tasks.py +125 -0
- video_to_text/resources/transcriptions.py +153 -0
- video_to_text/resources/uploads.py +195 -0
- video_to_text/types.py +68 -0
- video_to_text_python-1.0.0.dist-info/METADATA +46 -0
- video_to_text_python-1.0.0.dist-info/RECORD +12 -0
- video_to_text_python-1.0.0.dist-info/WHEEL +4 -0
|
@@ -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)
|
video_to_text/client.py
ADDED
|
@@ -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)
|
video_to_text/errors.py
ADDED
|
@@ -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,,
|