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
266 lines
9.4 KiB
Python
266 lines
9.4 KiB
Python
from __future__ import annotations
|
||
|
||
import asyncio
|
||
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, "诉求类型")
|
||
can_do = _extract_line(text, "可做").lower()
|
||
complexity = _extract_line(text, "复杂度").lower()
|
||
price_text = _extract_line(text, "建议报价")
|
||
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"
|
||
|
||
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,
|
||
"complexity": complexity,
|
||
"price_suggest": price,
|
||
"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)}
|
||
|