-
Notifications
You must be signed in to change notification settings - Fork 403
Expand file tree
/
Copy pathapp_settings.py
More file actions
200 lines (151 loc) · 6.33 KB
/
Copy pathapp_settings.py
File metadata and controls
200 lines (151 loc) · 6.33 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
"""Canonical app settings schema and patch models."""
from __future__ import annotations
from typing import Any, TypeGuard, TypeVar, cast, get_args
from pydantic import BaseModel, ConfigDict, Field, create_model, field_validator
def _to_camel_case(field_name: str) -> str:
special_aliases = {
"prompt_enhancer_enabled_t2v": "promptEnhancerEnabledT2V",
"prompt_enhancer_enabled_i2v": "promptEnhancerEnabledI2V",
}
if field_name in special_aliases:
return special_aliases[field_name]
head, *tail = field_name.split("_")
return head + "".join(part.title() for part in tail)
def _clamp_int(value: Any, minimum: int, maximum: int, default: int) -> int:
if value is None:
return default
parsed = int(value)
return max(minimum, min(maximum, parsed))
class SettingsBaseModel(BaseModel):
model_config = ConfigDict(
alias_generator=_to_camel_case,
populate_by_name=True,
validate_assignment=True,
extra="ignore",
)
class SettingsPatchModel(SettingsBaseModel):
model_config = ConfigDict(
alias_generator=_to_camel_case,
populate_by_name=True,
validate_assignment=True,
extra="forbid",
)
class FastModelSettings(SettingsBaseModel):
use_upscaler: bool = True
class ProModelSettings(SettingsBaseModel):
steps: int = 20
use_upscaler: bool = True
@field_validator("steps", mode="before")
@classmethod
def _clamp_steps(cls, value: Any) -> int:
return _clamp_int(value, minimum=1, maximum=100, default=20)
class AppSettings(SettingsBaseModel):
use_torch_compile: bool = False
load_on_startup: bool = False
ltx_api_key: str = ""
user_prefers_ltx_api_video_generations: bool = False
fal_api_key: str = ""
use_local_text_encoder: bool = False
fast_model: FastModelSettings = Field(default_factory=FastModelSettings)
pro_model: ProModelSettings = Field(default_factory=ProModelSettings)
prompt_cache_size: int = 100
prompt_enhancer_enabled_t2v: bool = True
prompt_enhancer_enabled_i2v: bool = False
gemini_api_key: str = ""
seed_locked: bool = False
locked_seed: int = 42
models_dir: str = ""
# Generation defaults (persisted across sessions)
default_model: str = "fast"
default_duration: int = 5
default_video_resolution: str = "540p"
default_fps: int = 24
default_aspect_ratio: str = "16:9"
default_camera_motion: str = "none"
# Player preferences
player_muted: bool = False
@field_validator("prompt_cache_size", mode="before")
@classmethod
def _clamp_prompt_cache_size(cls, value: Any) -> int:
return _clamp_int(value, minimum=0, maximum=1000, default=100)
@field_validator("locked_seed", mode="before")
@classmethod
def _clamp_locked_seed(cls, value: Any) -> int:
return _clamp_int(value, minimum=0, maximum=2_147_483_647, default=42)
@field_validator("default_duration", mode="before")
@classmethod
def _clamp_default_duration(cls, value: Any) -> int:
return _clamp_int(value, minimum=1, maximum=20, default=5)
@field_validator("default_fps", mode="before")
@classmethod
def _clamp_default_fps(cls, value: Any) -> int:
return _clamp_int(value, minimum=1, maximum=60, default=24)
SettingsModelT = TypeVar("SettingsModelT", bound=SettingsBaseModel)
_PARTIAL_MODEL_CACHE: dict[type[SettingsBaseModel], type[SettingsPatchModel]] = {}
def _wrap_optional(annotation: Any) -> Any:
if type(None) in get_args(annotation):
return annotation
return annotation | None
def _to_partial_annotation(annotation: Any) -> Any:
if _is_settings_model_annotation(annotation):
return make_partial_model(annotation)
return annotation
def make_partial_model(model: type[SettingsModelT]) -> type[SettingsPatchModel]:
cached = _PARTIAL_MODEL_CACHE.get(model)
if cached is not None:
return cached
fields: dict[str, tuple[Any, Any]] = {}
for field_name, field_info in model.model_fields.items():
partial_annotation = _wrap_optional(_to_partial_annotation(field_info.annotation))
fields[field_name] = (partial_annotation, Field(default=None))
partial_model = create_model(
f"{model.__name__}Patch",
__base__=SettingsPatchModel,
**cast(Any, fields),
)
_PARTIAL_MODEL_CACHE[model] = partial_model
return partial_model
def _is_settings_model_annotation(annotation: object) -> TypeGuard[type[SettingsBaseModel]]:
return isinstance(annotation, type) and issubclass(annotation, SettingsBaseModel)
AppSettingsPatch = make_partial_model(AppSettings)
UpdateSettingsRequest = AppSettingsPatch
class SettingsResponse(SettingsBaseModel):
use_torch_compile: bool = False
load_on_startup: bool = False
has_ltx_api_key: bool = False
user_prefers_ltx_api_video_generations: bool = False
has_fal_api_key: bool = False
use_local_text_encoder: bool = False
fast_model: FastModelSettings = Field(default_factory=FastModelSettings)
pro_model: ProModelSettings = Field(default_factory=ProModelSettings)
prompt_cache_size: int = 100
prompt_enhancer_enabled_t2v: bool = True
prompt_enhancer_enabled_i2v: bool = False
has_gemini_api_key: bool = False
seed_locked: bool = False
locked_seed: int = 42
models_dir: str = ""
# Generation defaults
default_model: str = "fast"
default_duration: int = 5
default_video_resolution: str = "540p"
default_fps: int = 24
default_aspect_ratio: str = "16:9"
default_camera_motion: str = "none"
# Player preferences
player_muted: bool = False
def to_settings_response(settings: AppSettings) -> SettingsResponse:
data = settings.model_dump(by_alias=False)
ltx_key = data.pop("ltx_api_key", "")
fal_key = data.pop("fal_api_key", "")
gemini_key = data.pop("gemini_api_key", "")
data["has_ltx_api_key"] = bool(ltx_key)
data["has_fal_api_key"] = bool(fal_key)
data["has_gemini_api_key"] = bool(gemini_key)
# models_dir passes through as-is (not secret)
return SettingsResponse.model_validate(data)
def should_video_generate_with_ltx_api(*, force_api_generations: bool, settings: AppSettings) -> bool:
has_ltx_api_key = bool(settings.ltx_api_key.strip())
return force_api_generations or (
settings.user_prefers_ltx_api_video_generations and has_ltx_api_key
)