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