Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions bun.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -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"],
Expand Down Expand Up @@ -2389,6 +2403,8 @@

"@fast-csv/parse/@types/node": ["@types/[email protected]", "", {}, "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ=="],

"@fastgpt-plugins/tool-openrouter-multi-modal/@types/bun": ["@types/[email protected]", "", { "dependencies": { "bun-types": "1.3.0" } }, "sha512-+lAGCYjXjip2qY375xX/scJeVRmZ5cY0wyHYyCYxNcdEXrQ4AOe3gACgd4iQ8ksOslJtW4VNxBJ8llUwc3a6AA=="],

"@inquirer/core/signal-exit": ["[email protected]", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],

"@inquirer/core/wrap-ansi": ["[email protected]", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="],
Expand Down Expand Up @@ -2651,6 +2667,8 @@

"@aws-sdk/xml-builder/fast-xml-parser/strnum": ["[email protected]", "", {}, "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw=="],

"@fastgpt-plugins/tool-openrouter-multi-modal/@types/bun/bun-types": ["[email protected]", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-u8X0thhx+yJ0KmkxuEo9HAtdfgCBaM/aI9K90VQcQioAmkVp3SG3FkwWGibUFz3WdXAdcsqOcbU40lK7tbHdkQ=="],

"@inquirer/core/wrap-ansi/strip-ansi": ["[email protected]", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],

"@isaacs/cliui/string-width/emoji-regex": ["[email protected]", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
Expand Down
1 change: 1 addition & 0 deletions modules/tool/packages/minmax/children/tts/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export default defineTool({
{
key: 'text',
label: '文本内容',
toolDescription: '文本内容',
renderTypeList: [FlowNodeInputTypeEnum.input, FlowNodeInputTypeEnum.reference],
valueType: WorkflowIOValueTypeEnum.string,
required: true
Expand Down
113 changes: 61 additions & 52 deletions modules/tool/packages/minmax/children/tts/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
});

Expand Down Expand Up @@ -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
Expand All @@ -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 };
}
Original file line number Diff line number Diff line change
Expand Up @@ -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: '图片链接'
}
]
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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<typeof InputType>): Promise<z.infer<typeof OutputType>> {
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
}
Expand All @@ -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');
}
Expand All @@ -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
};
Expand Down