```
feat(api): 新增gpt-image-2编辑模型支持 - 添加gpt-image-2模型的专门接口配置和API密钥 - 实现gpt-image-2编辑模式的完整处理流程,包括Base64图片解码、 图片验证和MinIO上传功能 - 更新前端界面以支持gpt-image-2的特定参数(如尺寸选项) - 修复prompt不能为空的校验逻辑 - 优化图片上传处理,支持编辑模式下的参考图必需校验 - 更新视频生成任务状态消息文案 ```
This commit is contained in:
parent
fec3426bad
commit
0adabaa235
@ -5,167 +5,191 @@ from middlewares.auth import login_required
|
||||
from services.logger import system_logger
|
||||
import json
|
||||
|
||||
# Import Services
|
||||
from services.system_service import get_system_config_data, get_user_latest_notification, mark_notification_as_read
|
||||
from services.system_service import (
|
||||
get_system_config_data,
|
||||
get_user_latest_notification,
|
||||
mark_notification_as_read,
|
||||
)
|
||||
from services.history_service import get_user_history_data
|
||||
from services.file_service import handle_file_uploads, get_remote_file_stream
|
||||
from services.generation_service import (
|
||||
validate_generation_request, deduct_points, handle_chat_generation_sync,
|
||||
start_async_image_task, validate_video_request, start_async_video_task
|
||||
validate_generation_request,
|
||||
deduct_points,
|
||||
handle_chat_generation_sync,
|
||||
start_async_image_task,
|
||||
validate_video_request,
|
||||
start_async_video_task,
|
||||
)
|
||||
|
||||
api_bp = Blueprint('api', __name__)
|
||||
api_bp = Blueprint("api", __name__)
|
||||
|
||||
@api_bp.route('/api/task_status/<task_id>')
|
||||
|
||||
@api_bp.route("/api/task_status/<task_id>")
|
||||
@login_required
|
||||
def get_task_status(task_id):
|
||||
"""查询异步任务状态"""
|
||||
try:
|
||||
data = redis_client.get(f"task:{task_id}")
|
||||
if not data:
|
||||
return jsonify({"status": "pending"})
|
||||
|
||||
if isinstance(data, bytes):
|
||||
data = data.decode('utf-8')
|
||||
data = data.decode("utf-8")
|
||||
|
||||
return jsonify(json.loads(data))
|
||||
except Exception as e:
|
||||
system_logger.error(f"查询任务状态异常: {str(e)}")
|
||||
return jsonify({"status": "error", "message": "状态查询失败"})
|
||||
|
||||
@api_bp.route('/api/config')
|
||||
|
||||
@api_bp.route("/api/config")
|
||||
def get_config():
|
||||
"""从本地数据库字典获取配置"""
|
||||
try:
|
||||
return jsonify(get_system_config_data())
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@api_bp.route('/api/upload', methods=['POST'])
|
||||
|
||||
@api_bp.route("/api/upload", methods=["POST"])
|
||||
@login_required
|
||||
def upload():
|
||||
try:
|
||||
files = request.files.getlist('images')
|
||||
files = request.files.getlist("images")
|
||||
img_urls = handle_file_uploads(files)
|
||||
system_logger.info(f"用户上传文件: {len(files)} 个", user_id=session.get('user_id'))
|
||||
system_logger.info(f"用户上传文件: {len(files)} 张", user_id=session.get("user_id"))
|
||||
return jsonify({"urls": img_urls})
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@api_bp.route('/api/generate', methods=['POST'])
|
||||
|
||||
@api_bp.route("/api/generate", methods=["POST"])
|
||||
@login_required
|
||||
def generate():
|
||||
try:
|
||||
user_id = session.get('user_id')
|
||||
user_id = session.get("user_id")
|
||||
user = db.session.get(User, user_id)
|
||||
|
||||
data = request.json if request.is_json else request.form
|
||||
|
||||
# 1. 验证请求与权限
|
||||
api_key, target_api, cost, use_trial, error = validate_generation_request(user, data)
|
||||
if error:
|
||||
return jsonify({"error": error}), 400
|
||||
|
||||
# 2. 扣除积分 (如果是试用模式)
|
||||
if use_trial:
|
||||
deduct_points(user_id, cost)
|
||||
|
||||
model_value = data.get('model')
|
||||
prompt = data.get('prompt')
|
||||
model_value = data.get("model")
|
||||
prompt = (data.get("prompt") or "").strip()
|
||||
if not prompt:
|
||||
return jsonify({"error": "提示词不能为空"}), 400
|
||||
model_lower = model_value.lower()
|
||||
is_chat_model = ("gemini" in model_lower or "gpt" in model_lower) and "image" not in model_lower
|
||||
|
||||
# 3. 处理聊天模型 (同步)
|
||||
if is_chat_model:
|
||||
result, status_code = handle_chat_generation_sync(user_id, api_key, model_value, prompt, use_trial, cost)
|
||||
result, status_code = handle_chat_generation_sync(
|
||||
user_id, api_key, model_value, prompt, use_trial, cost
|
||||
)
|
||||
return jsonify(result), status_code
|
||||
|
||||
# 4. 构造生图 Payload
|
||||
payload = {
|
||||
"prompt": prompt,
|
||||
"model": model_value,
|
||||
"response_format": "url",
|
||||
"aspect_ratio": data.get('ratio')
|
||||
}
|
||||
image_data = data.get('image_data', [])
|
||||
if image_data:
|
||||
payload["image"] = [img.split(',', 1)[1] if ',' in img else img for img in image_data]
|
||||
image_data = data.get("image_data", [])
|
||||
if model_value == "gpt-image-2":
|
||||
if not image_data:
|
||||
return jsonify({"error": "gpt-image-2 编辑模式至少需要上传 1 张参考图"}), 400
|
||||
|
||||
if model_value in ("nano-banana-2", "gemini-3.1-flash-image-preview") and data.get('size'):
|
||||
payload["image_size"] = data.get('size')
|
||||
payload = {
|
||||
"prompt": prompt,
|
||||
"model": model_value,
|
||||
"images": [{"image_url": img} for img in image_data],
|
||||
"size": data.get("size") or "2160x3840",
|
||||
"quality": "high",
|
||||
"output_format": "png",
|
||||
}
|
||||
else:
|
||||
payload = {
|
||||
"prompt": prompt,
|
||||
"model": model_value,
|
||||
"response_format": "url",
|
||||
"aspect_ratio": data.get("ratio"),
|
||||
}
|
||||
if image_data:
|
||||
payload["image"] = [img.split(",", 1)[1] if "," in img else img for img in image_data]
|
||||
|
||||
if model_value in ("nano-banana-2", "gemini-3.1-flash-image-preview") and data.get("size"):
|
||||
payload["image_size"] = data.get("size")
|
||||
|
||||
# 5. 启动异步生图任务
|
||||
app = current_app._get_current_object()
|
||||
task_id = start_async_image_task(app, user_id, payload, api_key, target_api, cost, data.get('mode'), model_value, use_trial)
|
||||
task_id = start_async_image_task(
|
||||
app,
|
||||
user_id,
|
||||
payload,
|
||||
api_key,
|
||||
target_api,
|
||||
cost,
|
||||
data.get("mode"),
|
||||
model_value,
|
||||
use_trial,
|
||||
)
|
||||
|
||||
return jsonify({
|
||||
"task_id": task_id,
|
||||
"message": "已开启异步生成任务"
|
||||
"message": "已开启异步生成任务",
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@api_bp.route('/api/video/generate', methods=['POST'])
|
||||
|
||||
@api_bp.route("/api/video/generate", methods=["POST"])
|
||||
@login_required
|
||||
def video_generate():
|
||||
try:
|
||||
user_id = session.get('user_id')
|
||||
user_id = session.get("user_id")
|
||||
user = db.session.get(User, user_id)
|
||||
|
||||
data = request.json
|
||||
|
||||
# 1. 验证请求
|
||||
model_value, cost, error = validate_video_request(user, data)
|
||||
if error:
|
||||
return jsonify({"error": error}), 400
|
||||
|
||||
# 2. 扣除积分
|
||||
deduct_points(user_id, cost)
|
||||
|
||||
# 3. 构造 Payload
|
||||
payload = {
|
||||
"model": model_value,
|
||||
"prompt": data.get('prompt'),
|
||||
"enhance_prompt": data.get('enhance_prompt', False),
|
||||
"images": data.get('images', []),
|
||||
"aspect_ratio": data.get('aspect_ratio', '9:16')
|
||||
"prompt": data.get("prompt"),
|
||||
"enhance_prompt": data.get("enhance_prompt", False),
|
||||
"images": data.get("images", []),
|
||||
"aspect_ratio": data.get("aspect_ratio", "9:16"),
|
||||
}
|
||||
|
||||
# 处理 Base64 图片 (去掉 header)
|
||||
if payload.get("images"):
|
||||
payload["images"] = [
|
||||
img.split(',', 1)[1] if ',' in img else img
|
||||
for img in payload["images"]
|
||||
]
|
||||
payload["images"] = [img.split(",", 1)[1] if "," in img else img for img in payload["images"]]
|
||||
|
||||
# 4. 启动异步视频任务
|
||||
app = current_app._get_current_object()
|
||||
task_id = start_async_video_task(app, user_id, payload, cost, model_value)
|
||||
|
||||
return jsonify({
|
||||
"task_id": task_id,
|
||||
"message": "视频生成任务已提交,系统正在导演中..."
|
||||
"message": "视频生成任务已提交,系统正在处理中",
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@api_bp.route('/api/notifications/latest', methods=['GET'])
|
||||
|
||||
@api_bp.route("/api/notifications/latest", methods=["GET"])
|
||||
@login_required
|
||||
def get_latest_notification():
|
||||
try:
|
||||
user_id = session.get('user_id')
|
||||
user_id = session.get("user_id")
|
||||
data = get_user_latest_notification(user_id)
|
||||
return jsonify(data)
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@api_bp.route('/api/notifications/read', methods=['POST'])
|
||||
|
||||
@api_bp.route("/api/notifications/read", methods=["POST"])
|
||||
@login_required
|
||||
def mark_notif_read():
|
||||
try:
|
||||
user_id = session.get('user_id')
|
||||
user_id = session.get("user_id")
|
||||
data = request.json
|
||||
notif_id = data.get('id')
|
||||
notif_id = data.get("id")
|
||||
if not notif_id:
|
||||
return jsonify({"error": "缺少通知 ID"}), 400
|
||||
|
||||
@ -174,111 +198,118 @@ def mark_notif_read():
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@api_bp.route('/api/history', methods=['GET'])
|
||||
|
||||
@api_bp.route("/api/history", methods=["GET"])
|
||||
@login_required
|
||||
def get_history():
|
||||
try:
|
||||
user_id = session.get('user_id')
|
||||
page = request.args.get('page', 1, type=int)
|
||||
per_page = request.args.get('per_page', 10, type=int)
|
||||
filter_type = request.args.get('filter_type', 'all')
|
||||
|
||||
user_id = session.get("user_id")
|
||||
page = request.args.get("page", 1, type=int)
|
||||
per_page = request.args.get("per_page", 10, type=int)
|
||||
filter_type = request.args.get("filter_type", "all")
|
||||
data = get_user_history_data(user_id, page, per_page, filter_type)
|
||||
return jsonify(data)
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@api_bp.route('/api/stats/points', methods=['GET'])
|
||||
|
||||
@api_bp.route("/api/stats/points", methods=["GET"])
|
||||
@login_required
|
||||
def get_user_point_stats():
|
||||
"""获取积分统计图表数据"""
|
||||
from services.stats_service import get_point_stats
|
||||
user_id = session.get('user_id')
|
||||
days = request.args.get('days', 7, type=int)
|
||||
|
||||
user_id = session.get("user_id")
|
||||
days = request.args.get("days", 7, type=int)
|
||||
return jsonify(get_point_stats(user_id, days))
|
||||
|
||||
@api_bp.route('/api/stats/details', methods=['GET'])
|
||||
|
||||
@api_bp.route("/api/stats/details", methods=["GET"])
|
||||
@login_required
|
||||
def get_user_point_details():
|
||||
"""获取积分消耗明细"""
|
||||
from services.stats_service import get_point_details
|
||||
user_id = session.get('user_id')
|
||||
page = request.args.get('page', 1, type=int)
|
||||
|
||||
user_id = session.get("user_id")
|
||||
page = request.args.get("page", 1, type=int)
|
||||
return jsonify(get_point_details(user_id, page))
|
||||
|
||||
@api_bp.route('/api/download_proxy', methods=['GET'])
|
||||
|
||||
@api_bp.route("/api/download_proxy", methods=["GET"])
|
||||
@login_required
|
||||
def download_proxy():
|
||||
import time
|
||||
url = request.args.get('url')
|
||||
# 默认文件名逻辑
|
||||
|
||||
url = request.args.get("url")
|
||||
default_name = f"video-{int(time.time())}.mp4"
|
||||
filename = request.args.get('filename') or default_name
|
||||
filename = request.args.get("filename") or default_name
|
||||
|
||||
if not url:
|
||||
return jsonify({"error": "缺少 URL 参数"}), 400
|
||||
|
||||
try:
|
||||
req, headers = get_remote_file_stream(url)
|
||||
headers['Content-Disposition'] = f'attachment; filename="{filename}"'
|
||||
headers["Content-Disposition"] = f'attachment; filename="{filename}"'
|
||||
|
||||
def generate():
|
||||
def generate_stream():
|
||||
for chunk in req.iter_content(chunk_size=4096):
|
||||
yield chunk
|
||||
|
||||
return current_app.response_class(generate(), headers=headers)
|
||||
|
||||
return current_app.response_class(generate_stream(), headers=headers)
|
||||
except Exception as e:
|
||||
system_logger.error(f"代理下载失败: {str(e)}")
|
||||
return jsonify({"error": "下载失败"}), 500
|
||||
|
||||
@api_bp.route('/api/prompts', methods=['GET'])
|
||||
|
||||
@api_bp.route("/api/prompts", methods=["GET"])
|
||||
@login_required
|
||||
def list_prompts():
|
||||
try:
|
||||
user_id = session.get('user_id')
|
||||
user_id = session.get("user_id")
|
||||
prompts = SavedPrompt.query.filter_by(user_id=user_id).order_by(SavedPrompt.created_at.desc()).all()
|
||||
return jsonify([{
|
||||
"id": p.id,
|
||||
"label": p.title, # Use label/value structure for frontend select
|
||||
"value": p.prompt
|
||||
} for p in prompts])
|
||||
return jsonify([
|
||||
{
|
||||
"id": p.id,
|
||||
"label": p.title,
|
||||
"value": p.prompt,
|
||||
}
|
||||
for p in prompts
|
||||
])
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@api_bp.route('/api/prompts', methods=['POST'])
|
||||
|
||||
@api_bp.route("/api/prompts", methods=["POST"])
|
||||
@login_required
|
||||
def save_prompt():
|
||||
try:
|
||||
user_id = session.get('user_id')
|
||||
user_id = session.get("user_id")
|
||||
data = request.json
|
||||
title = data.get('title')
|
||||
prompt_text = data.get('prompt')
|
||||
title = data.get("title")
|
||||
prompt_text = data.get("prompt")
|
||||
|
||||
if not title or not prompt_text:
|
||||
return jsonify({"error": "标题和内容不能为空"}), 400
|
||||
|
||||
# Limit to 50 saved prompts
|
||||
count = SavedPrompt.query.filter_by(user_id=user_id).count()
|
||||
if count >= 50:
|
||||
return jsonify({"error": "收藏数量已达上限 (50)"}), 400
|
||||
|
||||
p = SavedPrompt(user_id=user_id, title=title, prompt=prompt_text)
|
||||
db.session.add(p)
|
||||
prompt = SavedPrompt(user_id=user_id, title=title, prompt=prompt_text)
|
||||
db.session.add(prompt)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({"message": "保存成功", "id": p.id})
|
||||
return jsonify({"message": "保存成功", "id": prompt.id})
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@api_bp.route('/api/prompts/<int:id>', methods=['DELETE'])
|
||||
|
||||
@api_bp.route("/api/prompts/<int:id>", methods=["DELETE"])
|
||||
@login_required
|
||||
def delete_prompt(id):
|
||||
try:
|
||||
user_id = session.get('user_id')
|
||||
p = SavedPrompt.query.filter_by(id=id, user_id=user_id).first()
|
||||
if p:
|
||||
db.session.delete(p)
|
||||
user_id = session.get("user_id")
|
||||
prompt = SavedPrompt.query.filter_by(id=id, user_id=user_id).first()
|
||||
if prompt:
|
||||
db.session.delete(prompt)
|
||||
db.session.commit()
|
||||
return jsonify({"message": "删除成功"})
|
||||
except Exception as e:
|
||||
|
||||
@ -37,6 +37,8 @@ class Config:
|
||||
CHAT_API = f"{AI_BASE_URL}/v1/chat/completions"
|
||||
VIDEO_GEN_API = f"{AI_BASE_URL}/v2/videos/generations"
|
||||
VIDEO_POLL_API = f"{AI_BASE_URL}/v2/videos/generations/{{task_id}}"
|
||||
GPT_IMAGE_EDIT_API = "https://nas.4x4g.com:8317/v1/images/edits"
|
||||
GPT_IMAGE_EDIT_KEY = "sk-vDmmYNzoe7UjWskKx"
|
||||
|
||||
# 试用模式配置
|
||||
TRIAL_API = f"{AI_BASE_URL}/v1/images/generations"
|
||||
|
||||
@ -1,100 +1,114 @@
|
||||
import json
|
||||
import threading
|
||||
import uuid
|
||||
|
||||
import requests
|
||||
|
||||
from config import Config
|
||||
from models import SystemDict, GenerationRecord, User, db
|
||||
from extensions import redis_client
|
||||
from extensions import db, redis_client
|
||||
from models import GenerationRecord, SystemDict, User
|
||||
from services.logger import system_logger
|
||||
from services.task_service import process_image_generation, process_video_generation
|
||||
import requests
|
||||
import json
|
||||
import uuid
|
||||
import threading
|
||||
from flask import current_app
|
||||
from utils import get_api_candidates, get_proxied_url, should_switch_to_backup
|
||||
|
||||
|
||||
def get_model_cost(model_value, is_video=False):
|
||||
"""获取模型消耗积分"""
|
||||
dict_type = 'video_model' if is_video else 'ai_model'
|
||||
"""获取模型消耗积分。"""
|
||||
dict_type = "video_model" if is_video else "ai_model"
|
||||
model_dict = SystemDict.query.filter_by(dict_type=dict_type, value=model_value).first()
|
||||
|
||||
if model_dict:
|
||||
return model_dict.cost
|
||||
|
||||
# Default costs
|
||||
# 默认计费兜底
|
||||
if is_video:
|
||||
return 15 if "pro" in model_value.lower() or "3.1" in model_value else 10
|
||||
else:
|
||||
return 1
|
||||
return 1
|
||||
|
||||
|
||||
def validate_generation_request(user, data):
|
||||
"""验证生图请求并返回配置 (api_key, target_api, cost, use_trial)"""
|
||||
mode = data.get('mode', 'trial')
|
||||
is_premium = data.get('is_premium', False)
|
||||
input_key = data.get('apiKey')
|
||||
model_value = data.get('model')
|
||||
is_gemini_flash_image_preview = model_value == 'gemini-3.1-flash-image-preview'
|
||||
"""校验生图请求并返回 (api_key, target_api, cost, use_trial, error)。"""
|
||||
mode = data.get("mode", "trial")
|
||||
is_premium = data.get("is_premium", False)
|
||||
input_key = data.get("apiKey")
|
||||
model_value = data.get("model")
|
||||
is_gemini_flash_image_preview = model_value == "gemini-3.1-flash-image-preview"
|
||||
is_gpt_image_edit = model_value == "gpt-image-2"
|
||||
|
||||
if isinstance(is_premium, str):
|
||||
is_premium = is_premium.lower() in ('1', 'true', 'yes', 'on')
|
||||
is_premium = is_premium.lower() in ("1", "true", "yes", "on")
|
||||
|
||||
target_api = Config.AI_API
|
||||
# gpt-image-2 走独立的 edits 接口,其余模型沿用现有 generations 接口
|
||||
target_api = Config.GPT_IMAGE_EDIT_API if is_gpt_image_edit else Config.AI_API
|
||||
api_key = None
|
||||
use_trial = False
|
||||
|
||||
if mode == 'key':
|
||||
if mode == "key":
|
||||
api_key = input_key or user.api_key
|
||||
if not api_key:
|
||||
return None, None, 0, False, "请先输入您的 API 密钥"
|
||||
|
||||
# Update user key if changed
|
||||
# 用户手动输入了新 key 时,同步保存到账号
|
||||
if input_key and input_key != user.api_key:
|
||||
user.api_key = input_key
|
||||
db.session.commit()
|
||||
else:
|
||||
if user.points > 0:
|
||||
if is_premium:
|
||||
api_key = Config.GEMINI_FLASH_IMAGE_PREMIUM_KEY if is_gemini_flash_image_preview else Config.PREMIUM_KEY
|
||||
elif is_gemini_flash_image_preview:
|
||||
api_key = Config.PREMIUM_KEY
|
||||
else:
|
||||
api_key = Config.TRIAL_KEY
|
||||
target_api = Config.TRIAL_API
|
||||
use_trial = True
|
||||
else:
|
||||
if user.points <= 0:
|
||||
return None, None, 0, False, "可用积分已耗尽,请充值或切换至自定义 Key 模式"
|
||||
|
||||
# 计算单价
|
||||
# 积分模式下,gpt-image-2 使用固定的专用 key
|
||||
if is_gpt_image_edit:
|
||||
api_key = Config.GPT_IMAGE_EDIT_KEY
|
||||
elif is_premium:
|
||||
api_key = (
|
||||
Config.GEMINI_FLASH_IMAGE_PREMIUM_KEY
|
||||
if is_gemini_flash_image_preview
|
||||
else Config.PREMIUM_KEY
|
||||
)
|
||||
elif is_gemini_flash_image_preview:
|
||||
api_key = Config.PREMIUM_KEY
|
||||
else:
|
||||
api_key = Config.TRIAL_KEY
|
||||
|
||||
target_api = Config.GPT_IMAGE_EDIT_API if is_gpt_image_edit else Config.TRIAL_API
|
||||
use_trial = True
|
||||
|
||||
# 计算本次任务消耗
|
||||
cost = get_model_cost(model_value, is_video=False)
|
||||
if use_trial and is_premium:
|
||||
cost *= 2
|
||||
|
||||
if use_trial:
|
||||
if user.points < cost:
|
||||
return None, None, cost, True, f"可用积分不足(本次需要 {cost} 积分)"
|
||||
if use_trial and user.points < cost:
|
||||
return None, None, cost, True, f"可用积分不足,本次需要 {cost} 积分"
|
||||
|
||||
return api_key, target_api, cost, use_trial, None
|
||||
|
||||
|
||||
def deduct_points(user_id, cost):
|
||||
"""原子扣除积分"""
|
||||
"""原子扣减积分。"""
|
||||
user = db.session.query(User).filter_by(id=user_id).populate_existing().with_for_update().first()
|
||||
if user:
|
||||
user.points -= cost
|
||||
user.has_used_points = True
|
||||
db.session.commit()
|
||||
|
||||
|
||||
def refund_points(user_id, cost):
|
||||
"""原子退还积分"""
|
||||
"""原子退回积分。"""
|
||||
try:
|
||||
user = db.session.query(User).filter_by(id=user_id).populate_existing().with_for_update().first()
|
||||
if user:
|
||||
user.points += cost
|
||||
db.session.commit()
|
||||
except:
|
||||
except Exception:
|
||||
db.session.rollback()
|
||||
|
||||
|
||||
def handle_chat_generation_sync(user_id, api_key, model_value, prompt, use_trial, cost):
|
||||
"""同步处理对话类模型"""
|
||||
"""同步处理聊天类模型。"""
|
||||
chat_payload = {
|
||||
"model": model_value,
|
||||
"messages": [{"role": "user", "content": prompt}]
|
||||
"messages": [{"role": "user", "content": prompt}],
|
||||
}
|
||||
try:
|
||||
resp = None
|
||||
@ -102,13 +116,16 @@ def handle_chat_generation_sync(user_id, api_key, model_value, prompt, use_trial
|
||||
candidates = get_api_candidates(Config.CHAT_API, api_key)
|
||||
|
||||
for idx, candidate in enumerate(candidates):
|
||||
headers = {"Authorization": f"Bearer {candidate['api_key']}", "Content-Type": "application/json"}
|
||||
headers = {
|
||||
"Authorization": f"Bearer {candidate['api_key']}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
try:
|
||||
resp = requests.post(
|
||||
get_proxied_url(candidate['url']),
|
||||
get_proxied_url(candidate["url"]),
|
||||
json=chat_payload,
|
||||
headers=headers,
|
||||
timeout=Config.PROXY_TIMEOUT_LONG
|
||||
timeout=Config.PROXY_TIMEOUT_LONG,
|
||||
)
|
||||
if resp.status_code == 200:
|
||||
break
|
||||
@ -120,7 +137,7 @@ def handle_chat_generation_sync(user_id, api_key, model_value, prompt, use_trial
|
||||
"聊天主线路失败,切换备用线路",
|
||||
user_id=user_id,
|
||||
model=model_value,
|
||||
status_code=resp.status_code
|
||||
status_code=resp.status_code,
|
||||
)
|
||||
continue
|
||||
break
|
||||
@ -131,7 +148,7 @@ def handle_chat_generation_sync(user_id, api_key, model_value, prompt, use_trial
|
||||
"聊天请求异常,切换备用线路",
|
||||
user_id=user_id,
|
||||
model=model_value,
|
||||
error=last_error
|
||||
error=last_error,
|
||||
)
|
||||
continue
|
||||
raise
|
||||
@ -142,51 +159,57 @@ def handle_chat_generation_sync(user_id, api_key, model_value, prompt, use_trial
|
||||
return {"error": last_error or "聊天请求失败"}, resp.status_code if resp else 500
|
||||
|
||||
api_result = resp.json()
|
||||
content = api_result['choices'][0]['message']['content']
|
||||
content = api_result["choices"][0]["message"]["content"]
|
||||
|
||||
# 记录聊天历史
|
||||
# 非验光单解读的文本生成写入历史
|
||||
if prompt != "解读验光单":
|
||||
new_record = GenerationRecord(
|
||||
user_id=user_id,
|
||||
prompt=prompt,
|
||||
model=model_value,
|
||||
cost=cost,
|
||||
image_urls=json.dumps([{"type": "text", "content": content}])
|
||||
image_urls=json.dumps([{"type": "text", "content": content}]),
|
||||
)
|
||||
db.session.add(new_record)
|
||||
db.session.commit()
|
||||
|
||||
return {
|
||||
"data": [{"content": content, "type": "text"}],
|
||||
"message": "生成成功!"
|
||||
"message": "生成成功",
|
||||
}, 200
|
||||
except Exception as e:
|
||||
if use_trial:
|
||||
refund_points(user_id, cost)
|
||||
return {"error": str(e)}, 500
|
||||
|
||||
|
||||
def start_async_image_task(app, user_id, payload, api_key, target_api, cost, mode, model_value, use_trial=False):
|
||||
"""启动异步生图任务"""
|
||||
"""启动异步生图任务。"""
|
||||
task_id = str(uuid.uuid4())
|
||||
|
||||
log_msg = "用户发起验光单解读" if payload.get('prompt') == "解读验光单" else "用户发起生图任务"
|
||||
log_msg = "用户发起验光单解读" if payload.get("prompt") == "解读验光单" else "用户发起生图任务"
|
||||
system_logger.info(log_msg, model=model_value, mode=mode)
|
||||
|
||||
redis_client.setex(f"task:{task_id}", 3600, json.dumps({"status": "queued", "message": "任务已提交,等待处理..."}))
|
||||
redis_client.setex(
|
||||
f"task:{task_id}",
|
||||
3600,
|
||||
json.dumps({"status": "queued", "message": "任务已提交,等待处理..."}),
|
||||
)
|
||||
|
||||
threading.Thread(
|
||||
target=process_image_generation,
|
||||
args=(app, user_id, task_id, payload, api_key, target_api, cost, use_trial)
|
||||
args=(app, user_id, task_id, payload, api_key, target_api, cost, use_trial),
|
||||
).start()
|
||||
|
||||
return task_id
|
||||
|
||||
|
||||
def validate_video_request(user, data):
|
||||
"""验证视频生成请求"""
|
||||
"""校验视频生成请求。"""
|
||||
if user.points <= 0:
|
||||
return None, 0, "可用积分不足,请先充值"
|
||||
|
||||
model_value = data.get('model', 'veo3.1')
|
||||
model_value = data.get("model", "veo3.1")
|
||||
cost = get_model_cost(model_value, is_video=True)
|
||||
|
||||
if user.points < cost:
|
||||
@ -194,18 +217,23 @@ def validate_video_request(user, data):
|
||||
|
||||
return model_value, cost, None
|
||||
|
||||
|
||||
def start_async_video_task(app, user_id, payload, cost, model_value):
|
||||
"""启动异步视频任务"""
|
||||
"""启动异步视频任务。"""
|
||||
api_key = Config.VIDEO_KEY
|
||||
task_id = str(uuid.uuid4())
|
||||
|
||||
system_logger.info("用户发起视频生成任务 (积分模式)", model=model_value, cost=cost)
|
||||
|
||||
redis_client.setex(f"task:{task_id}", 3600, json.dumps({"status": "queued", "message": "视频任务已提交,准备开始导演..."}))
|
||||
redis_client.setex(
|
||||
f"task:{task_id}",
|
||||
3600,
|
||||
json.dumps({"status": "queued", "message": "视频任务已提交,准备开始处理..."}),
|
||||
)
|
||||
|
||||
threading.Thread(
|
||||
target=process_video_generation,
|
||||
args=(app, user_id, task_id, payload, api_key, cost, True) # 视频目前默认为积分模式
|
||||
args=(app, user_id, task_id, payload, api_key, cost, True),
|
||||
).start()
|
||||
|
||||
return task_id
|
||||
|
||||
@ -5,6 +5,7 @@ import requests
|
||||
import io
|
||||
import time
|
||||
import base64
|
||||
import binascii
|
||||
import threading
|
||||
from urllib.parse import quote
|
||||
from requests.exceptions import ConnectionError, ConnectTimeout
|
||||
@ -46,6 +47,171 @@ def _extract_error_detail(data, default="未知错误"):
|
||||
|
||||
return default
|
||||
|
||||
|
||||
def _guess_image_extension(content_type, fallback=".png"):
|
||||
mapping = {
|
||||
"image/png": ".png",
|
||||
"image/jpeg": ".jpg",
|
||||
"image/jpg": ".jpg",
|
||||
"image/webp": ".webp",
|
||||
}
|
||||
return mapping.get((content_type or "").lower(), fallback)
|
||||
|
||||
|
||||
def _put_object_bytes(object_name, content, content_type):
|
||||
s3_client.put_object(
|
||||
Bucket=Config.MINIO["bucket"],
|
||||
Key=object_name,
|
||||
Body=content,
|
||||
ContentType=content_type,
|
||||
)
|
||||
|
||||
|
||||
def _upload_image_bytes_to_minio(content, content_type="image/png"):
|
||||
ext = _guess_image_extension(content_type)
|
||||
base_filename = f"gen-{uuid.uuid4().hex}"
|
||||
full_filename = f"{base_filename}{ext}"
|
||||
thumb_filename = f"{base_filename}-thumb.jpg"
|
||||
|
||||
try:
|
||||
_put_object_bytes(full_filename, content, content_type)
|
||||
except Exception as upload_e:
|
||||
raise Exception(f"MinIO 上传原图失败: {upload_e}")
|
||||
|
||||
full_url = f"{Config.MINIO['public_url']}{quote(full_filename)}"
|
||||
thumb_url = full_url
|
||||
|
||||
try:
|
||||
img = Image.open(io.BytesIO(content))
|
||||
if img.mode in ("RGBA", "P"):
|
||||
img = img.convert("RGB")
|
||||
|
||||
w, h = img.size
|
||||
if w > 400:
|
||||
ratio = 400 / float(w)
|
||||
img.thumbnail((400, int(h * ratio)), Image.Resampling.LANCZOS)
|
||||
|
||||
thumb_io = io.BytesIO()
|
||||
img.save(thumb_io, format="JPEG", quality=80, optimize=True)
|
||||
thumb_io.seek(0)
|
||||
|
||||
_put_object_bytes(thumb_filename, thumb_io.getvalue(), "image/jpeg")
|
||||
thumb_url = f"{Config.MINIO['public_url']}{quote(thumb_filename)}"
|
||||
except Exception as thumb_e:
|
||||
system_logger.warning(f"缩略图生成失败: {thumb_e}")
|
||||
|
||||
return {"url": full_url, "thumb": thumb_url}
|
||||
|
||||
|
||||
def _decode_base64_image(image_value):
|
||||
raw_value = image_value or ""
|
||||
content_type = "image/png"
|
||||
if raw_value.startswith("data:") and "," in raw_value:
|
||||
header, raw_value = raw_value.split(",", 1)
|
||||
if ";" in header:
|
||||
content_type = header[5:].split(";", 1)[0] or content_type
|
||||
|
||||
# 清理换行和空白,兼容部分服务端返回的分段 Base64
|
||||
raw_value = "".join(raw_value.strip().split())
|
||||
padding = (-len(raw_value)) % 4
|
||||
if padding:
|
||||
raw_value += "=" * padding
|
||||
|
||||
try:
|
||||
decoded = base64.b64decode(raw_value, validate=True)
|
||||
except binascii.Error:
|
||||
try:
|
||||
decoded = base64.urlsafe_b64decode(raw_value)
|
||||
except Exception as decode_e:
|
||||
raise Exception(f"Base64 解码失败: {decode_e}")
|
||||
|
||||
if not decoded:
|
||||
raise Exception("Base64 解码失败: 结果为空")
|
||||
|
||||
# 提前验证图片内容,避免把损坏字节流误判成 MinIO 上传问题
|
||||
try:
|
||||
img = Image.open(io.BytesIO(decoded))
|
||||
img.verify()
|
||||
except Exception as verify_e:
|
||||
raise Exception(f"Base64 已解码,但图片数据无效: {verify_e}")
|
||||
|
||||
return decoded, content_type
|
||||
|
||||
|
||||
def _extract_b64_images(result):
|
||||
images = []
|
||||
for item in result.get("data", []) or []:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
if item.get("b64_json"):
|
||||
images.append(item["b64_json"])
|
||||
elif item.get("image_base64"):
|
||||
images.append(item["image_base64"])
|
||||
return images
|
||||
|
||||
|
||||
def _process_gpt_image_edit(app, user_id, task_id, payload, api_key, target_api, cost, use_trial):
|
||||
headers = {
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
# gpt-image-2 的 edits 接口走站内地址,直连比经过公共代理更稳定
|
||||
response = requests.post(
|
||||
target_api,
|
||||
json=payload,
|
||||
headers=headers,
|
||||
timeout=Config.PROXY_TIMEOUT_GENERATION,
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
try:
|
||||
error_data = response.json()
|
||||
except Exception:
|
||||
error_data = {"message": response.text}
|
||||
raise Exception(f"gpt-image-2 请求失败: {_extract_error_detail(error_data)}")
|
||||
|
||||
result = response.json()
|
||||
b64_images = _extract_b64_images(result)
|
||||
if not b64_images:
|
||||
raise Exception("gpt-image-2 未返回可用图片数据")
|
||||
|
||||
processed_data = []
|
||||
final_urls = []
|
||||
output_format = (payload.get("output_format") or "png").lower()
|
||||
default_content_type = f"image/{output_format}" if output_format in ("png", "jpeg", "jpg", "webp") else "image/png"
|
||||
|
||||
for b64_image in b64_images:
|
||||
try:
|
||||
image_bytes, content_type = _decode_base64_image(b64_image)
|
||||
except Exception as decode_e:
|
||||
raise Exception(f"gpt-image-2 返回的 Base64 图片无效: {decode_e}")
|
||||
if content_type == "image/png" and default_content_type != "image/png":
|
||||
content_type = default_content_type
|
||||
try:
|
||||
uploaded = _upload_image_bytes_to_minio(image_bytes, content_type)
|
||||
except Exception as upload_e:
|
||||
raise Exception(f"gpt-image-2 结果已生成,但同步到 MinIO 失败: {upload_e}")
|
||||
processed_data.append(uploaded)
|
||||
final_urls.append(uploaded["url"])
|
||||
|
||||
new_record = GenerationRecord(
|
||||
user_id=user_id,
|
||||
prompt=payload.get("prompt"),
|
||||
model=payload.get("model"),
|
||||
cost=cost,
|
||||
image_urls=json.dumps(processed_data),
|
||||
)
|
||||
db.session.add(new_record)
|
||||
db.session.commit()
|
||||
|
||||
system_logger.info("gpt-image-2 生图任务完成", user_id=user_id, task_id=task_id, model=payload.get("model"))
|
||||
redis_client.setex(
|
||||
f"task:{task_id}",
|
||||
3600,
|
||||
json.dumps({"status": "complete", "urls": final_urls, "record_id": new_record.id}),
|
||||
)
|
||||
|
||||
|
||||
def sync_images_background(app, record_id, raw_urls):
|
||||
"""后台同步图片至 MinIO,并生成缩略图,带重试机制"""
|
||||
with app.app_context():
|
||||
@ -129,6 +295,10 @@ def process_image_generation(app, user_id, task_id, payload, api_key, target_api
|
||||
redis_client.setex(f"task:{task_id}", 3600, json.dumps({"status": "processing", "message": "任务已提交,正在排队处理..."}))
|
||||
try:
|
||||
# 1. 提交异步请求 (带重试机制)
|
||||
if payload.get('model') == 'gpt-image-2':
|
||||
_process_gpt_image_edit(app, user_id, task_id, payload, api_key, target_api, cost, use_trial)
|
||||
return
|
||||
|
||||
submit_resp = None
|
||||
last_error = None
|
||||
active_api = target_api
|
||||
|
||||
@ -457,10 +457,46 @@ document.addEventListener('paste', (e) => {
|
||||
}
|
||||
});
|
||||
|
||||
const SIZE_MODELS = ['nano-banana-pro', 'gemini-3.1-flash-image-preview'];
|
||||
const GPT_IMAGE_EDIT_MODEL = 'gpt-image-2';
|
||||
const GPT_IMAGE_EDIT_SIZES = [
|
||||
{ label: '1024x1024 (square)', value: '1024x1024' },
|
||||
{ label: '1536x1024 (landscape)', value: '1536x1024' },
|
||||
{ label: '1024x1536 (portrait)', value: '1024x1536' },
|
||||
{ label: '2048x2048 (2K square)', value: '2048x2048' },
|
||||
{ label: '2048x1152 (2K landscape)', value: '2048x1152' },
|
||||
{ label: '3840x2160 (4K landscape)', value: '3840x2160' },
|
||||
{ label: '2160x3840 (4K portrait)', value: '2160x3840' },
|
||||
{ label: 'auto (default)', value: 'auto' }
|
||||
];
|
||||
const SIZE_MODELS = ['nano-banana-2', 'gemini-3.1-flash-image-preview', GPT_IMAGE_EDIT_MODEL];
|
||||
let defaultSizeOptions = [];
|
||||
|
||||
document.getElementById('modelSelect').onchange = (e) => {
|
||||
document.getElementById('sizeGroup').classList.toggle('hidden', !SIZE_MODELS.includes(e.target.value));
|
||||
function updateSizeOptions() {
|
||||
const modelSelect = document.getElementById('modelSelect');
|
||||
const sizeGroup = document.getElementById('sizeGroup');
|
||||
const sizeSelect = document.getElementById('sizeSelect');
|
||||
const ratioGroup = document.getElementById('ratioGroup');
|
||||
if (!modelSelect || !sizeGroup || !sizeSelect) return;
|
||||
|
||||
const selectedModel = modelSelect.value;
|
||||
const previousValue = sizeSelect.value;
|
||||
const nextSizes = selectedModel === GPT_IMAGE_EDIT_MODEL ? GPT_IMAGE_EDIT_SIZES : defaultSizeOptions;
|
||||
fillSelect('sizeSelect', nextSizes);
|
||||
|
||||
if (selectedModel === GPT_IMAGE_EDIT_MODEL) {
|
||||
sizeSelect.value = '2160x3840';
|
||||
} else if (nextSizes.some(item => item.value === previousValue)) {
|
||||
sizeSelect.value = previousValue;
|
||||
}
|
||||
|
||||
sizeGroup.classList.toggle('hidden', !SIZE_MODELS.includes(selectedModel));
|
||||
if (ratioGroup) {
|
||||
ratioGroup.classList.toggle('hidden', selectedModel === GPT_IMAGE_EDIT_MODEL);
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('modelSelect').onchange = () => {
|
||||
updateSizeOptions();
|
||||
updateCostPreview(); // 切换模型时更新计费预览
|
||||
};
|
||||
|
||||
@ -490,7 +526,7 @@ document.getElementById('promptTpl').onchange = (e) => {
|
||||
|
||||
document.getElementById('submitBtn').onclick = async () => {
|
||||
const btn = document.getElementById('submitBtn');
|
||||
const prompt = document.getElementById('manualPrompt').value;
|
||||
const prompt = document.getElementById('manualPrompt').value.trim();
|
||||
const apiKey = document.getElementById('apiKey').value;
|
||||
const num = parseInt(document.getElementById('numSelect').value);
|
||||
|
||||
@ -510,11 +546,24 @@ document.getElementById('submitBtn').onclick = async () => {
|
||||
}
|
||||
|
||||
// 允许文生图(不强制要求图片),但至少得有提示词或图片
|
||||
if (!prompt && uploadedFiles.length === 0) {
|
||||
if (!prompt) {
|
||||
return showToast('提示词不能为空', 'warning');
|
||||
return showToast('请至少输入提示词或上传参考图', 'warning');
|
||||
}
|
||||
|
||||
// UI 锁定
|
||||
if (document.getElementById('modelSelect').value === GPT_IMAGE_EDIT_MODEL && uploadedFiles.length === 0) {
|
||||
return showToast('gpt-image-2 requires a reference image', 'warning');
|
||||
}
|
||||
|
||||
if (document.getElementById('modelSelect').value === GPT_IMAGE_EDIT_MODEL && uploadedFiles.length === 0) {
|
||||
return showToast('gpt-image-2 妯″瀷蹇呴』涓婁紶鍙傝€冨浘', 'warning');
|
||||
}
|
||||
|
||||
if (document.getElementById('modelSelect').value === GPT_IMAGE_EDIT_MODEL && uploadedFiles.length === 0) {
|
||||
return showToast('gpt-image-2 requires a reference image', 'warning');
|
||||
}
|
||||
|
||||
btn.disabled = true;
|
||||
const btnText = btn.querySelector('span');
|
||||
btnText.innerText = uploadedFiles.length > 0 ? "正在同步参考图..." : "正在开启 AI 引擎...";
|
||||
@ -597,7 +646,7 @@ document.getElementById('submitBtn').onclick = async () => {
|
||||
apiKey: currentMode === 'key' ? apiKey : '',
|
||||
prompt: document.getElementById('manualPrompt').value,
|
||||
model: document.getElementById('modelSelect').value,
|
||||
ratio: document.getElementById('ratioSelect').value,
|
||||
ratio: document.getElementById('modelSelect').value === GPT_IMAGE_EDIT_MODEL ? undefined : document.getElementById('ratioSelect').value,
|
||||
size: document.getElementById('sizeSelect').value,
|
||||
image_data // 发送 Base64 数组
|
||||
})
|
||||
@ -787,7 +836,7 @@ window.addEventListener('message', (e) => {
|
||||
if (modelSelect) {
|
||||
modelSelect.value = 'nano-banana-2';
|
||||
modelSelect.disabled = true; // 锁定选择
|
||||
sizeGroup.classList.remove('hidden');
|
||||
updateSizeOptions();
|
||||
updateCostPreview();
|
||||
}
|
||||
|
||||
@ -1036,12 +1085,13 @@ async function refreshPromptsList() {
|
||||
|
||||
fillSelect('modelSelect', d.models);
|
||||
fillSelect('ratioSelect', d.ratios);
|
||||
fillSelect('sizeSelect', d.sizes);
|
||||
defaultSizeOptions = d.sizes || [];
|
||||
updateSizeOptions();
|
||||
|
||||
// 初始化后检查默认模型是否需要显示尺寸选择器
|
||||
const modelSelect = document.getElementById('modelSelect');
|
||||
if (modelSelect && typeof SIZE_MODELS !== 'undefined') {
|
||||
document.getElementById('sizeGroup').classList.toggle('hidden', !SIZE_MODELS.includes(modelSelect.value));
|
||||
updateSizeOptions();
|
||||
}
|
||||
|
||||
const userPrompts = await loadUserPrompts();
|
||||
|
||||
@ -81,7 +81,7 @@
|
||||
<select id="modelSelect"
|
||||
class="w-full bg-white border border-slate-200 rounded-xl p-3 text-xs font-bold outline-none focus:border-indigo-500 transition-all"></select>
|
||||
</div>
|
||||
<div class="space-y-1.5">
|
||||
<div id="ratioGroup" class="space-y-1.5">
|
||||
<label
|
||||
class="text-[10px] font-bold text-slate-400 uppercase tracking-widest ml-1 flex items-center gap-1.5"><i
|
||||
data-lucide="layout" class="w-3 h-3"></i>画面比例</label>
|
||||
|
||||
@ -191,7 +191,7 @@
|
||||
|
||||
<!-- 比例和数量 -->
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div class="space-y-2">
|
||||
<div id="ratioGroup" class="space-y-2">
|
||||
<label class="text-xs font-bold text-slate-500 uppercase tracking-wider flex items-center gap-2">
|
||||
<i data-lucide="layout" class="w-4 h-4"></i>画面比例
|
||||
</label>
|
||||
@ -424,7 +424,7 @@
|
||||
fillSelect('ratioSelect', config.ratios);
|
||||
|
||||
// 填充尺寸选择
|
||||
fillSelect('sizeSelect', config.sizes);
|
||||
defaultSizeOptions = config.sizes || [];
|
||||
|
||||
// 加载用户收藏的提示词
|
||||
const userPrompts = await loadUserPrompts();
|
||||
@ -449,14 +449,40 @@
|
||||
}
|
||||
|
||||
// 尺寸模型常量 (与PC端一致)
|
||||
const SIZE_MODELS = ['nano-banana-2', 'gemini-3.1-flash-image-preview'];
|
||||
const GPT_IMAGE_EDIT_MODEL = 'gpt-image-2';
|
||||
const GPT_IMAGE_EDIT_SIZES = [
|
||||
{ label: '1024x1024 (square)', value: '1024x1024' },
|
||||
{ label: '1536x1024 (landscape)', value: '1536x1024' },
|
||||
{ label: '1024x1536 (portrait)', value: '1024x1536' },
|
||||
{ label: '2048x2048 (2K square)', value: '2048x2048' },
|
||||
{ label: '2048x1152 (2K landscape)', value: '2048x1152' },
|
||||
{ label: '3840x2160 (4K landscape)', value: '3840x2160' },
|
||||
{ label: '2160x3840 (4K portrait)', value: '2160x3840' },
|
||||
{ label: 'auto (default)', value: 'auto' }
|
||||
];
|
||||
const SIZE_MODELS = ['nano-banana-2', 'gemini-3.1-flash-image-preview', GPT_IMAGE_EDIT_MODEL];
|
||||
let defaultSizeOptions = [];
|
||||
|
||||
// 更新尺寸选择器显示状态
|
||||
function updateSizeVisibility() {
|
||||
const modelSelect = document.getElementById('modelSelect');
|
||||
const sizeGroup = document.getElementById('sizeGroup');
|
||||
if (modelSelect && sizeGroup) {
|
||||
sizeGroup.classList.toggle('hidden', !SIZE_MODELS.includes(modelSelect.value));
|
||||
const sizeSelect = document.getElementById('sizeSelect');
|
||||
const ratioGroup = document.getElementById('ratioGroup');
|
||||
if (modelSelect && sizeGroup && sizeSelect) {
|
||||
const selectedModel = modelSelect.value;
|
||||
const previousValue = sizeSelect.value;
|
||||
const nextSizes = selectedModel === GPT_IMAGE_EDIT_MODEL ? GPT_IMAGE_EDIT_SIZES : defaultSizeOptions;
|
||||
fillSelect('sizeSelect', nextSizes);
|
||||
if (selectedModel === GPT_IMAGE_EDIT_MODEL) {
|
||||
sizeSelect.value = '2160x3840';
|
||||
} else if (nextSizes.some(item => item.value === previousValue)) {
|
||||
sizeSelect.value = previousValue;
|
||||
}
|
||||
sizeGroup.classList.toggle('hidden', !SIZE_MODELS.includes(selectedModel));
|
||||
if (ratioGroup) {
|
||||
ratioGroup.classList.toggle('hidden', selectedModel === GPT_IMAGE_EDIT_MODEL);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -627,6 +653,7 @@
|
||||
|
||||
const prompt = document.getElementById('manualPrompt').value.trim();
|
||||
if (!prompt) {
|
||||
showToast('提示词不能为空', 'warning');
|
||||
showToast('请输入提示词', 'warning');
|
||||
return;
|
||||
}
|
||||
@ -637,8 +664,14 @@
|
||||
const num = parseInt(document.getElementById('numSelect').value);
|
||||
const isPremium = document.getElementById('isPremium').checked;
|
||||
|
||||
if (model === GPT_IMAGE_EDIT_MODEL && uploadedFiles.length === 0) {
|
||||
showToast('gpt-image-2 模型必须上传参考图', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
// 显示加载状态
|
||||
document.getElementById('placeholder').classList.add('hidden');
|
||||
document.getElementById('placeholder').classList.add('hidden');
|
||||
document.getElementById('resultGrid').classList.add('hidden');
|
||||
document.getElementById('loadingState').classList.remove('hidden');
|
||||
|
||||
@ -674,7 +707,7 @@
|
||||
is_premium: isPremium,
|
||||
prompt,
|
||||
model,
|
||||
ratio,
|
||||
ratio: model === GPT_IMAGE_EDIT_MODEL ? undefined : ratio,
|
||||
size,
|
||||
image_data: imageData.length > 0 ? imageData : undefined
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user