861052359 commited on
Commit ·
22e2a53
1
Parent(s): d14bc39
feat: 生图冗余倍率配置与前端界面
Browse files- VERSION +1 -1
- config.json +6 -5
- services/config.py +11 -0
- services/protocol/conversation.py +47 -37
- web/src/app/settings/components/config-card.tsx +13 -0
- web/src/app/settings/store.ts +7 -0
- web/src/lib/api.ts +1 -0
VERSION
CHANGED
|
@@ -1 +1 @@
|
|
| 1 |
-
1.
|
|
|
|
| 1 |
+
1.5.1
|
config.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
| 1 |
{
|
| 2 |
"auth-key": "chatgpt2api",
|
| 3 |
"refresh_account_interval_minute": 60,
|
| 4 |
-
"image_retention_days":
|
| 5 |
"image_poll_timeout_secs": 120,
|
| 6 |
-
"auto_remove_rate_limited_accounts":
|
| 7 |
"auto_remove_invalid_accounts": true,
|
| 8 |
"log_levels": [
|
| 9 |
"debug",
|
|
@@ -48,6 +48,7 @@
|
|
| 48 |
},
|
| 49 |
"image_account_concurrency": 3,
|
| 50 |
"image_parallel_generation": true,
|
|
|
|
| 51 |
"image_poll_interval_secs": 10,
|
| 52 |
"image_poll_initial_wait_secs": 10,
|
| 53 |
"image_storage": {
|
|
@@ -70,9 +71,9 @@
|
|
| 70 |
"drop_adjacent_duplicates": true,
|
| 71 |
"drop_assistant_history": false
|
| 72 |
},
|
| 73 |
-
"image_settle_enabled":
|
| 74 |
-
"image_check_before_hit_enabled":
|
| 75 |
"image_settle_secs": 2,
|
| 76 |
-
"auto_relogin_after_refresh":
|
| 77 |
"image_timeout_retry_secs": 30
|
| 78 |
}
|
|
|
|
| 1 |
{
|
| 2 |
"auth-key": "chatgpt2api",
|
| 3 |
"refresh_account_interval_minute": 60,
|
| 4 |
+
"image_retention_days": 1000,
|
| 5 |
"image_poll_timeout_secs": 120,
|
| 6 |
+
"auto_remove_rate_limited_accounts": true,
|
| 7 |
"auto_remove_invalid_accounts": true,
|
| 8 |
"log_levels": [
|
| 9 |
"debug",
|
|
|
|
| 48 |
},
|
| 49 |
"image_account_concurrency": 3,
|
| 50 |
"image_parallel_generation": true,
|
| 51 |
+
"image_redundancy_multiplier": 1,
|
| 52 |
"image_poll_interval_secs": 10,
|
| 53 |
"image_poll_initial_wait_secs": 10,
|
| 54 |
"image_storage": {
|
|
|
|
| 71 |
"drop_adjacent_duplicates": true,
|
| 72 |
"drop_assistant_history": false
|
| 73 |
},
|
| 74 |
+
"image_settle_enabled": true,
|
| 75 |
+
"image_check_before_hit_enabled": true,
|
| 76 |
"image_settle_secs": 2,
|
| 77 |
+
"auto_relogin_after_refresh": true,
|
| 78 |
"image_timeout_retry_secs": 30
|
| 79 |
}
|
services/config.py
CHANGED
|
@@ -305,6 +305,16 @@ class ConfigStore:
|
|
| 305 |
return value.strip().lower() in {"1", "true", "yes", "on"}
|
| 306 |
return bool(value)
|
| 307 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 308 |
@property
|
| 309 |
def image_settle_enabled(self) -> bool:
|
| 310 |
"""图片二次确认机制:找到 file_ids 后等待一段时间再次确认。"""
|
|
@@ -423,6 +433,7 @@ class ConfigStore:
|
|
| 423 |
data["image_poll_initial_wait_secs"] = self.image_poll_initial_wait_secs
|
| 424 |
data["image_account_concurrency"] = self.image_account_concurrency
|
| 425 |
data["image_parallel_generation"] = self.image_parallel_generation
|
|
|
|
| 426 |
data["auto_remove_invalid_accounts"] = self.auto_remove_invalid_accounts
|
| 427 |
data["auto_remove_rate_limited_accounts"] = self.auto_remove_rate_limited_accounts
|
| 428 |
data["auto_relogin_after_refresh"] = self.auto_relogin_after_refresh
|
|
|
|
| 305 |
return value.strip().lower() in {"1", "true", "yes", "on"}
|
| 306 |
return bool(value)
|
| 307 |
|
| 308 |
+
@property
|
| 309 |
+
def image_redundancy_multiplier(self) -> float:
|
| 310 |
+
"""生图冗余倍率:请求 N 张时实际生成 N * multiplier 张,只返回 N 张。
|
| 311 |
+
通过多账号并发提高出图成功率(某个任务失败时可用其他结果替补)。
|
| 312 |
+
默认 1.0 表示不开启。"""
|
| 313 |
+
try:
|
| 314 |
+
return max(1.0, float(self.data.get("image_redundancy_multiplier", 1.0)))
|
| 315 |
+
except (TypeError, ValueError):
|
| 316 |
+
return 1.0
|
| 317 |
+
|
| 318 |
@property
|
| 319 |
def image_settle_enabled(self) -> bool:
|
| 320 |
"""图片二次确认机制:找到 file_ids 后等待一段时间再次确认。"""
|
|
|
|
| 433 |
data["image_poll_initial_wait_secs"] = self.image_poll_initial_wait_secs
|
| 434 |
data["image_account_concurrency"] = self.image_account_concurrency
|
| 435 |
data["image_parallel_generation"] = self.image_parallel_generation
|
| 436 |
+
data["image_redundancy_multiplier"] = self.image_redundancy_multiplier
|
| 437 |
data["auto_remove_invalid_accounts"] = self.auto_remove_invalid_accounts
|
| 438 |
data["auto_remove_rate_limited_accounts"] = self.auto_remove_rate_limited_accounts
|
| 439 |
data["auto_relogin_after_refresh"] = self.auto_relogin_after_refresh
|
services/protocol/conversation.py
CHANGED
|
@@ -1439,86 +1439,96 @@ def _generate_single_image(
|
|
| 1439 |
|
| 1440 |
|
| 1441 |
def stream_image_outputs_with_pool(request: ConversationRequest) -> Iterator[ImageOutput]:
|
| 1442 |
-
"""并行生成多张图片,每张图片使用独立线程和账号,互不阻塞。
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1443 |
if not is_supported_image_model(request.model):
|
| 1444 |
raise ImageGenerationError("unsupported image model,supported models: " + ", ".join(sorted(IMAGE_MODELS)))
|
| 1445 |
|
| 1446 |
-
|
| 1447 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1448 |
outputs = _generate_single_image(request, 1, 1)
|
| 1449 |
for output in outputs:
|
| 1450 |
yield output
|
| 1451 |
return
|
| 1452 |
|
| 1453 |
-
|
| 1454 |
-
if not config.image_parallel_generation:
|
| 1455 |
logger.info({
|
| 1456 |
"event": "image_serial_generation_start",
|
| 1457 |
-
"n":
|
| 1458 |
"model": request.model,
|
| 1459 |
})
|
| 1460 |
-
for index in range(1,
|
| 1461 |
-
outputs = _generate_single_image(request, index,
|
| 1462 |
for output in outputs:
|
| 1463 |
yield output
|
| 1464 |
return
|
| 1465 |
|
| 1466 |
logger.info({
|
| 1467 |
"event": "image_parallel_generation_start",
|
| 1468 |
-
"n":
|
|
|
|
|
|
|
| 1469 |
"model": request.model,
|
| 1470 |
})
|
| 1471 |
-
# 每张图片一个线程,同时启动
|
| 1472 |
futures = {}
|
| 1473 |
results: dict[int, list[ImageOutput]] = {}
|
| 1474 |
errors: dict[int, Exception] = {}
|
| 1475 |
-
with ThreadPoolExecutor(max_workers=
|
| 1476 |
-
for
|
| 1477 |
-
future = executor.submit(_generate_single_image, request,
|
| 1478 |
-
futures[future] =
|
| 1479 |
|
| 1480 |
-
# 按完成顺序收集结果
|
| 1481 |
for future in as_completed(futures):
|
| 1482 |
-
|
| 1483 |
try:
|
| 1484 |
-
results[
|
| 1485 |
except Exception as exc:
|
| 1486 |
-
errors[
|
| 1487 |
logger.warning({
|
| 1488 |
"event": "image_parallel_generation_error",
|
| 1489 |
-
"index":
|
| 1490 |
"error": str(exc)[:300],
|
| 1491 |
})
|
| 1492 |
|
| 1493 |
-
|
| 1494 |
-
emitted = False
|
| 1495 |
last_error = ""
|
| 1496 |
-
|
| 1497 |
-
|
| 1498 |
-
|
| 1499 |
-
|
| 1500 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1501 |
yield output
|
| 1502 |
-
elif
|
| 1503 |
-
last_error = str(errors[
|
| 1504 |
-
if
|
| 1505 |
logger.warning({
|
| 1506 |
"event": "image_parallel_failure_before_success",
|
| 1507 |
-
"failed_index":
|
| 1508 |
"error": last_error[:200],
|
| 1509 |
})
|
| 1510 |
|
| 1511 |
-
|
| 1512 |
-
|
| 1513 |
-
|
| 1514 |
-
if index in errors:
|
| 1515 |
logger.warning({
|
| 1516 |
"event": "image_parallel_partial_failure",
|
| 1517 |
-
"
|
| 1518 |
-
"error": str(errors[
|
| 1519 |
})
|
| 1520 |
|
| 1521 |
-
if
|
| 1522 |
if not last_error:
|
| 1523 |
last_error = "no account in the pool could generate images — check account quota and rate-limit status"
|
| 1524 |
raise ImageGenerationError(image_stream_error_message(last_error), conversation_id="")
|
|
|
|
| 1439 |
|
| 1440 |
|
| 1441 |
def stream_image_outputs_with_pool(request: ConversationRequest) -> Iterator[ImageOutput]:
|
| 1442 |
+
"""并行生成多张图片,每张图片使用独立线程和账号,互不阻塞。
|
| 1443 |
+
|
| 1444 |
+
支持冗余倍率:当 multiplier > 1 时,实际生成 N * multiplier 张,
|
| 1445 |
+
但只返回 N 张给用户,其余作为冗余提高出图成功率。
|
| 1446 |
+
"""
|
| 1447 |
if not is_supported_image_model(request.model):
|
| 1448 |
raise ImageGenerationError("unsupported image model,supported models: " + ", ".join(sorted(IMAGE_MODELS)))
|
| 1449 |
|
| 1450 |
+
user_n = request.n
|
| 1451 |
+
multiplier = config.image_redundancy_multiplier
|
| 1452 |
+
effective_n = user_n
|
| 1453 |
+
if multiplier > 1.0:
|
| 1454 |
+
effective_n = min(max(1, int(round(user_n * multiplier))), 8)
|
| 1455 |
+
|
| 1456 |
+
if effective_n <= 1:
|
| 1457 |
outputs = _generate_single_image(request, 1, 1)
|
| 1458 |
for output in outputs:
|
| 1459 |
yield output
|
| 1460 |
return
|
| 1461 |
|
| 1462 |
+
if not config.image_parallel_generation and multiplier <= 1.0:
|
|
|
|
| 1463 |
logger.info({
|
| 1464 |
"event": "image_serial_generation_start",
|
| 1465 |
+
"n": user_n,
|
| 1466 |
"model": request.model,
|
| 1467 |
})
|
| 1468 |
+
for index in range(1, effective_n + 1):
|
| 1469 |
+
outputs = _generate_single_image(request, index, effective_n)
|
| 1470 |
for output in outputs:
|
| 1471 |
yield output
|
| 1472 |
return
|
| 1473 |
|
| 1474 |
logger.info({
|
| 1475 |
"event": "image_parallel_generation_start",
|
| 1476 |
+
"n": user_n,
|
| 1477 |
+
"effective_n": effective_n,
|
| 1478 |
+
"multiplier": multiplier,
|
| 1479 |
"model": request.model,
|
| 1480 |
})
|
|
|
|
| 1481 |
futures = {}
|
| 1482 |
results: dict[int, list[ImageOutput]] = {}
|
| 1483 |
errors: dict[int, Exception] = {}
|
| 1484 |
+
with ThreadPoolExecutor(max_workers=effective_n) as executor:
|
| 1485 |
+
for idx in range(1, effective_n + 1):
|
| 1486 |
+
future = executor.submit(_generate_single_image, request, idx, effective_n)
|
| 1487 |
+
futures[future] = idx
|
| 1488 |
|
|
|
|
| 1489 |
for future in as_completed(futures):
|
| 1490 |
+
idx = futures[future]
|
| 1491 |
try:
|
| 1492 |
+
results[idx] = future.result()
|
| 1493 |
except Exception as exc:
|
| 1494 |
+
errors[idx] = exc
|
| 1495 |
logger.warning({
|
| 1496 |
"event": "image_parallel_generation_error",
|
| 1497 |
+
"index": idx,
|
| 1498 |
"error": str(exc)[:300],
|
| 1499 |
})
|
| 1500 |
|
| 1501 |
+
emitted = 0
|
|
|
|
| 1502 |
last_error = ""
|
| 1503 |
+
for idx in range(1, effective_n + 1):
|
| 1504 |
+
if emitted >= user_n:
|
| 1505 |
+
break
|
| 1506 |
+
if idx in results:
|
| 1507 |
+
for output in results[idx]:
|
| 1508 |
+
if output.kind == "result":
|
| 1509 |
+
if emitted >= user_n:
|
| 1510 |
+
break
|
| 1511 |
+
emitted += 1
|
| 1512 |
yield output
|
| 1513 |
+
elif idx in errors:
|
| 1514 |
+
last_error = str(errors[idx])
|
| 1515 |
+
if emitted == 0:
|
| 1516 |
logger.warning({
|
| 1517 |
"event": "image_parallel_failure_before_success",
|
| 1518 |
+
"failed_index": idx,
|
| 1519 |
"error": last_error[:200],
|
| 1520 |
})
|
| 1521 |
|
| 1522 |
+
if emitted >= user_n:
|
| 1523 |
+
for idx in range(1, effective_n + 1):
|
| 1524 |
+
if idx in errors:
|
|
|
|
| 1525 |
logger.warning({
|
| 1526 |
"event": "image_parallel_partial_failure",
|
| 1527 |
+
"index": idx,
|
| 1528 |
+
"error": str(errors[idx])[:200],
|
| 1529 |
})
|
| 1530 |
|
| 1531 |
+
if emitted == 0:
|
| 1532 |
if not last_error:
|
| 1533 |
last_error = "no account in the pool could generate images — check account quota and rate-limit status"
|
| 1534 |
raise ImageGenerationError(image_stream_error_message(last_error), conversation_id="")
|
web/src/app/settings/components/config-card.tsx
CHANGED
|
@@ -26,6 +26,7 @@ export function ConfigCard() {
|
|
| 26 |
const setImageRetentionDays = useSettingsStore((state) => state.setImageRetentionDays);
|
| 27 |
const setImagePollTimeoutSecs = useSettingsStore((state) => state.setImagePollTimeoutSecs);
|
| 28 |
const setImageAccountConcurrency = useSettingsStore((state) => state.setImageAccountConcurrency);
|
|
|
|
| 29 |
const setImageSettleEnabled = useSettingsStore((state) => state.setImageSettleEnabled);
|
| 30 |
const setImageSettleSecs = useSettingsStore((state) => state.setImageSettleSecs);
|
| 31 |
const setImageTimeoutRetrySecs = useSettingsStore((state) => state.setImageTimeoutRetrySecs);
|
|
@@ -173,6 +174,18 @@ export function ConfigCard() {
|
|
| 173 |
/>
|
| 174 |
<p className="text-xs text-stone-500">限制每个账号同时处理的图片请求数量,默认 3。</p>
|
| 175 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 176 |
<div className="space-y-2">
|
| 177 |
<label className="flex items-center gap-3 rounded-xl border border-stone-200 bg-white px-4 py-3 text-sm text-stone-700">
|
| 178 |
<Checkbox
|
|
|
|
| 26 |
const setImageRetentionDays = useSettingsStore((state) => state.setImageRetentionDays);
|
| 27 |
const setImagePollTimeoutSecs = useSettingsStore((state) => state.setImagePollTimeoutSecs);
|
| 28 |
const setImageAccountConcurrency = useSettingsStore((state) => state.setImageAccountConcurrency);
|
| 29 |
+
const setImageRedundancyMultiplier = useSettingsStore((state) => state.setImageRedundancyMultiplier);
|
| 30 |
const setImageSettleEnabled = useSettingsStore((state) => state.setImageSettleEnabled);
|
| 31 |
const setImageSettleSecs = useSettingsStore((state) => state.setImageSettleSecs);
|
| 32 |
const setImageTimeoutRetrySecs = useSettingsStore((state) => state.setImageTimeoutRetrySecs);
|
|
|
|
| 174 |
/>
|
| 175 |
<p className="text-xs text-stone-500">限制每个账号同时处理的图片请求数量,默认 3。</p>
|
| 176 |
</div>
|
| 177 |
+
<div className="space-y-2">
|
| 178 |
+
<label className="text-sm text-stone-700">生图冗余倍率</label>
|
| 179 |
+
<Input
|
| 180 |
+
value={String(config?.image_redundancy_multiplier || "1.0")}
|
| 181 |
+
onChange={(event) => setImageRedundancyMultiplier(event.target.value)}
|
| 182 |
+
placeholder="1.0"
|
| 183 |
+
className="h-10 rounded-xl border-stone-200 bg-white"
|
| 184 |
+
/>
|
| 185 |
+
<p className="text-xs text-stone-500">
|
| 186 |
+
请求 N 张时实际生成 N×倍率 张,只返回最佳 N 张。通过多账号并发提高出图成功率,默认 1.0(不开启)。
|
| 187 |
+
</p>
|
| 188 |
+
</div>
|
| 189 |
<div className="space-y-2">
|
| 190 |
<label className="flex items-center gap-3 rounded-xl border border-stone-200 bg-white px-4 py-3 text-sm text-stone-700">
|
| 191 |
<Checkbox
|
web/src/app/settings/store.ts
CHANGED
|
@@ -87,6 +87,7 @@ function normalizeConfig(config: SettingsConfig): SettingsConfig {
|
|
| 87 |
image_retention_days: Number(config.image_retention_days || 30),
|
| 88 |
image_poll_timeout_secs: Number(config.image_poll_timeout_secs || 120),
|
| 89 |
image_account_concurrency: Number(config.image_account_concurrency || 3),
|
|
|
|
| 90 |
image_settle_enabled: Boolean(config.image_settle_enabled !== false),
|
| 91 |
image_check_before_hit_enabled: Boolean(config.image_check_before_hit_enabled !== false),
|
| 92 |
image_settle_secs: Number(config.image_settle_secs || 2.0),
|
|
@@ -209,6 +210,7 @@ type SettingsStore = {
|
|
| 209 |
setImageRetentionDays: (value: string) => void;
|
| 210 |
setImagePollTimeoutSecs: (value: string) => void;
|
| 211 |
setImageAccountConcurrency: (value: string) => void;
|
|
|
|
| 212 |
setImageSettleEnabled: (value: boolean) => void;
|
| 213 |
setImageCheckBeforeHitEnabled: (value: boolean) => void;
|
| 214 |
setImageSettleSecs: (value: string) => void;
|
|
@@ -350,6 +352,7 @@ export const useSettingsStore = create<SettingsStore>((set, get) => ({
|
|
| 350 |
image_retention_days: Math.max(1, Number(config.image_retention_days) || 30),
|
| 351 |
image_poll_timeout_secs: Math.max(1, Number(config.image_poll_timeout_secs) || 120),
|
| 352 |
image_account_concurrency: Math.max(1, Number(config.image_account_concurrency) || 3),
|
|
|
|
| 353 |
image_settle_enabled: Boolean(config.image_settle_enabled !== false),
|
| 354 |
image_check_before_hit_enabled: Boolean(config.image_check_before_hit_enabled !== false),
|
| 355 |
image_settle_secs: Math.max(0.5, Number(config.image_settle_secs) || 2.0),
|
|
@@ -428,6 +431,10 @@ export const useSettingsStore = create<SettingsStore>((set, get) => ({
|
|
| 428 |
set((state) => state.config ? { config: { ...state.config, image_account_concurrency: value } } : {});
|
| 429 |
},
|
| 430 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 431 |
setImageSettleEnabled: (value) => {
|
| 432 |
set((state) => state.config ? { config: { ...state.config, image_settle_enabled: value, image_check_before_hit_enabled: value } } : {});
|
| 433 |
},
|
|
|
|
| 87 |
image_retention_days: Number(config.image_retention_days || 30),
|
| 88 |
image_poll_timeout_secs: Number(config.image_poll_timeout_secs || 120),
|
| 89 |
image_account_concurrency: Number(config.image_account_concurrency || 3),
|
| 90 |
+
image_redundancy_multiplier: Number(config.image_redundancy_multiplier || 1.0),
|
| 91 |
image_settle_enabled: Boolean(config.image_settle_enabled !== false),
|
| 92 |
image_check_before_hit_enabled: Boolean(config.image_check_before_hit_enabled !== false),
|
| 93 |
image_settle_secs: Number(config.image_settle_secs || 2.0),
|
|
|
|
| 210 |
setImageRetentionDays: (value: string) => void;
|
| 211 |
setImagePollTimeoutSecs: (value: string) => void;
|
| 212 |
setImageAccountConcurrency: (value: string) => void;
|
| 213 |
+
setImageRedundancyMultiplier: (value: string) => void;
|
| 214 |
setImageSettleEnabled: (value: boolean) => void;
|
| 215 |
setImageCheckBeforeHitEnabled: (value: boolean) => void;
|
| 216 |
setImageSettleSecs: (value: string) => void;
|
|
|
|
| 352 |
image_retention_days: Math.max(1, Number(config.image_retention_days) || 30),
|
| 353 |
image_poll_timeout_secs: Math.max(1, Number(config.image_poll_timeout_secs) || 120),
|
| 354 |
image_account_concurrency: Math.max(1, Number(config.image_account_concurrency) || 3),
|
| 355 |
+
image_redundancy_multiplier: Math.max(1.0, Number(config.image_redundancy_multiplier) || 1.0),
|
| 356 |
image_settle_enabled: Boolean(config.image_settle_enabled !== false),
|
| 357 |
image_check_before_hit_enabled: Boolean(config.image_check_before_hit_enabled !== false),
|
| 358 |
image_settle_secs: Math.max(0.5, Number(config.image_settle_secs) || 2.0),
|
|
|
|
| 431 |
set((state) => state.config ? { config: { ...state.config, image_account_concurrency: value } } : {});
|
| 432 |
},
|
| 433 |
|
| 434 |
+
setImageRedundancyMultiplier: (value) => {
|
| 435 |
+
set((state) => state.config ? { config: { ...state.config, image_redundancy_multiplier: value } } : {});
|
| 436 |
+
},
|
| 437 |
+
|
| 438 |
setImageSettleEnabled: (value) => {
|
| 439 |
set((state) => state.config ? { config: { ...state.config, image_settle_enabled: value, image_check_before_hit_enabled: value } } : {});
|
| 440 |
},
|
web/src/lib/api.ts
CHANGED
|
@@ -116,6 +116,7 @@ export type SettingsConfig = {
|
|
| 116 |
image_poll_timeout_secs?: number | string;
|
| 117 |
image_account_concurrency?: number | string;
|
| 118 |
image_parallel_generation?: boolean;
|
|
|
|
| 119 |
image_settle_enabled?: boolean;
|
| 120 |
image_check_before_hit_enabled?: boolean;
|
| 121 |
image_settle_secs?: number | string;
|
|
|
|
| 116 |
image_poll_timeout_secs?: number | string;
|
| 117 |
image_account_concurrency?: number | string;
|
| 118 |
image_parallel_generation?: boolean;
|
| 119 |
+
image_redundancy_multiplier?: number | string;
|
| 120 |
image_settle_enabled?: boolean;
|
| 121 |
image_check_before_hit_enabled?: boolean;
|
| 122 |
image_settle_secs?: number | string;
|