打包项目
This commit is contained in:
261
source_code/generate_api.py
Normal file
261
source_code/generate_api.py
Normal file
@@ -0,0 +1,261 @@
|
||||
"""
|
||||
生成图片 API - 供 pywebview 前端调用
|
||||
流程:上传图片 -> workflow_run -> 轮询 query_result -> 解析返回图片 URL
|
||||
"""
|
||||
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
import time
|
||||
import re
|
||||
import traceback
|
||||
|
||||
import requests
|
||||
|
||||
from coze import upload_file, workflow_run, query_result
|
||||
from config import workflow_id
|
||||
from ali_oss import upload_data_urls as oss_upload_data_urls
|
||||
|
||||
|
||||
def _data_url_to_temp_file(data_url: str, allow_video: bool = False) -> str:
|
||||
"""将 base64 data URL 转为临时文件路径"""
|
||||
# data:image/png;base64,xxxx 或 data:video/mp4;base64,xxxx
|
||||
if allow_video:
|
||||
match = re.match(r'data:(?:image|video)/(\w+);base64,(.+)', data_url)
|
||||
else:
|
||||
match = re.match(r'data:image/(\w+);base64,(.+)', data_url)
|
||||
if not match:
|
||||
raise ValueError('无效的 data URL 格式')
|
||||
mime = match.group(1).lower()
|
||||
ext_map = {'png': 'png', 'webp': 'png', 'jpeg': 'jpg', 'jpg': 'jpg', 'mp4': 'mp4', 'webm': 'webm'}
|
||||
ext = ext_map.get(mime, 'jpg')
|
||||
data = base64.b64decode(match.group(2))
|
||||
fd, path = tempfile.mkstemp(suffix=f'.{ext}')
|
||||
try:
|
||||
os.write(fd, data)
|
||||
finally:
|
||||
os.close(fd)
|
||||
return path
|
||||
|
||||
|
||||
def _upload_video(video_data_url: str) -> str:
|
||||
"""上传视频,返回 file_id"""
|
||||
if not video_data_url or not isinstance(video_data_url, str):
|
||||
raise ValueError('无效的视频数据')
|
||||
path = _data_url_to_temp_file(video_data_url, allow_video=True)
|
||||
try:
|
||||
resp = upload_file(path)
|
||||
if resp.get('code') == 0 and resp.get('data', {}).get('id'):
|
||||
return resp['data']['id']
|
||||
raise RuntimeError(f'视频上传失败: {resp.get("msg", resp)}')
|
||||
finally:
|
||||
try:
|
||||
os.unlink(path)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _upload_images(image_data_urls: list) -> list:
|
||||
"""上传多张图片,返回 file_id 列表"""
|
||||
file_ids = []
|
||||
temp_paths = []
|
||||
try:
|
||||
for data_url in (image_data_urls or []):
|
||||
if not data_url or not isinstance(data_url, str):
|
||||
continue
|
||||
if data_url.startswith("http"):
|
||||
fd, path = tempfile.mkstemp(suffix=f'.png')
|
||||
data_content = requests.get(data_url).content
|
||||
os.write(fd, data_content)
|
||||
else:
|
||||
path = _data_url_to_temp_file(data_url)
|
||||
temp_paths.append(path)
|
||||
resp = upload_file(path)
|
||||
if resp.get('code') == 0 and resp.get('data', {}).get('id'):
|
||||
file_ids.append(resp['data']['id'])
|
||||
else:
|
||||
raise RuntimeError(f'上传失败: {resp.get("msg", resp)}')
|
||||
return file_ids
|
||||
finally:
|
||||
for p in temp_paths:
|
||||
try:
|
||||
os.unlink(p)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _parse_output(output_str: str) -> list:
|
||||
"""解析 query_result 中的 output,提取图片 URL 列表"""
|
||||
try:
|
||||
outer = json.loads(output_str)
|
||||
inner_str = outer.get('Output', '{}')
|
||||
inner = json.loads(inner_str)
|
||||
data_str = inner.get('data', '[]')
|
||||
data = json.loads(data_str)
|
||||
urls = data.get("images")
|
||||
long_image_url = data.get("merged_image_url")
|
||||
return urls if isinstance(urls, list) else [],long_image_url
|
||||
except Exception:
|
||||
return [],None
|
||||
|
||||
|
||||
def _parse_output_text(output_str: str) -> list:
|
||||
"""解析 query_result 中的 output,提取图片 URL 列表"""
|
||||
try:
|
||||
outer = json.loads(output_str)
|
||||
inner_str = outer.get('Output', '{}')
|
||||
inner = json.loads(inner_str)
|
||||
data_str = inner.get('data', '')
|
||||
if isinstance(data_str,str):
|
||||
data = json.loads(data_str).get("reverse_prompt")
|
||||
return [data]
|
||||
pattern = r'【?图像 \d+】.*?(?=【?图像 \d+】|$)'
|
||||
segments = re.findall(pattern, data_str, re.DOTALL)
|
||||
return segments
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
|
||||
def _poll_until_done(wf_id: str, execute_id: str, interval: float =10, timeout: int = 600*2, is_text: bool = False) -> list:
|
||||
"""轮询直到成功或超时,返回图片 URL 列表"""
|
||||
start = time.time()
|
||||
while time.time() - start < timeout:
|
||||
resp = query_result(wf_id, execute_id)
|
||||
if resp.get('code') != 0:
|
||||
raise RuntimeError(f'查询失败: {resp.get("msg", resp)}')
|
||||
items = resp.get('data') or []
|
||||
if not items:
|
||||
time.sleep(interval)
|
||||
continue
|
||||
item = items[0]
|
||||
status = item.get('execute_status', '')
|
||||
if status == 'Success':
|
||||
output = item.get('output', '{}')
|
||||
if is_text:
|
||||
return _parse_output_text(output)
|
||||
return _parse_output(output)
|
||||
if status and status not in ('Running', 'Pending', ''):
|
||||
raise RuntimeError(f'执行失败: {status}')
|
||||
time.sleep(interval)
|
||||
raise RuntimeError('生成超时')
|
||||
|
||||
|
||||
def _res_to_2k(res: str) -> str:
|
||||
"""统一分辨率为 2K/4K"""
|
||||
r = (res or '2k').strip().upper()
|
||||
return '4K' if r == '4K' else '2K'
|
||||
|
||||
|
||||
def generate(params: dict) -> dict:
|
||||
"""
|
||||
生成图片
|
||||
params: {
|
||||
menu: int, # 1=图片反推 2=图片编辑 3=随机海报 4=克隆海报 5=服饰穿搭
|
||||
prompt: str, # 用户自定义指令 (menu=1 必填)
|
||||
ref_images: list, # base64 data URLs - 参考图
|
||||
video: str, # base64 data URL - 视频 (menu=1 可选,仅支持1个)
|
||||
model_images: list, # base64 data URLs - 多模特图 (menu!=1,最多5张)
|
||||
name: str, desc: str, ratio: str, resolution: str, count: int,
|
||||
language: str, style: str, batch_prompt: list, brand_name: str,
|
||||
Ingredients: str, activity: str, mode: str, texts: list,
|
||||
proc_images: list, layout_image: str,
|
||||
}
|
||||
返回: { success: bool, urls: list, prompts: list, error: str }
|
||||
"""
|
||||
original_urls = []
|
||||
api_key = (params.get('api_key') or '').strip()
|
||||
try:
|
||||
menu = int(params.get('menu', 2))
|
||||
ref_data = params.get('ref_images') or []
|
||||
proc_data = params.get('proc_images') or []
|
||||
layout_data = params.get('layout_image')
|
||||
video_data = params.get('video')
|
||||
model_data = params.get('model_images') or []
|
||||
|
||||
# menu 1: 反推词,简化参数
|
||||
if menu == 1:
|
||||
base_params = {'menu': 1, 'prompt': str(params.get('prompt', '')).strip() or ''}
|
||||
|
||||
ref_ids = _upload_images(ref_data) if ref_data else []
|
||||
if ref_ids:
|
||||
base_params['ref_images'] = [{'file_id': fid} for fid in ref_ids]
|
||||
if video_data:
|
||||
try:
|
||||
vid = _upload_video(video_data)
|
||||
base_params['video'] = {'file_id': vid}
|
||||
except Exception as ve:
|
||||
return {'success': False, 'urls': [], 'prompts': [], 'error': f'视频上传失败: {ve}'}
|
||||
base_params = {k: v for k, v in base_params.items() if v is not None and v != ''}
|
||||
base_params["api_key"] = api_key
|
||||
resp = workflow_run(workflow_id, base_params)
|
||||
if resp.get('code') != 0:
|
||||
return {'success': False, 'urls': [], 'prompts': [], 'error': resp.get('msg', str(resp))}
|
||||
execute_id = resp.get('execute_id')
|
||||
if not execute_id:
|
||||
return {'success': False, 'urls': [], 'prompts': [], 'error': '未返回 execute_id'}
|
||||
prompts = _poll_until_done(workflow_id, str(execute_id), is_text=True)
|
||||
# menu 1 返回提示词列表,统一转为字符串
|
||||
# prompts = [str(x) for x in result_list] if result_list else []
|
||||
# prompts = [prompts]
|
||||
return {'success': True, 'urls': [], 'prompts': prompts, 'error': ''}
|
||||
|
||||
# menu != 1: 原有逻辑
|
||||
all_ref = list(ref_data)
|
||||
if layout_data:
|
||||
all_ref = [layout_data] + list(ref_data)
|
||||
all_originals = list(all_ref) + list(proc_data) + list(model_data)
|
||||
|
||||
if all_originals:
|
||||
try:
|
||||
original_urls = oss_upload_data_urls(all_originals, prefix="originals")
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
pass
|
||||
|
||||
ref_ids = _upload_images(all_ref) if all_ref else []
|
||||
proc_ids = _upload_images(proc_data) if proc_data else []
|
||||
model_ids = _upload_images(model_data) if model_data else []
|
||||
|
||||
res = _res_to_2k(params.get('resolution', '2K'))
|
||||
|
||||
base_params = {
|
||||
'name': str(params.get('name', '')).strip(),
|
||||
'ratio': str(params.get('ratio', '')).strip(),
|
||||
'menu': menu,
|
||||
'resolution': res,
|
||||
'count': int(params.get('count', 1)),
|
||||
'desc': str(params.get('desc', '')).strip(),
|
||||
'language': str(params.get('language', '中文')).strip() or '中文',
|
||||
'style': str(params.get('style', '')).strip(),
|
||||
'prompt': str(params.get('prompt', '')).strip(),
|
||||
'batch_prompt': params.get('batch_prompt') or [],
|
||||
'brand_name': str(params.get('brand_name', '')).strip(),
|
||||
'Ingredients': str(params.get('Ingredients', '')).strip(),
|
||||
'activity': str(params.get('activity', '')).strip(),
|
||||
'mode': str(params.get('mode', '1')).strip() or '1',
|
||||
'texts': params.get('text') or [],
|
||||
"api_key" : api_key
|
||||
}
|
||||
|
||||
if ref_ids:
|
||||
base_params['ref_images'] = [{'file_id': fid} for fid in ref_ids]
|
||||
if proc_ids:
|
||||
base_params['proc_images'] = [{'file_id': fid} for fid in proc_ids]
|
||||
if model_ids:
|
||||
base_params['model_images'] = [{'file_id': fid} for fid in model_ids]
|
||||
base_params = {k: v for k, v in base_params.items() if v is not None and v != ''}
|
||||
|
||||
resp = workflow_run(workflow_id, base_params)
|
||||
if resp.get('code') != 0:
|
||||
return {'success': False, 'urls': [], 'error': resp.get('msg', str(resp))}
|
||||
execute_id = resp.get('execute_id')
|
||||
if not execute_id:
|
||||
return {'success': False, 'urls': [], 'error': '未返回 execute_id'}
|
||||
|
||||
urls,long_image_url = _poll_until_done(workflow_id, str(execute_id))
|
||||
return {'success': True, 'urls': urls, 'original_urls': original_urls, 'error': '',"long_image_url":long_image_url}
|
||||
except Exception as e:
|
||||
return {'success': False, 'urls': [], 'prompts': [], 'original_urls': [], 'error': str(e),"long_image_url":""}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user