861052359 commited on
Commit
22e2a53
·
1 Parent(s): d14bc39

feat: 生图冗余倍率配置与前端界面

Browse files
VERSION CHANGED
@@ -1 +1 @@
1
- 1.4.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": 15,
5
  "image_poll_timeout_secs": 120,
6
- "auto_remove_rate_limited_accounts": false,
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": false,
74
- "image_check_before_hit_enabled": false,
75
  "image_settle_secs": 2,
76
- "auto_relogin_after_refresh": false,
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
- if request.n <= 1:
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": request.n,
1458
  "model": request.model,
1459
  })
1460
- for index in range(1, request.n + 1):
1461
- outputs = _generate_single_image(request, index, request.n)
1462
  for output in outputs:
1463
  yield output
1464
  return
1465
 
1466
  logger.info({
1467
  "event": "image_parallel_generation_start",
1468
- "n": request.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=request.n) as executor:
1476
- for index in range(1, request.n + 1):
1477
- future = executor.submit(_generate_single_image, request, index, request.n)
1478
- futures[future] = index
1479
 
1480
- # 按完成顺序收集结果
1481
  for future in as_completed(futures):
1482
- index = futures[future]
1483
  try:
1484
- results[index] = future.result()
1485
  except Exception as exc:
1486
- errors[index] = exc
1487
  logger.warning({
1488
  "event": "image_parallel_generation_error",
1489
- "index": index,
1490
  "error": str(exc)[:300],
1491
  })
1492
 
1493
- # yield 结果:跳过索引顺序限制,不再让低索引失败阻塞高索引成功结果
1494
- emitted = False
1495
  last_error = ""
1496
- # 先 yield 所有成功的结果
1497
- for index in range(1, request.n + 1):
1498
- if index in results:
1499
- for output in results[index]:
1500
- emitted = True
 
 
 
 
1501
  yield output
1502
- elif index in errors:
1503
- last_error = str(errors[index])
1504
- if not emitted:
1505
  logger.warning({
1506
  "event": "image_parallel_failure_before_success",
1507
- "failed_index": index,
1508
  "error": last_error[:200],
1509
  })
1510
 
1511
- # 如果有失败但也有成功,记录警告
1512
- if emitted:
1513
- for index in range(1, request.n + 1):
1514
- if index in errors:
1515
  logger.warning({
1516
  "event": "image_parallel_partial_failure",
1517
- "failed_index": index,
1518
- "error": str(errors[index])[:200],
1519
  })
1520
 
1521
- if not emitted:
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;