fraime-sdk 1.0.0__tar.gz
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.
- fraime_sdk-1.0.0/LICENSE +21 -0
- fraime_sdk-1.0.0/PKG-INFO +164 -0
- fraime_sdk-1.0.0/README.md +145 -0
- fraime_sdk-1.0.0/fraime/__init__.py +45 -0
- fraime_sdk-1.0.0/fraime/exceptions.py +19 -0
- fraime_sdk-1.0.0/fraime/main.py +51 -0
- fraime_sdk-1.0.0/fraime/model.py +142 -0
- fraime_sdk-1.0.0/fraime/repository.py +30 -0
- fraime_sdk-1.0.0/fraime/service.py +14 -0
- fraime_sdk-1.0.0/fraime_sdk.egg-info/PKG-INFO +164 -0
- fraime_sdk-1.0.0/fraime_sdk.egg-info/SOURCES.txt +14 -0
- fraime_sdk-1.0.0/fraime_sdk.egg-info/dependency_links.txt +1 -0
- fraime_sdk-1.0.0/fraime_sdk.egg-info/requires.txt +6 -0
- fraime_sdk-1.0.0/fraime_sdk.egg-info/top_level.txt +1 -0
- fraime_sdk-1.0.0/pyproject.toml +33 -0
- fraime_sdk-1.0.0/setup.cfg +4 -0
fraime_sdk-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Santiago Melo Medina
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fraime-sdk
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Python SDK for the Fraime video generation API
|
|
5
|
+
Author: Santiago Melo Medina
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Requires-Python: >=3.11
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Dist: httpx
|
|
14
|
+
Requires-Dist: pydantic
|
|
15
|
+
Provides-Extra: dev
|
|
16
|
+
Requires-Dist: build; extra == "dev"
|
|
17
|
+
Requires-Dist: twine; extra == "dev"
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
|
|
20
|
+
# Fraime SDK
|
|
21
|
+
|
|
22
|
+
Python client for the [Fraime API](../api/README.md) — build a request with
|
|
23
|
+
typed models/enums instead of hand-writing JSON, point it at a running
|
|
24
|
+
Fraime instance, get a generated video back.
|
|
25
|
+
|
|
26
|
+
## Prerequisites
|
|
27
|
+
|
|
28
|
+
- Python 3.11+
|
|
29
|
+
- A running Fraime API instance to talk to (see [`api/README.md`](../api/README.md)
|
|
30
|
+
for how to run one) — you'll need its base URL, and its API key if one is
|
|
31
|
+
configured (`AUTH_API_KEY` on the API side).
|
|
32
|
+
|
|
33
|
+
## Install
|
|
34
|
+
|
|
35
|
+
### Option 1 — pip
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install fraime-sdk
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Option 2 — from a local clone
|
|
42
|
+
|
|
43
|
+
Step by step, from scratch:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
# 1. Clone the repo (skip if you already have it)
|
|
47
|
+
git clone <this-repo-url>
|
|
48
|
+
cd fraime
|
|
49
|
+
|
|
50
|
+
# 2. (Recommended) create a virtualenv for your own project
|
|
51
|
+
python3 -m venv .venv
|
|
52
|
+
source .venv/bin/activate
|
|
53
|
+
|
|
54
|
+
# 3. Install the SDK from the sdk/ folder
|
|
55
|
+
pip install ./sdk
|
|
56
|
+
# or, for local development on the SDK itself (editable install):
|
|
57
|
+
pip install -e ./sdk
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
That's it — `import fraime` is now available in that environment.
|
|
61
|
+
|
|
62
|
+
### Option 3 — straight from git, no local clone needed
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
pip install "git+ssh://git@santiago/santiagoMeloMedina/fraime.git#subdirectory=sdk"
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
(Adjust the URL to whatever remote you actually have push/pull access to —
|
|
69
|
+
this is this repo's own `origin` in SSH form.)
|
|
70
|
+
|
|
71
|
+
## Usage
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from fraime import FraimeClient, VideoType, GenerationParams, CinematicPromptFields
|
|
75
|
+
|
|
76
|
+
client = FraimeClient(
|
|
77
|
+
base_url="http://127.0.0.1:8000", # or set FRAIME_BASE_URL instead
|
|
78
|
+
api_key="your-api-key", # or set FRAIME_API_KEY instead; omit both if the API has none configured
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
response = client.generate(
|
|
82
|
+
video_type=VideoType.PIXAR,
|
|
83
|
+
fields=CinematicPromptFields(
|
|
84
|
+
subject="a small orange fox with oversized ears",
|
|
85
|
+
action="hops between rocks, pauses, and looks up curiously",
|
|
86
|
+
scene="a sunlit forest clearing at golden hour",
|
|
87
|
+
camera="medium shot, slow dolly-in",
|
|
88
|
+
lighting="warm rim lighting from the low sun",
|
|
89
|
+
style="3D animated feature style, stylized proportions, warm rim lighting",
|
|
90
|
+
),
|
|
91
|
+
params=GenerationParams(duration_s=3, fps=16, resolution="768x512"),
|
|
92
|
+
# model=... # optional: pin an exact model instead of auto-selecting
|
|
93
|
+
# references=[...] # optional: Reference(url=...) list, for image-to-video
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
print(response.video_path, response.model)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
`model` is optional on `client.generate()` — omit it and the API auto-selects
|
|
100
|
+
by hardware, same as calling it directly.
|
|
101
|
+
|
|
102
|
+
### Picking the right fields class per video type
|
|
103
|
+
|
|
104
|
+
Every `video_type` has its own field set — some add fields the base six
|
|
105
|
+
(`subject`, `action`, `scene`, `camera`, `lighting`, `style`,
|
|
106
|
+
`negative_prompt`) don't cover:
|
|
107
|
+
|
|
108
|
+
| `VideoType` | Fields class | Extra fields |
|
|
109
|
+
|---|---|---|
|
|
110
|
+
| `PIXAR`, `ACTION`, `ANIMATION`, `ANIME`, `DOCUMENTARY`, `FASHION` | `CinematicPromptFields` | — |
|
|
111
|
+
| `UGC_PRODUCT_REVIEW`, `COMMERCIAL_PRODUCT_AD`, `EXPLAINER_TESTIMONIAL` | `UGCPromptFields` | `dialogue`, `reference_image` |
|
|
112
|
+
| `PRESENTER_AVATAR` | `PresenterPromptFields` | + `voice_tone` |
|
|
113
|
+
| `SOCIAL_SHORT_FORM_AD` | `SocialAdPromptFields` | + `text_overlay`, `aspect_ratio` |
|
|
114
|
+
| `MUSIC_VIDEO` | `MusicVideoPromptFields` | `audio_reference`, `tempo_bpm` |
|
|
115
|
+
| `MOTION_GRAPHICS` | `MotionGraphicsPromptFields` | `text_content`, `transitions` |
|
|
116
|
+
|
|
117
|
+
Not sure which class a given `VideoType` needs? Look it up instead of
|
|
118
|
+
guessing:
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
from fraime import PROMPT_FIELDS_BY_VIDEO_TYPE, VideoType
|
|
122
|
+
|
|
123
|
+
fields_class = PROMPT_FIELDS_BY_VIDEO_TYPE[VideoType.SOCIAL_SHORT_FORM_AD]
|
|
124
|
+
# -> SocialAdPromptFields
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Reference images (image-to-video)
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
from fraime import Reference
|
|
131
|
+
|
|
132
|
+
response = client.generate(
|
|
133
|
+
video_type=VideoType.UGC_PRODUCT_REVIEW,
|
|
134
|
+
fields=ugc_fields,
|
|
135
|
+
params=params,
|
|
136
|
+
references=[Reference(url="https://example.com/product-photo.jpg")],
|
|
137
|
+
)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Error handling
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
from fraime import FraimeAuthError, FraimeAPIError, FraimeConnectionError
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
response = client.generate(video_type=VideoType.PIXAR, fields=fields, params=params)
|
|
147
|
+
except FraimeAuthError:
|
|
148
|
+
... # missing/invalid API key
|
|
149
|
+
except FraimeAPIError as e:
|
|
150
|
+
... # e.status_code, e.detail — the API reached but returned an error
|
|
151
|
+
except FraimeConnectionError:
|
|
152
|
+
... # couldn't reach the API at all
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Configuration reference
|
|
156
|
+
|
|
157
|
+
| `FraimeClient(...)` argument | Env var fallback | Default |
|
|
158
|
+
|---|---|---|
|
|
159
|
+
| `base_url` | `FRAIME_BASE_URL` | `http://127.0.0.1:8000` |
|
|
160
|
+
| `api_key` | `FRAIME_API_KEY` | none (open API) |
|
|
161
|
+
| `timeout` | — | `600.0` seconds |
|
|
162
|
+
|
|
163
|
+
`timeout` defaults high on purpose — real generation runs can take several
|
|
164
|
+
minutes; see [`api/README.md`](../api/README.md) for why.
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# Fraime SDK
|
|
2
|
+
|
|
3
|
+
Python client for the [Fraime API](../api/README.md) — build a request with
|
|
4
|
+
typed models/enums instead of hand-writing JSON, point it at a running
|
|
5
|
+
Fraime instance, get a generated video back.
|
|
6
|
+
|
|
7
|
+
## Prerequisites
|
|
8
|
+
|
|
9
|
+
- Python 3.11+
|
|
10
|
+
- A running Fraime API instance to talk to (see [`api/README.md`](../api/README.md)
|
|
11
|
+
for how to run one) — you'll need its base URL, and its API key if one is
|
|
12
|
+
configured (`AUTH_API_KEY` on the API side).
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
### Option 1 — pip
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install fraime-sdk
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Option 2 — from a local clone
|
|
23
|
+
|
|
24
|
+
Step by step, from scratch:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
# 1. Clone the repo (skip if you already have it)
|
|
28
|
+
git clone <this-repo-url>
|
|
29
|
+
cd fraime
|
|
30
|
+
|
|
31
|
+
# 2. (Recommended) create a virtualenv for your own project
|
|
32
|
+
python3 -m venv .venv
|
|
33
|
+
source .venv/bin/activate
|
|
34
|
+
|
|
35
|
+
# 3. Install the SDK from the sdk/ folder
|
|
36
|
+
pip install ./sdk
|
|
37
|
+
# or, for local development on the SDK itself (editable install):
|
|
38
|
+
pip install -e ./sdk
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
That's it — `import fraime` is now available in that environment.
|
|
42
|
+
|
|
43
|
+
### Option 3 — straight from git, no local clone needed
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install "git+ssh://git@santiago/santiagoMeloMedina/fraime.git#subdirectory=sdk"
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
(Adjust the URL to whatever remote you actually have push/pull access to —
|
|
50
|
+
this is this repo's own `origin` in SSH form.)
|
|
51
|
+
|
|
52
|
+
## Usage
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from fraime import FraimeClient, VideoType, GenerationParams, CinematicPromptFields
|
|
56
|
+
|
|
57
|
+
client = FraimeClient(
|
|
58
|
+
base_url="http://127.0.0.1:8000", # or set FRAIME_BASE_URL instead
|
|
59
|
+
api_key="your-api-key", # or set FRAIME_API_KEY instead; omit both if the API has none configured
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
response = client.generate(
|
|
63
|
+
video_type=VideoType.PIXAR,
|
|
64
|
+
fields=CinematicPromptFields(
|
|
65
|
+
subject="a small orange fox with oversized ears",
|
|
66
|
+
action="hops between rocks, pauses, and looks up curiously",
|
|
67
|
+
scene="a sunlit forest clearing at golden hour",
|
|
68
|
+
camera="medium shot, slow dolly-in",
|
|
69
|
+
lighting="warm rim lighting from the low sun",
|
|
70
|
+
style="3D animated feature style, stylized proportions, warm rim lighting",
|
|
71
|
+
),
|
|
72
|
+
params=GenerationParams(duration_s=3, fps=16, resolution="768x512"),
|
|
73
|
+
# model=... # optional: pin an exact model instead of auto-selecting
|
|
74
|
+
# references=[...] # optional: Reference(url=...) list, for image-to-video
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
print(response.video_path, response.model)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
`model` is optional on `client.generate()` — omit it and the API auto-selects
|
|
81
|
+
by hardware, same as calling it directly.
|
|
82
|
+
|
|
83
|
+
### Picking the right fields class per video type
|
|
84
|
+
|
|
85
|
+
Every `video_type` has its own field set — some add fields the base six
|
|
86
|
+
(`subject`, `action`, `scene`, `camera`, `lighting`, `style`,
|
|
87
|
+
`negative_prompt`) don't cover:
|
|
88
|
+
|
|
89
|
+
| `VideoType` | Fields class | Extra fields |
|
|
90
|
+
|---|---|---|
|
|
91
|
+
| `PIXAR`, `ACTION`, `ANIMATION`, `ANIME`, `DOCUMENTARY`, `FASHION` | `CinematicPromptFields` | — |
|
|
92
|
+
| `UGC_PRODUCT_REVIEW`, `COMMERCIAL_PRODUCT_AD`, `EXPLAINER_TESTIMONIAL` | `UGCPromptFields` | `dialogue`, `reference_image` |
|
|
93
|
+
| `PRESENTER_AVATAR` | `PresenterPromptFields` | + `voice_tone` |
|
|
94
|
+
| `SOCIAL_SHORT_FORM_AD` | `SocialAdPromptFields` | + `text_overlay`, `aspect_ratio` |
|
|
95
|
+
| `MUSIC_VIDEO` | `MusicVideoPromptFields` | `audio_reference`, `tempo_bpm` |
|
|
96
|
+
| `MOTION_GRAPHICS` | `MotionGraphicsPromptFields` | `text_content`, `transitions` |
|
|
97
|
+
|
|
98
|
+
Not sure which class a given `VideoType` needs? Look it up instead of
|
|
99
|
+
guessing:
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
from fraime import PROMPT_FIELDS_BY_VIDEO_TYPE, VideoType
|
|
103
|
+
|
|
104
|
+
fields_class = PROMPT_FIELDS_BY_VIDEO_TYPE[VideoType.SOCIAL_SHORT_FORM_AD]
|
|
105
|
+
# -> SocialAdPromptFields
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Reference images (image-to-video)
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
from fraime import Reference
|
|
112
|
+
|
|
113
|
+
response = client.generate(
|
|
114
|
+
video_type=VideoType.UGC_PRODUCT_REVIEW,
|
|
115
|
+
fields=ugc_fields,
|
|
116
|
+
params=params,
|
|
117
|
+
references=[Reference(url="https://example.com/product-photo.jpg")],
|
|
118
|
+
)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Error handling
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
from fraime import FraimeAuthError, FraimeAPIError, FraimeConnectionError
|
|
125
|
+
|
|
126
|
+
try:
|
|
127
|
+
response = client.generate(video_type=VideoType.PIXAR, fields=fields, params=params)
|
|
128
|
+
except FraimeAuthError:
|
|
129
|
+
... # missing/invalid API key
|
|
130
|
+
except FraimeAPIError as e:
|
|
131
|
+
... # e.status_code, e.detail — the API reached but returned an error
|
|
132
|
+
except FraimeConnectionError:
|
|
133
|
+
... # couldn't reach the API at all
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Configuration reference
|
|
137
|
+
|
|
138
|
+
| `FraimeClient(...)` argument | Env var fallback | Default |
|
|
139
|
+
|---|---|---|
|
|
140
|
+
| `base_url` | `FRAIME_BASE_URL` | `http://127.0.0.1:8000` |
|
|
141
|
+
| `api_key` | `FRAIME_API_KEY` | none (open API) |
|
|
142
|
+
| `timeout` | — | `600.0` seconds |
|
|
143
|
+
|
|
144
|
+
`timeout` defaults high on purpose — real generation runs can take several
|
|
145
|
+
minutes; see [`api/README.md`](../api/README.md) for why.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from fraime.exceptions import (
|
|
2
|
+
FraimeAPIError,
|
|
3
|
+
FraimeAuthError,
|
|
4
|
+
FraimeConnectionError,
|
|
5
|
+
FraimeError,
|
|
6
|
+
)
|
|
7
|
+
from fraime.main import FraimeClient
|
|
8
|
+
from fraime.model import (
|
|
9
|
+
PROMPT_FIELDS_BY_VIDEO_TYPE,
|
|
10
|
+
AspectRatio,
|
|
11
|
+
CinematicPromptFields,
|
|
12
|
+
GenerateVideoRequest,
|
|
13
|
+
GenerateVideoResponse,
|
|
14
|
+
GenerationParams,
|
|
15
|
+
MotionGraphicsPromptFields,
|
|
16
|
+
MusicVideoPromptFields,
|
|
17
|
+
PresenterPromptFields,
|
|
18
|
+
PromptFields,
|
|
19
|
+
Reference,
|
|
20
|
+
SocialAdPromptFields,
|
|
21
|
+
UGCPromptFields,
|
|
22
|
+
VideoType,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"FraimeClient",
|
|
27
|
+
"VideoType",
|
|
28
|
+
"PROMPT_FIELDS_BY_VIDEO_TYPE",
|
|
29
|
+
"AspectRatio",
|
|
30
|
+
"GenerationParams",
|
|
31
|
+
"Reference",
|
|
32
|
+
"PromptFields",
|
|
33
|
+
"CinematicPromptFields",
|
|
34
|
+
"UGCPromptFields",
|
|
35
|
+
"PresenterPromptFields",
|
|
36
|
+
"SocialAdPromptFields",
|
|
37
|
+
"MusicVideoPromptFields",
|
|
38
|
+
"MotionGraphicsPromptFields",
|
|
39
|
+
"GenerateVideoRequest",
|
|
40
|
+
"GenerateVideoResponse",
|
|
41
|
+
"FraimeError",
|
|
42
|
+
"FraimeConnectionError",
|
|
43
|
+
"FraimeAuthError",
|
|
44
|
+
"FraimeAPIError",
|
|
45
|
+
]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
class FraimeError(Exception):
|
|
2
|
+
"""Base class for all Fraime SDK errors."""
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class FraimeConnectionError(FraimeError):
|
|
6
|
+
"""The API couldn't be reached at all (network/DNS/timeout)."""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class FraimeAuthError(FraimeError):
|
|
10
|
+
"""The API rejected the request due to a missing or invalid API key."""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class FraimeAPIError(FraimeError):
|
|
14
|
+
"""The API reached and responded, but with an error status."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, status_code: int, detail: str):
|
|
17
|
+
self.status_code = status_code
|
|
18
|
+
self.detail = detail
|
|
19
|
+
super().__init__(f"Fraime API returned {status_code}: {detail}")
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from fraime.model import (
|
|
4
|
+
GenerateVideoRequest,
|
|
5
|
+
GenerateVideoResponse,
|
|
6
|
+
GenerationParams,
|
|
7
|
+
PromptFields,
|
|
8
|
+
Reference,
|
|
9
|
+
VideoType,
|
|
10
|
+
)
|
|
11
|
+
from fraime.repository import GenerationRepository
|
|
12
|
+
from fraime.service import GenerationService
|
|
13
|
+
|
|
14
|
+
DEFAULT_BASE_URL = "http://127.0.0.1:8000"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class FraimeClient:
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
base_url: str | None = None,
|
|
21
|
+
api_key: str | None = None,
|
|
22
|
+
timeout: float = 600.0,
|
|
23
|
+
):
|
|
24
|
+
base_url = base_url or os.environ.get("FRAIME_BASE_URL", DEFAULT_BASE_URL)
|
|
25
|
+
api_key = api_key or os.environ.get("FRAIME_API_KEY")
|
|
26
|
+
|
|
27
|
+
repository = GenerationRepository(base_url=base_url, api_key=api_key, timeout=timeout)
|
|
28
|
+
self._service = GenerationService(repository)
|
|
29
|
+
|
|
30
|
+
def generate(
|
|
31
|
+
self,
|
|
32
|
+
video_type: VideoType,
|
|
33
|
+
fields: PromptFields,
|
|
34
|
+
params: GenerationParams,
|
|
35
|
+
model: str | None = None,
|
|
36
|
+
references: list[Reference] | None = None,
|
|
37
|
+
vram_safety_margin: bool = True,
|
|
38
|
+
low_memory_decode: bool = True,
|
|
39
|
+
cpu_offload: bool = True,
|
|
40
|
+
) -> GenerateVideoResponse:
|
|
41
|
+
request = GenerateVideoRequest(
|
|
42
|
+
video_type=video_type,
|
|
43
|
+
fields=fields,
|
|
44
|
+
params=params,
|
|
45
|
+
model=model,
|
|
46
|
+
references=references,
|
|
47
|
+
vram_safety_margin=vram_safety_margin,
|
|
48
|
+
low_memory_decode=low_memory_decode,
|
|
49
|
+
cpu_offload=cpu_offload,
|
|
50
|
+
)
|
|
51
|
+
return self._service.generate_video(request)
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
from abc import ABC
|
|
2
|
+
from enum import Enum
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel, Field, HttpUrl
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class VideoType(str, Enum):
|
|
8
|
+
PIXAR = "pixar"
|
|
9
|
+
ACTION = "action"
|
|
10
|
+
ANIMATION = "animation"
|
|
11
|
+
ANIME = "anime"
|
|
12
|
+
DOCUMENTARY = "documentary"
|
|
13
|
+
FASHION = "fashion"
|
|
14
|
+
UGC_PRODUCT_REVIEW = "ugc_product_review"
|
|
15
|
+
COMMERCIAL_PRODUCT_AD = "commercial_product_ad"
|
|
16
|
+
EXPLAINER_TESTIMONIAL = "explainer_testimonial"
|
|
17
|
+
PRESENTER_AVATAR = "presenter_avatar"
|
|
18
|
+
SOCIAL_SHORT_FORM_AD = "social_short_form_ad"
|
|
19
|
+
MUSIC_VIDEO = "music_video"
|
|
20
|
+
MOTION_GRAPHICS = "motion_graphics"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class AspectRatio(str, Enum):
|
|
24
|
+
VERTICAL_9_16 = "9:16"
|
|
25
|
+
VERTICAL_4_5 = "4:5"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Reference(BaseModel):
|
|
29
|
+
url: HttpUrl = Field(description="Publicly accessible URL the file is downloaded from")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class GenerationParams(BaseModel):
|
|
33
|
+
duration_s: float = Field(gt=0, description="Requested clip duration in seconds")
|
|
34
|
+
fps: int = Field(gt=0, description="Frames per second")
|
|
35
|
+
resolution: str = Field(description="Target resolution, e.g. '1024x576'")
|
|
36
|
+
seed: int | None = Field(default=None, description="Seed for reproducible generation")
|
|
37
|
+
num_inference_steps: int | None = Field(
|
|
38
|
+
default=None,
|
|
39
|
+
gt=0,
|
|
40
|
+
description="Denoising steps; lower is faster/lower quality. Defaults to the model's own default (usually 50) when unset.",
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class PromptFields(BaseModel, ABC):
|
|
45
|
+
"""Fields shared by every video type."""
|
|
46
|
+
|
|
47
|
+
subject: str = Field(description="Main focus of the video: who/what")
|
|
48
|
+
action: str = Field(description="What happens over time; the motion")
|
|
49
|
+
scene: str = Field(description="Environment, background, time of day")
|
|
50
|
+
camera: str = Field(description="Shot type and camera movement")
|
|
51
|
+
lighting: str = Field(description="Lighting style and mood")
|
|
52
|
+
style: str = Field(description="Visual/cinematic style reference")
|
|
53
|
+
negative_prompt: str | None = Field(default=None, description="What to avoid in the generation")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class CinematicPromptFields(PromptFields):
|
|
57
|
+
"""pixar, action, animation, anime, documentary, fashion — no extra fields."""
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class UGCPromptFields(PromptFields):
|
|
61
|
+
"""ugc_product_review, commercial_product_ad, explainer_testimonial."""
|
|
62
|
+
|
|
63
|
+
dialogue: str | None = Field(
|
|
64
|
+
default=None, description="Spoken script/dialogue delivered by the subject"
|
|
65
|
+
)
|
|
66
|
+
reference_image: str | None = Field(
|
|
67
|
+
default=None, description="Reference image URL for product/subject fidelity"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class PresenterPromptFields(UGCPromptFields):
|
|
72
|
+
"""presenter_avatar."""
|
|
73
|
+
|
|
74
|
+
voice_tone: str | None = Field(
|
|
75
|
+
default=None, description="Directive for how the voice should sound, e.g. 'warm, confident, corporate'"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class SocialAdPromptFields(UGCPromptFields):
|
|
80
|
+
"""social_short_form_ad."""
|
|
81
|
+
|
|
82
|
+
text_overlay: str | None = Field(default=None, description="On-screen text/captions overlaid on the video")
|
|
83
|
+
aspect_ratio: AspectRatio = Field(default=AspectRatio.VERTICAL_9_16, description="Target aspect ratio")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class MusicVideoPromptFields(PromptFields):
|
|
87
|
+
"""music_video. No catalog model currently supports this — expect no viable model today."""
|
|
88
|
+
|
|
89
|
+
audio_reference: str = Field(description="Reference audio track/URL the visuals should sync to")
|
|
90
|
+
tempo_bpm: int | None = Field(default=None, description="Beats per minute to sync visual cuts to")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class MotionGraphicsPromptFields(PromptFields):
|
|
94
|
+
"""motion_graphics."""
|
|
95
|
+
|
|
96
|
+
text_content: str = Field(description="On-screen text/copy driving the animation")
|
|
97
|
+
transitions: str | None = Field(
|
|
98
|
+
default=None, description="Transition style between graphic elements, e.g. 'fade, slide, zoom'"
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
PROMPT_FIELDS_BY_VIDEO_TYPE: dict[VideoType, type[PromptFields]] = {
|
|
103
|
+
VideoType.PIXAR: CinematicPromptFields,
|
|
104
|
+
VideoType.ACTION: CinematicPromptFields,
|
|
105
|
+
VideoType.ANIMATION: CinematicPromptFields,
|
|
106
|
+
VideoType.ANIME: CinematicPromptFields,
|
|
107
|
+
VideoType.DOCUMENTARY: CinematicPromptFields,
|
|
108
|
+
VideoType.FASHION: CinematicPromptFields,
|
|
109
|
+
VideoType.UGC_PRODUCT_REVIEW: UGCPromptFields,
|
|
110
|
+
VideoType.COMMERCIAL_PRODUCT_AD: UGCPromptFields,
|
|
111
|
+
VideoType.EXPLAINER_TESTIMONIAL: UGCPromptFields,
|
|
112
|
+
VideoType.PRESENTER_AVATAR: PresenterPromptFields,
|
|
113
|
+
VideoType.SOCIAL_SHORT_FORM_AD: SocialAdPromptFields,
|
|
114
|
+
VideoType.MUSIC_VIDEO: MusicVideoPromptFields,
|
|
115
|
+
VideoType.MOTION_GRAPHICS: MotionGraphicsPromptFields,
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class GenerateVideoRequest(BaseModel):
|
|
120
|
+
video_type: VideoType
|
|
121
|
+
fields: PromptFields
|
|
122
|
+
params: GenerationParams
|
|
123
|
+
model: str | None = Field(
|
|
124
|
+
default=None, description="Explicit Hugging Face model id; omit to auto-select by hardware"
|
|
125
|
+
)
|
|
126
|
+
references: list[Reference] | None = Field(
|
|
127
|
+
default=None, description="Reference images; presence requires image-to-video capability"
|
|
128
|
+
)
|
|
129
|
+
vram_safety_margin: bool = Field(
|
|
130
|
+
default=True, description="Match against 85% of detected VRAM instead of 100%, for headroom"
|
|
131
|
+
)
|
|
132
|
+
low_memory_decode: bool = Field(
|
|
133
|
+
default=True, description="Decode the VAE output in slices/tiles instead of all at once"
|
|
134
|
+
)
|
|
135
|
+
cpu_offload: bool = Field(
|
|
136
|
+
default=True, description="Move pipeline components between CPU and accelerator instead of holding all at once"
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class GenerateVideoResponse(BaseModel):
|
|
141
|
+
video_path: str = Field(description="Path to the generated .mp4 on the API server")
|
|
142
|
+
model: str = Field(description="Hugging Face model id that actually ran")
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import httpx
|
|
2
|
+
|
|
3
|
+
from fraime.exceptions import FraimeAPIError, FraimeAuthError, FraimeConnectionError
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class GenerationRepository:
|
|
7
|
+
"""Raw HTTP transport to the Fraime API — no model knowledge beyond JSON in/out."""
|
|
8
|
+
|
|
9
|
+
def __init__(self, base_url: str, api_key: str | None = None, timeout: float = 600.0):
|
|
10
|
+
self._base_url = base_url.rstrip("/")
|
|
11
|
+
self._api_key = api_key
|
|
12
|
+
self._timeout = timeout
|
|
13
|
+
|
|
14
|
+
def post_generate(self, payload: dict) -> dict:
|
|
15
|
+
url = f"{self._base_url}/generate"
|
|
16
|
+
headers = {"Content-Type": "application/json"}
|
|
17
|
+
if self._api_key:
|
|
18
|
+
headers["Authorization"] = f"Bearer {self._api_key}"
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
response = httpx.post(url, json=payload, headers=headers, timeout=self._timeout)
|
|
22
|
+
except httpx.RequestError as e:
|
|
23
|
+
raise FraimeConnectionError(f"Failed to reach Fraime API at {url}: {e}") from e
|
|
24
|
+
|
|
25
|
+
if response.status_code == 401:
|
|
26
|
+
raise FraimeAuthError(response.text or "Invalid or missing API key")
|
|
27
|
+
if response.status_code >= 400:
|
|
28
|
+
raise FraimeAPIError(response.status_code, response.text)
|
|
29
|
+
|
|
30
|
+
return response.json()
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from fraime.model import GenerateVideoRequest, GenerateVideoResponse
|
|
2
|
+
from fraime.repository import GenerationRepository
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class GenerationService:
|
|
6
|
+
def __init__(self, repository: GenerationRepository):
|
|
7
|
+
self._repository = repository
|
|
8
|
+
|
|
9
|
+
def generate_video(self, request: GenerateVideoRequest) -> GenerateVideoResponse:
|
|
10
|
+
payload = request.model_dump(mode="json", exclude_none=True, exclude={"fields"})
|
|
11
|
+
payload["fields"] = request.fields.model_dump(mode="json", exclude_none=True)
|
|
12
|
+
|
|
13
|
+
raw = self._repository.post_generate(payload)
|
|
14
|
+
return GenerateVideoResponse.model_validate(raw)
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fraime-sdk
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Python SDK for the Fraime video generation API
|
|
5
|
+
Author: Santiago Melo Medina
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Requires-Python: >=3.11
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Dist: httpx
|
|
14
|
+
Requires-Dist: pydantic
|
|
15
|
+
Provides-Extra: dev
|
|
16
|
+
Requires-Dist: build; extra == "dev"
|
|
17
|
+
Requires-Dist: twine; extra == "dev"
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
|
|
20
|
+
# Fraime SDK
|
|
21
|
+
|
|
22
|
+
Python client for the [Fraime API](../api/README.md) — build a request with
|
|
23
|
+
typed models/enums instead of hand-writing JSON, point it at a running
|
|
24
|
+
Fraime instance, get a generated video back.
|
|
25
|
+
|
|
26
|
+
## Prerequisites
|
|
27
|
+
|
|
28
|
+
- Python 3.11+
|
|
29
|
+
- A running Fraime API instance to talk to (see [`api/README.md`](../api/README.md)
|
|
30
|
+
for how to run one) — you'll need its base URL, and its API key if one is
|
|
31
|
+
configured (`AUTH_API_KEY` on the API side).
|
|
32
|
+
|
|
33
|
+
## Install
|
|
34
|
+
|
|
35
|
+
### Option 1 — pip
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install fraime-sdk
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Option 2 — from a local clone
|
|
42
|
+
|
|
43
|
+
Step by step, from scratch:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
# 1. Clone the repo (skip if you already have it)
|
|
47
|
+
git clone <this-repo-url>
|
|
48
|
+
cd fraime
|
|
49
|
+
|
|
50
|
+
# 2. (Recommended) create a virtualenv for your own project
|
|
51
|
+
python3 -m venv .venv
|
|
52
|
+
source .venv/bin/activate
|
|
53
|
+
|
|
54
|
+
# 3. Install the SDK from the sdk/ folder
|
|
55
|
+
pip install ./sdk
|
|
56
|
+
# or, for local development on the SDK itself (editable install):
|
|
57
|
+
pip install -e ./sdk
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
That's it — `import fraime` is now available in that environment.
|
|
61
|
+
|
|
62
|
+
### Option 3 — straight from git, no local clone needed
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
pip install "git+ssh://git@santiago/santiagoMeloMedina/fraime.git#subdirectory=sdk"
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
(Adjust the URL to whatever remote you actually have push/pull access to —
|
|
69
|
+
this is this repo's own `origin` in SSH form.)
|
|
70
|
+
|
|
71
|
+
## Usage
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from fraime import FraimeClient, VideoType, GenerationParams, CinematicPromptFields
|
|
75
|
+
|
|
76
|
+
client = FraimeClient(
|
|
77
|
+
base_url="http://127.0.0.1:8000", # or set FRAIME_BASE_URL instead
|
|
78
|
+
api_key="your-api-key", # or set FRAIME_API_KEY instead; omit both if the API has none configured
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
response = client.generate(
|
|
82
|
+
video_type=VideoType.PIXAR,
|
|
83
|
+
fields=CinematicPromptFields(
|
|
84
|
+
subject="a small orange fox with oversized ears",
|
|
85
|
+
action="hops between rocks, pauses, and looks up curiously",
|
|
86
|
+
scene="a sunlit forest clearing at golden hour",
|
|
87
|
+
camera="medium shot, slow dolly-in",
|
|
88
|
+
lighting="warm rim lighting from the low sun",
|
|
89
|
+
style="3D animated feature style, stylized proportions, warm rim lighting",
|
|
90
|
+
),
|
|
91
|
+
params=GenerationParams(duration_s=3, fps=16, resolution="768x512"),
|
|
92
|
+
# model=... # optional: pin an exact model instead of auto-selecting
|
|
93
|
+
# references=[...] # optional: Reference(url=...) list, for image-to-video
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
print(response.video_path, response.model)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
`model` is optional on `client.generate()` — omit it and the API auto-selects
|
|
100
|
+
by hardware, same as calling it directly.
|
|
101
|
+
|
|
102
|
+
### Picking the right fields class per video type
|
|
103
|
+
|
|
104
|
+
Every `video_type` has its own field set — some add fields the base six
|
|
105
|
+
(`subject`, `action`, `scene`, `camera`, `lighting`, `style`,
|
|
106
|
+
`negative_prompt`) don't cover:
|
|
107
|
+
|
|
108
|
+
| `VideoType` | Fields class | Extra fields |
|
|
109
|
+
|---|---|---|
|
|
110
|
+
| `PIXAR`, `ACTION`, `ANIMATION`, `ANIME`, `DOCUMENTARY`, `FASHION` | `CinematicPromptFields` | — |
|
|
111
|
+
| `UGC_PRODUCT_REVIEW`, `COMMERCIAL_PRODUCT_AD`, `EXPLAINER_TESTIMONIAL` | `UGCPromptFields` | `dialogue`, `reference_image` |
|
|
112
|
+
| `PRESENTER_AVATAR` | `PresenterPromptFields` | + `voice_tone` |
|
|
113
|
+
| `SOCIAL_SHORT_FORM_AD` | `SocialAdPromptFields` | + `text_overlay`, `aspect_ratio` |
|
|
114
|
+
| `MUSIC_VIDEO` | `MusicVideoPromptFields` | `audio_reference`, `tempo_bpm` |
|
|
115
|
+
| `MOTION_GRAPHICS` | `MotionGraphicsPromptFields` | `text_content`, `transitions` |
|
|
116
|
+
|
|
117
|
+
Not sure which class a given `VideoType` needs? Look it up instead of
|
|
118
|
+
guessing:
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
from fraime import PROMPT_FIELDS_BY_VIDEO_TYPE, VideoType
|
|
122
|
+
|
|
123
|
+
fields_class = PROMPT_FIELDS_BY_VIDEO_TYPE[VideoType.SOCIAL_SHORT_FORM_AD]
|
|
124
|
+
# -> SocialAdPromptFields
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Reference images (image-to-video)
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
from fraime import Reference
|
|
131
|
+
|
|
132
|
+
response = client.generate(
|
|
133
|
+
video_type=VideoType.UGC_PRODUCT_REVIEW,
|
|
134
|
+
fields=ugc_fields,
|
|
135
|
+
params=params,
|
|
136
|
+
references=[Reference(url="https://example.com/product-photo.jpg")],
|
|
137
|
+
)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Error handling
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
from fraime import FraimeAuthError, FraimeAPIError, FraimeConnectionError
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
response = client.generate(video_type=VideoType.PIXAR, fields=fields, params=params)
|
|
147
|
+
except FraimeAuthError:
|
|
148
|
+
... # missing/invalid API key
|
|
149
|
+
except FraimeAPIError as e:
|
|
150
|
+
... # e.status_code, e.detail — the API reached but returned an error
|
|
151
|
+
except FraimeConnectionError:
|
|
152
|
+
... # couldn't reach the API at all
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Configuration reference
|
|
156
|
+
|
|
157
|
+
| `FraimeClient(...)` argument | Env var fallback | Default |
|
|
158
|
+
|---|---|---|
|
|
159
|
+
| `base_url` | `FRAIME_BASE_URL` | `http://127.0.0.1:8000` |
|
|
160
|
+
| `api_key` | `FRAIME_API_KEY` | none (open API) |
|
|
161
|
+
| `timeout` | — | `600.0` seconds |
|
|
162
|
+
|
|
163
|
+
`timeout` defaults high on purpose — real generation runs can take several
|
|
164
|
+
minutes; see [`api/README.md`](../api/README.md) for why.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
fraime/__init__.py
|
|
5
|
+
fraime/exceptions.py
|
|
6
|
+
fraime/main.py
|
|
7
|
+
fraime/model.py
|
|
8
|
+
fraime/repository.py
|
|
9
|
+
fraime/service.py
|
|
10
|
+
fraime_sdk.egg-info/PKG-INFO
|
|
11
|
+
fraime_sdk.egg-info/SOURCES.txt
|
|
12
|
+
fraime_sdk.egg-info/dependency_links.txt
|
|
13
|
+
fraime_sdk.egg-info/requires.txt
|
|
14
|
+
fraime_sdk.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
fraime
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "fraime-sdk"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Python SDK for the Fraime video generation API"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
requires-python = ">=3.11"
|
|
13
|
+
authors = [
|
|
14
|
+
{ name = "Santiago Melo Medina" },
|
|
15
|
+
]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.11",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
]
|
|
21
|
+
dependencies = [
|
|
22
|
+
"httpx",
|
|
23
|
+
"pydantic",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.optional-dependencies]
|
|
27
|
+
dev = [
|
|
28
|
+
"build",
|
|
29
|
+
"twine",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[tool.setuptools]
|
|
33
|
+
packages = ["fraime"]
|