feat: switch audio and storage providers to qwen3 tts and minio

This commit is contained in:
Codex
2026-02-18 13:58:23 +00:00
parent 31e8e07319
commit 445e5725b3
6 changed files with 273 additions and 258 deletions

View File

@@ -1,39 +1,36 @@
"use strict";
const { S3Client, PutObjectCommand, GetObjectCommand } = require("@aws-sdk/client-s3");
const { getSignedUrl } = require("@aws-sdk/s3-request-presigner");
const { Client } = require("minio");
function createStorageAdapter({
bucket,
region,
endpoint,
accessKeyId,
secretAccessKey,
endPoint,
port,
useSSL = true,
accessKey,
secretKey,
region = "us-east-1",
signedUrlTtlSec = 3600,
client,
signedUrlFactory,
} = {}) {
const s3 = client || (bucket && region && accessKeyId && secretAccessKey
? new S3Client({
const minio = client || (bucket && endPoint && accessKey && secretKey
? new Client({
endPoint,
port,
useSSL,
accessKey,
secretKey,
region,
endpoint: endpoint || undefined,
forcePathStyle: Boolean(endpoint),
credentials: {
accessKeyId,
secretAccessKey,
},
})
: null);
const sign = signedUrlFactory || getSignedUrl;
return {
isConfigured() {
return Boolean(s3 && bucket);
return Boolean(minio && bucket);
},
async uploadAudio({ key, body, contentType = "audio/mpeg" }) {
if (!s3 || !bucket) {
if (!minio || !bucket) {
throw new Error("storage_not_configured");
}
@@ -41,12 +38,11 @@ function createStorageAdapter({
throw new Error("storage_upload_payload_required");
}
await s3.send(new PutObjectCommand({
Bucket: bucket,
Key: key,
Body: body,
ContentType: contentType,
}));
const metadata = {
"Content-Type": contentType,
};
await minio.putObject(bucket, key, body, body.length, metadata);
return {
bucket,
@@ -55,13 +51,12 @@ function createStorageAdapter({
},
async getSignedDownloadUrl(key, ttlSec) {
if (!s3 || !bucket) {
if (!minio || !bucket) {
throw new Error("storage_not_configured");
}
const expiresIn = Number.isInteger(ttlSec) && ttlSec > 0 ? ttlSec : signedUrlTtlSec;
const command = new GetObjectCommand({ Bucket: bucket, Key: key });
return sign(s3, command, { expiresIn });
return minio.presignedGetObject(bucket, key, expiresIn);
},
};
}

View File

@@ -1,24 +1,22 @@
"use strict";
const { OpenAI } = require("openai");
function createTTSAdapter({
apiKey,
baseURL,
model = "gpt-4o-mini-tts",
voice = "alloy",
baseURL = "https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
model = "qwen-tts-latest",
voice = "Cherry",
format = "mp3",
client,
fetchImpl = fetch,
} = {}) {
const openai = client || (apiKey ? new OpenAI({ apiKey, baseURL: baseURL || undefined }) : null);
const endpoint = `${String(baseURL || "").replace(/\/+$/, "")}/audio/speech`;
return {
isConfigured() {
return Boolean(openai);
return Boolean(apiKey && fetchImpl);
},
async synthesize(text, options) {
if (!openai) {
if (!apiKey || !fetchImpl) {
throw new Error("tts_not_configured");
}
@@ -28,14 +26,26 @@ function createTTSAdapter({
const effectiveModel = options && options.model ? options.model : model;
const effectiveVoice = options && options.voice ? options.voice : voice;
const effectiveFormat = options && options.format ? options.format : format;
const response = await openai.audio.speech.create({
model: effectiveModel,
voice: effectiveVoice,
input: String(text),
format,
const response = await fetchImpl(endpoint, {
method: "POST",
headers: {
authorization: `Bearer ${apiKey}`,
"content-type": "application/json",
},
body: JSON.stringify({
model: effectiveModel,
voice: effectiveVoice,
input: String(text),
format: effectiveFormat,
}),
});
if (!response.ok) {
throw new Error(`tts_request_failed:${response.status}`);
}
const arrayBuffer = await response.arrayBuffer();
return Buffer.from(arrayBuffer);
},