Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 9 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# PicGen Console

一个面向 OpenAI 兼容图像生成 / 编辑接口的本地工作台,当前版本 **0.1.58**。它把
一个面向 OpenAI 兼容图像生成 / 编辑接口的本地工作台,当前版本 **0.1.59**。它把
`/v1/images/generations`、`/v1/images/edits` 与 `/v1/responses`(含 `image_generation` 工具)
包装成统一可观测的代理,前端是一套零依赖的 Web 控制台。

Expand All @@ -14,7 +14,9 @@

![PicGen Console 主程序界面](demo1.png)

## 0.1.58 主要特性
## 0.1.59 主要特性

- **上游参数错误不再误报审核**:`invalid_mask_image_format` 会提示客户改用像素尺寸一致的标准 PNG,不再仅因通用 `image_generation_user_error` 类型就提示“内容审核未通过”;明确的内容政策 code 和文案仍会正确分类。

- **Responses 候选数量收敛**:强制使用图像工具、关闭并行工具调用并把请求数量写入提示词;上游仍越界时在落盘前按 `sample_count` 截断,避免单图任务意外生成 6 张候选并拖到数百秒,同时不再为非官方 `n` 参数自动重发付费请求。

Expand Down Expand Up @@ -109,10 +111,10 @@ PICGEN_LOG_FORMAT=json \
### Docker

```bash
docker build -t minorli/picgen:0.1.58 .
docker build -t minorli/picgen:0.1.59 .
docker run --rm -p 8000:8000 \
-v picgen-data:/app/data \
minorli/picgen:0.1.58
minorli/picgen:0.1.59
```

或:
Expand All @@ -127,10 +129,10 @@ docker compose up -d
./scripts/docker-build-push.sh
```

默认会构建并推送 `minorli/picgen:0.1.58`。也可以覆盖:
默认会构建并推送 `minorli/picgen:0.1.59`。也可以覆盖:

```bash
IMAGE=minorli/picgen VERSION=0.1.58 PLATFORM=linux/amd64 ./scripts/docker-build-push.sh
IMAGE=minorli/picgen VERSION=0.1.59 PLATFORM=linux/amd64 ./scripts/docker-build-push.sh
```

镜像不会包含 `.env`、本地用户库或历史图片。容器内置 `HEALTHCHECK` 探测 `/api/health`,以非 root
Expand Down Expand Up @@ -243,7 +245,7 @@ Bug 反馈和找回密码申请会先写入本地认证库,再优先发送到

## 图像通道

PicGen 0.1.58 把四类图像操作统一提交给 `/api/image-jobs`,实际通道由服务端决定:
PicGen 0.1.59 把四类图像操作统一提交给 `/api/image-jobs`,实际通道由服务端决定:

| 用户操作 | 默认接口 | 默认模型 |
| --- | --- | --- |
Expand Down
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
services:
picgen:
image: minorli/picgen:0.1.58
image: minorli/picgen:0.1.59
build:
context: .
ports:
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "picgen"
version = "0.1.58"
version = "0.1.59"
description = "Enterprise-grade local web console for OpenAI-compatible image generation and editing APIs."
readme = "README.md"
requires-python = ">=3.12"
Expand Down
2 changes: 1 addition & 1 deletion scripts/docker-build-push.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
set -euo pipefail

IMAGE="${IMAGE:-minorli/picgen}"
VERSION="${VERSION:-0.1.58}"
VERSION="${VERSION:-0.1.59}"
PLATFORM="${PLATFORM:-linux/amd64}"

docker buildx build \
Expand Down
5 changes: 4 additions & 1 deletion src/picgen/upstream/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ def classify_upstream_error(status: int, message: str, details: str | None = Non
haystack = f"{message}\n{details or ''}".lower()
if status == 429 or "rate limit" in haystack or "rate_limit" in haystack:
return "upstream_rate_limited"
if "invalid_mask_image_format" in _error_values(details):
return "upstream_invalid_image_input"
if _looks_like_content_policy_error(status, message, details):
return "upstream_content_policy"
if "cloudflare" in haystack or "error 1010" in haystack:
Expand All @@ -76,7 +78,6 @@ def _looks_like_content_policy_error(status: int, message: str, details: str | N
"policy_violation",
"safety_policy_violation",
"moderation_blocked",
"image_generation_user_error",
}
for value in explicit_values
):
Expand Down Expand Up @@ -120,6 +121,8 @@ def _error_values(details: str | None) -> set[str]:
def public_upstream_error_message(status: int, code: str, action: str) -> str:
if code == "upstream_rate_limited":
return "图片生成服务当前请求较多,请稍后再试。"
if code == "upstream_invalid_image_input":
return "上游不接受当前输入图或蒙版的格式/尺寸,请改用像素尺寸一致的标准 PNG 后重试。"
if code == "upstream_content_policy":
return "这次提示词没有通过上游内容审核,请调整描述后重试。"
if code == "upstream_blocked":
Expand Down
4 changes: 2 additions & 2 deletions static/app.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { calculateLogoPlacementScore, chooseLogoPlacement } from "./logo-placement.mjs?v=0.1.58"
import { calculateLogoPlacementScore, chooseLogoPlacement } from "./logo-placement.mjs?v=0.1.59"
import {
DEFAULT_RESPONSES_MODEL,
RESPONSES_MODEL_STORAGE_VERSION,
RESPONSES_REASONING_STORAGE_VERSION,
migrateStoredResponsesReasoningSettings,
migrateStoredResponsesSettings,
} from "./responses-settings.mjs?v=0.1.58"
} from "./responses-settings.mjs?v=0.1.59"

const RESPONSES_REASONING_EFFORTS = new Set(["low", "medium", "high", "xhigh", "max", "ultra"])
const DEFAULT_RESPONSES_REASONING_EFFORT = "xhigh"
Expand Down
6 changes: 3 additions & 3 deletions static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>PicGen Console</title>
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='16' fill='%23108566'/%3E%3Cpath d='M32 13l16 9v20l-16 9-16-9V22l16-9z' fill='%23ecfdf5'/%3E%3Ccircle cx='32' cy='32' r='8' fill='%23108566'/%3E%3C/svg%3E">
<link rel="stylesheet" href="styles.css?v=0.1.58">
<link rel="stylesheet" href="styles.css?v=0.1.59">
</head>
<body class="auth-gate">
<div id="authOverlay" class="auth-page" aria-hidden="false">
Expand Down Expand Up @@ -1046,7 +1046,7 @@ <h2>管理员高级设置</h2>
</div>

<footer class="system-footer">
<span>PicGen Console v0.1.58</span>
<span>PicGen Console v0.1.59</span>
<span>本地代理保存结果到 <strong>data/outputs</strong></span>
</footer>
</div>
Expand Down Expand Up @@ -1356,6 +1356,6 @@ <h2 id="previewModalTitle">图片预览</h2>
<span id="toastMessageText"></span>
</div>

<script src="app.js?v=0.1.58" type="module"></script>
<script src="app.js?v=0.1.59" type="module"></script>
</body>
</html>
37 changes: 37 additions & 0 deletions tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -2454,6 +2454,43 @@ def test_edit_passes_mask_and_options(make_client, settings_factory):
assert field_names == ["image", "mask"]


def test_edit_invalid_mask_returns_actionable_input_error(make_client, settings_factory):
settings = settings_factory(default_api_key="sk-test")
client, fake, _ = make_client(settings=settings)
fake.run_multipart.side_effect = APIError(
400,
"上游不接受当前输入图或蒙版的格式/尺寸,请改用像素尺寸一致的标准 PNG 后重试。",
"Invalid mask image format - mask size does not match image size",
code="upstream_invalid_image_input",
)

response = client.post(
"/api/edit",
json={
"api_key": "sk-test",
"prompt": "只修改蒙版透明区域",
"model": "gpt-image-2",
"image": {
"name": "source.png",
"type": "image/png",
"data_url": f"data:image/png;base64,{TINY_PNG_B64}",
},
"mask": {
"name": "mask.png",
"type": "image/png",
"data_url": f"data:image/png;base64,{TINY_PNG_B64}",
},
},
)

assert response.status_code == 400
payload = response.json()
assert payload["code"] == "upstream_invalid_image_input"
assert "像素尺寸一致" in payload["error"]
assert "内容审核" not in payload["error"]
fake.run_multipart.assert_awaited_once()


def test_responses_image_requires_prompt(make_client):
client, _, _ = make_client()
response = client.post("/api/responses-image", json={"api_key": "sk-test"})
Expand Down
4 changes: 2 additions & 2 deletions tests/test_static_assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def test_legacy_responses_model_storage_is_migrated_once() -> None:
settings_js = (ROOT_DIR / "static" / "responses-settings.mjs").read_text(encoding="utf-8")

assert 'const DEPRECATED_RESPONSES_MODELS = new Set(["gpt-5.4"])' in app_js
assert 'from "./responses-settings.mjs?v=0.1.58"' in app_js
assert 'from "./responses-settings.mjs?v=0.1.59"' in app_js
assert 'const LEGACY_DEFAULT_RESPONSES_MODEL = "gpt-5.5"' in settings_js
assert "const RESPONSES_MODEL_STORAGE_VERSION = 4" in settings_js
assert "function migrateStoredResponsesSettings" in settings_js
Expand All @@ -38,7 +38,7 @@ def test_logo_overlay_uses_uploaded_asset_without_ai_guidance() -> None:
assert 'const COMPANY_LOGO_URL = "6renyou.png"' in app_js
assert "composeLogoOverlayForCandidates" in app_js
assert "createOfficialLogoCanvas" in app_js
assert 'from "./logo-placement.mjs?v=0.1.58"' in app_js
assert 'from "./logo-placement.mjs?v=0.1.59"' in app_js
assert "chooseLogoPlacement" in app_js
assert "calculateLogoPlacementScore" in app_js
assert "expandLogoSafetyRegion" in placement_js
Expand Down
57 changes: 57 additions & 0 deletions tests/test_upstream_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,63 @@ async def test_run_json_reports_retry_exhaustion_to_user() -> None:
await client.aclose()


async def test_run_multipart_invalid_mask_is_not_misclassified_as_content_policy() -> None:
body = json.dumps(
{
"error": {
"code": "invalid_mask_image_format",
"message": "Invalid mask image format - mask size does not match image size",
"type": "image_generation_user_error",
}
}
)
transport = httpx.MockTransport(lambda request: httpx.Response(400, text=body))
client = await _build_client(transport, max_retries=0)
try:
with pytest.raises(APIError) as info:
await client.run_multipart(
"https://upstream.test/images/edits",
"sk-test",
{"prompt": "add a lamp"},
[],
"UA",
)
assert info.value.status == 400
assert info.value.code == "upstream_invalid_image_input"
assert "内容审核" not in info.value.message
assert "像素尺寸一致" in info.value.message
assert "mask size does not match image size" in (info.value.details or "")
finally:
await client.aclose()


async def test_run_multipart_explicit_content_policy_code_is_still_classified() -> None:
body = json.dumps(
{
"error": {
"code": "content_policy_violation",
"message": "The request violates content policy",
"type": "image_generation_user_error",
}
}
)
transport = httpx.MockTransport(lambda request: httpx.Response(400, text=body))
client = await _build_client(transport, max_retries=0)
try:
with pytest.raises(APIError) as info:
await client.run_multipart(
"https://upstream.test/images/edits",
"sk-test",
{"prompt": "disallowed"},
[],
"UA",
)
assert info.value.code == "upstream_content_policy"
assert "内容审核" in info.value.message
finally:
await client.aclose()


async def test_run_json_translates_timeout() -> None:
calls = {"count": 0}

Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading