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 图像生成提示词专家。 请仔细分析这张图片,输出以下字段,每行一个,不要多余内容: 敏感内容: 平整度: 含文字: 含人脸: 阴影: 复杂度: 原因: <15字以内,说明复杂度判断依据> 主体: <图片核心内容,如:印花图案/logo/人物/产品/老照片/风景/文字/其他> 类型: <处理类型,如:印花提取/高清修复/去背景/老照片修复/logo提取/人像修复/其他> 质量: <原图质量,如:清晰/轻微模糊/严重模糊/低分辨率/截图/扫描件> 可做: 风险: 业务相关: 透视: 比例: <从以下选一个最合适的: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" "评估: \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)}