synapsai-python 0.1.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,257 @@
1
+ # Copyright 2026 SynapsAI Technologies Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """
16
+ Question Answering resource handlers
17
+ """
18
+
19
+ from typing import TYPE_CHECKING, Optional, Union, List, Dict
20
+
21
+ from ..types.question_answering import (
22
+ DocumentQuestionAnsweringRequest,
23
+ DocumentQuestionAnsweringResponse,
24
+ QuestionAnsweringRequest,
25
+ QuestionAnsweringResponse,
26
+ TableQuestionAnsweringRequest,
27
+ TableQuestionAnsweringResponse,
28
+ VisualQuestionAnsweringRequest,
29
+ VisualQuestionAnsweringResponse,
30
+ )
31
+
32
+ if TYPE_CHECKING:
33
+ from ..client import SynapsAI, AsyncSynapsAI
34
+
35
+
36
+ class QuestionAnsweringResource:
37
+ """Question Answering resource handler"""
38
+
39
+ def __init__(self, client: "SynapsAI"):
40
+ self._client = client
41
+
42
+ def document(
43
+ self,
44
+ model: str,
45
+ image,
46
+ question: str,
47
+ word_boxes: Optional[List] = None,
48
+ top_k: Optional[int] = None,
49
+ doc_stride: Optional[int] = None,
50
+ max_answer_len: Optional[int] = None,
51
+ max_seq_len: Optional[int] = None,
52
+ max_question_len: Optional[int] = None,
53
+ handle_impossible_answer: Optional[bool] = None,
54
+ lang: Optional[str] = None,
55
+ tesseract_config: Optional[str] = None,
56
+ timeout: Optional[float] = None,
57
+ ) -> DocumentQuestionAnsweringResponse:
58
+ request_data = self._client._build_request(
59
+ model=model,
60
+ image=image,
61
+ question=question,
62
+ word_boxes=word_boxes,
63
+ top_k=top_k,
64
+ doc_stride=doc_stride,
65
+ max_answer_len=max_answer_len,
66
+ max_seq_len=max_seq_len,
67
+ max_question_len=max_question_len,
68
+ handle_impossible_answer=handle_impossible_answer,
69
+ lang=lang,
70
+ tesseract_config=tesseract_config,
71
+ timeout=timeout,
72
+ )
73
+ endpoint = "question-answering/document"
74
+ response = self._client._post(endpoint, json_data=request_data)
75
+ return DocumentQuestionAnsweringResponse.model_validate(response.json())
76
+
77
+ def text(
78
+ self,
79
+ model: str,
80
+ question: Union[str, List[str]],
81
+ context: Union[str, List[str]],
82
+ top_k: Optional[int] = None,
83
+ doc_stride: Optional[int] = None,
84
+ max_answer_len: Optional[int] = None,
85
+ max_seq_len: Optional[int] = None,
86
+ max_question_len: Optional[int] = None,
87
+ handle_impossible_answer: Optional[bool] = None,
88
+ align_to_words: Optional[bool] = None,
89
+ ) -> QuestionAnsweringResponse:
90
+ request_data = self._client._build_request(
91
+ model=model,
92
+ question=question,
93
+ context=context,
94
+ top_k=top_k,
95
+ doc_stride=doc_stride,
96
+ max_answer_len=max_answer_len,
97
+ max_seq_len=max_seq_len,
98
+ max_question_len=max_question_len,
99
+ handle_impossible_answer=handle_impossible_answer,
100
+ align_to_words=align_to_words,
101
+ )
102
+ endpoint = "question-answering"
103
+ response = self._client._post(endpoint, json_data=request_data)
104
+ return QuestionAnsweringResponse.model_validate(response.json())
105
+
106
+ def table(
107
+ self,
108
+ model: str,
109
+ table: Dict,
110
+ query: Union[str, List[str]],
111
+ sequential: Optional[bool] = None,
112
+ padding: Optional[Union[bool, str]] = None,
113
+ truncation: Optional[Union[bool, str]] = None,
114
+ ) -> TableQuestionAnsweringResponse:
115
+ request_data = self._client._build_request(
116
+ model=model,
117
+ table=table,
118
+ query=query,
119
+ sequential=sequential,
120
+ padding=padding,
121
+ truncation=truncation,
122
+ )
123
+ endpoint = "question-answering/table"
124
+ response = self._client._post(endpoint, json_data=request_data)
125
+ return TableQuestionAnsweringResponse.model_validate(response.json())
126
+
127
+ def visual(
128
+ self,
129
+ model: str,
130
+ image,
131
+ question: Union[str, List[str]],
132
+ top_k: Optional[int] = None,
133
+ timeout: Optional[float] = None,
134
+ ) -> VisualQuestionAnsweringResponse:
135
+ request_data = self._client._build_request(
136
+ model=model,
137
+ image=image,
138
+ question=question,
139
+ top_k=top_k,
140
+ timeout=timeout,
141
+ )
142
+ endpoint = "question-answering/visual"
143
+ response = self._client._post(endpoint, json_data=request_data)
144
+ return VisualQuestionAnsweringResponse.model_validate(response.json())
145
+
146
+
147
+ class AsyncQuestionAnsweringResource:
148
+ """Async Question Answering resource handler"""
149
+
150
+ def __init__(self, client: "AsyncSynapsAI"):
151
+ self._client = client
152
+
153
+ async def document(
154
+ self,
155
+ model: str,
156
+ image,
157
+ question: str,
158
+ word_boxes: Optional[List] = None,
159
+ top_k: Optional[int] = None,
160
+ doc_stride: Optional[int] = None,
161
+ max_answer_len: Optional[int] = None,
162
+ max_seq_len: Optional[int] = None,
163
+ max_question_len: Optional[int] = None,
164
+ handle_impossible_answer: Optional[bool] = None,
165
+ lang: Optional[str] = None,
166
+ tesseract_config: Optional[str] = None,
167
+ timeout: Optional[float] = None,
168
+ ) -> DocumentQuestionAnsweringResponse:
169
+ request_data = self._client._build_request(
170
+ model=model,
171
+ image=image,
172
+ question=question,
173
+ word_boxes=word_boxes,
174
+ top_k=top_k,
175
+ doc_stride=doc_stride,
176
+ max_answer_len=max_answer_len,
177
+ max_seq_len=max_seq_len,
178
+ max_question_len=max_question_len,
179
+ handle_impossible_answer=handle_impossible_answer,
180
+ lang=lang,
181
+ tesseract_config=tesseract_config,
182
+ timeout=timeout,
183
+ )
184
+ endpoint = "question-answering/document"
185
+ response = await self._client._post(endpoint, json_data=request_data)
186
+ return DocumentQuestionAnsweringResponse.model_validate(response.json())
187
+
188
+ async def text(
189
+ self,
190
+ model: str,
191
+ question: Union[str, List[str]],
192
+ context: Union[str, List[str]],
193
+ top_k: Optional[int] = None,
194
+ doc_stride: Optional[int] = None,
195
+ max_answer_len: Optional[int] = None,
196
+ max_seq_len: Optional[int] = None,
197
+ max_question_len: Optional[int] = None,
198
+ handle_impossible_answer: Optional[bool] = None,
199
+ align_to_words: Optional[bool] = None,
200
+ ) -> QuestionAnsweringResponse:
201
+ request_data = self._client._build_request(
202
+ model=model,
203
+ question=question,
204
+ context=context,
205
+ top_k=top_k,
206
+ doc_stride=doc_stride,
207
+ max_answer_len=max_answer_len,
208
+ max_seq_len=max_seq_len,
209
+ max_question_len=max_question_len,
210
+ handle_impossible_answer=handle_impossible_answer,
211
+ align_to_words=align_to_words,
212
+ )
213
+ endpoint = "question-answering"
214
+ response = await self._client._post(endpoint, json_data=request_data)
215
+ return QuestionAnsweringResponse.model_validate(response.json())
216
+
217
+ async def table(
218
+ self,
219
+ model: str,
220
+ table: Dict,
221
+ query: Union[str, List[str]],
222
+ sequential: Optional[bool] = None,
223
+ padding: Optional[Union[bool, str]] = None,
224
+ truncation: Optional[Union[bool, str]] = None,
225
+ ) -> TableQuestionAnsweringResponse:
226
+ request_data = self._client._build_request(
227
+ model=model,
228
+ table=table,
229
+ query=query,
230
+ sequential=sequential,
231
+ padding=padding,
232
+ truncation=truncation,
233
+ )
234
+ endpoint = "question-answering/table"
235
+ response = await self._client._post(endpoint, json_data=request_data)
236
+ return TableQuestionAnsweringResponse.model_validate(response.json())
237
+
238
+ async def visual(
239
+ self,
240
+ model: str,
241
+ image,
242
+ question: Union[str, List[str]],
243
+ top_k: Optional[int] = None,
244
+ timeout: Optional[float] = None,
245
+ ) -> VisualQuestionAnsweringResponse:
246
+ request_data = self._client._build_request(
247
+ model=model,
248
+ image=image,
249
+ question=question,
250
+ top_k=top_k,
251
+ timeout=timeout,
252
+ )
253
+ endpoint = "question-answering/visual"
254
+ response = await self._client._post(endpoint, json_data=request_data)
255
+ return VisualQuestionAnsweringResponse.model_validate(response.json())
256
+
257
+
@@ -0,0 +1,90 @@
1
+ # Copyright 2026 SynapsAI Technologies Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """
16
+ Text ranking resource handlers
17
+ """
18
+
19
+ from typing import TYPE_CHECKING, List
20
+
21
+ from ..types.rerank import RerankResponse
22
+
23
+ if TYPE_CHECKING:
24
+ from ..client import SynapsAI, AsyncSynapsAI
25
+
26
+
27
+ class RerankResource:
28
+ """Text ranking resource handler"""
29
+
30
+ def __init__(self, client: "SynapsAI"):
31
+ self._client = client
32
+
33
+ def create(
34
+ self,
35
+ model: str,
36
+ query: str,
37
+ documents: List[str],
38
+ top_n: int | None = None,
39
+ max_tokens_per_doc: int = 4096,
40
+ **kwargs,
41
+ ) -> RerankResponse:
42
+ """Rank documents against a query using a text-ranking model."""
43
+
44
+ request_data = self._client._build_request(
45
+ model=model,
46
+ query=query,
47
+ documents=documents,
48
+ top_n=top_n,
49
+ max_tokens_per_doc=max_tokens_per_doc,
50
+ **kwargs,
51
+ )
52
+
53
+ endpoint = "rerank"
54
+
55
+ response = self._client._post(endpoint, json_data=request_data)
56
+ response_data = response.json()
57
+ return RerankResponse.model_validate(response_data)
58
+
59
+
60
+ class AsyncRerankResource:
61
+ """Async text ranking resource handler"""
62
+
63
+ def __init__(self, client: "AsyncSynapsAI"):
64
+ self._client = client
65
+
66
+ async def create(
67
+ self,
68
+ model: str,
69
+ query: str,
70
+ documents: List[str],
71
+ top_n: int | None = None,
72
+ max_tokens_per_doc: int = 4096,
73
+ **kwargs,
74
+ ) -> RerankResponse:
75
+ """Rank documents against a query using a text-ranking model."""
76
+
77
+ request_data = self._client._build_request(
78
+ model=model,
79
+ query=query,
80
+ documents=documents,
81
+ top_n=top_n,
82
+ max_tokens_per_doc=max_tokens_per_doc,
83
+ **kwargs,
84
+ )
85
+
86
+ endpoint = "rerank"
87
+
88
+ response = await self._client._post(endpoint, json_data=request_data)
89
+ response_data = response.json()
90
+ return RerankResponse.model_validate(response_data)
@@ -0,0 +1,303 @@
1
+ # Copyright 2026 SynapsAI Technologies Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """
16
+ Video resource handlers
17
+ """
18
+
19
+ import asyncio
20
+ import base64
21
+ import os
22
+ import time
23
+ from pathlib import Path
24
+ from typing import TYPE_CHECKING, AsyncIterator, Iterator, Optional, Union, Tuple
25
+ from urllib.parse import urlencode
26
+
27
+ from ..processing import process_image_input
28
+
29
+ from ..exceptions import APIError
30
+ from ..types.videos import (
31
+ Video,
32
+ VideoContentVariant,
33
+ VideoDeleteResponse,
34
+ VideoInputReference,
35
+ VideoStatus,
36
+ )
37
+
38
+ if TYPE_CHECKING:
39
+ from ..client import AsyncSynapsAI, SynapsAI
40
+
41
+
42
+ class VideoContentResponse:
43
+ """Streaming binary response for video content downloads."""
44
+
45
+ def __init__(self, response, content_type: str):
46
+ self._response = response
47
+ self.content_type = content_type
48
+
49
+ def iter_bytes(self, chunk_size: int = 8192) -> Iterator[bytes]:
50
+ for chunk in self._response.iter_bytes(chunk_size=chunk_size):
51
+ if chunk:
52
+ yield chunk
53
+
54
+ def write_to_file(self, file_path) -> None:
55
+ if hasattr(file_path, "write"):
56
+ for chunk in self.iter_bytes():
57
+ file_path.write(chunk)
58
+ else:
59
+ with Path(file_path).open("wb") as f:
60
+ for chunk in self.iter_bytes():
61
+ f.write(chunk)
62
+
63
+ def read(self) -> bytes:
64
+ return b"".join(self.iter_bytes())
65
+
66
+ def __enter__(self) -> "VideoContentResponse":
67
+ return self
68
+
69
+ def __exit__(self, exc_type, exc_val, exc_tb) -> None:
70
+ close = getattr(self._response, "close", None)
71
+ if callable(close):
72
+ close()
73
+
74
+
75
+ class AsyncVideoContentResponse:
76
+ """Async streaming binary response for video content downloads."""
77
+
78
+ def __init__(self, response, content_type: str):
79
+ self._response = response
80
+ self.content_type = content_type
81
+
82
+ async def iter_bytes(self, chunk_size: int = 8192) -> AsyncIterator[bytes]:
83
+ async for chunk in self._response.aiter_bytes(chunk_size=chunk_size):
84
+ if chunk:
85
+ yield chunk
86
+
87
+ async def write_to_file(self, file_path) -> None:
88
+ if hasattr(file_path, "write"):
89
+ async for chunk in self.iter_bytes():
90
+ file_path.write(chunk)
91
+ else:
92
+ with Path(file_path).open("wb") as f:
93
+ async for chunk in self.iter_bytes():
94
+ f.write(chunk)
95
+
96
+ async def read(self) -> bytes:
97
+ return b"".join([chunk async for chunk in self.iter_bytes()])
98
+
99
+ async def __aenter__(self) -> "AsyncVideoContentResponse":
100
+ return self
101
+
102
+ async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
103
+ aclose = getattr(self._response, "aclose", None)
104
+ if callable(aclose):
105
+ await aclose()
106
+
107
+
108
+ class VideosResource:
109
+ """Video generation resource"""
110
+
111
+ TERMINAL_STATUSES = {VideoStatus.COMPLETED.value, VideoStatus.FAILED.value}
112
+
113
+ def __init__(self, client: "SynapsAI"):
114
+ self._client = client
115
+
116
+ def _build_content_endpoint(
117
+ self,
118
+ video_id: str,
119
+ variant: Optional[Union[str, VideoContentVariant]] = None,
120
+ ) -> str:
121
+ endpoint = f"videos/{video_id}/content"
122
+ if variant is None:
123
+ return endpoint
124
+ query = urlencode({"variant": getattr(variant, "value", variant)})
125
+ return f"{endpoint}?{query}"
126
+
127
+ def create(
128
+ self,
129
+ model: str,
130
+ prompt: str,
131
+ input_reference: Optional[VideoInputReference] = None,
132
+ seconds: int = 4,
133
+ size: Optional[Union[str, Tuple[int, int]]] = "1280x720",
134
+ fps: int = 16,
135
+ num_inference_steps: int = 25,
136
+ guidance_scale: float = 5.0,
137
+ **kwargs,
138
+ ) -> Video:
139
+ request_data = self._client._build_request(
140
+ prompt=prompt,
141
+ input_reference=process_image_input(input_reference),
142
+ model=model,
143
+ seconds=seconds,
144
+ size=size,
145
+ fps=fps,
146
+ num_inference_steps=num_inference_steps,
147
+ guidance_scale=guidance_scale,
148
+ **kwargs,
149
+ )
150
+ response = self._client._post("videos", json_data=request_data)
151
+ return Video.model_validate(response.json())
152
+
153
+ def retrieve(self, video_id: str) -> Video:
154
+ response = self._client._get(f"videos/{video_id}")
155
+ return Video.model_validate(response.json())
156
+
157
+ def delete(self, video_id: str) -> VideoDeleteResponse:
158
+ response = self._client._delete(f"videos/{video_id}")
159
+ return VideoDeleteResponse.model_validate(response.json())
160
+
161
+ def download_content(
162
+ self,
163
+ video_id: str,
164
+ variant: Optional[Union[str, VideoContentVariant]] = None,
165
+ ) -> VideoContentResponse:
166
+ endpoint = self._build_content_endpoint(video_id=video_id, variant=variant)
167
+ response = self._client._get_stream(endpoint)
168
+ content_type = response.headers.get("content-type", "application/octet-stream")
169
+ return VideoContentResponse(response, content_type=content_type)
170
+
171
+ def create_and_poll(
172
+ self,
173
+ model: str,
174
+ prompt: str,
175
+ input_reference: Optional[VideoInputReference] = None,
176
+ seconds: int = 4,
177
+ size: Optional[Union[str, Tuple[int, int]]] = "1280x720",
178
+ poll_interval: float = 2.0,
179
+ timeout: Optional[float] = None,
180
+ fps: int = 16,
181
+ num_inference_steps: int = 25,
182
+ guidance_scale: float = 5.0,
183
+ **kwargs,
184
+ ) -> Video:
185
+ video = self.create(
186
+ prompt=prompt,
187
+ input_reference=input_reference,
188
+ model=model,
189
+ seconds=seconds,
190
+ size=size,
191
+ fps=fps,
192
+ num_inference_steps=num_inference_steps,
193
+ guidance_scale=guidance_scale,
194
+ **kwargs,
195
+ )
196
+ started_at = time.monotonic()
197
+
198
+ while getattr(video.status, "value", video.status) not in self.TERMINAL_STATUSES:
199
+ if timeout is not None and time.monotonic() - started_at >= timeout:
200
+ raise APIError(f"Timed out while waiting for video '{video.id}' to complete")
201
+ time.sleep(poll_interval)
202
+ video = self.retrieve(video.id)
203
+
204
+ return video
205
+
206
+
207
+ class AsyncVideosResource:
208
+ """Async video generation resource"""
209
+
210
+ TERMINAL_STATUSES = {VideoStatus.COMPLETED.value, VideoStatus.FAILED.value}
211
+
212
+ def __init__(self, client: "AsyncSynapsAI"):
213
+ self._client = client
214
+
215
+ def _build_content_endpoint(
216
+ self,
217
+ video_id: str,
218
+ variant: Optional[Union[str, VideoContentVariant]] = None,
219
+ ) -> str:
220
+ endpoint = f"videos/{video_id}/content"
221
+ if variant is None:
222
+ return endpoint
223
+ query = urlencode({"variant": getattr(variant, "value", variant)})
224
+ return f"{endpoint}?{query}"
225
+
226
+ async def create(
227
+ self,
228
+ model: str,
229
+ prompt: str,
230
+ input_reference: Optional[VideoInputReference] = None,
231
+ seconds: Optional[int] = 4,
232
+ size: Optional[Union[str, Tuple[int, int]]] = "1280x720",
233
+ fps: int = 16,
234
+ num_inference_steps: int = 25,
235
+ guidance_scale: float = 5.0,
236
+ **kwargs,
237
+ ) -> Video:
238
+ request_data = self._client._build_request(
239
+ prompt=prompt,
240
+ input_reference=process_image_input(input_reference),
241
+ model=model,
242
+ seconds=seconds,
243
+ size=size,
244
+ fps=fps,
245
+ num_inference_steps=num_inference_steps,
246
+ guidance_scale=guidance_scale,
247
+ **kwargs,
248
+ )
249
+ response = await self._client._post("videos", json_data=request_data)
250
+ return Video.model_validate(response.json())
251
+
252
+ async def retrieve(self, video_id: str) -> Video:
253
+ response = await self._client._get(f"videos/{video_id}")
254
+ return Video.model_validate(response.json())
255
+
256
+ async def delete(self, video_id: str) -> VideoDeleteResponse:
257
+ response = await self._client._delete(f"videos/{video_id}")
258
+ return VideoDeleteResponse.model_validate(response.json())
259
+
260
+ async def download_content(
261
+ self,
262
+ video_id: str,
263
+ variant: Optional[Union[str, VideoContentVariant]] = None,
264
+ ) -> AsyncVideoContentResponse:
265
+ endpoint = self._build_content_endpoint(video_id=video_id, variant=variant)
266
+ response = await self._client._get_stream(endpoint)
267
+ content_type = response.headers.get("content-type", "application/octet-stream")
268
+ return AsyncVideoContentResponse(response, content_type=content_type)
269
+
270
+ async def create_and_poll(
271
+ self,
272
+ model: str,
273
+ prompt: str,
274
+ input_reference: Optional[VideoInputReference] = None,
275
+ seconds: Optional[int] = 4,
276
+ size: Optional[Union[str, Tuple[int, int]]] = "1280x720",
277
+ fps: int = 16,
278
+ num_inference_steps: int = 25,
279
+ guidance_scale: float = 5.0,
280
+ poll_interval: float = 2.0,
281
+ timeout: Optional[float] = None,
282
+ **kwargs,
283
+ ) -> Video:
284
+ video = await self.create(
285
+ prompt=prompt,
286
+ input_reference=input_reference,
287
+ model=model,
288
+ seconds=seconds,
289
+ size=size,
290
+ fps=fps,
291
+ num_inference_steps=num_inference_steps,
292
+ guidance_scale=guidance_scale,
293
+ **kwargs,
294
+ )
295
+ started_at = time.monotonic()
296
+
297
+ while getattr(video.status, "value", video.status) not in self.TERMINAL_STATUSES:
298
+ if timeout is not None and time.monotonic() - started_at >= timeout:
299
+ raise APIError(f"Timed out while waiting for video '{video.id}' to complete")
300
+ await asyncio.sleep(poll_interval)
301
+ video = await self.retrieve(video.id)
302
+
303
+ return video