tech-bid-manageV1.120260424/utils/qwen_image_client.py
2026-04-24 14:44:38 +08:00

194 lines
6.9 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.

"""
通义千问文生图DashScope 多模态生成 API
默认模型qwen-image-2.0-pro与阿里云 Model Studio 文档一致)。
文档https://www.alibabacloud.com/help/en/model-studio/qwen-image-api
"""
from __future__ import annotations
import json
import logging
import re
from typing import Any, Optional
import requests
import config
logger = logging.getLogger(__name__)
# 北京地域同步接口(与 QWEN_BASE_URL 常用配置同账号)
DEFAULT_MULTIMODAL_BASE = 'https://dashscope.aliyuncs.com/api/v1'
MULTIMODAL_GENERATION_PATH = '/services/aigc/multimodal-generation/generation'
_NEG_DEFAULT = (
'低分辨率, 模糊, 畸形手指, 过度饱和, 蜡像感, 杂乱构图, 扭曲文字, '
'公司商标, 企业LOGO, 投标人名称, 水印, 证件照'
)
_PLACEHOLDER_KEY_RE = re.compile(r'^sk-your|^sk-xxxx', re.I)
def _effective_multimodal_base() -> str:
raw = (getattr(config, 'QWEN_MULTIMODAL_BASE', '') or '').strip().rstrip('/')
if raw:
return raw
return DEFAULT_MULTIMODAL_BASE
def _generation_url() -> str:
return f'{_effective_multimodal_base()}{MULTIMODAL_GENERATION_PATH}'
def _qwen_api_key() -> str:
key = (getattr(config, 'QWEN_API_KEY', '') or '').strip()
if not key or _PLACEHOLDER_KEY_RE.match(key):
return ''
return key
def _extract_image_url(payload: dict[str, Any]) -> Optional[str]:
try:
out = payload.get('output') or {}
choices = out.get('choices') or []
if not choices:
return None
msg = (choices[0] or {}).get('message') or {}
content = msg.get('content')
if isinstance(content, list):
for part in content:
if isinstance(part, dict):
url = part.get('image')
if isinstance(url, str) and url.startswith('http'):
return url
if isinstance(content, dict):
url = content.get('image')
if isinstance(url, str) and url.startswith('http'):
return url
except Exception:
return None
return None
def _truncate_prompt(text: str, max_chars: int = 780) -> str:
t = (text or '').strip().replace('\r\n', '\n').replace('\r', '\n')
if len(t) <= max_chars:
return t
return t[: max_chars - 3] + '...'
def build_attachment_figure_prompt(title: str, body: str) -> str:
"""投标附件用图:专业工程示意风格,避免标识与暗标敏感信息。"""
core = _truncate_prompt(f'{title}\n{body}', 720)
head = (
'建筑施工投标技术标附件插图:清晰线稿或浅色块示意,横版;可含简短中文标注。'
'禁止公司名、商标、LOGO、投标人信息。'
)
sched_keys = ('进度', '网络图', '横道', '开工', '完工', '工期', '里程碑')
site_keys = ('总平面', '平面布置', '布置图')
t = f'{title}\n{body}'
extra = ''
if any(k in t for k in sched_keys):
extra += (
'【进度图】仅表现文字中已给出的开工完工逻辑、清单节点与工期关系;'
'不得添加正文中未出现的日历日、工序名称或里程碑;无具体日期时不写具体年月日数字。'
)
if any(k in t for k in site_keys):
extra += (
'【总平面图】临时设施分区与面积须与文字中的临时用地表一致;'
'施工机械仅绘制文字明确列出的机型,禁止无关挖掘机、塔吊等;无机械列表时只画分区块不写机械剪影。'
)
extra = _truncate_prompt(extra, 200) if extra else ''
parts = [head]
if extra:
parts.append(extra)
parts.append('内容要点:\n' + core)
return '\n'.join(parts)
def generate_qwen_image_png(
prompt: str,
*,
size: Optional[str] = None,
timeout: Optional[int] = None,
) -> tuple[Optional[bytes], Optional[str]]:
"""
调用 qwen-image-2.0-pro 同步文生图,返回 PNG 字节。
失败时 (None, error_message)。
"""
api_key = _qwen_api_key()
if not api_key:
return None, '未配置有效的通义千问 API Key文生图与文本模型共用 DashScope Key'
model = getattr(config, 'QWEN_IMAGE_MODEL', 'qwen-image-2.0-pro') or 'qwen-image-2.0-pro'
sz = (size or getattr(config, 'QWEN_IMAGE_SIZE', '1536*1024') or '1536*1024').strip()
prompt_clean = _truncate_prompt(prompt, 800)
url = _generation_url()
body = {
'model': model,
'input': {
'messages': [
{
'role': 'user',
'content': [{'text': prompt_clean}],
}
]
},
'parameters': {
'negative_prompt': getattr(config, 'QWEN_IMAGE_NEGATIVE_PROMPT', _NEG_DEFAULT),
'prompt_extend': bool(getattr(config, 'QWEN_IMAGE_PROMPT_EXTEND', True)),
'watermark': bool(getattr(config, 'QWEN_IMAGE_WATERMARK', False)),
'size': sz,
},
}
to = timeout if timeout is not None else max(120, int(getattr(config, 'REQUEST_TIMEOUT', 180) or 180))
try:
resp = requests.post(
url,
headers={
'Authorization': f'Bearer {api_key}',
'Content-Type': 'application/json',
},
data=json.dumps(body, ensure_ascii=False).encode('utf-8'),
timeout=to,
)
except requests.RequestException as e:
logger.warning('qwen-image 请求异常: %s', e)
return None, str(e)
try:
data = resp.json()
except Exception:
return None, f'文生图接口返回非 JSONHTTP {resp.status_code}'
if resp.status_code != 200:
msg = data.get('message') or data.get('code') or resp.text[:500]
logger.warning('qwen-image HTTP %s: %s', resp.status_code, msg)
return None, f'文生图失败 HTTP {resp.status_code}: {msg}'
code = data.get('code')
if code and str(code).upper() not in ('OK', 'SUCCESS', '200', ''):
msg = data.get('message') or str(code)
logger.warning('qwen-image 业务错误: %s', msg)
return None, f'文生图接口错误: {msg}'
img_url = _extract_image_url(data)
if not img_url:
logger.warning('qwen-image 响应无图片 URL: %s', str(data)[:800])
return None, '文生图响应中未找到图片 URL'
try:
ir = requests.get(img_url, timeout=min(to, 120))
ir.raise_for_status()
return ir.content, None
except requests.RequestException as e:
logger.warning('下载生成图片失败: %s', e)
return None, f'下载图片失败: {e}'
def generate_attachment_figure_png(title: str, content: str) -> tuple[Optional[bytes], Optional[str]]:
"""附件 [FIGURE] 专用:拼装提示词并生成 PNG。"""
prompt = build_attachment_figure_prompt(title or '附图', content or '')
return generate_qwen_image_png(prompt)