mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-24 02:02:42 +00:00
Clean up OpenAI TTS test seam
This commit is contained in:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user