feat: 升级图像生成SDK支持multipart/form-data格式
- 升级generateImage方法支持multipart/form-data提交格式 - 添加aspect_ratio参数,默认9:16比例 - 更新默认模型为gemini-2.5-flash-image-preview - 为getTaskStatus方法添加失败重试机制,最多重试3次,每次间隔5秒 - 新增useSdk hook封装SDK使用 - 更新示例页面集成图像生成功能
This commit is contained in:
parent
9b3bc7bf2d
commit
77ccaf8acd
|
|
@ -0,0 +1,4 @@
|
|||
|
||||
|
||||
export { useAd } from './useAd'
|
||||
export { useSdk } from './useSdk'
|
||||
|
|
@ -8,7 +8,7 @@ interface UseAdReturn {
|
|||
}
|
||||
|
||||
export function useAd(): UseAdReturn {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const adRef = useRef<RewardedVideoAd | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
import { bowongAI } from "../sdk";
|
||||
|
||||
|
||||
export function useSdk() {
|
||||
return bowongAI;
|
||||
}
|
||||
|
|
@ -1,23 +1,28 @@
|
|||
import { View, Text, Button } from '@tarojs/components'
|
||||
import { useLoad } from '@tarojs/taro'
|
||||
import './index.css'
|
||||
import { useAd } from '../../hooks/useAd'
|
||||
import { useAd, useSdk } from '../../hooks/index'
|
||||
|
||||
export default function Index() {
|
||||
|
||||
const { showAd, loadAd } = useAd()
|
||||
const sdk = useSdk()
|
||||
useLoad(() => {
|
||||
loadAd()
|
||||
return () => {}
|
||||
return () => { }
|
||||
})
|
||||
|
||||
return (
|
||||
<View className='index'>
|
||||
<Text>Hello world Ymm !!!!</Text>
|
||||
<Button onClick={() => {
|
||||
showAd()
|
||||
}}
|
||||
>广告</Button>
|
||||
sdk.chooseAndGenerateImage().then(task_id => {
|
||||
console.log({ task_id })
|
||||
return sdk.getTaskStatus(task_id)
|
||||
}).then(res => {
|
||||
console.log(res)
|
||||
})
|
||||
}}>
|
||||
生图
|
||||
</Button>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,362 @@
|
|||
import Taro from "@tarojs/taro"
|
||||
|
||||
/**
|
||||
* API 响应基础接口
|
||||
*/
|
||||
interface ApiResponse<T = any> {
|
||||
status: boolean | string;
|
||||
data: T;
|
||||
msg: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件上传参数
|
||||
*/
|
||||
interface UploadParams {
|
||||
/** 文件路径(小程序中的临时文件路径) */
|
||||
filePath: string;
|
||||
/** 文件名,可选,默认会自动生成 */
|
||||
name?: string;
|
||||
/** 文件类型,可选,会根据文件扩展名自动判断 */
|
||||
type?: string;
|
||||
/** 上传进度回调 */
|
||||
onProgress?: (progress: number) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 图像生成请求参数
|
||||
*/
|
||||
interface GenerateImageParams {
|
||||
/** 生成提示词 */
|
||||
prompt: string;
|
||||
/** 模型名称,默认使用 gemini-2.5-flash-image-preview */
|
||||
model_name?: string;
|
||||
/** 图像宽高比,如 '9:16', '16:9', '1:1' 等,默认为 '9:16' */
|
||||
aspect_ratio?: string;
|
||||
/** 生成模式,默认为 turbo */
|
||||
mode?: 'turbo' | 'standard';
|
||||
/** 是否启用 webhook 回调,默认为 false */
|
||||
webhook_flag?: boolean;
|
||||
img_file: string;
|
||||
}
|
||||
|
||||
export function isGetFileInfoSuccessCallbackResult(val: any): val is Taro.getFileInfo.SuccessCallbackResult {
|
||||
return val && Reflect.has(val, 'digest')
|
||||
}
|
||||
|
||||
/**
|
||||
* BowongAI SDK 类
|
||||
* 提供图像生成等 AI 功能的接口封装
|
||||
*/
|
||||
export class BowongAISDK {
|
||||
/** API 服务基础地址 */
|
||||
private readonly baseUrl = 'https://bowongai-test--text-video-agent-fastapi-app.modal.run';
|
||||
|
||||
/**
|
||||
* 上传文件到 S3
|
||||
* 支持图片、视频等多种文件类型上传
|
||||
* @param params 上传参数
|
||||
* @returns Promise<string> 返回上传后的文件地址
|
||||
*/
|
||||
async upload(params: UploadParams): Promise<string> {
|
||||
const { filePath, name, type, onProgress } = params;
|
||||
const url = `${this.baseUrl}/api/file/upload/s3`;
|
||||
|
||||
try {
|
||||
// 获取文件信息
|
||||
const fileInfo = await Taro.getFileInfo({ filePath });
|
||||
if (!isGetFileInfoSuccessCallbackResult(fileInfo)) {
|
||||
throw new Error(fileInfo.errMsg)
|
||||
}
|
||||
// 自动生成文件名(如果未提供)
|
||||
const fileName = name || `upload_${Date.now()}.${this._getFileExtension(filePath)}`;
|
||||
|
||||
// 自动判断文件类型(如果未提供)
|
||||
const fileType = type || this._getMimeType(filePath);
|
||||
|
||||
console.log('开始上传文件:', {
|
||||
fileName,
|
||||
fileSize: fileInfo.size,
|
||||
fileType
|
||||
});
|
||||
|
||||
// 使用 Taro.uploadFile 进行文件上传
|
||||
const response = await new Promise<Taro.uploadFile.SuccessCallbackResult>((resolve, reject) => {
|
||||
const uploadTask = Taro.uploadFile({
|
||||
url,
|
||||
filePath,
|
||||
name: 'file', // 服务端接收的字段名
|
||||
header: {
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
formData: {
|
||||
filename: fileName,
|
||||
type: fileType
|
||||
},
|
||||
success: resolve,
|
||||
fail: reject
|
||||
});
|
||||
|
||||
// 监听上传进度
|
||||
if (onProgress) {
|
||||
uploadTask.progress((res) => {
|
||||
const progress = Math.round((res.totalBytesSent / res.totalBytesExpectedToSend) * 100);
|
||||
onProgress(progress);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 检查 HTTP 状态码
|
||||
if (response.statusCode !== 200) {
|
||||
throw new Error(`HTTP ${response.statusCode}: 文件上传失败`);
|
||||
}
|
||||
|
||||
// 解析响应数据
|
||||
let result: ApiResponse<string>;
|
||||
try {
|
||||
result = JSON.parse(response.data) as ApiResponse<string>;
|
||||
} catch (parseError) {
|
||||
throw new Error('服务器响应格式错误');
|
||||
}
|
||||
|
||||
// 检查业务状态码
|
||||
if (!result.status) {
|
||||
throw new Error(result.msg || '文件上传失败');
|
||||
}
|
||||
|
||||
console.log('文件上传成功:', result.data);
|
||||
return result.data;
|
||||
|
||||
} catch (error) {
|
||||
console.error('文件上传失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 选择并上传图片
|
||||
* 封装了选择图片和上传的完整流程
|
||||
* @param options 选择图片的配置选项
|
||||
* @returns Promise<string> 返回上传后的文件地址
|
||||
*/
|
||||
async chooseAndGenerateImage(options?: {
|
||||
/** 最多可以选择的图片张数,默认为1 */
|
||||
count?: number;
|
||||
/** 所选的图片的尺寸,默认为 ['original', 'compressed'] */
|
||||
sizeType?: ('original' | 'compressed')[];
|
||||
/** 选择图片的来源,默认为 ['album', 'camera'] */
|
||||
sourceType?: ('album' | 'camera')[];
|
||||
}): Promise<string> {
|
||||
const {
|
||||
count = 1,
|
||||
sizeType = ['original', 'compressed'],
|
||||
sourceType = ['album', 'camera'],
|
||||
} = options || {};
|
||||
|
||||
try {
|
||||
// 选择图片
|
||||
const chooseResult = await Taro.chooseImage({
|
||||
count,
|
||||
sizeType,
|
||||
sourceType
|
||||
});
|
||||
|
||||
if (!chooseResult.tempFilePaths || chooseResult.tempFilePaths.length === 0) {
|
||||
throw new Error('未选择图片');
|
||||
}
|
||||
|
||||
// 上传第一张图片
|
||||
const filePath = chooseResult.tempFilePaths[0];
|
||||
return await this.generateImage({
|
||||
img_file: filePath,
|
||||
prompt: `将画面中的角色重塑为顶级收藏级树脂手办,全身动态姿势,置于角色主题底座;高精度材质,手工涂装,肌肤纹理与服装材质真实分明。
|
||||
戏剧性硬光为主光源,凸显立体感,无过曝;强效补光消除死黑,细节完整可见。背景为窗边景深模糊,侧后方隐约可见产品包装盒。
|
||||
博物馆级摄影质感,全身细节无损,面部结构精准。禁止:任何2D元素或照搬原图、塑料感、面部模糊、五官错位、细节丢失`
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('选择并上传图片失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据文件路径获取文件扩展名
|
||||
* @private
|
||||
*/
|
||||
private _getFileExtension(filePath: string): string {
|
||||
const match = filePath.match(/\.([^.]+)$/);
|
||||
return match ? match[1] : 'jpg';
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据文件路径获取 MIME 类型
|
||||
* @private
|
||||
*/
|
||||
private _getMimeType(filePath: string): string {
|
||||
const extension = this._getFileExtension(filePath).toLowerCase();
|
||||
const mimeTypes: Record<string, string> = {
|
||||
'jpg': 'image/jpeg',
|
||||
'jpeg': 'image/jpeg',
|
||||
'png': 'image/png',
|
||||
'gif': 'image/gif',
|
||||
'webp': 'image/webp',
|
||||
'mp4': 'video/mp4',
|
||||
'mov': 'video/quicktime',
|
||||
'avi': 'video/x-msvideo'
|
||||
};
|
||||
return mimeTypes[extension] || 'application/octet-stream';
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成图像
|
||||
* @param params 图像生成参数
|
||||
* @returns Promise<string> 返回任务ID
|
||||
*
|
||||
* 使用 multipart/form-data 格式提交,支持文件上传和URL参数
|
||||
* 对应 curl 命令:
|
||||
* curl -X 'POST' \
|
||||
* 'https://bowongai-test--text-video-agent-fastapi-app.modal.run/api/custom/image/submit/task' \
|
||||
* -H 'accept: application/json' \
|
||||
* -H 'Content-Type: multipart/form-data' \
|
||||
* -F 'prompt=...' \
|
||||
* -F 'model_name=gemini-2.5-flash-image-preview' \
|
||||
* -F 'aspect_ratio=9:16' \
|
||||
* -F 'mode=turbo' \
|
||||
* -F 'webhook_flag=false' \
|
||||
* -F 'img_url=https://...' \
|
||||
* -F 'img_file='
|
||||
*/
|
||||
async generateImage(params: GenerateImageParams): Promise<string> {
|
||||
const {
|
||||
prompt,
|
||||
model_name = 'gemini-2.5-flash-image-preview',
|
||||
aspect_ratio = '9:16',
|
||||
mode = 'turbo',
|
||||
webhook_flag = false,
|
||||
img_file
|
||||
} = params;
|
||||
|
||||
const url = `${this.baseUrl}/api/custom/image/submit/task`;
|
||||
|
||||
try {
|
||||
// 改为选择图片
|
||||
|
||||
// 如果提供了本地文件路径,使用 uploadFile
|
||||
if (img_file) {
|
||||
const response = await new Promise<Taro.uploadFile.SuccessCallbackResult>((resolve, reject) => {
|
||||
Taro.uploadFile({
|
||||
url,
|
||||
filePath: img_file,
|
||||
name: 'img_file',
|
||||
header: {
|
||||
'accept': 'application/json'
|
||||
},
|
||||
formData: {
|
||||
prompt,
|
||||
model_name,
|
||||
aspect_ratio,
|
||||
mode: mode.toString(),
|
||||
webhook_flag: webhook_flag.toString(),
|
||||
img_url: ''
|
||||
},
|
||||
success: resolve,
|
||||
fail: reject
|
||||
});
|
||||
});
|
||||
|
||||
if (response.statusCode !== 200) {
|
||||
throw new Error(`HTTP ${response.statusCode}: 图像生成请求失败`);
|
||||
}
|
||||
|
||||
const result = JSON.parse(response.data) as ApiResponse<string>;
|
||||
|
||||
if (!result.status) {
|
||||
throw new Error(result.msg || '图像生成请求失败');
|
||||
}
|
||||
|
||||
console.log('图像生成任务提交成功:', result.data);
|
||||
return result.data;
|
||||
}
|
||||
throw new Error(`请选择图片`)
|
||||
} catch (error) {
|
||||
console.error('图像生成失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询任务状态
|
||||
* @param taskId 任务ID
|
||||
* @param retryCount 内部重试计数,外部调用不需要传入
|
||||
* @returns Promise<any> 返回任务状态信息
|
||||
*/
|
||||
async getTaskStatus(taskId: string, retryCount: number = 0): Promise<any> {
|
||||
const url = `${this.baseUrl}/api/custom/task/status?task_id=${taskId}`;
|
||||
const maxRetries = 3;
|
||||
|
||||
try {
|
||||
const response = await Taro.request({
|
||||
url,
|
||||
method: 'GET'
|
||||
});
|
||||
|
||||
if (response.statusCode !== 200) {
|
||||
throw new Error(`HTTP ${response.statusCode}: 查询任务状态失败`);
|
||||
}
|
||||
|
||||
const result = response.data as ApiResponse;
|
||||
|
||||
if (!result.status) {
|
||||
throw new Error(result.msg || '查询任务状态失败');
|
||||
}
|
||||
|
||||
if (result.status === 'running') {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000 * 5))
|
||||
return this.getTaskStatus(taskId)
|
||||
}
|
||||
|
||||
return result.data;
|
||||
|
||||
} catch (error) {
|
||||
console.error(`查询任务状态失败 (第${retryCount + 1}次尝试):`, error);
|
||||
|
||||
// 如果重试次数未达到最大值,等待5秒后重试
|
||||
if (retryCount < maxRetries) {
|
||||
console.log(`将在5秒后进行第${retryCount + 2}次重试...`);
|
||||
await new Promise((resolve) => setTimeout(resolve, 5000));
|
||||
return this.getTaskStatus(taskId, retryCount + 1);
|
||||
}
|
||||
|
||||
// 超过最大重试次数,抛出异常
|
||||
throw new Error(`查询任务状态失败,已重试${maxRetries}次: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用示例:
|
||||
*
|
||||
* // 1. 选择并上传图片
|
||||
* const uploadResult = await bowongAI.chooseAndUploadImage({
|
||||
* onProgress: (progress) => console.log(`上传进度: ${progress}%`)
|
||||
* });
|
||||
*
|
||||
* // 2. 使用上传的图片生成新图像
|
||||
* const generateResult = await bowongAI.generateImage({
|
||||
* img_url: uploadResult.file_url,
|
||||
* prompt: '转换为油画风格'
|
||||
* });
|
||||
*
|
||||
* // 3. 查询生成状态
|
||||
* const status = await bowongAI.getTaskStatus(generateResult.task_id);
|
||||
*
|
||||
* // 4. 获取最终结果
|
||||
* const finalResult = await bowongAI.getTaskResult(generateResult.task_id);
|
||||
*/
|
||||
|
||||
// 导出 SDK 实例
|
||||
export const bowongAI = new BowongAISDK();
|
||||
|
||||
// 向后兼容的类名导出
|
||||
export const Sdk = BowongAISDK;
|
||||
Loading…
Reference in New Issue