Files
tw2/qingjian_cs/app/image_quote_analyzer.py
jimi 484f1f6be4
Some checks failed
Pre-commit / run (ubuntu-latest) (push) Has been cancelled
Deploy Sphinx documentation to Pages / build_en (ubuntu-latest, 3.10) (push) Has been cancelled
Deploy Sphinx documentation to Pages / build_zh (ubuntu-latest, 3.10) (push) Has been cancelled
Python Unittest Coverage / test (macos-15, 3.10) (push) Has been cancelled
Python Unittest Coverage / test (macos-15, 3.11) (push) Has been cancelled
Python Unittest Coverage / test (macos-15, 3.12) (push) Has been cancelled
Python Unittest Coverage / test (ubuntu-latest, 3.10) (push) Has been cancelled
Python Unittest Coverage / test (ubuntu-latest, 3.11) (push) Has been cancelled
Python Unittest Coverage / test (ubuntu-latest, 3.12) (push) Has been cancelled
Python Unittest Coverage / test (windows-latest, 3.10) (push) Has been cancelled
Python Unittest Coverage / test (windows-latest, 3.11) (push) Has been cancelled
Python Unittest Coverage / test (windows-latest, 3.12) (push) Has been cancelled
feat: integrate quality-gated draw flow with online dispatch transfer
2026-03-03 14:10:00 +08:00

345 lines
13 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
import asyncio
import base64
import logging
import os
import re
from typing import Any
from openai import AsyncOpenAI
from .config import OPENAI_API_KEY, OPENAI_BASE_URL, OPENAI_MODEL_NAME
logger = logging.getLogger(__name__)
VISION_MODEL = OPENAI_MODEL_NAME
ANALYZE_PROMPT = """
你是一个电商图片处理评估专家,同时也是 Gemini 图像生成提示词专家。
请仔细分析这张图片,输出以下字段,每行一个,不要多余内容:
敏感内容: <yes|no>
平整度: <flat|mild|rough>
含文字: <yes|no>
含人脸: <yes|no>
阴影: <yes|no>
复杂度: <simple|normal|complex|hard>
原因: <15字以内说明复杂度判断依据>
主体: <图片核心内容,如:印花图案/logo/人物/产品/老照片/风景/文字/其他>
类型: <处理类型,如:印花提取/高清修复/去背景/老照片修复/logo提取/人像修复/其他>
质量: <原图质量,如:清晰/轻微模糊/严重模糊/低分辨率/截图/扫描件>
可做: <yes|partial|no>
风险: <none|low|high>
业务相关: <yes|no>
透视: <no|mild|strong>
比例: <从以下选一个最合适的1:1 / 9:16 / 16:9 / 3:4 / 4:3 / 3:2 / 2:3 / 5:4 / 4:5>
提示词: <为 Gemini 写处理指令中文60字以内说明要做什么、保留什么、去掉什么>
备注: <给客服AI的特别提示没有则填无>
判断规则:
【报价核心:越平整越便宜】
- 平整度 flat画面平整、无褶皱、无透视 → 便宜
- 平整度 mild轻微褶皱/透视 → 中等
- 平整度 rough有褶皱/透视/曲面 → 贵
- 含文字:大字没关系不加价;小字需精细保留/清晰化 → 加价(含文字填 yes 仅指有小字的情况)
- 含人脸 yes有人脸 → 加价
- 阴影 yes有明显阴影需处理 → 加价
综合以上因素,越平整、无小字、无人脸、无阴影 → 越便宜simple
【含文字】
- yes含小字需精细保留/清晰化(小字难处理 → 加价)
- no无文字或仅有大字大字没关系 → 不加价)
【文字数量加价规则】
- none无文字不加价
- 少量 (1-10 字)+5 元
- 中量 (11-50 字)+10-15 元
- 大量 (51-200 字)+20-30 元
- 极多 (200 字以上)+30-50 元
【文字分层需求】
- yes客户要求可编辑分层文件PSD 等) → 基础价格 x2 或 +50 元起
- no普通图片处理 → 正常价格
【文字分层 + 大量文字】
- 如果 文字数量=大量/极多 且 文字分层需求=yes → 总价可达 60-80 元
【含人脸】
- yes图中有真实人物面孔人像照/集体照/证件照/老照片等)
- no无人脸或人脸极小不影响主体
【风险评估 - 重要!】
- none印花/图案/logo/风景/产品AI处理效果稳定可直接报价接单
- low有人脸但清晰度尚可AI修复后人脸相似度70-90%,可以接单但要说明风险
- high以下任一情况 → 严重模糊的人脸照片/老照片人像/需要打印/客户问能否找回原图
high情况下可做改为partial备注写明风险话术谨慎接单
【敏感内容检测 - 必须严格判断!】
- yes含以下任一内容 → 色情/黄色/擦边/裸露/性暗示/大尺度/涉政/暴力/血腥/违禁品/地图类
敏感内容=yes 时,可做必须填 no直接拒绝不接单
- no无上述敏感内容可以正常接单处理
【可做判断 - 决定是否接单】
- yes效果有把握可以接单处理
- partial能处理但有明显限制人脸变形风险/分辨率极低/严重损坏)→ 可以接单但要说明风险
- no无法接单纯黑/纯白/完全损坏/找原始 RAW 文件/敏感内容/违法内容)
【敏感内容】优先判断,若为 yes 则 可做 必填 no
- yes图片含色情/黄色/擦边/裸露/性暗示/大尺度等违规内容
- no无上述敏感内容
【可做判断】
- yes效果有把握可直接处理
- partial能处理但有明显限制人脸变形风险/分辨率极低/严重损坏)
- no无法处理纯黑/纯白/完全损坏/找原始RAW文件/敏感内容)
【业务相关判断 - 必须由你判断】
- yes需求与印花/印刷素材处理,或高清修复业务相关
- no与以上业务无关例如闲聊、非图像处理诉求
- 若业务相关=no则可做必须填 no
【风险话术模板(备注字段)】
- 含人脸+需打印AI修复后人脸可能有轻微变化建议先看效果确认再打印
- 严重模糊人脸:这张模糊程度较高,修复后清晰了但人脸可能跟原来有差异
- 找原图:找不到原始文件,只能对现有图片做高清修复处理
- 完全损坏:这张无法处理
【透视判断】
- no正面拍摄无明显变形
- mild轻微透视衣服悬挂/桌面小角度斜拍)
- strong严重透视俯拍/贴墙/大角度倾斜)
【比例选择】
- 印花/图案/logo/正方形 -> 1:1
- 竖屏壁纸/短视频封面 -> 9:16
- 宽屏/横版视频 -> 16:9
- 移动广告/Instagram竖图 -> 4:5
- 竖向人像/海报/证件照 -> 3:4
- 竖向相机照片 -> 2:3
- 接近正方形产品图 -> 5:4
- 横向标准图/风景 -> 4:3
- 横向相机照片/产品实拍 -> 3:2
示例1印花无风险
敏感内容: no
平整度: mild
含文字: no
含人脸: no
阴影: no
复杂度: complex
原因: 印花细节密集颜色层次多
主体: 印花图案
类型: 印花提取
质量: 轻微模糊
可做: yes
风险: none
透视: mild
比例: 1:1
提示词: 提取衣物印花图案去除褶皱和背景杂色补全缺失部分保持颜色细节100%还原,输出干净平面印花图
备注: 无
示例2人像老照片要打印
敏感内容: no
平整度: flat
含文字: no
含人脸: yes
阴影: no
复杂度: hard
原因: 严重模糊人脸细节丢失
主体: 人物照片
类型: 人像修复
质量: 严重模糊
可做: partial
风险: high
透视: no
比例: 3:4
提示词: 对模糊人像进行高清修复,增强细节,保持人物特征不变
备注: AI修复后人脸可能有轻微变化建议先看效果确认满意再用于打印
示例3平整印花最便宜
敏感内容: no
平整度: flat
含文字: no
含人脸: no
阴影: no
复杂度: simple
原因: 画面平整无褶皱无文字无人脸
主体: 印花图案
类型: 印花提取
质量: 清晰
可做: yes
风险: none
透视: no
比例: 1:1
提示词: 提取印花图案,去除背景,输出干净平面图
备注: 无
"""
def _clip(text: str, n: int = 180) -> str:
t = re.sub(r"\s+", " ", str(text or "")).strip()
if len(t) <= n:
return t
return f"{t[:n]}..."
def _extract_line(text: str, key: str) -> str:
m = re.search(rf"{re.escape(key)}\s*[:]\s*(.+)", text or "")
return m.group(1).strip() if m else ""
def _parse_result(text: str) -> dict[str, Any]:
intent = _extract_line(text, "诉求类型")
if not intent:
intent = _extract_line(text, "类型")
can_do = _extract_line(text, "可做").lower()
business_related = _extract_line(text, "业务相关").lower()
complexity = _extract_line(text, "复杂度").lower()
price_text = _extract_line(text, "建议报价")
if not price_text:
price_text = _extract_line(text, "建议价格")
gemini_prompt = _extract_line(text, "提示词")
aspect_ratio = _extract_line(text, "比例")
risk = _extract_line(text, "风险").lower()
note = _extract_line(text, "说明")
if not note:
note = _extract_line(text, "备注")
if intent not in {"找图", "高清修复", "其他"}:
intent = "其他"
if can_do not in {"yes", "partial", "no"}:
can_do = "partial"
if complexity not in {"simple", "normal", "complex", "hard"}:
complexity = "normal"
if business_related not in {"yes", "no"}:
business_related = "yes"
if aspect_ratio not in {"1:1", "9:16", "16:9", "3:4", "4:3", "3:2", "2:3", "5:4", "4:5"}:
aspect_ratio = "1:1"
if risk not in {"none", "low", "high"}:
risk = "none"
price = 0
m = re.search(r"\d+", price_text or "")
if m:
price = int(m.group(0))
return {
"intent_type": intent,
"can_do": can_do,
"business_related": business_related,
"complexity": complexity,
"price_suggest": price,
"gemini_prompt": gemini_prompt or "",
"aspect_ratio": aspect_ratio,
"risk": risk,
"note": note or "已看图",
}
async def analyze_image_for_quote(image_url: str, customer_text: str = "", goods_name: str = "") -> dict[str, Any]:
"""
识图报价分析(用于 QuoteAgent 上下文增强)
"""
image_url = str(image_url or "").strip()
if not image_url:
return {"ok": False, "error": "empty_image_url"}
if not OPENAI_API_KEY:
return {"ok": False, "error": "OPENAI_API_KEY 未配置"}
model_name = VISION_MODEL
client = AsyncOpenAI(base_url=OPENAI_BASE_URL, api_key=OPENAI_API_KEY)
user_text = (
f"客户文本: {customer_text or ''}\n"
f"商品标题: {goods_name or ''}\n"
"请按固定字段输出。"
)
logger.info("[识图报价] 豆包模型=%s 图片=%s 提示词=%s", model_name, image_url, _clip(ANALYZE_PROMPT, 90))
try:
resp = await asyncio.wait_for(
client.chat.completions.create(
model=model_name,
temperature=0.1,
messages=[
{"role": "system", "content": "你是电商图片识别报价助手。"},
{
"role": "user",
"content": [
{"type": "text", "text": f"{ANALYZE_PROMPT}\n{user_text}"},
{"type": "image_url", "image_url": {"url": image_url}},
],
},
],
),
timeout=30,
)
text = str((resp.choices[0].message.content if resp and resp.choices else "") or "").strip()
logger.info("[识图报价] 模型输出=%s", _clip(text, 260))
parsed = _parse_result(text)
parsed["ok"] = True
parsed["raw"] = text
return parsed
except Exception as e:
logger.error("[识图报价] 调用失败: %s", e)
return {"ok": False, "error": str(e)}
def _image_file_to_data_url(path: str) -> str:
with open(path, "rb") as f:
b64 = base64.b64encode(f.read()).decode("utf-8")
return f"data:image/jpeg;base64,{b64}"
async def evaluate_generated_image(
original_image_url: str,
generated_image_path: str,
requirement: str = "",
) -> dict[str, Any]:
"""
生成后质量评估:不通过则建议直接退出,不发送给客户。
"""
if not OPENAI_API_KEY:
return {"ok": False, "pass": True, "reason": "no_api_key_skip"}
if not os.path.exists(generated_image_path):
return {"ok": False, "pass": False, "reason": "generated_file_missing"}
client = AsyncOpenAI(base_url=OPENAI_BASE_URL, api_key=OPENAI_API_KEY)
review_prompt = (
"你是印花/印刷交付质检。请对比原图和生成图,判断是否可发给客户。\n"
"重点看主体是否跑偏、细节是否糊、是否明显AI味、是否不适合印刷。\n"
"只输出两行:\n"
"评估: <pass|fail>\n"
"原因: <20字内>\n"
f"客户要求: {requirement or ''}"
)
try:
gen_url = _image_file_to_data_url(generated_image_path)
resp = await asyncio.wait_for(
client.chat.completions.create(
model=VISION_MODEL,
temperature=0.1,
messages=[
{"role": "system", "content": "你是严格的图像交付质检员。"},
{
"role": "user",
"content": [
{"type": "text", "text": review_prompt},
{"type": "image_url", "image_url": {"url": original_image_url}},
{"type": "image_url", "image_url": {"url": gen_url}},
],
},
],
),
timeout=30,
)
text = str((resp.choices[0].message.content if resp and resp.choices else "") or "").strip()
flag = _extract_line(text, "评估").lower()
reason = _extract_line(text, "原因") or _clip(text, 40)
passed = flag == "pass"
logger.info("[作图评估] result=%s reason=%s", "pass" if passed else "fail", reason)
return {"ok": True, "pass": passed, "reason": reason, "raw": text}
except Exception as e:
logger.error("[作图评估] 调用失败: %s", e)
return {"ok": False, "pass": False, "reason": str(e)}