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:
4
src/hooks/index.ts
Normal file
4
src/hooks/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
|
||||||
|
|
||||||
|
export { useAd } from './useAd'
|
||||||
|
export { useSdk } from './useSdk'
|
||||||
@@ -8,7 +8,7 @@ interface UseAdReturn {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useAd(): UseAdReturn {
|
export function useAd(): UseAdReturn {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(true);
|
||||||
const adRef = useRef<RewardedVideoAd | null>(null);
|
const adRef = useRef<RewardedVideoAd | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
6
src/hooks/useSdk.ts
Normal file
6
src/hooks/useSdk.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { bowongAI } from "../sdk";
|
||||||
|
|
||||||
|
|
||||||
|
export function useSdk() {
|
||||||
|
return bowongAI;
|
||||||
|
}
|
||||||
@@ -1,13 +1,12 @@
|
|||||||
import { View, Text, Button } from '@tarojs/components'
|
import { View, Text, Button } from '@tarojs/components'
|
||||||
import { useLoad } from '@tarojs/taro'
|
import { useLoad } from '@tarojs/taro'
|
||||||
import './index.css'
|
import './index.css'
|
||||||
import { useAd } from '../../hooks/useAd'
|
import { useAd, useSdk } from '../../hooks/index'
|
||||||
|
|
||||||
export default function Index() {
|
export default function Index() {
|
||||||
|
|
||||||
const { showAd, loadAd } = useAd()
|
const sdk = useSdk()
|
||||||
useLoad(() => {
|
useLoad(() => {
|
||||||
loadAd()
|
|
||||||
return () => { }
|
return () => { }
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -15,9 +14,15 @@ export default function Index() {
|
|||||||
<View className='index'>
|
<View className='index'>
|
||||||
<Text>Hello world Ymm !!!!</Text>
|
<Text>Hello world Ymm !!!!</Text>
|
||||||
<Button onClick={() => {
|
<Button onClick={() => {
|
||||||
showAd()
|
sdk.chooseAndGenerateImage().then(task_id => {
|
||||||
}}
|
console.log({ task_id })
|
||||||
>广告</Button>
|
return sdk.getTaskStatus(task_id)
|
||||||
|
}).then(res => {
|
||||||
|
console.log(res)
|
||||||
|
})
|
||||||
|
}}>
|
||||||
|
生图
|
||||||
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
362
src/sdk/index.ts
Normal file
362
src/sdk/index.ts
Normal file
@@ -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;
|
||||||
Reference in New Issue
Block a user