diff --git a/bun.lock b/bun.lock index a4486e3f..e2ac872f 100644 --- a/bun.lock +++ b/bun.lock @@ -424,6 +424,18 @@ "typescript": "^5.0.0", }, }, + "modules/tool/packages/openrouterMultiModal": { + "name": "@fastgpt-plugins/tool-openrouter-multi-modal", + "dependencies": { + "zod": "^3.24.2", + }, + "devDependencies": { + "@types/bun": "latest", + }, + "peerDependencies": { + "typescript": "^5.0.0", + }, + }, "modules/tool/packages/perplexity": { "name": "@fastgpt-plugins/tool-perplexity", "dependencies": { @@ -769,6 +781,8 @@ "@fastgpt-plugins/tool-moji-weather": ["@fastgpt-plugins/tool-moji-weather@workspace:modules/tool/packages/mojiWeather"], + "@fastgpt-plugins/tool-openrouter-multi-modal": ["@fastgpt-plugins/tool-openrouter-multi-modal@workspace:modules/tool/packages/openrouterMultiModal"], + "@fastgpt-plugins/tool-perplexity": ["@fastgpt-plugins/tool-perplexity@workspace:modules/tool/packages/perplexity"], "@fastgpt-plugins/tool-search-infinity": ["@fastgpt-plugins/tool-search-infinity@workspace:modules/tool/packages/searchInfinity"], @@ -2389,6 +2403,8 @@ "@fast-csv/parse/@types/node": ["@types/node@14.18.63", "", {}, "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ=="], + "@fastgpt-plugins/tool-openrouter-multi-modal/@types/bun": ["@types/bun@1.3.0", "", { "dependencies": { "bun-types": "1.3.0" } }, "sha512-+lAGCYjXjip2qY375xX/scJeVRmZ5cY0wyHYyCYxNcdEXrQ4AOe3gACgd4iQ8ksOslJtW4VNxBJ8llUwc3a6AA=="], + "@inquirer/core/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], "@inquirer/core/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], @@ -2651,6 +2667,8 @@ "@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.1.1", "", {}, "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw=="], + "@fastgpt-plugins/tool-openrouter-multi-modal/@types/bun/bun-types": ["bun-types@1.3.0", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-u8X0thhx+yJ0KmkxuEo9HAtdfgCBaM/aI9K90VQcQioAmkVp3SG3FkwWGibUFz3WdXAdcsqOcbU40lK7tbHdkQ=="], + "@inquirer/core/wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], "@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], diff --git a/modules/tool/packages/minmax/children/tts/config.ts b/modules/tool/packages/minmax/children/tts/config.ts index 4dc4b824..586652b2 100644 --- a/modules/tool/packages/minmax/children/tts/config.ts +++ b/modules/tool/packages/minmax/children/tts/config.ts @@ -20,6 +20,7 @@ export default defineTool({ { key: 'text', label: '文本内容', + toolDescription: '文本内容', renderTypeList: [FlowNodeInputTypeEnum.input, FlowNodeInputTypeEnum.reference], valueType: WorkflowIOValueTypeEnum.string, required: true diff --git a/modules/tool/packages/minmax/children/tts/src/index.ts b/modules/tool/packages/minmax/children/tts/src/index.ts index ad98581f..2b894c95 100644 --- a/modules/tool/packages/minmax/children/tts/src/index.ts +++ b/modules/tool/packages/minmax/children/tts/src/index.ts @@ -2,16 +2,24 @@ import { z } from 'zod'; import { POST, GET } from '@tool/utils/request'; import { uploadFile } from '@tool/utils/uploadFile'; import { delay } from '@tool/utils/delay'; +import { addLog } from '@/utils/log'; export const InputType = z.object({ - apiKey: z.string(), + apiKey: z.string().nonempty(), text: z.string().nonempty(), - model: z.string().nonempty(), - voice_id: z.string(), - speed: z.number(), - vol: z.number(), - pitch: z.number(), - emotion: z.string(), + model: z.enum([ + 'speech-2.5-hd-preview', + 'speech-2.5-turbo-preview', + 'speech-02-hd', + 'speech-02-turbo', + 'speech-01-hd', + 'speech-01-turbo' + ]), + voice_id: z.enum(['male-qn-qingse', 'male-qn-jingying', 'female-shaonv', 'female-chengshu']), + speed: z.number().min(0.5).max(2), + vol: z.number().min(0.1).max(10), + pitch: z.number().min(-12).max(12), + emotion: z.enum(['auto', 'happy', 'sad', 'angry', 'fearful', 'disgusted', 'surprised', 'calm']), english_normalization: z.boolean() }); @@ -55,35 +63,37 @@ export async function tool({ } }; - try { - // create tts task - const { data: taskData } = await POST( - `${MINIMAX_BASE_URL}/t2a_async_v2`, - { - model, - text, - language_boost: 'auto', - voice_setting: { - voice_id, - speed, - vol, - pitch, - emotion, - english_normalization - }, - ...defaultSetting + // 1. Create tts task + const { data: taskData } = await POST( + `${MINIMAX_BASE_URL}/t2a_async_v2`, + { + model, + text, + language_boost: 'auto', + voice_setting: { + voice_id, + speed, + vol, + pitch, + emotion, + english_normalization }, - { - headers - } - ); + ...defaultSetting + }, + { + headers + } + ); - const task_id = taskData.task_id; - // polling task status until success or failed - // file can be downloaded when task status is success - const pollTaskStatus = async () => { - const maxRetries = 180; - for (let i = 0; i < maxRetries; i++) { + const task_id = taskData.task_id; + console.log(taskData, 222); + // 2. Polling task status until success or failed + // file can be downloaded when task status is success + const pollTaskStatus = async () => { + const maxRetries = 180; + for (let i = 0; i < maxRetries; i++) { + try { + await delay(2000); const { data: statusData } = await GET(`${MINIMAX_BASE_URL}/query/t2a_async_query_v2`, { params: { task_id }, headers @@ -95,26 +105,25 @@ export async function tool({ if (status === 'Failed') { return Promise.reject('TTS task failed'); } - await delay(1000); + } catch (error) { + addLog.error('TTS task polling failed', { error }); } - return Promise.reject('TTS task timeout'); - }; - const file_id = await pollTaskStatus(); + } + return Promise.reject('TTS task timeout'); + }; + const file_id = await pollTaskStatus(); - // retrieve file content - const { data: fileBuffer } = await GET(`${MINIMAX_BASE_URL}/files/retrieve_content`, { - params: { file_id }, - headers, - responseType: 'arrayBuffer' - }); + // 3. Retrieve file content + const { data: fileBuffer } = await GET(`${MINIMAX_BASE_URL}/files/retrieve_content`, { + params: { file_id }, + headers, + responseType: 'arrayBuffer' + }); - const { accessUrl: audioUrl } = await uploadFile({ - buffer: Buffer.from(fileBuffer), - defaultFilename: 'minimax_tts.mp3' - }); + const { accessUrl: audioUrl } = await uploadFile({ + buffer: Buffer.from(fileBuffer), + defaultFilename: 'tts.mp3' + }); - return { audioUrl }; - } catch (error) { - throw new Error(`TTS failed: ${error}`); - } + return { audioUrl }; } diff --git a/modules/tool/packages/openrouterMultiModal/children/NanoBanana/config.ts b/modules/tool/packages/openrouterMultiModal/children/NanoBanana/config.ts index b09869dd..1ff22c61 100644 --- a/modules/tool/packages/openrouterMultiModal/children/NanoBanana/config.ts +++ b/modules/tool/packages/openrouterMultiModal/children/NanoBanana/config.ts @@ -46,14 +46,27 @@ export default defineTool({ { label: '16:9', value: '16:9' }, { label: '21:9', value: '21:9' } ] + }, + { + key: 'model', + label: '模型', + renderTypeList: [FlowNodeInputTypeEnum.select], + defaultValue: 'google/gemini-2.5-flash-image-preview', + valueType: WorkflowIOValueTypeEnum.string, + required: true, + list: [ + { + label: 'google/gemini-2.5-flash-image-preview', + value: 'google/gemini-2.5-flash-image-preview' + } + ] } ], outputs: [ { valueType: WorkflowIOValueTypeEnum.string, key: 'imageUrl', - label: '图片', - description: '生成的图片' + label: '图片链接' } ] } diff --git a/modules/tool/packages/openrouterMultiModal/children/NanoBanana/src/index.ts b/modules/tool/packages/openrouterMultiModal/children/NanoBanana/src/index.ts index c1129b07..313d3ca9 100644 --- a/modules/tool/packages/openrouterMultiModal/children/NanoBanana/src/index.ts +++ b/modules/tool/packages/openrouterMultiModal/children/NanoBanana/src/index.ts @@ -5,7 +5,8 @@ import { uploadFile } from '@tool/utils/uploadFile'; export const InputType = z.object({ apiKey: z.string(), text: z.string(), - aspect_ratio: z.enum(['1:1', '2:3', '3:4', '4:3', '2:1', '3:2', '16:9', '9:16', '21:9', '9:21']) + aspect_ratio: z.enum(['1:1', '2:3', '3:4', '4:3', '2:1', '3:2', '16:9', '9:16', '21:9', '9:21']), + model: z.string().default('google/gemini-2.5-flash-image-preview') }); export const OutputType = z.object({ @@ -17,18 +18,19 @@ const OPENROUTER_BASE_URL = 'https://openrouter.ai/api/v1/chat/completions'; export async function tool({ apiKey, text, - aspect_ratio + aspect_ratio, + model }: z.infer): Promise> { const token = `Bearer ${apiKey}`; const { data } = await POST( OPENROUTER_BASE_URL, { - model: 'google/gemini-2.5-flash-image-preview', + model, messages: [ { role: 'user', content: text, - modalities: ['image', 'text'], + modalities: ['image'], image_config: { aspect_ratio: aspect_ratio } @@ -44,7 +46,7 @@ export async function tool({ ); // modal response is a base64 string - const dataUrl = data.choices[0].message.images[0].image_url.url; + const dataUrl = data.choices?.[0]?.message?.images[0]?.image_url?.url; if (!dataUrl || !dataUrl.startsWith('data:')) { return Promise.reject('Failed to generate image'); } @@ -58,9 +60,7 @@ export async function tool({ const defaultFilename = `image.${ext}`; const meta = await uploadFile({ base64: dataUrl, defaultFilename }); - if (!meta.accessUrl) { - return Promise.reject('Failed to upload image'); - } + return { imageUrl: meta.accessUrl };