refactor(server): 移除鉴权并归档遗留路由至 legacy/
- 删除 auth.py 与 deps.py,各路由去除 get_current_user 依赖 - collection.py 更名为 materials.py,冻结链路(ozon/publish/shops/categories)移入 legacy/ - 扩展默认生图服务端口并入 8800 并自动迁移旧配置,水印默认文案改为 Panda Store - 新增 docs/v2.1/backend-structure.md 后端结构盘点文档
This commit is contained in:
@@ -1,23 +0,0 @@
|
||||
"""鉴权路由。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import secrets
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
|
||||
from config import get_settings
|
||||
from core.security import create_access_token
|
||||
from schemas.auth import LoginRequest, LoginResponse
|
||||
|
||||
router = APIRouter(prefix="/api/auth", tags=["auth"])
|
||||
|
||||
|
||||
@router.post("/login", response_model=LoginResponse)
|
||||
async def login(body: LoginRequest) -> LoginResponse:
|
||||
settings = get_settings()
|
||||
if not settings.app_token:
|
||||
raise HTTPException(status_code=500, detail="服务端未配置 APP_TOKEN")
|
||||
if not secrets.compare_digest(body.token, settings.app_token):
|
||||
raise HTTPException(status_code=401, detail="Token 不正确")
|
||||
token, expires_at = create_access_token("app")
|
||||
return LoginResponse(access_token=token, expires_at=expires_at)
|
||||
+2
-3
@@ -1,14 +1,13 @@
|
||||
"""汇率路由。"""
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from fastapi import APIRouter
|
||||
|
||||
from deps import get_current_user
|
||||
from services.fx import get_fx_rate
|
||||
|
||||
router = APIRouter(prefix="/api/fx", tags=["fx"])
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def fx(_user: dict = Depends(get_current_user)):
|
||||
async def fx():
|
||||
return await get_fx_rate()
|
||||
|
||||
@@ -0,0 +1,267 @@
|
||||
"""采集入库:插件上传文本 + 图片 URL,落库后异步转存。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, UploadFile, File, Form
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from db import get_db, get_session_factory
|
||||
from models import Product, ProductAsset
|
||||
from models.enums import AssetStatus, Stage
|
||||
from schemas.collection import MaterialsRequest, MaterialsResponse, TextMaterial
|
||||
|
||||
router = APIRouter(prefix="/api", tags=["collection"])
|
||||
|
||||
|
||||
def _parse_number(text: str | None) -> float | None:
|
||||
"""'1 290 ₽' / '3.5 кг' / '48*18*25' → 1290.0 / 3.5 / 48"""
|
||||
if not text:
|
||||
return None
|
||||
m = re.search(r"(\d+(?:[.,]\d+)?)", text.replace(" ", "").replace(",", "."))
|
||||
return float(m.group(1)) if m else None
|
||||
|
||||
|
||||
def _find_param(pairs: list[dict] | None, keys: list[str]) -> str | None:
|
||||
for p in pairs or []:
|
||||
k = (p.get("key") or "").lower()
|
||||
if any(kw in k for kw in keys):
|
||||
return p.get("value")
|
||||
return None
|
||||
|
||||
|
||||
def _apply_texts(product: Product, texts: list[TextMaterial]) -> None:
|
||||
raw = dict(product.raw or {})
|
||||
raw_texts: list[dict] = list(raw.get("texts") or [])
|
||||
for t in texts:
|
||||
raw_texts.append({"kind": t.kind, "content": t.content, "pairs": t.pairs})
|
||||
if t.kind == "title" and t.content and not product.name:
|
||||
product.name = t.content
|
||||
raw["title"] = t.content
|
||||
elif t.kind == "price":
|
||||
raw["price"] = t.content
|
||||
num = _parse_number(t.content)
|
||||
if num is not None and (product.price is None or product.price == 0):
|
||||
product.price = num
|
||||
elif t.kind == "params":
|
||||
raw["params"] = t.pairs
|
||||
_apply_weight_dims(product, t.pairs)
|
||||
elif t.kind == "selling_point":
|
||||
raw["sellingPoints"] = t.content
|
||||
elif t.kind == "desc":
|
||||
raw["desc"] = t.content
|
||||
if not product.description:
|
||||
product.description = t.content
|
||||
elif t.kind == "brand":
|
||||
raw["brand"] = t.content
|
||||
raw["texts"] = raw_texts
|
||||
product.raw = raw
|
||||
|
||||
|
||||
def _apply_weight_dims(product: Product, pairs: list[dict] | None) -> None:
|
||||
"""从参数表里解析「包装重量 / 包装尺寸(长宽高)」,统一换算成克 / 毫米回填。"""
|
||||
weight = _find_param(pairs, ["包装重量", "重量", "вес"])
|
||||
if weight is not None:
|
||||
num = _parse_number(weight)
|
||||
if num is not None:
|
||||
is_kg = any(u in weight.lower() for u in ("кг", "kg"))
|
||||
product.weight = num * 1000 if is_kg else num # 统一为克
|
||||
product.weight_unit = "g"
|
||||
|
||||
l = _find_param(pairs, ["包装长度", "长度", "длина"])
|
||||
w = _find_param(pairs, ["包装宽度", "宽度", "ширина"])
|
||||
h = _find_param(pairs, ["包装高度", "高度", "высота"])
|
||||
if l or w or h:
|
||||
combined = (l or "") + (w or "") + (h or "")
|
||||
factor = 1 if any(u in combined.lower() for u in ("мм", "mm")) else 10 # 厘米→毫米
|
||||
product.depth = (_parse_number(l) or 0) * factor if l else None
|
||||
product.width = (_parse_number(w) or 0) * factor if w else None
|
||||
product.height = (_parse_number(h) or 0) * factor if h else None
|
||||
product.dimension_unit = "mm"
|
||||
else:
|
||||
dim = _find_param(pairs, ["包装尺寸", "размер", "габарит", "尺寸"])
|
||||
if dim is not None:
|
||||
nums = re.findall(r"\d+(?:[.,]\d+)?", dim.replace(",", "."))
|
||||
if len(nums) >= 3:
|
||||
factor = 1 if any(u in dim.lower() for u in ("мм", "mm")) else 10
|
||||
product.depth = float(nums[0]) * factor
|
||||
product.width = float(nums[1]) * factor
|
||||
product.height = float(nums[2]) * factor
|
||||
product.dimension_unit = "mm"
|
||||
|
||||
|
||||
async def _get_or_create_product(db: AsyncSession, req: MaterialsRequest) -> Product:
|
||||
if req.product_id:
|
||||
product = await db.get(Product, UUID(req.product_id))
|
||||
if product is None:
|
||||
raise HTTPException(status_code=404, detail="商品不存在")
|
||||
return product
|
||||
product = Product(
|
||||
stage=Stage.collected,
|
||||
source_platform=req.source.platform,
|
||||
source_item_id=req.source.itemId,
|
||||
source_url=req.source.url,
|
||||
)
|
||||
db.add(product)
|
||||
await db.flush()
|
||||
return product
|
||||
|
||||
|
||||
@router.post("/materials", response_model=MaterialsResponse)
|
||||
async def create_materials(
|
||||
req: MaterialsRequest,
|
||||
background: BackgroundTasks,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
) -> MaterialsResponse:
|
||||
product = await _get_or_create_product(db, req)
|
||||
_apply_texts(product, req.texts)
|
||||
|
||||
# 采集溯源(追加来源)
|
||||
if not product.source_url:
|
||||
product.source_url = req.source.url
|
||||
if not product.source_platform:
|
||||
product.source_platform = req.source.platform
|
||||
|
||||
# 去重 + 建素材
|
||||
existing = set()
|
||||
if req.images:
|
||||
rows = (await db.execute(
|
||||
select(ProductAsset.dedupe_key).where(
|
||||
ProductAsset.product_id == product.id,
|
||||
ProductAsset.dedupe_key.isnot(None),
|
||||
)
|
||||
)).scalars().all()
|
||||
existing = {k for k in rows if k}
|
||||
|
||||
queued, skipped = 0, 0
|
||||
for img in req.images:
|
||||
if img.dedupeKey and img.dedupeKey in existing:
|
||||
skipped += 1
|
||||
continue
|
||||
db.add(ProductAsset(
|
||||
product_id=product.id,
|
||||
group_key=img.groupKey,
|
||||
variant_name=img.variantName,
|
||||
sort_order=img.index,
|
||||
type=img.type,
|
||||
source_url=img.url,
|
||||
status=AssetStatus.pending,
|
||||
dedupe_key=img.dedupeKey,
|
||||
))
|
||||
if img.dedupeKey:
|
||||
existing.add(img.dedupeKey)
|
||||
queued += 1
|
||||
|
||||
# 更新分组计数
|
||||
counts: dict = {}
|
||||
for a in await db.scalars(select(ProductAsset).where(ProductAsset.product_id == product.id)):
|
||||
counts[a.group_key] = counts.get(a.group_key, 0) + 1
|
||||
product.asset_counts = counts
|
||||
product.stage = Stage.collected if product.stage == Stage.collected else product.stage
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(product)
|
||||
|
||||
if queued:
|
||||
background.add_task(process_product_assets, str(product.id))
|
||||
return MaterialsResponse(
|
||||
product_id=str(product.id),
|
||||
stage=product.stage.value,
|
||||
assets_queued=queued,
|
||||
assets_skipped=skipped,
|
||||
)
|
||||
|
||||
|
||||
async def process_product_assets(product_id: str) -> None:
|
||||
"""后台:下载 pending 素材 → 转存 storage。失败逐张标记,不中断。"""
|
||||
from services.storage import get_storage
|
||||
|
||||
storage = get_storage()
|
||||
async with get_session_factory()() as db:
|
||||
assets = (await db.scalars(
|
||||
select(ProductAsset).where(
|
||||
ProductAsset.product_id == UUID(product_id),
|
||||
ProductAsset.status == AssetStatus.pending,
|
||||
)
|
||||
)).all()
|
||||
for a in assets:
|
||||
a.status = AssetStatus.downloading
|
||||
await db.commit()
|
||||
try:
|
||||
stored = await storage.save_from_url(a.source_url, key_prefix="assets")
|
||||
a.stored_url = stored
|
||||
a.status = AssetStatus.uploaded
|
||||
except Exception as exc: # noqa: BLE001
|
||||
a.status = AssetStatus.failed
|
||||
a.error = str(exc)[:500]
|
||||
await db.commit()
|
||||
|
||||
|
||||
@router.post("/materials/bytes")
|
||||
async def upload_material_bytes(
|
||||
background: BackgroundTasks,
|
||||
product_id: str = Form(...),
|
||||
group_key: str = Form("main"),
|
||||
variant_name: str | None = Form(None),
|
||||
sort_order: int = Form(0),
|
||||
type: str = Form("img"),
|
||||
file: UploadFile = File(...),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
product = await db.get(Product, UUID(product_id))
|
||||
if product is None:
|
||||
raise HTTPException(status_code=404, detail="商品不存在")
|
||||
data = await file.read()
|
||||
asset = ProductAsset(
|
||||
product_id=product.id,
|
||||
group_key=group_key,
|
||||
variant_name=variant_name,
|
||||
sort_order=sort_order,
|
||||
type=type,
|
||||
source_url="",
|
||||
status=AssetStatus.pending,
|
||||
)
|
||||
db.add(asset)
|
||||
await db.flush()
|
||||
# 直接转存字节
|
||||
from services.storage import get_storage
|
||||
storage = get_storage()
|
||||
try:
|
||||
asset.stored_url = await storage.save_bytes(data, f"assets/{asset.id}", file.content_type or "")
|
||||
asset.status = AssetStatus.uploaded
|
||||
except Exception as exc: # noqa: BLE001
|
||||
asset.status = AssetStatus.failed
|
||||
asset.error = str(exc)[:500]
|
||||
await db.commit()
|
||||
return {"asset_id": str(asset.id), "status": asset.status.value}
|
||||
|
||||
|
||||
@router.get("/products/{product_id}/fingerprints")
|
||||
async def product_fingerprints(
|
||||
product_id: str,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
rows = (await db.scalars(
|
||||
select(ProductAsset.dedupe_key).where(
|
||||
ProductAsset.product_id == UUID(product_id),
|
||||
ProductAsset.dedupe_key.isnot(None),
|
||||
)
|
||||
)).all()
|
||||
return {"dedupe_keys": list(rows)}
|
||||
|
||||
|
||||
@router.get("/collected")
|
||||
async def is_collected(
|
||||
platform: str,
|
||||
itemId: str,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
rows = (await db.execute(
|
||||
select(Product).where(
|
||||
Product.source_platform == platform,
|
||||
Product.source_item_id == itemId,
|
||||
)
|
||||
)).scalars().all()
|
||||
return {"collected": len(rows) > 0, "count": len(rows)}
|
||||
+7
-15
@@ -8,7 +8,6 @@ from sqlalchemy import select, func
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from db import get_db
|
||||
from deps import get_current_user
|
||||
from models import Product, ProductAsset
|
||||
from models.enums import Stage
|
||||
from schemas.product import ProductDetail, ProductListItem, ProductUpdate
|
||||
@@ -22,8 +21,7 @@ async def list_products(
|
||||
q: str | None = None,
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=100),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_user: dict = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
stmt = select(Product)
|
||||
if stage:
|
||||
@@ -42,8 +40,7 @@ async def list_products(
|
||||
@router.get("/{product_id}", response_model=ProductDetail)
|
||||
async def get_product(
|
||||
product_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_user: dict = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
product = await db.get(Product, UUID(product_id))
|
||||
if product is None:
|
||||
@@ -54,8 +51,7 @@ async def get_product(
|
||||
@router.post("", response_model=ProductDetail)
|
||||
async def create_product(
|
||||
body: ProductUpdate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_user: dict = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
product = Product(stage=Stage.collected)
|
||||
_apply_update(product, body)
|
||||
@@ -68,8 +64,7 @@ async def create_product(
|
||||
@router.post("/{product_id}/copy", response_model=ProductDetail)
|
||||
async def copy_product(
|
||||
product_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_user: dict = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""复制商品为新变体:继承标题/描述/属性/型号名称/计价,重置货号与图片。"""
|
||||
src = await db.get(Product, UUID(product_id))
|
||||
@@ -118,8 +113,7 @@ async def copy_product(
|
||||
async def update_product(
|
||||
product_id: str,
|
||||
body: ProductUpdate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_user: dict = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
product = await db.get(Product, UUID(product_id))
|
||||
if product is None:
|
||||
@@ -134,8 +128,7 @@ async def update_product(
|
||||
async def delete_product(
|
||||
product_id: str,
|
||||
hard: bool = False,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_user: dict = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
product = await db.get(Product, UUID(product_id))
|
||||
if product is None:
|
||||
@@ -151,8 +144,7 @@ async def delete_product(
|
||||
@router.get("/{product_id}/assets")
|
||||
async def list_assets(
|
||||
product_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_user: dict = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
rows = (await db.scalars(
|
||||
select(ProductAsset)
|
||||
|
||||
+14
-82
@@ -3,26 +3,23 @@
|
||||
按 docs/v2.1/api.md §2-6 实现:引擎平移自 image-suite-studio(services/planner|generator|
|
||||
tasks|watermark + services/prompts),任务存进程内内存表(重启即失效)。
|
||||
生成图回调回写 product_assets(group_key='generated');product_id 缺省时只落盘不回写。
|
||||
业务逻辑(texts_to_raw / 模型校验 / generated 回写)在 services/suite_service.py。
|
||||
注:V2.1 阶段本组接口暂不接鉴权(现有鉴权后续可能重做)。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import mimetypes
|
||||
import uuid
|
||||
import zipfile
|
||||
from functools import partial
|
||||
|
||||
from fastapi import APIRouter, BackgroundTasks, HTTPException
|
||||
from fastapi.responses import StreamingResponse
|
||||
|
||||
from config import get_settings
|
||||
from db import get_session_factory
|
||||
from models import Product, ProductAsset
|
||||
from schemas.suite import (
|
||||
PLATFORM_SPECS,
|
||||
RIGHTAPI_MODELS,
|
||||
SUPPORTED_TYPES,
|
||||
TONGYI_MODELS,
|
||||
ImageEditSingleRequest,
|
||||
ImageEditSingleResponse,
|
||||
PlanItemOut,
|
||||
@@ -31,88 +28,18 @@ from schemas.suite import (
|
||||
SuiteGenerateRequest,
|
||||
SuiteOut,
|
||||
SuitePlanRequest,
|
||||
TextMaterial,
|
||||
resolve_provider,
|
||||
)
|
||||
from services.generator import _image_size, GENERATORS, run_suite
|
||||
from services.planner import generate_plan
|
||||
from services.prompts import build_context, build_prompt, type_name
|
||||
from services.storage import download_bytes, get_storage, local_path
|
||||
from services.tasks import IMG_OK, Task, TaskImage, create_task, get_task
|
||||
from api.proxy import guess_referer
|
||||
from services.suite_service import append_generated_asset, texts_to_raw, validate_model
|
||||
from services.tasks import IMG_OK, create_task, get_task
|
||||
|
||||
router = APIRouter(prefix="/api", tags=["suite"])
|
||||
|
||||
|
||||
def texts_to_raw(texts: list[TextMaterial]) -> dict:
|
||||
"""前端组装的文本素材 → prompt 上下文用的 raw dict(后写的覆盖先写的)。"""
|
||||
raw: dict = {}
|
||||
for t in texts:
|
||||
if t.kind == "title" and t.content:
|
||||
raw["title"] = t.content
|
||||
elif t.kind == "price" and t.content:
|
||||
raw["price"] = t.content
|
||||
elif t.kind == "brand" and t.content:
|
||||
raw["brand"] = t.content
|
||||
elif t.kind == "params" and t.pairs:
|
||||
merged = {p["key"]: p["value"] for p in (raw.get("params") or [])}
|
||||
for p in t.pairs:
|
||||
merged.setdefault(p["key"], p["value"])
|
||||
raw["params"] = [{"key": k, "value": v} for k, v in merged.items()]
|
||||
elif t.kind == "selling_point" and t.content:
|
||||
raw["sellingPoints"] = t.content
|
||||
elif t.kind == "desc" and t.content:
|
||||
raw["desc"] = t.content
|
||||
elif t.kind == "sales" and t.content:
|
||||
raw["sales"] = t.content
|
||||
elif t.kind == "shop" and t.content:
|
||||
raw["shop"] = t.content
|
||||
return raw
|
||||
|
||||
|
||||
def _validate_model(provider_name: str, model: str | None) -> None:
|
||||
if provider_name == "tongyi" and model and model not in TONGYI_MODELS:
|
||||
raise HTTPException(status_code=400, detail=f"不支持的模型: {model}(tongyi 支持: {TONGYI_MODELS})")
|
||||
if provider_name == "rightapi" and model and model not in RIGHTAPI_MODELS:
|
||||
raise HTTPException(status_code=400, detail=f"不支持的模型: {model}(rightapi 支持: {RIGHTAPI_MODELS})")
|
||||
|
||||
|
||||
# ── 生成图回写商品素材 ────────────────────────────────────────────────────
|
||||
|
||||
async def _append_generated_asset(product_id: str, image: TaskImage) -> str | None:
|
||||
"""把一张生成完成的图追加为 product_assets(generated),并累加 asset_counts。返回 asset_id。"""
|
||||
from sqlalchemy import func, select
|
||||
|
||||
pid = uuid.UUID(product_id)
|
||||
async with get_session_factory()() as db:
|
||||
count = await db.scalar(
|
||||
select(func.count(ProductAsset.id)).where(
|
||||
ProductAsset.product_id == pid,
|
||||
ProductAsset.group_key == "generated",
|
||||
)
|
||||
)
|
||||
asset = ProductAsset(
|
||||
product_id=pid,
|
||||
group_key="generated",
|
||||
variant_name=None,
|
||||
sort_order=count or 0,
|
||||
type="img",
|
||||
source_url="",
|
||||
stored_url=image.url,
|
||||
status="uploaded",
|
||||
)
|
||||
db.add(asset)
|
||||
await db.flush()
|
||||
|
||||
product = await db.get(Product, pid)
|
||||
if product is not None:
|
||||
counts = dict(product.asset_counts or {})
|
||||
counts["generated"] = int(counts.get("generated") or 0) + 1
|
||||
product.asset_counts = counts
|
||||
await db.commit()
|
||||
return str(asset.id)
|
||||
|
||||
|
||||
# ── 出图方案规划 ──────────────────────────────────────────────────────────
|
||||
|
||||
@router.post("/suite/plan", response_model=PlanResponse)
|
||||
@@ -173,7 +100,7 @@ async def generate_suite(req: SuiteGenerateRequest, background: BackgroundTasks)
|
||||
settings = get_settings()
|
||||
# 前端只传模型名:已知模型直接路由到对应 provider(如 gpt-image-2-vip → rightapi)
|
||||
provider_name = resolve_provider(req.model, None, settings.image_provider)
|
||||
_validate_model(provider_name, req.model)
|
||||
validate_model(provider_name, req.model)
|
||||
|
||||
product_id = (req.product_id or "").strip() or None
|
||||
if product_id:
|
||||
@@ -207,8 +134,13 @@ async def generate_suite(req: SuiteGenerateRequest, background: BackgroundTasks)
|
||||
],
|
||||
)
|
||||
|
||||
# 每张成功即回写 generated 组;未关联商品时仅落存储不回写
|
||||
background.add_task(run_suite, task, _append_generated_asset if product_id else None)
|
||||
# 每张成功即回写 generated 组;未关联商品时仅落存储不回写。
|
||||
# partial 绑定 product_id:run_suite 回调只传 image,签名须为 (image)
|
||||
background.add_task(
|
||||
run_suite,
|
||||
task,
|
||||
partial(append_generated_asset, product_id) if product_id else None,
|
||||
)
|
||||
return SuiteCreateResponse(suite_id=task.id)
|
||||
|
||||
|
||||
@@ -279,7 +211,7 @@ async def suite_image_edit(req: ImageEditSingleRequest) -> ImageEditSingleRespon
|
||||
provider_name = resolve_provider(req.model, None, settings.image_provider)
|
||||
if provider_name not in GENERATORS:
|
||||
raise HTTPException(status_code=400, detail=f"未知 provider: {provider_name}")
|
||||
_validate_model(provider_name, req.model)
|
||||
validate_model(provider_name, req.model)
|
||||
|
||||
spec = {"lang": "ru", "ratio": "3:4"} # 试算页固定 Ozon 规格(俄文图内文案 · 3:4)
|
||||
model = req.model
|
||||
@@ -311,7 +243,7 @@ async def suite_image_edit(req: ImageEditSingleRequest) -> ImageEditSingleRespon
|
||||
asset_id: str | None = None
|
||||
if req.append and req.product_id:
|
||||
try:
|
||||
asset_id = await _append_generated_asset(
|
||||
asset_id = await append_generated_asset(
|
||||
req.product_id,
|
||||
TaskImage(type_id="custom", name="AI生图", url=url, status="ok"),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user