Clean up OpenAI TTS test seam

This commit is contained in:
Mohamed Boudra
2026-06-08 14:29:52 +07:00
parent 9c342efe74
commit e3bbbae3bd
2 changed files with 69 additions and 47 deletions
@@ -1,45 +1,40 @@
import type pino from "pino";
import { ReadableStream } from "node:stream/web";
import { beforeEach, describe, expect, test, vi } from "vitest";
import pinoLogger from "pino";
import { describe, expect, test } from "vitest";
const openAiMocks = vi.hoisted(() => ({
createSpeech: vi.fn(),
}));
import {
OpenAITTS,
type OpenAITTSAudioClient,
type OpenAITTSSpeechRequest,
type OpenAITTSSpeechResponse,
} from "./tts.js";
vi.mock("openai", () => ({
OpenAI: vi.fn().mockImplementation(
class {
audio = {
speech: {
create: openAiMocks.createSpeech,
class FakeOpenAITTSClient implements OpenAITTSAudioClient {
public readonly speechRequests: OpenAITTSSpeechRequest[] = [];
public readonly audio: OpenAITTSAudioClient["audio"];
private readonly speechResponse: OpenAITTSSpeechResponse;
constructor(speechResponse: OpenAITTSSpeechResponse) {
this.speechResponse = speechResponse;
this.audio = {
speech: {
create: async (request: OpenAITTSSpeechRequest) => {
this.speechRequests.push(request);
return this.speechResponse;
},
};
},
),
}));
import { OpenAITTS } from "./tts.js";
},
};
}
}
function createLogger(): pino.Logger {
const logger = {
child: vi.fn(),
debug: vi.fn(),
error: vi.fn(),
info: vi.fn(),
};
logger.child.mockReturnValue(logger);
return logger as unknown as pino.Logger;
return pinoLogger({ enabled: false });
}
describe("OpenAITTS", () => {
beforeEach(() => {
openAiMocks.createSpeech.mockReset();
});
test("returns a Node stream from the OpenAI web response body", async () => {
openAiMocks.createSpeech.mockResolvedValue({
const openaiClient = new FakeOpenAITTSClient({
body: new ReadableStream<Uint8Array>({
start(controller) {
controller.enqueue(Buffer.from("audio"));
@@ -48,7 +43,7 @@ describe("OpenAITTS", () => {
}),
});
const tts = new OpenAITTS({ apiKey: "sk-test" }, createLogger());
const tts = new OpenAITTS({ apiKey: "sk-test" }, createLogger(), { openaiClient });
const result = await tts.synthesizeSpeech("hello");
const chunks: Buffer[] = [];
@@ -58,5 +53,13 @@ describe("OpenAITTS", () => {
expect(Buffer.concat(chunks).toString()).toBe("audio");
expect(result.format).toBe("pcm");
expect(openaiClient.speechRequests).toEqual([
{
model: "tts-1",
voice: "alloy",
input: "hello",
response_format: "pcm",
},
]);
});
});
@@ -13,12 +13,35 @@ export interface TTSConfig {
responseFormat?: "mp3" | "opus" | "aac" | "flac" | "wav" | "pcm";
}
export interface OpenAITTSSpeechRequest {
model: NonNullable<TTSConfig["model"]>;
voice: NonNullable<TTSConfig["voice"]>;
input: string;
response_format: NonNullable<TTSConfig["responseFormat"]>;
}
export interface OpenAITTSSpeechResponse {
body: unknown;
}
export interface OpenAITTSAudioClient {
audio: {
speech: {
create(request: OpenAITTSSpeechRequest): Promise<OpenAITTSSpeechResponse>;
};
};
}
export interface OpenAITTSOptions {
openaiClient?: OpenAITTSAudioClient;
}
export class OpenAITTS implements TextToSpeechProvider {
private readonly openaiClient: OpenAI;
private readonly config: TTSConfig;
private readonly openaiClient: OpenAITTSAudioClient;
private readonly config: Required<TTSConfig>;
private readonly logger: pino.Logger;
constructor(ttsConfig: TTSConfig, parentLogger: pino.Logger) {
constructor(ttsConfig: TTSConfig, parentLogger: pino.Logger, options: OpenAITTSOptions = {}) {
this.config = {
model: "tts-1",
voice: "alloy",
@@ -26,9 +49,11 @@ export class OpenAITTS implements TextToSpeechProvider {
...ttsConfig,
};
this.logger = parentLogger.child({ module: "agent", provider: "openai", component: "tts" });
this.openaiClient = new OpenAI({
apiKey: ttsConfig.apiKey,
});
this.openaiClient =
options.openaiClient ??
new OpenAI({
apiKey: ttsConfig.apiKey,
});
this.logger.info(
{ voice: this.config.voice, model: this.config.model, format: this.config.responseFormat },
@@ -54,16 +79,10 @@ export class OpenAITTS implements TextToSpeechProvider {
);
const response = await this.openaiClient.audio.speech.create({
model: this.config.model!,
voice: this.config.voice!,
model: this.config.model,
voice: this.config.voice,
input: text,
response_format: this.config.responseFormat as
| "mp3"
| "opus"
| "aac"
| "flac"
| "wav"
| "pcm",
response_format: this.config.responseFormat,
});
if (!response.body) {