diff --git a/.memory/worklog.json b/.memory/worklog.json index ab90b86..e4bf203 100644 --- a/.memory/worklog.json +++ b/.memory/worklog.json @@ -1,31 +1,5 @@ { "entries": [ - { - "files_changed": 1, - "message": "Codex 会话活跃 · 最近命令:codex · 分支 main · 1 项未提交变更 · 最近提交:docs: record sharp text layout deployment", - "ts": "2026-05-20T11:25:31Z", - "type": "session-heartbeat" - }, - { - "files_changed": 4, - "hash": "6f1bb5d", - "message": "auto-save 2026-05-20 19:33 (~4)", - "ts": "2026-05-20T19:33:19+08:00", - "type": "commit" - }, - { - "files_changed": 3, - "hash": "e33463e", - "message": "fix: collapse subject agent confirmations", - "ts": "2026-05-20T19:33:51+08:00", - "type": "commit" - }, - { - "files_changed": 1, - "message": "Codex 会话活跃 · 最近命令:codex · 分支 main · 1 项未提交变更 · 最近提交:fix: collapse subject agent confirmations", - "ts": "2026-05-20T11:35:31Z", - "type": "session-heartbeat" - }, { "files_changed": 1, "hash": "5b13a5c", @@ -3194,6 +3168,34 @@ "message": "auto-save 2026-05-27 15:20 (~3)", "hash": "fdef7f7", "files_changed": 3 + }, + { + "ts": "2026-05-27T15:28:29+08:00", + "type": "commit", + "message": "auto-save 2026-05-27 15:26 (~3)", + "hash": "52e7a01", + "files_changed": 3 + }, + { + "ts": "2026-05-27T15:33:10+08:00", + "type": "commit", + "message": "docs: record canvas click performance optimization", + "hash": "685a6c4", + "files_changed": 1 + }, + { + "ts": "2026-05-27T15:50:24+08:00", + "type": "commit", + "message": "auto-save 2026-05-27 15:50 (~4)", + "hash": "2041282", + "files_changed": 4 + }, + { + "ts": "2026-05-27T15:54:22+08:00", + "type": "commit", + "message": "fix: persist uploaded canvas reference images", + "hash": "ec38215", + "files_changed": 4 } ] } diff --git a/api/main.py b/api/main.py index ab63645..bb0ee3e 100644 --- a/api/main.py +++ b/api/main.py @@ -109,6 +109,10 @@ REWRITE_MODEL = gpt_model_env("REWRITE_MODEL") VISION_MODEL = gpt_model_env("VISION_MODEL") IMAGE_BASE_URL = os.getenv("IMAGE_BASE_URL", LLM_BASE_URL).strip() IMAGE_API_KEY = os.getenv("IMAGE_API_KEY", LLM_API_KEY).strip() +ARK_IMAGE_BASE_URL = os.getenv("ARK_IMAGE_BASE_URL", "https://ark.cn-beijing.volces.com/api/v3").strip() +ARK_IMAGE_API_KEY = (os.getenv("ARK_IMAGE_API_KEY") or os.getenv("SEEDREAM_API_KEY") or "").strip() +ARK_SEEDREAM_IMAGE_MODEL = os.getenv("ARK_SEEDREAM_IMAGE_MODEL", "doubao-seedream-4-5-251128").strip() or "doubao-seedream-4-5-251128" +ARK_SEEDREAM_ENABLED = os.getenv("ARK_SEEDREAM_ENABLED", "true").strip().lower() not in {"0", "false", "no", "off"} AI_HTTP_PROXY = ( os.getenv("AI_HTTP_PROXY") or os.getenv("IMAGE_HTTP_PROXY") @@ -118,15 +122,43 @@ AI_HTTP_PROXY = ( or os.getenv("http_proxy") or "" ).strip() -# Product decision: gpt-image-2 remains the primary image model. Gemini is only -# allowed as an outage fallback when the primary gateway times out or returns -# transient upstream failures. -GPT_IMAGE_MODEL = "gpt-image-2" +DEFAULT_GPT_IMAGE_MODEL = "gpt-image-2" +GPT_IMAGE_MODEL = os.getenv("GPT_IMAGE_MODEL", DEFAULT_GPT_IMAGE_MODEL).strip() or DEFAULT_GPT_IMAGE_MODEL +IMAGE_MODEL = os.getenv("IMAGE_MODEL", GPT_IMAGE_MODEL).strip() or GPT_IMAGE_MODEL IMAGE_FALLBACK_MODEL = os.getenv("IMAGE_FALLBACK_MODEL", "gemini-3-pro-image-preview").strip() or "" IMAGE_FALLBACK_ENABLED = os.getenv("IMAGE_FALLBACK_ENABLED", "true").strip().lower() not in {"0", "false", "no", "off"} -IMAGE_MODEL = GPT_IMAGE_MODEL -PRODUCT_VIEW_MODEL = GPT_IMAGE_MODEL -SUBJECT_ASSET_IMAGE_MODEL = GPT_IMAGE_MODEL +IMAGE_FALLBACK_MODELS = [ + item.strip() + for item in os.getenv("IMAGE_FALLBACK_MODELS", IMAGE_FALLBACK_MODEL).split(",") + if item.strip() +] +IMAGE_EXTRA_MODELS = [ + item.strip() + for item in os.getenv("IMAGE_EXTRA_MODELS", "").split(",") + if item.strip() +] +PRODUCT_VIEW_MODEL = os.getenv("PRODUCT_VIEW_MODEL", GPT_IMAGE_MODEL).strip() or GPT_IMAGE_MODEL +SUBJECT_ASSET_IMAGE_MODEL = os.getenv("SUBJECT_ASSET_IMAGE_MODEL", IMAGE_MODEL).strip() or IMAGE_MODEL +IMAGE_MODEL_CONFIGS_JSON = os.getenv("IMAGE_MODEL_CONFIGS_JSON", "").strip() +try: + _IMAGE_MODEL_CONFIG_OVERRIDES_RAW = json.loads(IMAGE_MODEL_CONFIGS_JSON) if IMAGE_MODEL_CONFIGS_JSON else {} + if isinstance(_IMAGE_MODEL_CONFIG_OVERRIDES_RAW, list): + IMAGE_MODEL_CONFIG_OVERRIDES = { + str(item.get("id") or item.get("model")).strip(): item + for item in _IMAGE_MODEL_CONFIG_OVERRIDES_RAW + if isinstance(item, dict) and (item.get("id") or item.get("model")) + } + elif isinstance(_IMAGE_MODEL_CONFIG_OVERRIDES_RAW, dict): + IMAGE_MODEL_CONFIG_OVERRIDES = { + str(key).strip(): value + for key, value in _IMAGE_MODEL_CONFIG_OVERRIDES_RAW.items() + if str(key).strip() and isinstance(value, dict) + } + else: + IMAGE_MODEL_CONFIG_OVERRIDES = {} +except Exception as e: + print(f"[image config] invalid IMAGE_MODEL_CONFIGS_JSON ignored: {e}", flush=True) + IMAGE_MODEL_CONFIG_OVERRIDES = {} IMAGE_SIZE_CHOICES = [ { "id": "auto", @@ -153,6 +185,54 @@ IMAGE_SIZE_CHOICES = [ "description": "适合横版封面和详情页配图", }, ] +ARK_SEEDREAM_SIZE_CHOICES = [ + { + "id": "2K", + "label": "自动 2K", + "value": "2K", + "description": "Seedream 自行决定接近 2K 的输出尺寸", + }, + { + "id": "2048x2048", + "label": "方图 2048", + "value": "2048x2048", + "description": "Seedream 方图,满足 4.5 最低像素要求", + }, + { + "id": "1440x2560", + "label": "竖图 9:16", + "value": "1440x2560", + "description": "Seedream 竖版营销图", + }, + { + "id": "2560x1440", + "label": "横图 16:9", + "value": "2560x1440", + "description": "Seedream 横版封面或网页图", + }, + { + "id": "2160x2160", + "label": "方图 2160", + "value": "2160x2160", + "description": "Seedream 高清方图", + }, + { + "id": "2160x3840", + "label": "竖图 4K", + "value": "2160x3840", + "description": "Seedream 竖版高分辨率输出", + }, + { + "id": "3840x2160", + "label": "横图 4K", + "value": "3840x2160", + "description": "Seedream 横版高分辨率输出", + }, +] +ALL_IMAGE_SIZE_CHOICES = IMAGE_SIZE_CHOICES + [ + item for item in ARK_SEEDREAM_SIZE_CHOICES + if item["value"] not in {base["value"] for base in IMAGE_SIZE_CHOICES} +] VIDEO_SIZE_CHOICES = [ { "id": "720x1280", @@ -183,9 +263,14 @@ SubjectModelBundle = Literal["gpt", "gemini"] SubjectAgentMode = Literal["realistic", "cartoon", "elements", "custom"] SUBJECT_AGENT_GPT_MODEL = gpt_model_env("SUBJECT_AGENT_GPT_MODEL", VISION_MODEL) SUBJECT_AGENT_GEMINI_MODEL = os.getenv("SUBJECT_AGENT_GEMINI_MODEL", "gemini-2.5-flash").strip() or "gemini-2.5-flash" -SUBJECT_ASSET_IMAGE_MODELS = [GPT_IMAGE_MODEL] + ( - [IMAGE_FALLBACK_MODEL] if IMAGE_FALLBACK_ENABLED and IMAGE_FALLBACK_MODEL and IMAGE_FALLBACK_MODEL != GPT_IMAGE_MODEL else [] -) +SUBJECT_ASSET_IMAGE_MODELS = [ + item.strip() + for item in os.getenv( + "SUBJECT_ASSET_IMAGE_MODELS", + ",".join([SUBJECT_ASSET_IMAGE_MODEL, *IMAGE_FALLBACK_MODELS]), + ).split(",") + if item.strip() +] IMAGE_REQUEST_TIMEOUT_SECONDS = max(15, min(180, int(os.getenv("IMAGE_REQUEST_TIMEOUT_SECONDS", "60")))) IMAGE_CIRCUIT_FAILURE_THRESHOLD = max(1, int(os.getenv("IMAGE_CIRCUIT_FAILURE_THRESHOLD", "2"))) IMAGE_CIRCUIT_COOLDOWN_SECONDS = max(60, int(os.getenv("IMAGE_CIRCUIT_COOLDOWN_SECONDS", "600")))