修改一些直接使用中垒火焱大模型网关的服务
This commit is contained in:
parent
496c478aa2
commit
4c0474fcfe
|
|
@ -69,6 +69,8 @@ backend/logs/
|
||||||
.env.*
|
.env.*
|
||||||
!.env.example
|
!.env.example
|
||||||
!**/.env.example
|
!**/.env.example
|
||||||
|
!.env.docker
|
||||||
|
!**/.env.docker
|
||||||
|
|
||||||
# IDE
|
# IDE
|
||||||
.vscode/
|
.vscode/
|
||||||
|
|
@ -99,3 +101,4 @@ Thumbs.db
|
||||||
# 本地导出的 IDE / 对话历史等(按需)
|
# 本地导出的 IDE / 对话历史等(按需)
|
||||||
history.txt
|
history.txt
|
||||||
.cursor/
|
.cursor/
|
||||||
|
backend/.env.docker
|
||||||
|
|
@ -2,6 +2,8 @@ import { defineConfig } from "vite";
|
||||||
import vue from "@vitejs/plugin-vue";
|
import vue from "@vitejs/plugin-vue";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
// Docker 部署在 /admin/ 子路径下时需构建前设置 VITE_BASE_PATH=/admin/
|
||||||
|
base: process.env.VITE_BASE_PATH || "/",
|
||||||
plugins: [vue()],
|
plugins: [vue()],
|
||||||
server: {
|
server: {
|
||||||
port: 5174,
|
port: 5174,
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,24 @@ APP.NAME=星云 API Server
|
||||||
DB_HOST=106.15.186.110
|
DB_HOST=106.15.186.110
|
||||||
DB_PORT=5432
|
DB_PORT=5432
|
||||||
DB_NAME=qiyeban_huoyanai
|
DB_NAME=qiyeban_huoyanai
|
||||||
DB_USER=zuoleiroot
|
DB_USER=changeme_in_production
|
||||||
DB_PASSWORD=C1C0DDleRy4wgSkD
|
DB_PASSWORD=changeme_in_production
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== Neo4j 图数据库配置 ====================
|
||||||
|
NEO4J_URI=bolt://ip:7687
|
||||||
|
NEO4J_USER=neo4j
|
||||||
|
NEO4J_PASSWORD=graph123
|
||||||
|
|
||||||
|
# 数据库连接池配置
|
||||||
|
DB_POOL_MIN_SIZE=5
|
||||||
|
DB_POOL_MAX_SIZE=20
|
||||||
|
DB_COMMAND_TIMEOUT=60
|
||||||
|
CHECKPOINTER_POOL_MAX_SIZE=20
|
||||||
|
|
||||||
|
# ==================== ChromaDB(与原项目同网络)====================
|
||||||
|
CHROMA_HOST=106.15.186.110
|
||||||
|
CHROMA_PORT=9527
|
||||||
|
|
||||||
|
|
||||||
# ==================== JWT 认证配置 ====================
|
# ==================== JWT 认证配置 ====================
|
||||||
|
|
@ -32,23 +48,18 @@ logging.enable_console=True
|
||||||
# HTTP 请求超时时间(秒)
|
# HTTP 请求超时时间(秒)
|
||||||
HTTPX_DEFAULT_TIMEOUT=120
|
HTTPX_DEFAULT_TIMEOUT=120
|
||||||
|
|
||||||
# ==================== 代理配置 ====================
|
# ==================== 业务 / 第三方 MCP ====================
|
||||||
# 如果需要通过代理访问 GitHub(可选)
|
|
||||||
# HTTP_PROXY=http://127.0.0.1:7890
|
|
||||||
# HTTPS_PROXY=http://127.0.0.1:7890
|
|
||||||
|
|
||||||
MCP_JUHE_TOKEN=SLIC4Zv3KnCkxyOYsZj4FabImp0RDdz8Td17Io0Tn2YHio
|
MCP_JUHE_TOKEN=SLIC4Zv3KnCkxyOYsZj4FabImp0RDdz8Td17Io0Tn2YHio
|
||||||
OSS_ACCESS_KEY_ID = 'LTAI5tFGRDXbWyCzJL2e8Apd'
|
|
||||||
OSS_ACCESS_KEY_SECRET = 'QMEBsDhuAX6YwSmAbbILvsA7WFU58w'
|
# ==================== 阿里云 OSS(按需)====================
|
||||||
|
OSS_ACCESS_KEY_ID = changeme_in_production
|
||||||
|
OSS_ACCESS_KEY_SECRET = changeme_in_production
|
||||||
OSS_ENDPOINT = 'https://oss-cn-hangzhou.aliyuncs.com' # 根据你的区域修改
|
OSS_ENDPOINT = 'https://oss-cn-hangzhou.aliyuncs.com' # 根据你的区域修改
|
||||||
|
OSS_BUCKET_NAME = changeme_in_production
|
||||||
OSS_BUCKET_NAME = 'zhongleiai'
|
|
||||||
CHROMA_HOST=106.15.186.110
|
|
||||||
CHROMA_PORT=9527
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# RAG 配置
|
# ==================== RAG / Embedding ====================
|
||||||
RAG_CHUNK_SIZE=512 # 文本分块大小
|
RAG_CHUNK_SIZE=512 # 文本分块大小
|
||||||
RAG_CHUNK_OVERLAP=50 # 分块重叠大小
|
RAG_CHUNK_OVERLAP=50 # 分块重叠大小
|
||||||
RAG_TOP_K=5 # 检索返回的文档数量
|
RAG_TOP_K=5 # 检索返回的文档数量
|
||||||
|
|
@ -59,27 +70,28 @@ EMBEDDING_MODEL=text-embedding-v4 # 通义千问 Embedding 模型
|
||||||
EMBEDDING_DIMENSION=1536 # Embedding 维度
|
EMBEDDING_DIMENSION=1536 # Embedding 维度
|
||||||
|
|
||||||
|
|
||||||
# OCR_ACCESS_KEY_ID=LTAI5tE5oGfC37bh3Vg1KLNK
|
# ==================== 阿里云 OCR ====================
|
||||||
# OCR_ACCESS_KEY_SECRET=WBAa3Fh8Tw9Kvgx4zzagWDOcPlSp4L
|
OCR_ACCESS_KEY_ID=changeme_in_production
|
||||||
# OCR_ENDPOINT=ocr-api.cn-hangzhou.aliyuncs.com
|
OCR_ACCESS_KEY_SECRET=changeme_in_production
|
||||||
# OCR_USE_LOCAL=false
|
|
||||||
|
|
||||||
OCR_ACCESS_KEY_ID=LTAI5tHAbs3umUtnMS1yR8Ti
|
|
||||||
OCR_ACCESS_KEY_SECRET=eByWHxrWrrDtKgOmKIu9jood6RTtwS
|
|
||||||
OCR_ENDPOINT=ocr-api.cn-hangzhou.aliyuncs.com
|
OCR_ENDPOINT=ocr-api.cn-hangzhou.aliyuncs.com
|
||||||
OCR_USE_LOCAL=false
|
OCR_USE_LOCAL=false
|
||||||
MODERATION_ENABLED=false
|
MODERATION_ENABLED=false
|
||||||
|
|
||||||
# ==================== Neo4j 图数据库配置 ====================
|
# DEEPSEEK_API_KEY=sk-changeme_in_production
|
||||||
NEO4J_URI=bolt://47.110.73.142:7687
|
# DASHSCOPE_API_KEY=sk-changeme_in_production
|
||||||
NEO4J_USER=neo4j
|
# DEEPSEEK_API_BASE=https://api.zlapi.com.cn/api/v1
|
||||||
NEO4J_PASSWORD=graph123
|
# DASHSCOPE_API_BASE=https://api.zlapi.com.cn/api/v1
|
||||||
|
|
||||||
|
|
||||||
DEEPSEEK_API_KEY=sk-CvmggZnFVo0JlaBOa1EL9FRjn4bEprK
|
|
||||||
DASHSCOPE_API_KEY=sk-CvmggZnFVo0JlaBOa1EL9FRjn4bEprK
|
|
||||||
DEEPSEEK_API_BASE=https://api.zlapi.com.cn/api/v1
|
|
||||||
DASHSCOPE_API_BASE=https://api.zlapi.com.cn/api/v1
|
|
||||||
|
|
||||||
# 通义:ChatOpenAI / 视觉等走此兼容 base(如 .../compatible-mode/v1)。USE_ORIGIN_MODEL=true 时 ChatTongyi / 文生图等原生 SDK 会自动用同主机 .../api/v1(勿把兼容 URL 直接当原生 base)。
|
DEEPSEEK_API_KEY=changeme_in_production
|
||||||
USE_ORIGIN_MODEL=false
|
DASHSCOPE_API_KEY=changeme_in_production
|
||||||
|
#DEEPSEEK_API_BASE=https://api.deepseek.com/v1
|
||||||
|
DASHSCOPE_API_BASE=https://dashscope.aliyuncs.com/compatible-mode/v1
|
||||||
|
|
||||||
|
# 当此处为 true 时,表示聊天模型,使用的是原生的厂商模型以及原生厂商地址,否则,使用的是中垒的d大模型网关
|
||||||
|
USE_ORIGIN_MODEL=True
|
||||||
|
|
||||||
|
# 此处为必填,表示一定要使用中垒的dashscope网关,设计到的服务有text_to_image, text_to_video, text_to_poster以及向量化服务的embedding模型
|
||||||
|
ZL_DASHSCOPE_API_BASE=https://api.zlapi.com.cn/api/v1
|
||||||
|
ZL_DASHSCOPE_API_KEY=changeme_in_production
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
# Python 3.11 + uv(与 pyproject 中 requires-python 一致)
|
||||||
|
FROM ghcr.io/astral-sh/uv:python3.11-bookworm-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY pyproject.toml uv.lock ./
|
||||||
|
RUN uv sync --frozen --no-dev
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
|
EXPOSE 7861 7862
|
||||||
|
|
||||||
|
# 端口以环境变量 API.PORT / API_PORT 为准(见 core.config)
|
||||||
|
CMD ["uv", "run", "python", "-m", "main"]
|
||||||
|
|
@ -131,8 +131,8 @@ class VectorService:
|
||||||
print(settings.dashscope_api_key, settings.dashscope_api_base)
|
print(settings.dashscope_api_key, settings.dashscope_api_base)
|
||||||
self.embedding = OpenAIEmbeddings(
|
self.embedding = OpenAIEmbeddings(
|
||||||
model="text-embedding-v4",
|
model="text-embedding-v4",
|
||||||
api_key=settings.dashscope_api_key,
|
api_key=os.getenv("ZL_DASHSCOPE_API_KEY"), # 如果您没有配置环境变量,请在此处用您的API Key进行替换
|
||||||
base_url=settings.dashscope_api_base,
|
base_url=os.getenv("ZL_DASHSCOPE_API_BASE"),
|
||||||
check_embedding_ctx_length=False,
|
check_embedding_ctx_length=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
"""
|
"""
|
||||||
视觉模型服务
|
视觉模型服务
|
||||||
|
|
||||||
基于阿里云通义千问视觉模型 (qwen-vl-max-latest) 提供图片理解能力。
|
基于 OpenAI 兼容 ``chat.completions`` 接口(参见阿里云百炼视觉文档),使用支持图像输入的多模态模型。
|
||||||
参考 server/aaa/jenius_attachment_knowledge_base/jenius_rag_util.py 的实现。
|
默认模型 ``qwen3-vl-plus``;网关与密钥优先读 ``ZL_OPENAI_*``,未配置时回退 ``DASHSCOPE_*`` / ``settings``。
|
||||||
"""
|
"""
|
||||||
import asyncio
|
import asyncio
|
||||||
import base64
|
import base64
|
||||||
|
import os
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from openai import OpenAI, AsyncOpenAI
|
from openai import OpenAI, AsyncOpenAI
|
||||||
|
|
@ -15,6 +16,27 @@ from logger.logging import get_logger
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
# 与 OpenAI 兼容视觉示例一致;可通过环境变量覆盖
|
||||||
|
VISION_CHAT_MODEL = (os.getenv("ZL_OPENAI_VISION_MODEL") or "qwen3-vl-plus").strip()
|
||||||
|
|
||||||
|
|
||||||
|
def _vision_openai_api_key() -> str:
|
||||||
|
zl = (os.getenv("ZL_OPENAI_API_KEY") or "").strip()
|
||||||
|
if zl:
|
||||||
|
return zl
|
||||||
|
return (settings.dashscope_api_key or "").strip()
|
||||||
|
|
||||||
|
|
||||||
|
def _vision_openai_base_url() -> str:
|
||||||
|
zl = (os.getenv("ZL_OPENAI_BASE_URL") or "").strip().rstrip("/")
|
||||||
|
if zl:
|
||||||
|
return zl
|
||||||
|
return tongyi_openai_compatible_base_url().strip().rstrip("/")
|
||||||
|
|
||||||
|
|
||||||
|
def _vision_client_signature() -> tuple[str, str]:
|
||||||
|
return (_vision_openai_api_key(), _vision_openai_base_url())
|
||||||
|
|
||||||
|
|
||||||
def _is_vision_image_url(url: str) -> bool:
|
def _is_vision_image_url(url: str) -> bool:
|
||||||
if not url:
|
if not url:
|
||||||
|
|
@ -45,39 +67,33 @@ def image_bytes_to_data_url(image_bytes: bytes, mime_hint: Optional[str] = None)
|
||||||
|
|
||||||
|
|
||||||
class VisionService:
|
class VisionService:
|
||||||
"""视觉模型服务类
|
"""视觉模型服务:OpenAI SDK ``chat.completions`` + ``image_url``(URL 或 base64 data URL)。"""
|
||||||
|
|
||||||
使用阿里云通义千问视觉模型进行图片理解和描述。
|
|
||||||
"""
|
|
||||||
|
|
||||||
_client_cache: Optional[AsyncOpenAI] = None
|
_client_cache: Optional[AsyncOpenAI] = None
|
||||||
_sync_client_cache: Optional[OpenAI] = None
|
_sync_client_cache: Optional[OpenAI] = None
|
||||||
|
_async_client_sig: Optional[tuple[str, str]] = None
|
||||||
|
_sync_client_sig: Optional[tuple[str, str]] = None
|
||||||
_lock = asyncio.Lock()
|
_lock = asyncio.Lock()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def _get_async_client(cls) -> AsyncOpenAI:
|
async def _get_async_client(cls) -> AsyncOpenAI:
|
||||||
"""获取或创建异步客户端(单例模式)"""
|
"""获取或创建异步客户端(凭证变更时重建)。"""
|
||||||
if cls._client_cache is not None:
|
sig = _vision_client_signature()
|
||||||
return cls._client_cache
|
|
||||||
|
|
||||||
async with cls._lock:
|
async with cls._lock:
|
||||||
if cls._client_cache is None:
|
if cls._client_cache is not None and cls._async_client_sig == sig:
|
||||||
cls._client_cache = AsyncOpenAI(
|
return cls._client_cache
|
||||||
api_key=settings.dashscope_api_key,
|
cls._async_client_sig = sig
|
||||||
base_url=tongyi_openai_compatible_base_url(),
|
cls._client_cache = AsyncOpenAI(api_key=sig[0], base_url=sig[1])
|
||||||
)
|
|
||||||
return cls._client_cache
|
return cls._client_cache
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _get_sync_client(cls) -> OpenAI:
|
def _get_sync_client(cls) -> OpenAI:
|
||||||
"""获取或创建同步客户端(单例模式)"""
|
"""获取或创建同步客户端(凭证变更时重建)。"""
|
||||||
if cls._sync_client_cache is not None:
|
sig = _vision_client_signature()
|
||||||
|
if cls._sync_client_cache is not None and cls._sync_client_sig == sig:
|
||||||
return cls._sync_client_cache
|
return cls._sync_client_cache
|
||||||
|
cls._sync_client_sig = sig
|
||||||
cls._sync_client_cache = OpenAI(
|
cls._sync_client_cache = OpenAI(api_key=sig[0], base_url=sig[1])
|
||||||
api_key=settings.dashscope_api_key,
|
|
||||||
base_url=tongyi_openai_compatible_base_url(),
|
|
||||||
)
|
|
||||||
return cls._sync_client_cache
|
return cls._sync_client_cache
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
@ -104,27 +120,21 @@ class VisionService:
|
||||||
client = await cls._get_async_client()
|
client = await cls._get_async_client()
|
||||||
|
|
||||||
completion = await client.chat.completions.create(
|
completion = await client.chat.completions.create(
|
||||||
model="qwen-vl-max-latest",
|
model=VISION_CHAT_MODEL,
|
||||||
messages=[
|
messages=[
|
||||||
{
|
|
||||||
"role": "system",
|
|
||||||
"content": [{"type": "text", "text": "You are a helpful assistant."}]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"role": "user",
|
"role": "user",
|
||||||
"content": [
|
"content": [
|
||||||
{
|
{"type": "image_url", "image_url": {"url": image_url}},
|
||||||
"type": "image_url",
|
{"type": "text", "text": prompt},
|
||||||
"image_url": {"url": image_url}
|
],
|
||||||
},
|
|
||||||
{"type": "text", "text": prompt}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
description = completion.choices[0].message.content
|
description = completion.choices[0].message.content or ""
|
||||||
logger.info(f"成功获取图片描述: {description[:50]}...")
|
if description:
|
||||||
|
logger.info(f"成功获取图片描述: {description[:50]}...")
|
||||||
return description
|
return description
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -142,8 +152,8 @@ class VisionService:
|
||||||
从内存中的图片字节获取描述(异步),使用 data URL 调用通义 VL。
|
从内存中的图片字节获取描述(异步),使用 data URL 调用通义 VL。
|
||||||
用于知识图谱上传等无公网 URL 的场景。
|
用于知识图谱上传等无公网 URL 的场景。
|
||||||
"""
|
"""
|
||||||
if not settings.dashscope_api_key:
|
if not _vision_openai_api_key():
|
||||||
logger.warning("未配置 DASHSCOPE_API_KEY,无法进行视觉理解")
|
logger.warning("未配置 ZL_OPENAI_API_KEY 或 DASHSCOPE_API_KEY,无法进行视觉理解")
|
||||||
return ""
|
return ""
|
||||||
if not image_bytes:
|
if not image_bytes:
|
||||||
return ""
|
return ""
|
||||||
|
|
@ -174,27 +184,21 @@ class VisionService:
|
||||||
client = cls._get_sync_client()
|
client = cls._get_sync_client()
|
||||||
|
|
||||||
completion = client.chat.completions.create(
|
completion = client.chat.completions.create(
|
||||||
model="qwen-vl-max-latest",
|
model=VISION_CHAT_MODEL,
|
||||||
messages=[
|
messages=[
|
||||||
{
|
|
||||||
"role": "system",
|
|
||||||
"content": [{"type": "text", "text": "You are a helpful assistant."}]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"role": "user",
|
"role": "user",
|
||||||
"content": [
|
"content": [
|
||||||
{
|
{"type": "image_url", "image_url": {"url": image_url}},
|
||||||
"type": "image_url",
|
{"type": "text", "text": prompt},
|
||||||
"image_url": {"url": image_url}
|
],
|
||||||
},
|
|
||||||
{"type": "text", "text": prompt}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
description = completion.choices[0].message.content
|
description = completion.choices[0].message.content or ""
|
||||||
logger.info(f"成功获取图片描述: {description[:50]}...")
|
if description:
|
||||||
|
logger.info(f"成功获取图片描述: {description[:50]}...")
|
||||||
return description
|
return description
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -225,27 +229,21 @@ class VisionService:
|
||||||
client = await cls._get_async_client()
|
client = await cls._get_async_client()
|
||||||
|
|
||||||
completion = await client.chat.completions.create(
|
completion = await client.chat.completions.create(
|
||||||
model="qwen-vl-max-latest",
|
model=VISION_CHAT_MODEL,
|
||||||
messages=[
|
messages=[
|
||||||
{
|
|
||||||
"role": "system",
|
|
||||||
"content": [{"type": "text", "text": "You are a helpful assistant that can analyze images and answer questions about them."}]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"role": "user",
|
"role": "user",
|
||||||
"content": [
|
"content": [
|
||||||
{
|
{"type": "image_url", "image_url": {"url": image_url}},
|
||||||
"type": "image_url",
|
{"type": "text", "text": question},
|
||||||
"image_url": {"url": image_url}
|
],
|
||||||
},
|
|
||||||
{"type": "text", "text": question}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
answer = completion.choices[0].message.content
|
answer = completion.choices[0].message.content or ""
|
||||||
logger.info(f"成功分析图片并回答问题")
|
if answer:
|
||||||
|
logger.info("成功分析图片并回答问题")
|
||||||
return answer
|
return answer
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
|
||||||
|
|
@ -3,20 +3,18 @@
|
||||||
|
|
||||||
定义各种 AI 工具函数,包括网络搜索、文生图、文生视频、RAG 检索等。
|
定义各种 AI 工具函数,包括网络搜索、文生图、文生视频、RAG 检索等。
|
||||||
"""
|
"""
|
||||||
|
import os
|
||||||
import time
|
import time
|
||||||
import uuid
|
import uuid
|
||||||
import requests
|
import requests
|
||||||
from typing import Literal, Optional
|
from typing import Literal, Optional
|
||||||
from http import HTTPStatus
|
from openai import OpenAI
|
||||||
from langchain.tools import tool
|
from langchain.tools import tool
|
||||||
from dashscope import ImageSynthesis, VideoSynthesis
|
|
||||||
import dashscope
|
|
||||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from tavily import TavilyClient
|
from tavily import TavilyClient
|
||||||
from core.config import settings
|
from core.config import settings
|
||||||
from core import llm_env
|
|
||||||
from utils.datetime_utils import format_beijing_time_for_agent
|
from utils.datetime_utils import format_beijing_time_for_agent
|
||||||
from logger.logging import get_logger
|
from logger.logging import get_logger
|
||||||
from services.vector_service import get_vector_service
|
from services.vector_service import get_vector_service
|
||||||
|
|
@ -26,11 +24,6 @@ from services.oss_service import get_oss_service
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def _dashscope_http_api_base() -> str:
|
|
||||||
"""``dashscope`` 原生 SDK 使用的 HTTP 根路径(与 OpenAI 兼容 ``DASHSCOPE_API_BASE`` 可能不同)。"""
|
|
||||||
return llm_env.dashscope_native_http_api_base().strip().rstrip("/")
|
|
||||||
|
|
||||||
|
|
||||||
# 初始化 Tavily 客户端
|
# 初始化 Tavily 客户端
|
||||||
tavily_client = TavilyClient(api_key=settings.tavily_api_key)
|
tavily_client = TavilyClient(api_key=settings.tavily_api_key)
|
||||||
|
|
||||||
|
|
@ -125,91 +118,90 @@ def _download_and_upload_image_to_oss(image_url: str, image_index: int) -> tuple
|
||||||
return image_url, None, 0.0, 0.0
|
return image_url, None, 0.0, 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_openai_image_size(size: str) -> str:
|
||||||
|
"""将「宽*高」转为 OpenAI 兼容接口要求的「宽x高」。"""
|
||||||
|
return (size or "").strip().replace("*", "x")
|
||||||
|
|
||||||
|
|
||||||
@tool
|
@tool
|
||||||
def text_to_image(
|
def text_to_image(
|
||||||
prompt: str,
|
prompt: str,
|
||||||
negative_prompt: str = "",
|
negative_prompt: str = "",
|
||||||
size: str = "1280*720",
|
size: str = "1024x1024",
|
||||||
n: int = 1,
|
n: int = 1,
|
||||||
)->str:
|
prompt_extend: bool = True,
|
||||||
|
watermark: bool = False,
|
||||||
|
seed: Optional[int] = None,
|
||||||
|
) -> str:
|
||||||
"""
|
"""
|
||||||
文生图工具:根据文本描述生成高质量图片。
|
文生图工具:根据文本描述生成高质量图片。
|
||||||
|
|
||||||
当用户需要生成图片、创建图像、制作插图、设计视觉内容时,使用此工具。
|
通过 OpenAI 兼容接口 ``/v1/images/generations`` 调用通义千问文生图模型(如同步返回的 Qwen-Image 系列)。
|
||||||
该工具使用阿里云百炼平台的 AI 图像生成模型,可以根据文字描述生成相应的图片。
|
生成的图片会自动上传到 OSS,返回永久可访问的 URL。
|
||||||
生成的图片会自动上传到 OSS 存储,返回永久可访问的 URL。
|
|
||||||
|
|
||||||
使用场景:
|
使用场景:
|
||||||
- 用户说"生成一张...的图片"、"画一个..."、"创建...的图像"
|
- 用户说「生成一张…的图片」「画一个…」「创建…的图像」
|
||||||
- 需要为文章、演示文稿、社交媒体创建配图
|
- 需要配图、插图或可视化某个概念/场景
|
||||||
- 用户想要可视化某个概念、场景、物体或人物
|
|
||||||
- 需要生成多个不同风格的图片供选择
|
|
||||||
|
|
||||||
参数说明:
|
参数说明:
|
||||||
prompt (必需): 详细描述想要生成的图片内容。应该包含:
|
prompt: 正向提示词(建议详细描述主体、场景、风格、光线与构图)。
|
||||||
- 主体对象(人物、动物、物品等)
|
negative_prompt: 反向提示词;不需要可留空。
|
||||||
- 场景和环境(背景、地点、氛围)
|
size: 分辨率,OpenAI 标准 ``宽x高``(也可用 ``宽*高``,会自动转为 ``x``)。
|
||||||
- 风格和艺术效果(写实、卡通、油画、水彩等)
|
qwen-image-2.0 系列常用:``2048x2048``(默认 1:1)、``2688x1536``(16:9)、
|
||||||
- 颜色和光线(明亮、昏暗、暖色调等)
|
``1536x2688``(9:16)、``2368x1728``(4:3)、``1728x2368``(3:4)。
|
||||||
- 构图和视角(正面、侧面、俯视、特写等)
|
plus / qwen-image 系列可参考:``1664x928``、``1472x1104``、``1328x1328`` 等。
|
||||||
示例:"一只可爱的橘色小猫坐在窗台上,阳光透过窗户洒在它身上,背景是温馨的客厅,写实风格"
|
n: 生成张数;qwen-image-2.0 系列多为 1–6,其它系列常为 1。
|
||||||
|
prompt_extend: 是否开启提示词智能改写(True 时模型会润色提示词)。
|
||||||
negative_prompt (可选): 描述不希望在图片中出现的内容,用于排除不想要的元素。
|
watermark: 是否在图像上加 Qwen-Image 水印。
|
||||||
示例:"模糊,低质量,文字,水印,变形,多余的手指"
|
seed: 随机种子 [0, 2147483647];不传则由服务端随机。
|
||||||
|
|
||||||
size (可选): 图片尺寸,格式为 "宽*高"。支持的官方尺寸:
|
|
||||||
- "1280*1280" - 1:1 正方形(适合头像、图标、社交媒体头像)
|
|
||||||
- "800*1200" - 2:3 竖向(适合手机壁纸、竖版海报)
|
|
||||||
- "1200*800" - 3:2 横向(适合横向展示、横幅)
|
|
||||||
- "960*1280" - 3:4 竖向(适合手机屏幕、竖版内容)
|
|
||||||
- "1280*960" - 4:3 横向(适合传统显示器比例、横版内容)
|
|
||||||
- "720*1280" - 9:16 竖向(适合手机竖屏、短视频封面)
|
|
||||||
- "1280*720" - 16:9 横向(默认,适合宽屏显示器、视频封面、网页横幅)
|
|
||||||
- "1344*576" - 21:9 超宽屏(适合电影比例、超宽屏展示)
|
|
||||||
默认值:"1280*720"
|
|
||||||
|
|
||||||
n (可选): 生成图片的数量,范围 1-4。生成多张图片时会并行处理以提高效率。
|
|
||||||
默认值:1
|
|
||||||
|
|
||||||
返回值:
|
返回值:
|
||||||
返回包含图片的 Markdown 格式字符串,图片会自动显示在对话中。
|
Markdown 文本,包含可供渲染的图片链接(OSS 永久 URL,失败时回退临时 URL)。
|
||||||
如果生成多张图片,会按顺序展示所有图片。
|
|
||||||
|
|
||||||
注意事项:
|
注意:
|
||||||
- 生成图片需要一定时间,请耐心等待
|
平台返回的原始图像 URL 通常约 24 小时有效;本工具会尽快转存 OSS。
|
||||||
- 提示词越详细,生成的图片质量越好
|
|
||||||
- 生成多张图片时,总耗时会更长,但会并行处理以提高效率
|
|
||||||
- 如果用户没有明确指定尺寸,使用默认尺寸即可
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
api_key = settings.dashscope_api_key
|
api_key = (os.getenv("ZL_DASHSCOPE_API_KEY") or "").strip()
|
||||||
|
base_url = (os.getenv("ZL_DASHSCOPE_API_BASE") or "").strip().rstrip("/")
|
||||||
if not api_key:
|
if not api_key:
|
||||||
return "错误:未配置 DASHSCOPE_API_KEY 环境变量"
|
return "错误:未配置 DASHSCOPE_API_KEY 环境变量"
|
||||||
|
if not base_url:
|
||||||
|
return "错误:未配置 DASHSCOPE_API_BASE 环境变量"
|
||||||
|
|
||||||
dashscope.base_http_api_url = _dashscope_http_api_base()
|
client = OpenAI(api_key=api_key, base_url=base_url)
|
||||||
|
model_image = "qwen-image-2.0"
|
||||||
|
|
||||||
logger.info(f"开始生成图片,prompt: {prompt}, n={n}")
|
size_norm = _normalize_openai_image_size(size)
|
||||||
|
n_req = max(1, min(int(n), 6))
|
||||||
|
|
||||||
# 创建异步任务
|
extra_body: dict = {
|
||||||
rsp = ImageSynthesis.call(api_key=api_key,
|
"prompt_extend": prompt_extend,
|
||||||
model="wan2.2-t2i-flash",
|
"watermark": watermark,
|
||||||
prompt=prompt,
|
}
|
||||||
n=n,
|
np = (negative_prompt or "").strip()
|
||||||
size=size,
|
if np:
|
||||||
negative_prompt=negative_prompt,
|
extra_body["negative_prompt"] = np
|
||||||
prompt_extend=True,
|
if seed is not None:
|
||||||
watermark=True)
|
extra_body["seed"] = seed
|
||||||
print(f'response: {rsp}')
|
|
||||||
if rsp.status_code != HTTPStatus.OK:
|
|
||||||
print(f'同步调用失败, status_code: {rsp.status_code}, code: {rsp.code}, message: {rsp.message}')
|
|
||||||
return "图片生成失败"
|
|
||||||
|
|
||||||
# 提取图片 URL
|
logger.info(
|
||||||
image_urls = []
|
f"开始生成图片(OpenAI 兼容 images/generations),model={model_image}, n={n_req}, size={size_norm}"
|
||||||
if rsp.output and rsp.output.results:
|
)
|
||||||
for result in rsp.output.results:
|
|
||||||
if hasattr(result, 'url') and result.url:
|
response = client.images.generate(
|
||||||
image_urls.append(result.url)
|
model=model_image,
|
||||||
|
prompt=prompt,
|
||||||
|
size=size_norm,
|
||||||
|
n=n_req,
|
||||||
|
extra_body=extra_body,
|
||||||
|
)
|
||||||
|
|
||||||
|
image_urls: list[str] = []
|
||||||
|
for item in response.data or []:
|
||||||
|
url = getattr(item, "url", None)
|
||||||
|
if url:
|
||||||
|
image_urls.append(url)
|
||||||
|
|
||||||
if not image_urls:
|
if not image_urls:
|
||||||
return "图片生成完成,但未获取到图片URL"
|
return "图片生成完成,但未获取到图片URL"
|
||||||
|
|
@ -266,12 +258,9 @@ def text_to_image(
|
||||||
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from langchain_core.tools import tool
|
from langchain_core.tools import tool
|
||||||
import os
|
|
||||||
import logging
|
import logging
|
||||||
import uuid
|
import uuid
|
||||||
import requests
|
import requests
|
||||||
from dashscope import VideoSynthesis
|
|
||||||
from http import HTTPStatus
|
|
||||||
from services.oss_service import get_oss_service
|
from services.oss_service import get_oss_service
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -279,141 +268,127 @@ from services.oss_service import get_oss_service
|
||||||
def text_to_video(
|
def text_to_video(
|
||||||
prompt: str,
|
prompt: str,
|
||||||
negative_prompt: str = "",
|
negative_prompt: str = "",
|
||||||
size: str = "832*480",
|
size: str = "1280x720",
|
||||||
duration: int = 5,
|
duration: int = 5,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
文生视频工具:根据文本描述生成动态视频。
|
文生视频工具:根据文本描述生成动态视频。
|
||||||
|
|
||||||
当用户需要生成视频、创建动画、制作短视频、需要动态视觉内容时,使用此工具。
|
通过 OpenAI 兼容接口(``POST /v1/videos`` / ``GET /v1/videos/{id}``)调用万相文生视频模型
|
||||||
该工具使用阿里云百炼平台的 AI 视频生成模型,可以根据文字描述生成相应的视频。
|
``wan2.6-t2v``。任务为异步队列:queued → in_progress → completed / failed;
|
||||||
生成的视频会自动上传到 OSS 存储,返回永久可访问的 URL。
|
SDK 使用 ``create_and_poll`` 自动轮询直至结束。
|
||||||
|
生成的视频会自动下载并上传到 OSS,返回永久可访问的播放地址。
|
||||||
|
|
||||||
使用场景:
|
使用场景:
|
||||||
- 用户说"生成一个...的视频"、"创建一个...的动画"、"制作...的短视频"
|
- 用户说「生成一个…的视频」「做一个短视频」「需要动态画面」
|
||||||
- 需要为产品演示、营销推广创建动态视频内容
|
- 产品演示、营销素材、社交媒体短视频脚本可视化
|
||||||
- 社交媒体短视频内容生成(抖音、快手、小红书等)
|
|
||||||
- 需要展示动态场景、运动过程、变化效果
|
|
||||||
- 用户想要可视化动态概念或过程
|
|
||||||
|
|
||||||
参数说明:
|
参数说明:
|
||||||
prompt (必需): 详细描述想要生成的视频内容。应该包含:
|
prompt: 视频内容描述(中英文均可,建议写清主体、动作、镜头与风格)。
|
||||||
- 主体对象和动作(什么在做什么)
|
negative_prompt: 不希望出现的内容;若填写则附加到提示中约束生成方向。
|
||||||
- 场景和环境(背景、地点、氛围)
|
size: 分辨率,OpenAI 标准 ``宽x高``(也可用 ``宽*高``,会自动转为 ``x``)。
|
||||||
- 运动方式和动态效果(移动、旋转、变化等)
|
例如 ``1280x720``、``1920x1080``、``720x1280``(竖屏)等。
|
||||||
- 风格和视觉效果(写实、动画、电影感等)
|
duration: 时长(秒),平台支持 **2–15** 秒,超出范围会自动裁剪到该区间。
|
||||||
- 颜色和光线(明亮、昏暗、暖色调等)
|
|
||||||
示例:"一只橘色小猫在窗台上玩耍,阳光透过窗户洒在它身上,它好奇地看向窗外,背景是温馨的客厅,写实风格,画面流畅自然"
|
|
||||||
|
|
||||||
negative_prompt (可选): 描述不希望在视频中出现的内容,用于排除不想要的元素。
|
|
||||||
示例:"模糊,低质量,文字,水印,画面抖动,不自然的运动,变形"
|
|
||||||
|
|
||||||
size (可选): 视频尺寸,格式为 "宽*高"。支持的尺寸:
|
|
||||||
- "832*480" - 标准横向(默认,适合通用视频)
|
|
||||||
- "1280*720" - 高清横向(适合高质量视频)
|
|
||||||
- "720*1280" - 竖向(适合手机竖屏视频、短视频平台)
|
|
||||||
默认值:"832*480"
|
|
||||||
|
|
||||||
duration (可选): 视频时长,单位为秒。支持的时长:
|
|
||||||
- 5 秒(默认,适合短视频)
|
|
||||||
- 10 秒(适合中等长度视频)
|
|
||||||
- 15 秒(适合较长视频)
|
|
||||||
默认值:5
|
|
||||||
|
|
||||||
返回值:
|
返回值:
|
||||||
返回包含视频的 HTML 格式字符串,视频会自动显示在对话中,用户可以直接播放。
|
包含 ``<video>`` 的 HTML 片段,可直接播放;优先使用 OSS URL。
|
||||||
视频已上传到 OSS,返回的是永久可访问的 URL。
|
平台返回的原始视频链接通常约 24 小时内有效,请及时依赖 OSS 结果。
|
||||||
|
|
||||||
注意事项:
|
注意事项:
|
||||||
- 视频生成需要较长时间(通常需要几十秒到几分钟),请耐心等待
|
- 生成耗时常为数十秒至数分钟,请耐心等待轮询完成
|
||||||
- 提示词越详细,生成的视频质量越好
|
- 提示词越具体,成片越可控
|
||||||
- 视频生成后会自动下载并上传到 OSS,这个过程可能需要额外时间
|
|
||||||
- 如果用户没有明确指定尺寸和时长,使用默认值即可
|
|
||||||
- 视频生成是异步过程,完成后会返回可播放的视频链接
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# 地域与 ``DASHSCOPE_API_BASE`` 一致;新加坡等请改环境变量(例:https://dashscope-intl.aliyuncs.com/api/v1)
|
api_key = (os.getenv("ZL_DASHSCOPE_API_KEY") or "").strip()
|
||||||
dashscope.base_http_api_url = _dashscope_http_api_base()
|
base_url = (os.getenv("ZL_DASHSCOPE_API_BASE") or "").strip().rstrip("/")
|
||||||
|
if not api_key:
|
||||||
|
return "错误:未配置 DASHSCOPE_API_KEY 环境变量"
|
||||||
|
if not base_url:
|
||||||
|
return "错误:未配置 DASHSCOPE_API_BASE 环境变量"
|
||||||
|
|
||||||
api_key = settings.dashscope_api_key
|
client = OpenAI(api_key=api_key, base_url=base_url)
|
||||||
|
model_video = "wan2.6-t2v"
|
||||||
|
|
||||||
|
size_norm = _normalize_openai_image_size(size)
|
||||||
|
seconds = max(2, min(int(duration), 15))
|
||||||
|
|
||||||
|
full_prompt = prompt
|
||||||
|
np = (negative_prompt or "").strip()
|
||||||
|
if np:
|
||||||
|
full_prompt = f"{prompt}\n(避免出现以下内容:{np})"
|
||||||
|
|
||||||
|
resolution = "1080P" if "1920" in size_norm else "720P"
|
||||||
|
extra_body = {"resolution": resolution, "duration": seconds}
|
||||||
|
|
||||||
# call sync api, will return the result
|
|
||||||
start = time.time()
|
start = time.time()
|
||||||
print('开始时间-->',time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()))
|
logger.info(
|
||||||
rsp = VideoSynthesis.call(api_key=api_key,
|
f"提交文生视频任务(videos API):model={model_video}, size={size_norm}, seconds={seconds}"
|
||||||
model='wan2.2-t2v-plus',
|
)
|
||||||
prompt=prompt,
|
|
||||||
size="832*480",
|
|
||||||
duration=5,
|
|
||||||
negative_prompt=negative_prompt,
|
|
||||||
# audio=True,
|
|
||||||
prompt_extend=True,
|
|
||||||
watermark=True)
|
|
||||||
print("请求结果:",rsp)
|
|
||||||
video_url = ""
|
|
||||||
result = ""
|
|
||||||
if rsp.status_code == HTTPStatus.OK:
|
|
||||||
print("请求链接地址:",rsp.output.video_url)
|
|
||||||
print("结束时间-->",time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()))
|
|
||||||
print("耗时-->",time.time()-start,"秒")
|
|
||||||
video_url = rsp.output.video_url
|
|
||||||
|
|
||||||
# 下载视频并上传到 OSS
|
result = client.videos.create_and_poll(
|
||||||
try:
|
model=model_video,
|
||||||
upload_start_time = time.time()
|
prompt=full_prompt,
|
||||||
logger.info(f"开始下载视频:{video_url}")
|
size=size_norm,
|
||||||
|
seconds=seconds,
|
||||||
|
poll_interval_ms=5000,
|
||||||
|
extra_body=extra_body,
|
||||||
|
)
|
||||||
|
|
||||||
# 下载视频内容
|
status = getattr(result, "status", None)
|
||||||
download_start = time.time()
|
video_url = getattr(result, "url", None) or ""
|
||||||
response = requests.get(video_url, timeout=300) # 5分钟超时
|
|
||||||
response.raise_for_status()
|
|
||||||
video_content = response.content
|
|
||||||
download_time = time.time() - download_start
|
|
||||||
logger.info(f"视频下载完成,耗时:{download_time:.2f} 秒,大小:{len(video_content) / 1024 / 1024:.2f} MB")
|
|
||||||
|
|
||||||
# 生成 OSS 对象名称
|
if status != "completed" or not video_url:
|
||||||
timestamp = int(time.time())
|
err_obj = getattr(result, "error", None)
|
||||||
unique_id = str(uuid.uuid4())[:8]
|
err_msg = getattr(err_obj, "message", None) if err_obj else None
|
||||||
oss_object_name = f"videos/{timestamp}_{unique_id}.mp4"
|
if not err_msg:
|
||||||
|
err_msg = str(status) if status else "未知状态"
|
||||||
|
logger.error(f"视频任务未成功:status={status}, error={err_msg}")
|
||||||
|
return f"视频生成失败:{err_msg}"
|
||||||
|
|
||||||
# 上传到 OSS
|
logger.info(f"视频任务完成,耗时 {time.time() - start:.1f}s,开始下载并上传 OSS:{video_url}")
|
||||||
upload_start = time.time()
|
|
||||||
oss_service = get_oss_service()
|
try:
|
||||||
oss_url = oss_service.upload_file_from_bytes(
|
upload_start_time = time.time()
|
||||||
file_content=video_content,
|
download_start = time.time()
|
||||||
oss_object_name=oss_object_name,
|
response = requests.get(video_url, timeout=300)
|
||||||
file_name="generated_video.mp4"
|
response.raise_for_status()
|
||||||
|
video_content = response.content
|
||||||
|
download_time = time.time() - download_start
|
||||||
|
logger.info(
|
||||||
|
f"视频下载完成,耗时:{download_time:.2f} 秒,大小:{len(video_content) / 1024 / 1024:.2f} MB"
|
||||||
|
)
|
||||||
|
|
||||||
|
timestamp = int(time.time())
|
||||||
|
unique_id = str(uuid.uuid4())[:8]
|
||||||
|
oss_object_name = f"videos/{timestamp}_{unique_id}.mp4"
|
||||||
|
|
||||||
|
upload_start = time.time()
|
||||||
|
oss_service = get_oss_service()
|
||||||
|
oss_url = oss_service.upload_file_from_bytes(
|
||||||
|
file_content=video_content,
|
||||||
|
oss_object_name=oss_object_name,
|
||||||
|
file_name="generated_video.mp4",
|
||||||
|
)
|
||||||
|
upload_time = time.time() - upload_start
|
||||||
|
total_time = time.time() - upload_start_time
|
||||||
|
|
||||||
|
if oss_url:
|
||||||
|
logger.info(f"✅ 视频已上传到 OSS:{oss_url}")
|
||||||
|
logger.info(
|
||||||
|
f"📊 上传统计 - 下载:{download_time:.2f}s,上传:{upload_time:.2f}s,总计:{total_time:.2f}s"
|
||||||
)
|
)
|
||||||
upload_time = time.time() - upload_start
|
video_url = oss_url
|
||||||
|
else:
|
||||||
|
logger.warning("⚠️ OSS 上传失败,使用平台返回的临时视频 URL")
|
||||||
|
except Exception as upload_error:
|
||||||
|
logger.error(f"❌ 上传视频到 OSS 失败:{upload_error}", exc_info=True)
|
||||||
|
logger.warning("⚠️ 使用原始临时视频 URL")
|
||||||
|
|
||||||
# 计算总耗时
|
logger.info(f"✅ 视频生成流程结束:{video_url}")
|
||||||
total_time = time.time() - upload_start_time
|
return f"""<video controls width="100%" style="max-width: 600px;">
|
||||||
|
|
||||||
if oss_url:
|
|
||||||
logger.info(f"✅ 视频已上传到 OSS:{oss_url}")
|
|
||||||
logger.info(f"📊 上传统计 - 下载耗时:{download_time:.2f} 秒,上传耗时:{upload_time:.2f} 秒,总耗时:{total_time:.2f} 秒")
|
|
||||||
# 使用 OSS URL 替换临时 URL
|
|
||||||
video_url = oss_url
|
|
||||||
else:
|
|
||||||
logger.warning("⚠️ OSS 上传失败,使用原始临时 URL")
|
|
||||||
logger.warning(f"📊 上传统计 - 下载耗时:{download_time:.2f} 秒,上传耗时:{upload_time:.2f} 秒,总耗时:{total_time:.2f} 秒(上传失败)")
|
|
||||||
# 如果 OSS 上传失败,继续使用原始 URL
|
|
||||||
|
|
||||||
except Exception as upload_error:
|
|
||||||
logger.error(f"❌ 上传视频到 OSS 失败:{upload_error}", exc_info=True)
|
|
||||||
# 如果上传失败,继续使用原始临时 URL
|
|
||||||
logger.warning("⚠️ 使用原始临时视频 URL")
|
|
||||||
|
|
||||||
result = f"""<video controls width="100%" style="max-width: 600px;">
|
|
||||||
<source src="{video_url}" type="video/mp4">
|
<source src="{video_url}" type="video/mp4">
|
||||||
您的浏览器不支持视频播放
|
您的浏览器不支持视频播放
|
||||||
</video>"""
|
</video>"""
|
||||||
else:
|
|
||||||
print('视频请求失败, status_code: %s, code: %s, message: %s' %
|
|
||||||
(rsp.status_code, rsp.code, rsp.message))
|
|
||||||
result = "视频生成失败"
|
|
||||||
logger.info(f"✅ 视频生成完成:{video_url}")
|
|
||||||
return result
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_msg = f"❌ 生成视频异常:{str(e)}"
|
error_msg = f"❌ 生成视频异常:{str(e)}"
|
||||||
|
|
@ -428,95 +403,60 @@ def text_to_poster(
|
||||||
body_text: str = "",
|
body_text: str = "",
|
||||||
prompt_text_zh: str = "",
|
prompt_text_zh: str = "",
|
||||||
prompt_text_en: str = "",
|
prompt_text_en: str = "",
|
||||||
size: str = "1280*1280",
|
size: str = "2048x2048",
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
创意海报生成工具:根据标题、副标题和正文内容生成创意海报。
|
创意海报生成工具:根据标题、副标题和正文生成海报图。
|
||||||
|
|
||||||
当用户需要生成海报、创建宣传图、制作营销图片、需要创意设计时,使用此工具。
|
与 ``text_to_image`` 相同,通过 OpenAI 兼容接口 ``/v1/images/generations``
|
||||||
该工具使用阿里云百炼平台的文生图(万相)模型,根据海报文案生成相应的创意海报图片。
|
调用通义千问文生图模型生成海报;结果上传到 OSS 后以 Markdown 图片链接返回。
|
||||||
生成的海报会自动上传到 OSS 存储,返回永久可访问的 URL。海报右下角带有「AI生成」水印。
|
海报场景默认 **打水印**、开启提示词扩展,并附带画质相关反向提示。
|
||||||
|
|
||||||
使用场景:
|
使用场景:
|
||||||
- 用户说"生成一张...的海报"、"创建一个...的宣传图"、"制作...的创意海报"
|
- 「生成一张…海报」「活动宣传图」「营销配图」等
|
||||||
- 需要为活动、产品、品牌创建宣传海报
|
|
||||||
- 社交媒体营销图片生成(微信、微博、小红书等)
|
|
||||||
- 需要展示标题、副标题和正文内容的创意设计
|
|
||||||
- 用户想要可视化某个主题或概念的海报
|
|
||||||
|
|
||||||
参数说明:
|
参数说明:
|
||||||
title (必需): 海报的主标题。应该简洁有力,能够吸引注意力。
|
title: 主标题。
|
||||||
示例:"春季新品发布"、"限时优惠活动"、"品牌宣传"
|
sub_title: 副标题。
|
||||||
|
body_text: 正文(过长时在 prompt 内截取约 200 字摘要)。
|
||||||
sub_title (可选): 海报的副标题。用于补充说明主标题或提供更多信息。
|
prompt_text_zh / prompt_text_en: 视觉风格描述;均未填则用默认「高质量海报风格」。
|
||||||
示例:"全场8折起"、"限时3天"、"专业团队打造"
|
size: 分辨率 ``宽x高``(也可用 ``宽*高``);正方形海报常用 ``2048x2048``,
|
||||||
|
亦可选 ``2688x1536``、``1536x2688`` 等与 ``text_to_image`` 一致的取值。
|
||||||
body_text (可选): 海报的正文内容。可以包含详细说明、活动规则、联系方式等。
|
|
||||||
示例:"活动时间:2024年3月1日-3月31日\n活动地点:全国门店\n咨询热线:400-xxx-xxxx"
|
|
||||||
|
|
||||||
prompt_text_zh (可选): 中文提示文本,用于描述海报的视觉风格和设计元素。
|
|
||||||
示例:"小朋友画的可爱的龙,白色背景"、"温馨的节日氛围,红色和金色主题"
|
|
||||||
如果未提供,将根据标题和副标题自动生成。
|
|
||||||
|
|
||||||
prompt_text_en (可选): 英文提示文本,用于描述海报的视觉风格和设计元素。
|
|
||||||
示例:"Children draw a lovely dragon, white background"、"Warm festive atmosphere, red and gold theme"
|
|
||||||
如果未提供,将根据标题和副标题自动生成。
|
|
||||||
注意:prompt_text_zh 和 prompt_text_en 至少需要设置其中一个。
|
|
||||||
|
|
||||||
size string (可选)
|
|
||||||
|
|
||||||
输出图像的分辨率,格式为宽*高。
|
|
||||||
|
|
||||||
默认值为 1280*1280。
|
|
||||||
|
|
||||||
总像素在 [1280*1280, 1440*1440] 之间且宽高比范围为 [1:4, 4:1]。例如,768*2700符合要求。
|
|
||||||
|
|
||||||
示例值:1280*1280。
|
|
||||||
|
|
||||||
常见比例推荐的分辨率
|
|
||||||
|
|
||||||
1:1:1280*1280
|
|
||||||
|
|
||||||
3:4:1104*1472
|
|
||||||
|
|
||||||
4:3:1472*1104
|
|
||||||
|
|
||||||
9:16:960*1696
|
|
||||||
|
|
||||||
16:9:1696*960
|
|
||||||
|
|
||||||
返回值:
|
返回值:
|
||||||
返回包含海报图片的 Markdown 格式字符串,海报会自动显示在对话中。
|
Markdown 文本:海报图片 URL(OSS)及标题信息。
|
||||||
|
|
||||||
注意事项:
|
注意:
|
||||||
- 生成海报需要一定时间,请耐心等待
|
原始生成 URL 有效期有限,请以 OSS 链接为准。
|
||||||
- 标题、副标题和正文内容越清晰,生成的海报质量越好
|
|
||||||
- 生成的海报带有 AI 水印标识
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
api_key = settings.dashscope_api_key
|
api_key = (os.getenv("ZL_DASHSCOPE_API_KEY") or "").strip()
|
||||||
|
base_url = (os.getenv("ZL_DASHSCOPE_API_BASE") or "").strip().rstrip("/")
|
||||||
if not api_key:
|
if not api_key:
|
||||||
return "错误:未配置 DASHSCOPE_API_KEY 环境变量"
|
return "错误:未配置 DASHSCOPE_API_KEY 环境变量"
|
||||||
|
if not base_url:
|
||||||
|
return "错误:未配置 DASHSCOPE_API_BASE 环境变量"
|
||||||
|
|
||||||
dashscope.base_http_api_url = _dashscope_http_api_base()
|
client = OpenAI(api_key=api_key, base_url=base_url)
|
||||||
|
model_image = "qwen-image-2.0"
|
||||||
|
|
||||||
logger.info(f"开始生成创意海报,title: {title}, sub_title: {sub_title}, body_text: {(body_text[:50] + '...') if len(body_text) > 50 else body_text}")
|
logger.info(
|
||||||
|
f"开始生成创意海报(OpenAI 兼容 images/generations),title: {title}, "
|
||||||
|
f"sub_title: {sub_title}, body_text: {(body_text[:50] + '...') if len(body_text) > 50 else body_text}"
|
||||||
|
)
|
||||||
|
|
||||||
# 构建海报专用 prompt:将标题、副标题、正文与视觉风格融合为文生图提示词
|
prompt_parts = ["创意海报设计,宣传海报,专业排版,醒目吸睛,文字清晰可读"]
|
||||||
prompt_parts = ["创意海报设计,宣传海报,专业排版,醒目吸睛"]
|
|
||||||
|
|
||||||
if title:
|
if title:
|
||||||
prompt_parts.append(f"主标题:{title}")
|
prompt_parts.append(f"主标题:{title}")
|
||||||
if sub_title:
|
if sub_title:
|
||||||
prompt_parts.append(f"副标题:{sub_title}")
|
prompt_parts.append(f"副标题:{sub_title}")
|
||||||
if body_text:
|
if body_text:
|
||||||
# 正文可能较长,截取关键信息(限制约 200 字符)
|
|
||||||
body_summary = body_text.replace("\n", " ")[:200]
|
body_summary = body_text.replace("\n", " ")[:200]
|
||||||
if len(body_text) > 200:
|
if len(body_text) > 200:
|
||||||
body_summary += "..."
|
body_summary += "..."
|
||||||
prompt_parts.append(f"正文内容:{body_summary}")
|
prompt_parts.append(f"正文内容:{body_summary}")
|
||||||
|
|
||||||
# 视觉风格:优先使用用户提供的提示词
|
|
||||||
if prompt_text_zh:
|
if prompt_text_zh:
|
||||||
prompt_parts.append(f"视觉风格:{prompt_text_zh}")
|
prompt_parts.append(f"视觉风格:{prompt_text_zh}")
|
||||||
elif prompt_text_en:
|
elif prompt_text_en:
|
||||||
|
|
@ -527,28 +467,27 @@ def text_to_poster(
|
||||||
prompt = ",".join(prompt_parts)
|
prompt = ",".join(prompt_parts)
|
||||||
logger.info(f"海报生成 prompt: {prompt[:200]}...")
|
logger.info(f"海报生成 prompt: {prompt[:200]}...")
|
||||||
|
|
||||||
# 使用与 text_to_image 相同的文生图 API(万相 wan2.2-t2i-flash)
|
size_norm = _normalize_openai_image_size(size)
|
||||||
# 海报需要水印,故 watermark=True
|
negative = "低分辨率,低画质,画面模糊,文字扭曲,构图混乱,画面过饱和"
|
||||||
rsp = ImageSynthesis.call(
|
extra_body: dict = {
|
||||||
api_key=api_key,
|
"prompt_extend": True,
|
||||||
model="wan2.5-t2i-preview",
|
"watermark": True,
|
||||||
|
"negative_prompt": negative,
|
||||||
|
}
|
||||||
|
|
||||||
|
response = client.images.generate(
|
||||||
|
model=model_image,
|
||||||
prompt=prompt,
|
prompt=prompt,
|
||||||
|
size=size_norm,
|
||||||
n=1,
|
n=1,
|
||||||
size=size,
|
extra_body=extra_body,
|
||||||
negative_prompt="低分辨率,低画质,画面模糊,文字扭曲,构图混乱,画面过饱和",
|
|
||||||
prompt_extend=True,
|
|
||||||
watermark=True,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if rsp.status_code != HTTPStatus.OK:
|
image_urls: list[str] = []
|
||||||
logger.error(f"海报生成失败, status_code: {rsp.status_code}, code: {rsp.code}, message: {rsp.message}")
|
for item in response.data or []:
|
||||||
return f"海报生成失败:{rsp.message or '请稍后重试'}"
|
url = getattr(item, "url", None)
|
||||||
|
if url:
|
||||||
image_urls = []
|
image_urls.append(url)
|
||||||
if rsp.output and rsp.output.results:
|
|
||||||
for result in rsp.output.results:
|
|
||||||
if hasattr(result, 'url') and result.url:
|
|
||||||
image_urls.append(result.url)
|
|
||||||
|
|
||||||
if not image_urls:
|
if not image_urls:
|
||||||
return "海报生成完成,但未获取到图片URL"
|
return "海报生成完成,但未获取到图片URL"
|
||||||
|
|
@ -556,7 +495,6 @@ def text_to_poster(
|
||||||
image_url = image_urls[0]
|
image_url = image_urls[0]
|
||||||
logger.info(f"海报生成成功,图片URL: {image_url},开始上传到 OSS")
|
logger.info(f"海报生成成功,图片URL: {image_url},开始上传到 OSS")
|
||||||
|
|
||||||
# 复用 text_to_image 的 OSS 上传逻辑
|
|
||||||
_, oss_url, _, _ = _download_and_upload_image_to_oss(image_url, 1)
|
_, oss_url, _, _ = _download_and_upload_image_to_oss(image_url, 1)
|
||||||
final_url = oss_url if oss_url else image_url
|
final_url = oss_url if oss_url else image_url
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue