feat: add local audio test mode

This commit is contained in:
2026-04-30 02:37:54 +09:00
parent 9dee708b64
commit cf6398f50a
12 changed files with 766 additions and 256 deletions

View File

@@ -1,6 +1,6 @@
import WebSocket from "ws";
import type { AppConfig } from "../config.js";
import type { AssistantRuntimeConfig } from "../config.js";
interface ElevenLabsMessage {
message_type?: string;
@@ -13,7 +13,7 @@ const NON_FATAL_ERROR_TYPES = new Set([
]);
export class ElevenLabsSttService {
constructor(private readonly config: AppConfig) {}
constructor(private readonly config: AssistantRuntimeConfig) {}
async transcribePcm16(pcm16MonoAudio: Buffer): Promise<string | null> {
if (pcm16MonoAudio.byteLength === 0) {

View File

@@ -2,24 +2,23 @@ import { Readable } from "node:stream";
import ffmpegStatic from "ffmpeg-static";
import prism from "prism-media";
import { StreamType, createAudioResource, type AudioResource } from "@discordjs/voice";
import type { AppConfig } from "../config.js";
import type { AssistantRuntimeConfig } from "../config.js";
export interface PreparedSpeechPlayback {
resource: AudioResource;
export interface PreparedSpeechAudio {
stream: Readable;
dispose: () => void;
}
export class ElevenLabsTtsService {
constructor(private readonly config: AppConfig) {
constructor(private readonly config: AssistantRuntimeConfig) {
const resolvedFfmpegPath = ffmpegStatic as unknown as string | null;
if (resolvedFfmpegPath && !process.env.FFMPEG_PATH) {
process.env.FFMPEG_PATH = resolvedFfmpegPath;
}
}
async preparePlayback(text: string, signal?: AbortSignal): Promise<PreparedSpeechPlayback> {
async preparePlayback(text: string, signal?: AbortSignal): Promise<PreparedSpeechAudio> {
const url = new URL(`https://api.elevenlabs.io/v1/text-to-speech/${this.config.ELEVENLABS_VOICE_ID}/stream`);
url.searchParams.set("output_format", "mp3_44100_128");
url.searchParams.set("enable_logging", "false");
@@ -68,12 +67,8 @@ export class ElevenLabsTtsService {
input.pipe(ffmpeg);
const resource = createAudioResource(ffmpeg, {
inputType: StreamType.Raw,
});
return {
resource,
stream: ffmpeg,
dispose: () => {
input.destroy();
ffmpeg.destroy();

View File

@@ -1,6 +1,6 @@
import OpenAI from "openai";
import type { AppConfig } from "../config.js";
import type { AssistantRuntimeConfig } from "../config.js";
import type { ConversationMemory, UserUtterance } from "./conversation.js";
const ASSISTANT_INSTRUCTIONS = [
@@ -30,7 +30,7 @@ function normalizeReply(text: string): string {
export class OpenAiLlmService {
private readonly client: OpenAI;
constructor(private readonly config: AppConfig) {
constructor(private readonly config: AssistantRuntimeConfig) {
this.client = new OpenAI({
apiKey: this.config.OPENAI_API_KEY,
});