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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -402,7 +402,7 @@ await client.generate({

## Text to speech

`qwen-tts`, `qwen-audio-3.0-tts-plus` and `qwen-audio-3.0-tts-flash` are removed in 0.2.0, and `qwen-audio-3.1-tts-plus`, declared in 0.2.0, is withdrawn in 0.2.1. Migrate to `qwen-audio-3.1-tts-flash`; its input must have at least 15 Unicode code points.
The Qwen TTS model is `qwen-audio-3.1-tts-flash`; its input must have at least 15 Unicode code points.

Each TTS request accepts exactly one non-empty text block and returns one URL audio block. Choose the model from the requested voice mode and priority before constructing the request:

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@neta-art/generation",
"version": "0.2.6",
"version": "0.2.7",
"description": "A lightweight multimodal generation SDK with built-in model presets and adapter-based provider calls.",
"keywords": [
"ai",
Expand Down
9 changes: 1 addition & 8 deletions src/adapters/audio-speech.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,6 @@ import type {

const REQUEST_TIMEOUT_MS = 210_000;
const QWEN_MODEL = "qwen-audio-3.1-tts-flash";
// A retired public id, accepted here and never declared (not in builtins.ts, not in models/).
// Why it stays: Cohub's platform config still declares it with the QWEN_MODEL shape, and Voice Studio calls it through
// that declaration; rejecting it would break Voice Studio the moment Cohub deploys this SDK. The gateway accepts it and
// synthesises with the same model as QWEN_MODEL. The payload carries the declared id unchanged.
// Delete it once Voice Studio sends QWEN_MODEL and the platform declaration for this id is gone.
const QWEN_RETIRED_ALIAS = "qwen-audio-3.0-tts-plus";
const QWEN_MODELS: ReadonlySet<string> = new Set([QWEN_MODEL, QWEN_RETIRED_ALIAS]);
const SCENE3_MODEL = "scene3";
const SCENE3_MAX_REFERENCES = 16;
const SCENE3_MAX_INSTRUCTIONS = 300;
Expand Down Expand Up @@ -146,7 +139,7 @@ function scene3InstructionsLength(instructions: string): number {

function validateAudioSpeechRequest(input: ResolvedGenerationRequest): void {
const { text, audio } = validateCommonContent(input);
if (QWEN_MODELS.has(input.declaration.model)) {
if (input.declaration.model === QWEN_MODEL) {
validateQwen(input, text, audio);
return;
}
Expand Down
85 changes: 0 additions & 85 deletions test/adapters/audio-speech.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -477,91 +477,6 @@ describe("openai.audioSpeech Qwen instructions and seed", () => {
});
});

describe("openai.audioSpeech Qwen retired alias qwen-audio-3.0-tts-plus", () => {
const FLASH = "qwen-audio-3.1-tts-flash";
const RETIRED_ALIAS = "qwen-audio-3.0-tts-plus";

// Cohub's platform config declares the retired id with the same shape as the flash model; the SDK never declares it.
function platformDeclaration(model: string): GenerationModelDeclaration {
const declaration = getBuiltinGenerationModel(FLASH);
if (!declaration) throw new Error(`${FLASH} declaration is unavailable`);
return { ...structuredClone(declaration), model };
}

function platformClient(model: string, fetchMock: typeof fetch = vi.fn() as unknown as typeof fetch) {
return createGenerationClient({
models: [platformDeclaration(model)],
includeBuiltinModels: false,
apiKey: "key",
fetch: fetchMock,
});
}

const designWithDirections = (model: string): GenerateRequest => ({
model,
content: [{ type: "text", text: QWEN_TEXT }],
meta: { voice_prompt: "沉稳清晰的男性播音员声音" },
parameters: { instructions: " 低声耳语 ", seed: 7 },
});
const cloneWithDirections = (model: string): GenerateRequest => ({
model,
content: [{ type: "text", text: QWEN_TEXT }, audio(REFERENCE_URL, { role: "reference_audio" })],
parameters: { instructions: "请慢一点" },
});

it.each([
{ name: "design with instructions and seed", request: designWithDirections },
{ name: "clone with instructions", request: cloneWithDirections },
])("builds the same payload as the flash model, apart from model: $name", async ({ request }) => {
const sent: string[] = [];
const fetchMock = (async (_url: string | URL | Request, init?: RequestInit) => {
sent.push(String(init?.body));
return routerSuccess();
}) as typeof fetch;

await platformClient(RETIRED_ALIAS, fetchMock).generate(request(RETIRED_ALIAS));
await platformClient(FLASH, fetchMock).generate(request(FLASH));

const [aliasBody, flashBody] = sent.map((body) => JSON.parse(body) as Record<string, unknown>);
expect(aliasBody?.model).toBe(RETIRED_ALIAS);
expect(flashBody?.model).toBe(FLASH);
expect({ ...aliasBody, model: FLASH }).toEqual(flashBody);
});

it("applies the flash validation to the retired alias", () => {
const client = platformClient(RETIRED_ALIAS);

expect(() =>
client.validate({
model: RETIRED_ALIAS,
content: [{ type: "text", text: "太短" }],
meta: { voice_prompt: "声音" },
}),
).toThrow("requires input of at least 15 Unicode code points");
expect(() =>
client.validate({
model: RETIRED_ALIAS,
content: [{ type: "text", text: QWEN_TEXT }],
meta: { voice_prompt: "声音", preview_text: "旧字段" },
}),
).toThrow("Unknown request.meta field: preview_text");
});

it("does not declare the retired alias", () => {
expect(getBuiltinGenerationModel(RETIRED_ALIAS)).toBeNull();
expect(createGenerationClient({ apiKey: "key" }).getModel(RETIRED_ALIAS)).toBeNull();
});

it.each([
"qwen-tts",
"qwen-audio-3.0-tts-flash",
])("still rejects a platform declaration of %s at the audio speech adapter", (model) => {
const client = platformClient(model);

expect(() => client.validate(qwenDesignRequest({ model }))).toThrow(`Unsupported audio speech model: ${model}`);
});
});

describe("openai.audioSpeech scene3 requests", () => {
it("sends a built-in voice request with the text exactly as given and the default drama_value", async () => {
const { client, calls } = recordingClient();
Expand Down