nice conversation ok avec openai & elevenlab en text to speach
This commit is contained in:
parent
e246e96faa
commit
4baabf3727
24
.env.example
24
.env.example
@ -15,7 +15,23 @@ PORT=3000
|
|||||||
NODE_ENV=development
|
NODE_ENV=development
|
||||||
ENCRYPTION_KEY=change-me-32-char-encryption-key!
|
ENCRYPTION_KEY=change-me-32-char-encryption-key!
|
||||||
|
|
||||||
# LLM (à configurer plus tard)
|
# Auth
|
||||||
# LLM_API_KEY=
|
JWT_SECRET=dev-secret-change-in-production
|
||||||
# STT_API_KEY=
|
|
||||||
# TTS_API_KEY=
|
# STT (Deepgram)
|
||||||
|
DEEPGRAM_API_KEY=
|
||||||
|
|
||||||
|
# LLM — choisir le provider : "anthropic" ou "openai"
|
||||||
|
LLM_PROVIDER=anthropic
|
||||||
|
|
||||||
|
# Anthropic (utilisé si LLM_PROVIDER=anthropic)
|
||||||
|
ANTHROPIC_API_KEY=
|
||||||
|
ANTHROPIC_MODEL=claude-sonnet-4-20250514
|
||||||
|
|
||||||
|
# OpenAI (utilisé si LLM_PROVIDER=openai)
|
||||||
|
# OPENAI_API_KEY=
|
||||||
|
# OPENAI_MODEL=gpt-4o
|
||||||
|
|
||||||
|
# TTS (ElevenLabs)
|
||||||
|
ELEVENLABS_API_KEY=
|
||||||
|
ELEVENLABS_VOICE_ID=pFZP5JQG7iQjIQuC4Bku
|
||||||
|
|||||||
@ -22,6 +22,7 @@
|
|||||||
"migration:revert": "pnpm typeorm migration:revert -d src/config/typeorm.config.ts"
|
"migration:revert": "pnpm typeorm migration:revert -d src/config/typeorm.config.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@anthropic-ai/sdk": "^0.80.0",
|
||||||
"@deepgram/sdk": "^5.0.0",
|
"@deepgram/sdk": "^5.0.0",
|
||||||
"@nestjs/common": "^11.1.17",
|
"@nestjs/common": "^11.1.17",
|
||||||
"@nestjs/config": "^4.0.3",
|
"@nestjs/config": "^4.0.3",
|
||||||
@ -36,7 +37,9 @@
|
|||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.15.1",
|
"class-validator": "^0.15.1",
|
||||||
"dotenv": "^17.3.1",
|
"dotenv": "^17.3.1",
|
||||||
|
"elevenlabs": "^1.59.0",
|
||||||
"ioredis": "^5.10.1",
|
"ioredis": "^5.10.1",
|
||||||
|
"openai": "^6.33.0",
|
||||||
"passport": "^0.7.0",
|
"passport": "^0.7.0",
|
||||||
"passport-jwt": "^4.0.1",
|
"passport-jwt": "^4.0.1",
|
||||||
"pg": "^8.20.0",
|
"pg": "^8.20.0",
|
||||||
|
|||||||
@ -7,12 +7,13 @@ import {
|
|||||||
MessageBody,
|
MessageBody,
|
||||||
ConnectedSocket,
|
ConnectedSocket,
|
||||||
} from '@nestjs/websockets';
|
} from '@nestjs/websockets';
|
||||||
import { Inject, Logger } from '@nestjs/common';
|
import { Inject, Logger, forwardRef } from '@nestjs/common';
|
||||||
import { JwtService } from '@nestjs/jwt';
|
import { JwtService } from '@nestjs/jwt';
|
||||||
import { Server, Socket } from 'socket.io';
|
import { Server, Socket } from 'socket.io';
|
||||||
import { DeviceService } from '../../../core/services/device.service';
|
import { DeviceService } from '../../../core/services/device.service';
|
||||||
import { JwtPayload } from '../rest/auth/strategies/jwt.strategy';
|
import { JwtPayload } from '../rest/auth/strategies/jwt.strategy';
|
||||||
import { IConversationPort, CONVERSATION_PORT } from '../../../core/ports/inbound/conversation.port';
|
import { IConversationPort, CONVERSATION_PORT } from '../../../core/ports/inbound/conversation.port';
|
||||||
|
import { IDeviceGatewayPort } from '../../../core/ports/outbound/device-gateway.port';
|
||||||
|
|
||||||
interface AuthenticatedSocket extends Socket {
|
interface AuthenticatedSocket extends Socket {
|
||||||
data: {
|
data: {
|
||||||
@ -32,7 +33,7 @@ type RobotState = 'listening' | 'thinking' | 'speaking' | 'idle';
|
|||||||
namespace: '/ws/robot',
|
namespace: '/ws/robot',
|
||||||
cors: { origin: '*' },
|
cors: { origin: '*' },
|
||||||
})
|
})
|
||||||
export class RobotGateway implements OnGatewayConnection, OnGatewayDisconnect {
|
export class RobotGateway implements OnGatewayConnection, OnGatewayDisconnect, IDeviceGatewayPort {
|
||||||
@WebSocketServer()
|
@WebSocketServer()
|
||||||
server!: Server;
|
server!: Server;
|
||||||
|
|
||||||
@ -42,7 +43,7 @@ export class RobotGateway implements OnGatewayConnection, OnGatewayDisconnect {
|
|||||||
constructor(
|
constructor(
|
||||||
private readonly jwtService: JwtService,
|
private readonly jwtService: JwtService,
|
||||||
private readonly deviceService: DeviceService,
|
private readonly deviceService: DeviceService,
|
||||||
@Inject(CONVERSATION_PORT) private readonly conversationPort: IConversationPort,
|
@Inject(forwardRef(() => CONVERSATION_PORT)) private readonly conversationPort: IConversationPort,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
async handleConnection(client: AuthenticatedSocket) {
|
async handleConnection(client: AuthenticatedSocket) {
|
||||||
@ -112,13 +113,7 @@ export class RobotGateway implements OnGatewayConnection, OnGatewayDisconnect {
|
|||||||
@SubscribeMessage('speech_end')
|
@SubscribeMessage('speech_end')
|
||||||
async handleSpeechEnd(@ConnectedSocket() client: AuthenticatedSocket) {
|
async handleSpeechEnd(@ConnectedSocket() client: AuthenticatedSocket) {
|
||||||
this.logger.log(`Speech ended on device ${client.data.deviceId}`);
|
this.logger.log(`Speech ended on device ${client.data.deviceId}`);
|
||||||
client.emit('status', { state: 'thinking' as RobotState });
|
await this.conversationPort.stopListening(client.data.deviceId);
|
||||||
|
|
||||||
const transcription = await this.conversationPort.stopListening(client.data.deviceId);
|
|
||||||
this.logger.log(`Transcription: "${transcription}"`);
|
|
||||||
|
|
||||||
// TODO: plus tard, le ConversationService enverra la réponse LLM+TTS
|
|
||||||
client.emit('status', { state: 'idle' as RobotState });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@SubscribeMessage('user_interrupt')
|
@SubscribeMessage('user_interrupt')
|
||||||
@ -131,7 +126,8 @@ export class RobotGateway implements OnGatewayConnection, OnGatewayDisconnect {
|
|||||||
// --- Helpers ---
|
// --- Helpers ---
|
||||||
|
|
||||||
sendAudioChunk(deviceId: string, chunk: Buffer) {
|
sendAudioChunk(deviceId: string, chunk: Buffer) {
|
||||||
this.connectedDevices.get(deviceId)?.emit('audio_chunk', { data: chunk });
|
const base64 = chunk.toString('base64');
|
||||||
|
this.connectedDevices.get(deviceId)?.emit('audio_chunk', { data: base64 });
|
||||||
}
|
}
|
||||||
|
|
||||||
sendStatus(deviceId: string, state: RobotState) {
|
sendStatus(deviceId: string, state: RobotState) {
|
||||||
|
|||||||
114
apps/backend/src/adapters/outbound/llm/anthropic.adapter.ts
Normal file
114
apps/backend/src/adapters/outbound/llm/anthropic.adapter.ts
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import Anthropic from '@anthropic-ai/sdk';
|
||||||
|
import {
|
||||||
|
ILLMPort,
|
||||||
|
LLMMessage,
|
||||||
|
LLMResponse,
|
||||||
|
LLMToolCall,
|
||||||
|
LLMToolDefinition,
|
||||||
|
} from '../../../core/ports/outbound/llm.port';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AnthropicAdapter implements ILLMPort {
|
||||||
|
private readonly logger = new Logger(AnthropicAdapter.name);
|
||||||
|
private readonly client: Anthropic;
|
||||||
|
private readonly model: string;
|
||||||
|
|
||||||
|
constructor(private readonly configService: ConfigService) {
|
||||||
|
const apiKey = this.configService.get<string>('ANTHROPIC_API_KEY');
|
||||||
|
if (!apiKey) {
|
||||||
|
throw new Error('ANTHROPIC_API_KEY is not set');
|
||||||
|
}
|
||||||
|
this.client = new Anthropic({ apiKey });
|
||||||
|
this.model = this.configService.get<string>('ANTHROPIC_MODEL', 'claude-sonnet-4-20250514');
|
||||||
|
}
|
||||||
|
|
||||||
|
async chat(messages: LLMMessage[], tools?: LLMToolDefinition[]): Promise<LLMResponse> {
|
||||||
|
const systemMessage = messages.find((m) => m.role === 'system');
|
||||||
|
const conversationMessages = messages
|
||||||
|
.filter((m) => m.role !== 'system')
|
||||||
|
.map((m) => this.toAnthropicMessage(m));
|
||||||
|
|
||||||
|
const params: Anthropic.MessageCreateParams = {
|
||||||
|
model: this.model,
|
||||||
|
max_tokens: 1024,
|
||||||
|
messages: conversationMessages,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (systemMessage) {
|
||||||
|
params.system = systemMessage.content;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tools && tools.length > 0) {
|
||||||
|
params.tools = tools.map((t) => ({
|
||||||
|
name: t.name,
|
||||||
|
description: t.description,
|
||||||
|
input_schema: t.parameters as Anthropic.Tool.InputSchema,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await this.client.messages.create(params);
|
||||||
|
|
||||||
|
let content = '';
|
||||||
|
const toolCalls: LLMToolCall[] = [];
|
||||||
|
|
||||||
|
for (const block of response.content) {
|
||||||
|
if (block.type === 'text') {
|
||||||
|
content += block.text;
|
||||||
|
} else if (block.type === 'tool_use') {
|
||||||
|
toolCalls.push({
|
||||||
|
id: block.id,
|
||||||
|
name: block.name,
|
||||||
|
arguments: JSON.stringify(block.input),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.debug(
|
||||||
|
`LLM response: ${content.substring(0, 100)}... (${response.usage.input_tokens}+${response.usage.output_tokens} tokens)`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: content || null,
|
||||||
|
toolCalls,
|
||||||
|
tokensUsed: response.usage.input_tokens + response.usage.output_tokens,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private toAnthropicMessage(msg: LLMMessage): Anthropic.MessageParam {
|
||||||
|
if (msg.role === 'tool') {
|
||||||
|
return {
|
||||||
|
role: 'user',
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'tool_result',
|
||||||
|
tool_use_id: msg.toolCallId!,
|
||||||
|
content: msg.content,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.role === 'assistant' && msg.toolCalls && msg.toolCalls.length > 0) {
|
||||||
|
const content: Anthropic.ContentBlockParam[] = [];
|
||||||
|
if (msg.content) {
|
||||||
|
content.push({ type: 'text', text: msg.content });
|
||||||
|
}
|
||||||
|
for (const tc of msg.toolCalls) {
|
||||||
|
content.push({
|
||||||
|
type: 'tool_use',
|
||||||
|
id: tc.id,
|
||||||
|
name: tc.name,
|
||||||
|
input: JSON.parse(tc.arguments),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return { role: 'assistant', content };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
role: msg.role as 'user' | 'assistant',
|
||||||
|
content: msg.content,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
99
apps/backend/src/adapters/outbound/llm/openai.adapter.ts
Normal file
99
apps/backend/src/adapters/outbound/llm/openai.adapter.ts
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import OpenAI from 'openai';
|
||||||
|
import {
|
||||||
|
ILLMPort,
|
||||||
|
LLMMessage,
|
||||||
|
LLMResponse,
|
||||||
|
LLMToolCall,
|
||||||
|
LLMToolDefinition,
|
||||||
|
} from '../../../core/ports/outbound/llm.port';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class OpenAIAdapter implements ILLMPort {
|
||||||
|
private readonly logger = new Logger(OpenAIAdapter.name);
|
||||||
|
private readonly client: OpenAI;
|
||||||
|
private readonly model: string;
|
||||||
|
|
||||||
|
constructor(private readonly configService: ConfigService) {
|
||||||
|
const apiKey = this.configService.get<string>('OPENAI_API_KEY');
|
||||||
|
if (!apiKey) {
|
||||||
|
throw new Error('OPENAI_API_KEY is not set');
|
||||||
|
}
|
||||||
|
this.client = new OpenAI({ apiKey });
|
||||||
|
this.model = this.configService.get<string>('OPENAI_MODEL', 'gpt-4o');
|
||||||
|
}
|
||||||
|
|
||||||
|
async chat(messages: LLMMessage[], tools?: LLMToolDefinition[]): Promise<LLMResponse> {
|
||||||
|
const openaiMessages = messages.map((m) => this.toOpenAIMessage(m));
|
||||||
|
|
||||||
|
const params: OpenAI.ChatCompletionCreateParams = {
|
||||||
|
model: this.model,
|
||||||
|
max_tokens: 1024,
|
||||||
|
messages: openaiMessages,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (tools && tools.length > 0) {
|
||||||
|
params.tools = tools.map((t) => ({
|
||||||
|
type: 'function' as const,
|
||||||
|
function: {
|
||||||
|
name: t.name,
|
||||||
|
description: t.description,
|
||||||
|
parameters: t.parameters,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await this.client.chat.completions.create(params);
|
||||||
|
const choice = response.choices[0];
|
||||||
|
|
||||||
|
const content = choice.message.content ?? '';
|
||||||
|
const toolCalls: LLMToolCall[] = (choice.message.tool_calls ?? [])
|
||||||
|
.filter((tc): tc is OpenAI.ChatCompletionMessageToolCall & { type: 'function' } => tc.type === 'function')
|
||||||
|
.map((tc) => ({
|
||||||
|
id: tc.id,
|
||||||
|
name: tc.function.name,
|
||||||
|
arguments: tc.function.arguments,
|
||||||
|
}));
|
||||||
|
|
||||||
|
this.logger.debug(
|
||||||
|
`LLM response: ${content.substring(0, 100)}... (${response.usage?.total_tokens ?? 0} tokens)`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: content || null,
|
||||||
|
toolCalls,
|
||||||
|
tokensUsed: response.usage?.total_tokens ?? 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private toOpenAIMessage(msg: LLMMessage): OpenAI.ChatCompletionMessageParam {
|
||||||
|
if (msg.role === 'tool') {
|
||||||
|
return {
|
||||||
|
role: 'tool',
|
||||||
|
tool_call_id: msg.toolCallId!,
|
||||||
|
content: msg.content,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.role === 'assistant' && msg.toolCalls && msg.toolCalls.length > 0) {
|
||||||
|
return {
|
||||||
|
role: 'assistant',
|
||||||
|
content: msg.content || null,
|
||||||
|
tool_calls: msg.toolCalls.map((tc) => ({
|
||||||
|
id: tc.id,
|
||||||
|
type: 'function' as const,
|
||||||
|
function: {
|
||||||
|
name: tc.name,
|
||||||
|
arguments: tc.arguments,
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
role: msg.role as 'system' | 'user' | 'assistant',
|
||||||
|
content: msg.content,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,14 +1,13 @@
|
|||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { DeepgramClient } from '@deepgram/sdk';
|
import { DeepgramClient } from '@deepgram/sdk';
|
||||||
import { ISTTPort, TranscriptionResult } from '../../../core/ports/outbound/stt.port';
|
import { ISTTPort, ISTTStream, TranscriptionResult } from '../../../core/ports/outbound/stt.port';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class DeepgramAdapter implements ISTTPort {
|
export class DeepgramAdapter implements ISTTPort {
|
||||||
private readonly logger = new Logger(DeepgramAdapter.name);
|
private readonly logger = new Logger(DeepgramAdapter.name);
|
||||||
private readonly deepgram: DeepgramClient;
|
private readonly deepgram: DeepgramClient;
|
||||||
private readonly apiKey: string;
|
private readonly apiKey: string;
|
||||||
private socket: any = null;
|
|
||||||
|
|
||||||
constructor(private readonly configService: ConfigService) {
|
constructor(private readonly configService: ConfigService) {
|
||||||
const apiKey = this.configService.get<string>('DEEPGRAM_API_KEY');
|
const apiKey = this.configService.get<string>('DEEPGRAM_API_KEY');
|
||||||
@ -19,7 +18,7 @@ export class DeepgramAdapter implements ISTTPort {
|
|||||||
this.deepgram = new DeepgramClient({ apiKey });
|
this.deepgram = new DeepgramClient({ apiKey });
|
||||||
}
|
}
|
||||||
|
|
||||||
async startStream(onResult: (result: TranscriptionResult) => void): Promise<void> {
|
async openStream(onResult: (result: TranscriptionResult) => void): Promise<ISTTStream> {
|
||||||
const socket = await this.deepgram.listen.v1.connect({
|
const socket = await this.deepgram.listen.v1.connect({
|
||||||
model: 'nova-3',
|
model: 'nova-3',
|
||||||
language: 'fr',
|
language: 'fr',
|
||||||
@ -56,22 +55,16 @@ export class DeepgramAdapter implements ISTTPort {
|
|||||||
|
|
||||||
socket.connect();
|
socket.connect();
|
||||||
await socket.waitForOpen();
|
await socket.waitForOpen();
|
||||||
this.socket = socket;
|
|
||||||
this.logger.log('Deepgram stream opened');
|
this.logger.log('Deepgram stream opened');
|
||||||
}
|
|
||||||
|
|
||||||
sendAudio(chunk: Buffer): void {
|
return {
|
||||||
if (!this.socket) {
|
sendAudio(chunk: Buffer): void {
|
||||||
this.logger.warn('No active Deepgram stream, ignoring audio chunk');
|
socket.sendMedia(chunk);
|
||||||
return;
|
},
|
||||||
}
|
async close(): Promise<void> {
|
||||||
this.socket.sendMedia(chunk);
|
socket.close();
|
||||||
}
|
},
|
||||||
|
};
|
||||||
async endStream(): Promise<void> {
|
|
||||||
if (!this.socket) return;
|
|
||||||
this.socket.close();
|
|
||||||
this.socket = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async transcribe(audioChunk: Buffer, sampleRate: number): Promise<TranscriptionResult> {
|
async transcribe(audioChunk: Buffer, sampleRate: number): Promise<TranscriptionResult> {
|
||||||
|
|||||||
53
apps/backend/src/adapters/outbound/tts/elevenlabs.adapter.ts
Normal file
53
apps/backend/src/adapters/outbound/tts/elevenlabs.adapter.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { ElevenLabsClient } from 'elevenlabs';
|
||||||
|
import { ITTSPort } from '../../../core/ports/outbound/tts.port';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ElevenLabsAdapter implements ITTSPort {
|
||||||
|
private readonly logger = new Logger(ElevenLabsAdapter.name);
|
||||||
|
private readonly client: ElevenLabsClient;
|
||||||
|
private readonly defaultVoiceId: string;
|
||||||
|
|
||||||
|
constructor(private readonly configService: ConfigService) {
|
||||||
|
const apiKey = this.configService.get<string>('ELEVENLABS_API_KEY');
|
||||||
|
if (!apiKey) {
|
||||||
|
throw new Error('ELEVENLABS_API_KEY is not set');
|
||||||
|
}
|
||||||
|
this.client = new ElevenLabsClient({ apiKey });
|
||||||
|
this.defaultVoiceId = this.configService.get<string>('ELEVENLABS_VOICE_ID', 'pFZP5JQG7iQjIQuC4Bku');
|
||||||
|
}
|
||||||
|
|
||||||
|
async synthesize(text: string, voice?: string): Promise<Buffer> {
|
||||||
|
const stream = await this.client.textToSpeech.convert(voice || this.defaultVoiceId, {
|
||||||
|
text,
|
||||||
|
model_id: 'eleven_multilingual_v2',
|
||||||
|
output_format: 'pcm_16000',
|
||||||
|
});
|
||||||
|
|
||||||
|
const chunks: Buffer[] = [];
|
||||||
|
for await (const chunk of stream) {
|
||||||
|
chunks.push(Buffer.from(chunk));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Buffer.concat(chunks);
|
||||||
|
}
|
||||||
|
|
||||||
|
async synthesizeStream(
|
||||||
|
text: string,
|
||||||
|
voice?: string,
|
||||||
|
onChunk?: (chunk: Buffer) => void,
|
||||||
|
): Promise<void> {
|
||||||
|
const stream = await this.client.textToSpeech.convertAsStream(voice || this.defaultVoiceId, {
|
||||||
|
text,
|
||||||
|
model_id: 'eleven_multilingual_v2',
|
||||||
|
output_format: 'pcm_16000',
|
||||||
|
});
|
||||||
|
|
||||||
|
for await (const chunk of stream) {
|
||||||
|
const buffer = Buffer.from(chunk);
|
||||||
|
this.logger.debug(`TTS chunk: ${buffer.length} bytes`);
|
||||||
|
onChunk?.(buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
@ -22,8 +21,14 @@ import { AuthController } from './adapters/inbound/rest/auth/auth.controller';
|
|||||||
import { DeviceController } from './adapters/inbound/rest/device/device.controller';
|
import { DeviceController } from './adapters/inbound/rest/device/device.controller';
|
||||||
import { RobotGateway } from './adapters/inbound/websocket/robot.gateway';
|
import { RobotGateway } from './adapters/inbound/websocket/robot.gateway';
|
||||||
import { DeepgramAdapter } from './adapters/outbound/stt/deepgram.adapter';
|
import { DeepgramAdapter } from './adapters/outbound/stt/deepgram.adapter';
|
||||||
|
import { AnthropicAdapter } from './adapters/outbound/llm/anthropic.adapter';
|
||||||
|
import { OpenAIAdapter } from './adapters/outbound/llm/openai.adapter';
|
||||||
|
import { ElevenLabsAdapter } from './adapters/outbound/tts/elevenlabs.adapter';
|
||||||
import { CONVERSATION_PORT } from './core/ports/inbound/conversation.port';
|
import { CONVERSATION_PORT } from './core/ports/inbound/conversation.port';
|
||||||
import { STT_PORT } from './core/ports/outbound/stt.port';
|
import { STT_PORT } from './core/ports/outbound/stt.port';
|
||||||
|
import { LLM_PORT } from './core/ports/outbound/llm.port';
|
||||||
|
import { TTS_PORT } from './core/ports/outbound/tts.port';
|
||||||
|
import { DEVICE_GATEWAY_PORT } from './core/ports/outbound/device-gateway.port';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@ -63,6 +68,25 @@ import { STT_PORT } from './core/ports/outbound/stt.port';
|
|||||||
provide: STT_PORT,
|
provide: STT_PORT,
|
||||||
useClass: DeepgramAdapter,
|
useClass: DeepgramAdapter,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: LLM_PORT,
|
||||||
|
inject: [ConfigService],
|
||||||
|
useFactory: (configService: ConfigService) => {
|
||||||
|
const provider = configService.get<string>('LLM_PROVIDER', 'anthropic');
|
||||||
|
if (provider === 'openai') {
|
||||||
|
return new OpenAIAdapter(configService);
|
||||||
|
}
|
||||||
|
return new AnthropicAdapter(configService);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: TTS_PORT,
|
||||||
|
useClass: ElevenLabsAdapter,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: DEVICE_GATEWAY_PORT,
|
||||||
|
useExisting: RobotGateway,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AppModule { }
|
export class AppModule {}
|
||||||
|
|||||||
@ -0,0 +1,8 @@
|
|||||||
|
export interface IDeviceGatewayPort {
|
||||||
|
sendAudioChunk(deviceId: string, chunk: Buffer): void;
|
||||||
|
sendStatus(deviceId: string, state: 'listening' | 'thinking' | 'speaking' | 'idle'): void;
|
||||||
|
sendNotification(deviceId: string, payload: Record<string, unknown>): void;
|
||||||
|
isDeviceConnected(deviceId: string): boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DEVICE_GATEWAY_PORT = Symbol('DEVICE_GATEWAY_PORT');
|
||||||
@ -4,11 +4,14 @@ export interface TranscriptionResult {
|
|||||||
isFinal: boolean;
|
isFinal: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ISTTPort {
|
export interface ISTTStream {
|
||||||
transcribe(audioChunk: Buffer, sampleRate: number): Promise<TranscriptionResult>;
|
|
||||||
startStream(onResult: (result: TranscriptionResult) => void): Promise<void>;
|
|
||||||
endStream(): Promise<void>;
|
|
||||||
sendAudio(chunk: Buffer): void;
|
sendAudio(chunk: Buffer): void;
|
||||||
|
close(): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISTTPort {
|
||||||
|
openStream(onResult: (result: TranscriptionResult) => void): Promise<ISTTStream>;
|
||||||
|
transcribe(audioChunk: Buffer, sampleRate: number): Promise<TranscriptionResult>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const STT_PORT = Symbol('STT_PORT');
|
export const STT_PORT = Symbol('STT_PORT');
|
||||||
|
|||||||
@ -1,76 +1,139 @@
|
|||||||
import { Inject, Injectable, Logger } from '@nestjs/common';
|
import { Inject, Injectable, Logger, forwardRef } from '@nestjs/common';
|
||||||
import { IConversationPort } from '../ports/inbound/conversation.port';
|
import { IConversationPort } from '../ports/inbound/conversation.port';
|
||||||
import { ISTTPort, STT_PORT, TranscriptionResult } from '../ports/outbound/stt.port';
|
import { ISTTPort, ISTTStream, STT_PORT, TranscriptionResult } from '../ports/outbound/stt.port';
|
||||||
|
import { ILLMPort, LLM_PORT, LLMMessage } from '../ports/outbound/llm.port';
|
||||||
|
import { ITTSPort, TTS_PORT } from '../ports/outbound/tts.port';
|
||||||
|
import { IDeviceGatewayPort, DEVICE_GATEWAY_PORT } from '../ports/outbound/device-gateway.port';
|
||||||
|
|
||||||
interface ActiveSession {
|
interface ActiveSession {
|
||||||
deviceId: string;
|
deviceId: string;
|
||||||
transcription: string;
|
transcription: string;
|
||||||
|
messages: LLMMessage[];
|
||||||
|
sttStream: ISTTStream | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const SYSTEM_PROMPT = `Tu es Ti-Pote, un petit robot de bureau animatronique, chaleureux et serviable.
|
||||||
|
Tu parles en français. Tu es concis dans tes réponses car elles seront lues à voix haute.
|
||||||
|
Tu as une personnalité enjouée mais tu restes utile et pertinent.
|
||||||
|
Réponds en 1 à 3 phrases maximum.`;
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ConversationService implements IConversationPort {
|
export class ConversationService implements IConversationPort {
|
||||||
private readonly logger = new Logger(ConversationService.name);
|
private readonly logger = new Logger(ConversationService.name);
|
||||||
private readonly activeSessions = new Map<string, ActiveSession>();
|
private readonly activeSessions = new Map<string, ActiveSession>();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(STT_PORT) private readonly sttPort: ISTTPort,
|
@Inject(STT_PORT) private readonly sttPort: ISTTPort,
|
||||||
) { }
|
@Inject(LLM_PORT) private readonly llmPort: ILLMPort,
|
||||||
|
@Inject(TTS_PORT) private readonly ttsPort: ITTSPort,
|
||||||
|
@Inject(forwardRef(() => DEVICE_GATEWAY_PORT)) private readonly deviceGateway: IDeviceGatewayPort,
|
||||||
|
) {}
|
||||||
|
|
||||||
async startListening(deviceId: string): Promise<void> {
|
async startListening(deviceId: string): Promise<void> {
|
||||||
this.logger.log(`Start listening for device ${deviceId}`);
|
this.logger.log(`Start listening for device ${deviceId}`);
|
||||||
|
|
||||||
const session: ActiveSession = {
|
// Close previous STT stream if any
|
||||||
deviceId,
|
const existing = this.activeSessions.get(deviceId);
|
||||||
transcription: '',
|
if (existing?.sttStream) {
|
||||||
};
|
await existing.sttStream.close();
|
||||||
|
|
||||||
this.activeSessions.set(deviceId, session);
|
|
||||||
|
|
||||||
await this.sttPort.startStream((result: TranscriptionResult) => {
|
|
||||||
this.logger.debug(`STT [${deviceId}]: "${result.text}" (final: ${result.isFinal}, confidence: ${result.confidence})`);
|
|
||||||
|
|
||||||
if (result.isFinal) {
|
|
||||||
session.transcription += result.text + ' ';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
processAudioChunk(deviceId: string, chunk: Buffer, sampleRate: number): void {
|
const session: ActiveSession = {
|
||||||
const session = this.activeSessions.get(deviceId);
|
deviceId,
|
||||||
if (!session) {
|
transcription: '',
|
||||||
this.logger.warn(`No active session for device ${deviceId}, ignoring audio chunk`);
|
messages: existing?.messages ?? [],
|
||||||
return;
|
sttStream: null,
|
||||||
}
|
};
|
||||||
|
|
||||||
this.sttPort.sendAudio(chunk);
|
this.activeSessions.set(deviceId, session);
|
||||||
|
|
||||||
|
const sttStream = await this.sttPort.openStream((result: TranscriptionResult) => {
|
||||||
|
this.logger.debug(
|
||||||
|
`STT [${deviceId}]: "${result.text}" (final: ${result.isFinal}, confidence: ${result.confidence})`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.isFinal) {
|
||||||
|
session.transcription += result.text + ' ';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
session.sttStream = sttStream;
|
||||||
|
}
|
||||||
|
|
||||||
|
processAudioChunk(deviceId: string, chunk: Buffer, sampleRate: number): void {
|
||||||
|
const session = this.activeSessions.get(deviceId);
|
||||||
|
if (!session?.sttStream) {
|
||||||
|
this.logger.warn(`No active STT stream for device ${deviceId}, ignoring audio chunk`);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
async stopListening(deviceId: string): Promise<string | null> {
|
session.sttStream.sendAudio(chunk);
|
||||||
const session = this.activeSessions.get(deviceId);
|
}
|
||||||
if (!session) {
|
|
||||||
this.logger.warn(`No active session for device ${deviceId}`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.sttPort.endStream();
|
async stopListening(deviceId: string): Promise<string | null> {
|
||||||
|
const session = this.activeSessions.get(deviceId);
|
||||||
const finalText = session.transcription.trim() || null;
|
if (!session) {
|
||||||
this.activeSessions.delete(deviceId);
|
this.logger.warn(`No active session for device ${deviceId}`);
|
||||||
|
return null;
|
||||||
this.logger.log(`Final transcription for ${deviceId}: "${finalText}"`);
|
|
||||||
|
|
||||||
// TODO: plus tard, envoyer finalText au LLM via ILLMPort
|
|
||||||
|
|
||||||
return finalText;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interrupt(deviceId: string): void {
|
if (session.sttStream) {
|
||||||
const session = this.activeSessions.get(deviceId);
|
await session.sttStream.close();
|
||||||
if (!session) return;
|
session.sttStream = null;
|
||||||
|
|
||||||
this.logger.log(`Interrupting session for device ${deviceId}`);
|
|
||||||
this.activeSessions.delete(deviceId);
|
|
||||||
|
|
||||||
// TODO: plus tard, couper aussi le TTS en cours
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const finalText = session.transcription.trim() || null;
|
||||||
|
this.logger.log(`Final transcription for ${deviceId}: "${finalText}"`);
|
||||||
|
|
||||||
|
if (!finalText) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
session.messages.push({ role: 'user', content: finalText });
|
||||||
|
session.transcription = '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.deviceGateway.sendStatus(deviceId, 'thinking');
|
||||||
|
|
||||||
|
const llmMessages: LLMMessage[] = [
|
||||||
|
{ role: 'system', content: SYSTEM_PROMPT },
|
||||||
|
...session.messages,
|
||||||
|
];
|
||||||
|
|
||||||
|
const llmResponse = await this.llmPort.chat(llmMessages);
|
||||||
|
const responseText = llmResponse.content ?? '';
|
||||||
|
this.logger.log(`LLM response for ${deviceId}: "${responseText}"`);
|
||||||
|
|
||||||
|
session.messages.push({ role: 'assistant', content: responseText });
|
||||||
|
|
||||||
|
if (responseText) {
|
||||||
|
this.deviceGateway.sendStatus(deviceId, 'speaking');
|
||||||
|
|
||||||
|
const audioBuffer = await this.ttsPort.synthesize(responseText);
|
||||||
|
this.logger.debug(`TTS complete: ${audioBuffer.length} bytes`);
|
||||||
|
this.deviceGateway.sendAudioChunk(deviceId, audioBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.deviceGateway.sendStatus(deviceId, 'idle');
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Error processing conversation for ${deviceId}:`, error);
|
||||||
|
this.deviceGateway.sendStatus(deviceId, 'idle');
|
||||||
|
}
|
||||||
|
|
||||||
|
return finalText;
|
||||||
|
}
|
||||||
|
|
||||||
|
interrupt(deviceId: string): void {
|
||||||
|
const session = this.activeSessions.get(deviceId);
|
||||||
|
if (!session) return;
|
||||||
|
|
||||||
|
this.logger.log(`Interrupting session for device ${deviceId}`);
|
||||||
|
|
||||||
|
if (session.sttStream) {
|
||||||
|
session.sttStream.close();
|
||||||
|
session.sttStream = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.activeSessions.delete(deviceId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,8 +21,10 @@ const STATE_LABELS: Record<RobotState, string> = {
|
|||||||
function App() {
|
function App() {
|
||||||
const [serverUrl, setServerUrl] = useState('http://localhost:3000');
|
const [serverUrl, setServerUrl] = useState('http://localhost:3000');
|
||||||
const [deviceToken, setDeviceToken] = useState('');
|
const [deviceToken, setDeviceToken] = useState('');
|
||||||
|
const [conversationActive, setConversationActive] = useState(false);
|
||||||
const { state, connected, logs, connect, disconnect, emit, clearLogs } = useSocket();
|
const { state, connected, logs, connect, disconnect, emit, clearLogs } = useSocket();
|
||||||
const logsEndRef = useRef<HTMLDivElement>(null);
|
const logsEndRef = useRef<HTMLDivElement>(null);
|
||||||
|
const prevStateRef = useRef<RobotState>('disconnected');
|
||||||
|
|
||||||
const onAudioChunk = useCallback(
|
const onAudioChunk = useCallback(
|
||||||
(chunk: ArrayBuffer, sampleRate: number) => {
|
(chunk: ArrayBuffer, sampleRate: number) => {
|
||||||
@ -37,6 +39,17 @@ function App() {
|
|||||||
|
|
||||||
const { recording, start: startMic, stop: stopMic } = useMicrophone({ onAudioChunk, onSpeechEnd });
|
const { recording, start: startMic, stop: stopMic } = useMicrophone({ onAudioChunk, onSpeechEnd });
|
||||||
|
|
||||||
|
// Auto-restart listening when Ti-Pote finishes speaking
|
||||||
|
useEffect(() => {
|
||||||
|
const prevState = prevStateRef.current;
|
||||||
|
prevStateRef.current = state;
|
||||||
|
|
||||||
|
if (conversationActive && state === 'idle' && (prevState === 'speaking' || prevState === 'thinking')) {
|
||||||
|
emit('wake_word_detected');
|
||||||
|
startMic();
|
||||||
|
}
|
||||||
|
}, [state, conversationActive, emit, startMic]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
logsEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
logsEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||||
}, [logs]);
|
}, [logs]);
|
||||||
@ -44,6 +57,7 @@ function App() {
|
|||||||
const handleConnect = () => {
|
const handleConnect = () => {
|
||||||
if (connected) {
|
if (connected) {
|
||||||
disconnect();
|
disconnect();
|
||||||
|
setConversationActive(false);
|
||||||
} else {
|
} else {
|
||||||
if (!deviceToken.trim()) return alert('Device token requis');
|
if (!deviceToken.trim()) return alert('Device token requis');
|
||||||
connect(serverUrl, deviceToken.trim());
|
connect(serverUrl, deviceToken.trim());
|
||||||
@ -51,11 +65,13 @@ function App() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleWakeWord = () => {
|
const handleWakeWord = () => {
|
||||||
|
setConversationActive(true);
|
||||||
emit('wake_word_detected');
|
emit('wake_word_detected');
|
||||||
if (!recording) startMic();
|
if (!recording) startMic();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleInterrupt = () => {
|
const handleInterrupt = () => {
|
||||||
|
setConversationActive(false);
|
||||||
emit('user_interrupt');
|
emit('user_interrupt');
|
||||||
if (recording) stopMic();
|
if (recording) stopMic();
|
||||||
};
|
};
|
||||||
|
|||||||
60
apps/simulator/src/hooks/useAudioPlayer.ts
Normal file
60
apps/simulator/src/hooks/useAudioPlayer.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import { useRef, useCallback } from 'react';
|
||||||
|
|
||||||
|
const SAMPLE_RATE = 16000;
|
||||||
|
|
||||||
|
export function useAudioPlayer() {
|
||||||
|
const contextRef = useRef<AudioContext | null>(null);
|
||||||
|
|
||||||
|
const getContext = useCallback(() => {
|
||||||
|
if (!contextRef.current || contextRef.current.state === 'closed') {
|
||||||
|
contextRef.current = new AudioContext({ sampleRate: SAMPLE_RATE });
|
||||||
|
}
|
||||||
|
if (contextRef.current.state === 'suspended') {
|
||||||
|
contextRef.current.resume();
|
||||||
|
}
|
||||||
|
return contextRef.current;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const playChunk = useCallback(
|
||||||
|
(base64: string) => {
|
||||||
|
const ctx = getContext();
|
||||||
|
|
||||||
|
// Decode base64 → bytes
|
||||||
|
const binaryStr = atob(base64);
|
||||||
|
const bytes = new Uint8Array(binaryStr.length);
|
||||||
|
for (let i = 0; i < binaryStr.length; i++) {
|
||||||
|
bytes[i] = binaryStr.charCodeAt(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure even byte count for Int16
|
||||||
|
const evenLength = bytes.length - (bytes.length % 2);
|
||||||
|
const int16 = new Int16Array(bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + evenLength));
|
||||||
|
|
||||||
|
// Int16 PCM → Float32
|
||||||
|
const float32 = new Float32Array(int16.length);
|
||||||
|
for (let i = 0; i < int16.length; i++) {
|
||||||
|
float32[i] = int16[i] / 0x7fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
const buffer = ctx.createBuffer(1, float32.length, SAMPLE_RATE);
|
||||||
|
buffer.copyToChannel(float32, 0);
|
||||||
|
|
||||||
|
const source = ctx.createBufferSource();
|
||||||
|
source.buffer = buffer;
|
||||||
|
source.connect(ctx.destination);
|
||||||
|
source.start();
|
||||||
|
},
|
||||||
|
[getContext],
|
||||||
|
);
|
||||||
|
|
||||||
|
const flush = useCallback(() => {}, []);
|
||||||
|
|
||||||
|
const stop = useCallback(() => {
|
||||||
|
if (contextRef.current) {
|
||||||
|
contextRef.current.close();
|
||||||
|
contextRef.current = null;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { playChunk, flush, stop };
|
||||||
|
}
|
||||||
@ -3,16 +3,51 @@ import { useRef, useState, useCallback } from 'react';
|
|||||||
interface UseMicrophoneOptions {
|
interface UseMicrophoneOptions {
|
||||||
onAudioChunk: (chunk: ArrayBuffer, sampleRate: number) => void;
|
onAudioChunk: (chunk: ArrayBuffer, sampleRate: number) => void;
|
||||||
onSpeechEnd: () => void;
|
onSpeechEnd: () => void;
|
||||||
|
/** Silence duration in ms before triggering speech end (default: 1500) */
|
||||||
|
silenceTimeout?: number;
|
||||||
|
/** RMS threshold below which audio is considered silence (default: 0.01) */
|
||||||
|
silenceThreshold?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useMicrophone({ onAudioChunk, onSpeechEnd }: UseMicrophoneOptions) {
|
export function useMicrophone({
|
||||||
|
onAudioChunk,
|
||||||
|
onSpeechEnd,
|
||||||
|
silenceTimeout = 1500,
|
||||||
|
silenceThreshold = 0.01,
|
||||||
|
}: UseMicrophoneOptions) {
|
||||||
const [recording, setRecording] = useState(false);
|
const [recording, setRecording] = useState(false);
|
||||||
const streamRef = useRef<MediaStream | null>(null);
|
const streamRef = useRef<MediaStream | null>(null);
|
||||||
const contextRef = useRef<AudioContext | null>(null);
|
const contextRef = useRef<AudioContext | null>(null);
|
||||||
const processorRef = useRef<ScriptProcessorNode | null>(null);
|
const processorRef = useRef<ScriptProcessorNode | null>(null);
|
||||||
|
const silenceTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
const hasSpeechRef = useRef(false);
|
||||||
|
const stoppedRef = useRef(false);
|
||||||
|
|
||||||
|
const clearSilenceTimer = useCallback(() => {
|
||||||
|
if (silenceTimerRef.current) {
|
||||||
|
clearTimeout(silenceTimerRef.current);
|
||||||
|
silenceTimerRef.current = null;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const cleanup = useCallback(() => {
|
||||||
|
clearSilenceTimer();
|
||||||
|
processorRef.current?.disconnect();
|
||||||
|
contextRef.current?.close();
|
||||||
|
streamRef.current?.getTracks().forEach((t) => t.stop());
|
||||||
|
processorRef.current = null;
|
||||||
|
contextRef.current = null;
|
||||||
|
streamRef.current = null;
|
||||||
|
hasSpeechRef.current = false;
|
||||||
|
stoppedRef.current = false;
|
||||||
|
setRecording(false);
|
||||||
|
}, [clearSilenceTimer]);
|
||||||
|
|
||||||
const start = useCallback(async () => {
|
const start = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
|
stoppedRef.current = false;
|
||||||
|
hasSpeechRef.current = false;
|
||||||
|
|
||||||
const stream = await navigator.mediaDevices.getUserMedia({
|
const stream = await navigator.mediaDevices.getUserMedia({
|
||||||
audio: { sampleRate: 16000, channelCount: 1, echoCancellation: true },
|
audio: { sampleRate: 16000, channelCount: 1, echoCancellation: true },
|
||||||
});
|
});
|
||||||
@ -22,19 +57,42 @@ export function useMicrophone({ onAudioChunk, onSpeechEnd }: UseMicrophoneOption
|
|||||||
contextRef.current = context;
|
contextRef.current = context;
|
||||||
|
|
||||||
const source = context.createMediaStreamSource(stream);
|
const source = context.createMediaStreamSource(stream);
|
||||||
// 4096 samples per chunk at 16kHz = ~256ms per chunk
|
|
||||||
const processor = context.createScriptProcessor(4096, 1, 1);
|
const processor = context.createScriptProcessor(4096, 1, 1);
|
||||||
processorRef.current = processor;
|
processorRef.current = processor;
|
||||||
|
|
||||||
processor.onaudioprocess = (e) => {
|
processor.onaudioprocess = (e) => {
|
||||||
|
if (stoppedRef.current) return;
|
||||||
|
|
||||||
const float32 = e.inputBuffer.getChannelData(0);
|
const float32 = e.inputBuffer.getChannelData(0);
|
||||||
// Convert Float32 to Int16 PCM
|
|
||||||
|
// Calculate RMS volume
|
||||||
|
let sum = 0;
|
||||||
|
for (let i = 0; i < float32.length; i++) {
|
||||||
|
sum += float32[i] * float32[i];
|
||||||
|
}
|
||||||
|
const rms = Math.sqrt(sum / float32.length);
|
||||||
|
|
||||||
|
// Convert and send audio
|
||||||
const int16 = new Int16Array(float32.length);
|
const int16 = new Int16Array(float32.length);
|
||||||
for (let i = 0; i < float32.length; i++) {
|
for (let i = 0; i < float32.length; i++) {
|
||||||
const s = Math.max(-1, Math.min(1, float32[i]));
|
const s = Math.max(-1, Math.min(1, float32[i]));
|
||||||
int16[i] = s < 0 ? s * 0x8000 : s * 0x7fff;
|
int16[i] = s < 0 ? s * 0x8000 : s * 0x7fff;
|
||||||
}
|
}
|
||||||
onAudioChunk(int16.buffer, context.sampleRate);
|
onAudioChunk(int16.buffer, context.sampleRate);
|
||||||
|
|
||||||
|
// VAD logic
|
||||||
|
if (rms > silenceThreshold) {
|
||||||
|
hasSpeechRef.current = true;
|
||||||
|
clearSilenceTimer();
|
||||||
|
} else if (hasSpeechRef.current && !silenceTimerRef.current) {
|
||||||
|
// Speech detected before, now silence — start countdown
|
||||||
|
silenceTimerRef.current = setTimeout(() => {
|
||||||
|
if (stoppedRef.current) return;
|
||||||
|
stoppedRef.current = true;
|
||||||
|
cleanup();
|
||||||
|
onSpeechEnd();
|
||||||
|
}, silenceTimeout);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
source.connect(processor);
|
source.connect(processor);
|
||||||
@ -43,18 +101,14 @@ export function useMicrophone({ onAudioChunk, onSpeechEnd }: UseMicrophoneOption
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Microphone access denied:', err);
|
console.error('Microphone access denied:', err);
|
||||||
}
|
}
|
||||||
}, [onAudioChunk]);
|
}, [onAudioChunk, onSpeechEnd, silenceTimeout, silenceThreshold, clearSilenceTimer, cleanup]);
|
||||||
|
|
||||||
const stop = useCallback(() => {
|
const stop = useCallback(() => {
|
||||||
processorRef.current?.disconnect();
|
if (stoppedRef.current) return;
|
||||||
contextRef.current?.close();
|
stoppedRef.current = true;
|
||||||
streamRef.current?.getTracks().forEach((t) => t.stop());
|
cleanup();
|
||||||
processorRef.current = null;
|
|
||||||
contextRef.current = null;
|
|
||||||
streamRef.current = null;
|
|
||||||
setRecording(false);
|
|
||||||
onSpeechEnd();
|
onSpeechEnd();
|
||||||
}, [onSpeechEnd]);
|
}, [onSpeechEnd, cleanup]);
|
||||||
|
|
||||||
return { recording, start, stop };
|
return { recording, start, stop };
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { useRef, useState, useCallback } from 'react';
|
import { useRef, useState, useCallback } from 'react';
|
||||||
import { io, Socket } from 'socket.io-client';
|
import { io, Socket } from 'socket.io-client';
|
||||||
|
import { useAudioPlayer } from './useAudioPlayer';
|
||||||
|
|
||||||
export type RobotState = 'disconnected' | 'idle' | 'listening' | 'thinking' | 'speaking';
|
export type RobotState = 'disconnected' | 'idle' | 'listening' | 'thinking' | 'speaking';
|
||||||
|
|
||||||
@ -15,6 +16,7 @@ export function useSocket() {
|
|||||||
const [state, setState] = useState<RobotState>('disconnected');
|
const [state, setState] = useState<RobotState>('disconnected');
|
||||||
const [connected, setConnected] = useState(false);
|
const [connected, setConnected] = useState(false);
|
||||||
const [logs, setLogs] = useState<LogEntry[]>([]);
|
const [logs, setLogs] = useState<LogEntry[]>([]);
|
||||||
|
const audioPlayer = useAudioPlayer();
|
||||||
|
|
||||||
const addLog = useCallback((direction: LogEntry['direction'], event: string, data?: string) => {
|
const addLog = useCallback((direction: LogEntry['direction'], event: string, data?: string) => {
|
||||||
setLogs((prev) => [...prev.slice(-200), { timestamp: new Date(), direction, event, data }]);
|
setLogs((prev) => [...prev.slice(-200), { timestamp: new Date(), direction, event, data }]);
|
||||||
@ -50,11 +52,16 @@ export function useSocket() {
|
|||||||
socket.on('status', (payload: { state: RobotState }) => {
|
socket.on('status', (payload: { state: RobotState }) => {
|
||||||
setState(payload.state);
|
setState(payload.state);
|
||||||
addLog('in', 'status', payload.state);
|
addLog('in', 'status', payload.state);
|
||||||
|
if (payload.state === 'idle') {
|
||||||
|
audioPlayer.flush();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('audio_chunk', (payload: { data: ArrayBuffer }) => {
|
socket.on('audio_chunk', (payload: { data: string }) => {
|
||||||
addLog('in', 'audio_chunk', `${payload.data?.byteLength ?? 0} bytes`);
|
addLog('in', 'audio_chunk', `${payload.data?.length ?? 0} chars (base64)`);
|
||||||
// TODO: play audio through speakers
|
if (payload.data) {
|
||||||
|
audioPlayer.playChunk(payload.data);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('notification', (payload: Record<string, unknown>) => {
|
socket.on('notification', (payload: Record<string, unknown>) => {
|
||||||
|
|||||||
203
pnpm-lock.yaml
generated
203
pnpm-lock.yaml
generated
@ -10,6 +10,9 @@ importers:
|
|||||||
|
|
||||||
apps/backend:
|
apps/backend:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@anthropic-ai/sdk':
|
||||||
|
specifier: ^0.80.0
|
||||||
|
version: 0.80.0
|
||||||
'@deepgram/sdk':
|
'@deepgram/sdk':
|
||||||
specifier: ^5.0.0
|
specifier: ^5.0.0
|
||||||
version: 5.0.0
|
version: 5.0.0
|
||||||
@ -52,9 +55,15 @@ importers:
|
|||||||
dotenv:
|
dotenv:
|
||||||
specifier: ^17.3.1
|
specifier: ^17.3.1
|
||||||
version: 17.3.1
|
version: 17.3.1
|
||||||
|
elevenlabs:
|
||||||
|
specifier: ^1.59.0
|
||||||
|
version: 1.59.0
|
||||||
ioredis:
|
ioredis:
|
||||||
specifier: ^5.10.1
|
specifier: ^5.10.1
|
||||||
version: 5.10.1
|
version: 5.10.1
|
||||||
|
openai:
|
||||||
|
specifier: ^6.33.0
|
||||||
|
version: 6.33.0(ws@8.18.3)
|
||||||
passport:
|
passport:
|
||||||
specifier: ^0.7.0
|
specifier: ^0.7.0
|
||||||
version: 0.7.0
|
version: 0.7.0
|
||||||
@ -196,6 +205,15 @@ packages:
|
|||||||
resolution: {integrity: sha512-J4Jarr0SohdrHcb40gTL4wGPCQ952IMWF1G/MSAQfBAPvA9ZKApYhpxcY7PmehVePve+ujpus1dGsJ7dPxz8Kg==}
|
resolution: {integrity: sha512-J4Jarr0SohdrHcb40gTL4wGPCQ952IMWF1G/MSAQfBAPvA9ZKApYhpxcY7PmehVePve+ujpus1dGsJ7dPxz8Kg==}
|
||||||
engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'}
|
engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'}
|
||||||
|
|
||||||
|
'@anthropic-ai/sdk@0.80.0':
|
||||||
|
resolution: {integrity: sha512-WeXLn7zNVk3yjeshn+xZHvld6AoFUOR3Sep6pSoHho5YbSi6HwcirqgPA5ccFuW8QTVJAAU7N8uQQC6Wa9TG+g==}
|
||||||
|
hasBin: true
|
||||||
|
peerDependencies:
|
||||||
|
zod: ^3.25.0 || ^4.0.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
zod:
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@babel/code-frame@7.29.0':
|
'@babel/code-frame@7.29.0':
|
||||||
resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==}
|
resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
@ -346,6 +364,10 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@babel/core': ^7.0.0-0
|
'@babel/core': ^7.0.0-0
|
||||||
|
|
||||||
|
'@babel/runtime@7.29.2':
|
||||||
|
resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==}
|
||||||
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
'@babel/template@7.28.6':
|
'@babel/template@7.28.6':
|
||||||
resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==}
|
resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
@ -1468,6 +1490,10 @@ packages:
|
|||||||
'@xtuc/long@4.2.2':
|
'@xtuc/long@4.2.2':
|
||||||
resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==}
|
resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==}
|
||||||
|
|
||||||
|
abort-controller@3.0.0:
|
||||||
|
resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==}
|
||||||
|
engines: {node: '>=6.5'}
|
||||||
|
|
||||||
accepts@1.3.8:
|
accepts@1.3.8:
|
||||||
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
|
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
@ -1586,6 +1612,9 @@ packages:
|
|||||||
array-timsort@1.0.3:
|
array-timsort@1.0.3:
|
||||||
resolution: {integrity: sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==}
|
resolution: {integrity: sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==}
|
||||||
|
|
||||||
|
asynckit@0.4.0:
|
||||||
|
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
|
||||||
|
|
||||||
available-typed-arrays@1.0.7:
|
available-typed-arrays@1.0.7:
|
||||||
resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
|
resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@ -1788,6 +1817,13 @@ packages:
|
|||||||
color-name@1.1.4:
|
color-name@1.1.4:
|
||||||
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
||||||
|
|
||||||
|
combined-stream@1.0.8:
|
||||||
|
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
|
||||||
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
|
command-exists@1.2.9:
|
||||||
|
resolution: {integrity: sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w==}
|
||||||
|
|
||||||
commander@2.20.3:
|
commander@2.20.3:
|
||||||
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
|
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
|
||||||
|
|
||||||
@ -1889,6 +1925,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==}
|
resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
delayed-stream@1.0.0:
|
||||||
|
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
|
||||||
|
engines: {node: '>=0.4.0'}
|
||||||
|
|
||||||
denque@2.1.0:
|
denque@2.1.0:
|
||||||
resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==}
|
resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==}
|
||||||
engines: {node: '>=0.10'}
|
engines: {node: '>=0.10'}
|
||||||
@ -1941,6 +1981,10 @@ packages:
|
|||||||
electron-to-chromium@1.5.328:
|
electron-to-chromium@1.5.328:
|
||||||
resolution: {integrity: sha512-QNQ5l45DzYytThO21403XN3FvK0hOkWDG8viNf6jqS42msJ8I4tGDSpBCgvDRRPnkffafiwAym2X2eHeGD2V0w==}
|
resolution: {integrity: sha512-QNQ5l45DzYytThO21403XN3FvK0hOkWDG8viNf6jqS42msJ8I4tGDSpBCgvDRRPnkffafiwAym2X2eHeGD2V0w==}
|
||||||
|
|
||||||
|
elevenlabs@1.59.0:
|
||||||
|
resolution: {integrity: sha512-OVKOd+lxNya8h4Rn5fcjv00Asd+DGWfTT6opGrQ16sTI+1HwdLn/kYtjl8tRMhDXbNmksD/9SBRKjb9neiUuVg==}
|
||||||
|
deprecated: This package has moved to @elevenlabs/elevenlabs-js
|
||||||
|
|
||||||
emittery@0.13.1:
|
emittery@0.13.1:
|
||||||
resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==}
|
resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@ -1988,6 +2032,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
|
resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
es-set-tostringtag@2.1.0:
|
||||||
|
resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
esbuild@0.27.4:
|
esbuild@0.27.4:
|
||||||
resolution: {integrity: sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==}
|
resolution: {integrity: sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@ -2087,6 +2135,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
|
resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
|
event-target-shim@5.0.1:
|
||||||
|
resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
events@3.3.0:
|
events@3.3.0:
|
||||||
resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
|
resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
|
||||||
engines: {node: '>=0.8.x'}
|
engines: {node: '>=0.8.x'}
|
||||||
@ -2179,6 +2231,18 @@ packages:
|
|||||||
typescript: '>3.6.0'
|
typescript: '>3.6.0'
|
||||||
webpack: ^5.11.0
|
webpack: ^5.11.0
|
||||||
|
|
||||||
|
form-data-encoder@4.1.0:
|
||||||
|
resolution: {integrity: sha512-G6NsmEW15s0Uw9XnCg+33H3ViYRyiM0hMrMhhqQOR8NFc5GhYrI+6I3u7OTw7b91J2g8rtvMBZJDbcGb2YUniw==}
|
||||||
|
engines: {node: '>= 18'}
|
||||||
|
|
||||||
|
form-data@4.0.5:
|
||||||
|
resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==}
|
||||||
|
engines: {node: '>= 6'}
|
||||||
|
|
||||||
|
formdata-node@6.0.3:
|
||||||
|
resolution: {integrity: sha512-8e1++BCiTzUno9v5IZ2J6bv4RU+3UKDmqWUQD0MIMVCd9AdhWkO1gw57oo1mNEX1dMq2EGI+FbWz4B92pscSQg==}
|
||||||
|
engines: {node: '>= 18'}
|
||||||
|
|
||||||
forwarded@0.2.0:
|
forwarded@0.2.0:
|
||||||
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
|
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
@ -2566,6 +2630,10 @@ packages:
|
|||||||
json-parse-even-better-errors@2.3.1:
|
json-parse-even-better-errors@2.3.1:
|
||||||
resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==}
|
resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==}
|
||||||
|
|
||||||
|
json-schema-to-ts@3.1.1:
|
||||||
|
resolution: {integrity: sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==}
|
||||||
|
engines: {node: '>=16'}
|
||||||
|
|
||||||
json-schema-traverse@0.4.1:
|
json-schema-traverse@0.4.1:
|
||||||
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
|
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
|
||||||
|
|
||||||
@ -2869,6 +2937,15 @@ packages:
|
|||||||
node-emoji@1.11.0:
|
node-emoji@1.11.0:
|
||||||
resolution: {integrity: sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==}
|
resolution: {integrity: sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==}
|
||||||
|
|
||||||
|
node-fetch@2.7.0:
|
||||||
|
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
|
||||||
|
engines: {node: 4.x || >=6.0.0}
|
||||||
|
peerDependencies:
|
||||||
|
encoding: ^0.1.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
encoding:
|
||||||
|
optional: true
|
||||||
|
|
||||||
node-gyp-build@4.8.4:
|
node-gyp-build@4.8.4:
|
||||||
resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==}
|
resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@ -2910,6 +2987,18 @@ packages:
|
|||||||
resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==}
|
resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
openai@6.33.0:
|
||||||
|
resolution: {integrity: sha512-xAYN1W3YsDXJWA5F277135YfkEk6H7D3D6vWwRhJ3OEkzRgcyK8z/P5P9Gyi/wB4N8kK9kM5ZjprfvyHagKmpw==}
|
||||||
|
hasBin: true
|
||||||
|
peerDependencies:
|
||||||
|
ws: ^8.18.0
|
||||||
|
zod: ^3.25 || ^4.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
ws:
|
||||||
|
optional: true
|
||||||
|
zod:
|
||||||
|
optional: true
|
||||||
|
|
||||||
optionator@0.9.4:
|
optionator@0.9.4:
|
||||||
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
|
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
|
||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
@ -3096,6 +3185,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==}
|
resolution: {integrity: sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==}
|
||||||
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
|
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
|
||||||
|
|
||||||
|
process@0.11.10:
|
||||||
|
resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==}
|
||||||
|
engines: {node: '>= 0.6.0'}
|
||||||
|
|
||||||
proxy-addr@2.0.7:
|
proxy-addr@2.0.7:
|
||||||
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
|
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
|
||||||
engines: {node: '>= 0.10'}
|
engines: {node: '>= 0.10'}
|
||||||
@ -3135,6 +3228,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
|
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
|
|
||||||
|
readable-stream@4.7.0:
|
||||||
|
resolution: {integrity: sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==}
|
||||||
|
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||||
|
|
||||||
readdirp@4.1.2:
|
readdirp@4.1.2:
|
||||||
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
|
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
|
||||||
engines: {node: '>= 14.18.0'}
|
engines: {node: '>= 14.18.0'}
|
||||||
@ -3439,6 +3536,12 @@ packages:
|
|||||||
resolution: {integrity: sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==}
|
resolution: {integrity: sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==}
|
||||||
engines: {node: '>=14.16'}
|
engines: {node: '>=14.16'}
|
||||||
|
|
||||||
|
tr46@0.0.3:
|
||||||
|
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
|
||||||
|
|
||||||
|
ts-algebra@2.0.0:
|
||||||
|
resolution: {integrity: sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==}
|
||||||
|
|
||||||
ts-api-utils@2.5.0:
|
ts-api-utils@2.5.0:
|
||||||
resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==}
|
resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==}
|
||||||
engines: {node: '>=18.12'}
|
engines: {node: '>=18.12'}
|
||||||
@ -3634,6 +3737,9 @@ packages:
|
|||||||
uri-js@4.4.1:
|
uri-js@4.4.1:
|
||||||
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
||||||
|
|
||||||
|
url-join@4.0.1:
|
||||||
|
resolution: {integrity: sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==}
|
||||||
|
|
||||||
util-deprecate@1.0.2:
|
util-deprecate@1.0.2:
|
||||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||||
|
|
||||||
@ -3713,6 +3819,9 @@ packages:
|
|||||||
wcwidth@1.0.1:
|
wcwidth@1.0.1:
|
||||||
resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==}
|
resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==}
|
||||||
|
|
||||||
|
webidl-conversions@3.0.1:
|
||||||
|
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
|
||||||
|
|
||||||
webpack-node-externals@3.0.0:
|
webpack-node-externals@3.0.0:
|
||||||
resolution: {integrity: sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ==}
|
resolution: {integrity: sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@ -3731,6 +3840,9 @@ packages:
|
|||||||
webpack-cli:
|
webpack-cli:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
whatwg-url@5.0.0:
|
||||||
|
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
|
||||||
|
|
||||||
which-typed-array@1.1.20:
|
which-typed-array@1.1.20:
|
||||||
resolution: {integrity: sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==}
|
resolution: {integrity: sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@ -3869,6 +3981,10 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- chokidar
|
- chokidar
|
||||||
|
|
||||||
|
'@anthropic-ai/sdk@0.80.0':
|
||||||
|
dependencies:
|
||||||
|
json-schema-to-ts: 3.1.1
|
||||||
|
|
||||||
'@babel/code-frame@7.29.0':
|
'@babel/code-frame@7.29.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/helper-validator-identifier': 7.28.5
|
'@babel/helper-validator-identifier': 7.28.5
|
||||||
@ -4033,6 +4149,8 @@ snapshots:
|
|||||||
'@babel/core': 7.29.0
|
'@babel/core': 7.29.0
|
||||||
'@babel/helper-plugin-utils': 7.28.6
|
'@babel/helper-plugin-utils': 7.28.6
|
||||||
|
|
||||||
|
'@babel/runtime@7.29.2': {}
|
||||||
|
|
||||||
'@babel/template@7.28.6':
|
'@babel/template@7.28.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/code-frame': 7.29.0
|
'@babel/code-frame': 7.29.0
|
||||||
@ -5220,6 +5338,10 @@ snapshots:
|
|||||||
|
|
||||||
'@xtuc/long@4.2.2': {}
|
'@xtuc/long@4.2.2': {}
|
||||||
|
|
||||||
|
abort-controller@3.0.0:
|
||||||
|
dependencies:
|
||||||
|
event-target-shim: 5.0.1
|
||||||
|
|
||||||
accepts@1.3.8:
|
accepts@1.3.8:
|
||||||
dependencies:
|
dependencies:
|
||||||
mime-types: 2.1.35
|
mime-types: 2.1.35
|
||||||
@ -5321,6 +5443,8 @@ snapshots:
|
|||||||
|
|
||||||
array-timsort@1.0.3: {}
|
array-timsort@1.0.3: {}
|
||||||
|
|
||||||
|
asynckit@0.4.0: {}
|
||||||
|
|
||||||
available-typed-arrays@1.0.7:
|
available-typed-arrays@1.0.7:
|
||||||
dependencies:
|
dependencies:
|
||||||
possible-typed-array-names: 1.1.0
|
possible-typed-array-names: 1.1.0
|
||||||
@ -5547,6 +5671,12 @@ snapshots:
|
|||||||
|
|
||||||
color-name@1.1.4: {}
|
color-name@1.1.4: {}
|
||||||
|
|
||||||
|
combined-stream@1.0.8:
|
||||||
|
dependencies:
|
||||||
|
delayed-stream: 1.0.0
|
||||||
|
|
||||||
|
command-exists@1.2.9: {}
|
||||||
|
|
||||||
commander@2.20.3: {}
|
commander@2.20.3: {}
|
||||||
|
|
||||||
commander@4.1.1: {}
|
commander@4.1.1: {}
|
||||||
@ -5626,6 +5756,8 @@ snapshots:
|
|||||||
es-errors: 1.3.0
|
es-errors: 1.3.0
|
||||||
gopd: 1.2.0
|
gopd: 1.2.0
|
||||||
|
|
||||||
|
delayed-stream@1.0.0: {}
|
||||||
|
|
||||||
denque@2.1.0: {}
|
denque@2.1.0: {}
|
||||||
|
|
||||||
depd@2.0.0: {}
|
depd@2.0.0: {}
|
||||||
@ -5662,6 +5794,20 @@ snapshots:
|
|||||||
|
|
||||||
electron-to-chromium@1.5.328: {}
|
electron-to-chromium@1.5.328: {}
|
||||||
|
|
||||||
|
elevenlabs@1.59.0:
|
||||||
|
dependencies:
|
||||||
|
command-exists: 1.2.9
|
||||||
|
execa: 5.1.1
|
||||||
|
form-data: 4.0.5
|
||||||
|
form-data-encoder: 4.1.0
|
||||||
|
formdata-node: 6.0.3
|
||||||
|
node-fetch: 2.7.0
|
||||||
|
qs: 6.15.0
|
||||||
|
readable-stream: 4.7.0
|
||||||
|
url-join: 4.0.1
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- encoding
|
||||||
|
|
||||||
emittery@0.13.1: {}
|
emittery@0.13.1: {}
|
||||||
|
|
||||||
emoji-regex@8.0.0: {}
|
emoji-regex@8.0.0: {}
|
||||||
@ -5720,6 +5866,13 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
es-errors: 1.3.0
|
es-errors: 1.3.0
|
||||||
|
|
||||||
|
es-set-tostringtag@2.1.0:
|
||||||
|
dependencies:
|
||||||
|
es-errors: 1.3.0
|
||||||
|
get-intrinsic: 1.3.0
|
||||||
|
has-tostringtag: 1.0.2
|
||||||
|
hasown: 2.0.2
|
||||||
|
|
||||||
esbuild@0.27.4:
|
esbuild@0.27.4:
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@esbuild/aix-ppc64': 0.27.4
|
'@esbuild/aix-ppc64': 0.27.4
|
||||||
@ -5846,6 +5999,8 @@ snapshots:
|
|||||||
|
|
||||||
etag@1.8.1: {}
|
etag@1.8.1: {}
|
||||||
|
|
||||||
|
event-target-shim@5.0.1: {}
|
||||||
|
|
||||||
events@3.3.0: {}
|
events@3.3.0: {}
|
||||||
|
|
||||||
execa@5.1.1:
|
execa@5.1.1:
|
||||||
@ -5991,6 +6146,18 @@ snapshots:
|
|||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
webpack: 5.104.1
|
webpack: 5.104.1
|
||||||
|
|
||||||
|
form-data-encoder@4.1.0: {}
|
||||||
|
|
||||||
|
form-data@4.0.5:
|
||||||
|
dependencies:
|
||||||
|
asynckit: 0.4.0
|
||||||
|
combined-stream: 1.0.8
|
||||||
|
es-set-tostringtag: 2.1.0
|
||||||
|
hasown: 2.0.2
|
||||||
|
mime-types: 2.1.35
|
||||||
|
|
||||||
|
formdata-node@6.0.3: {}
|
||||||
|
|
||||||
forwarded@0.2.0: {}
|
forwarded@0.2.0: {}
|
||||||
|
|
||||||
fresh@2.0.0: {}
|
fresh@2.0.0: {}
|
||||||
@ -6559,6 +6726,11 @@ snapshots:
|
|||||||
|
|
||||||
json-parse-even-better-errors@2.3.1: {}
|
json-parse-even-better-errors@2.3.1: {}
|
||||||
|
|
||||||
|
json-schema-to-ts@3.1.1:
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.29.2
|
||||||
|
ts-algebra: 2.0.0
|
||||||
|
|
||||||
json-schema-traverse@0.4.1: {}
|
json-schema-traverse@0.4.1: {}
|
||||||
|
|
||||||
json-schema-traverse@1.0.0: {}
|
json-schema-traverse@1.0.0: {}
|
||||||
@ -6799,6 +6971,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
lodash: 4.17.23
|
lodash: 4.17.23
|
||||||
|
|
||||||
|
node-fetch@2.7.0:
|
||||||
|
dependencies:
|
||||||
|
whatwg-url: 5.0.0
|
||||||
|
|
||||||
node-gyp-build@4.8.4: {}
|
node-gyp-build@4.8.4: {}
|
||||||
|
|
||||||
node-int64@0.4.0: {}
|
node-int64@0.4.0: {}
|
||||||
@ -6829,6 +7005,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
mimic-fn: 2.1.0
|
mimic-fn: 2.1.0
|
||||||
|
|
||||||
|
openai@6.33.0(ws@8.18.3):
|
||||||
|
optionalDependencies:
|
||||||
|
ws: 8.18.3
|
||||||
|
|
||||||
optionator@0.9.4:
|
optionator@0.9.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
deep-is: 0.1.4
|
deep-is: 0.1.4
|
||||||
@ -7001,6 +7181,8 @@ snapshots:
|
|||||||
ansi-styles: 5.2.0
|
ansi-styles: 5.2.0
|
||||||
react-is: 18.3.1
|
react-is: 18.3.1
|
||||||
|
|
||||||
|
process@0.11.10: {}
|
||||||
|
|
||||||
proxy-addr@2.0.7:
|
proxy-addr@2.0.7:
|
||||||
dependencies:
|
dependencies:
|
||||||
forwarded: 0.2.0
|
forwarded: 0.2.0
|
||||||
@ -7038,6 +7220,14 @@ snapshots:
|
|||||||
string_decoder: 1.3.0
|
string_decoder: 1.3.0
|
||||||
util-deprecate: 1.0.2
|
util-deprecate: 1.0.2
|
||||||
|
|
||||||
|
readable-stream@4.7.0:
|
||||||
|
dependencies:
|
||||||
|
abort-controller: 3.0.0
|
||||||
|
buffer: 6.0.3
|
||||||
|
events: 3.3.0
|
||||||
|
process: 0.11.10
|
||||||
|
string_decoder: 1.3.0
|
||||||
|
|
||||||
readdirp@4.1.2: {}
|
readdirp@4.1.2: {}
|
||||||
|
|
||||||
redis-errors@1.2.0: {}
|
redis-errors@1.2.0: {}
|
||||||
@ -7383,6 +7573,10 @@ snapshots:
|
|||||||
'@tokenizer/token': 0.3.0
|
'@tokenizer/token': 0.3.0
|
||||||
ieee754: 1.2.1
|
ieee754: 1.2.1
|
||||||
|
|
||||||
|
tr46@0.0.3: {}
|
||||||
|
|
||||||
|
ts-algebra@2.0.0: {}
|
||||||
|
|
||||||
ts-api-utils@2.5.0(typescript@5.8.3):
|
ts-api-utils@2.5.0(typescript@5.8.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
typescript: 5.8.3
|
typescript: 5.8.3
|
||||||
@ -7554,6 +7748,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
punycode: 2.3.1
|
punycode: 2.3.1
|
||||||
|
|
||||||
|
url-join@4.0.1: {}
|
||||||
|
|
||||||
util-deprecate@1.0.2: {}
|
util-deprecate@1.0.2: {}
|
||||||
|
|
||||||
utils-merge@1.0.1: {}
|
utils-merge@1.0.1: {}
|
||||||
@ -7599,6 +7795,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
defaults: 1.0.4
|
defaults: 1.0.4
|
||||||
|
|
||||||
|
webidl-conversions@3.0.1: {}
|
||||||
|
|
||||||
webpack-node-externals@3.0.0: {}
|
webpack-node-externals@3.0.0: {}
|
||||||
|
|
||||||
webpack-sources@3.3.4: {}
|
webpack-sources@3.3.4: {}
|
||||||
@ -7635,6 +7833,11 @@ snapshots:
|
|||||||
- esbuild
|
- esbuild
|
||||||
- uglify-js
|
- uglify-js
|
||||||
|
|
||||||
|
whatwg-url@5.0.0:
|
||||||
|
dependencies:
|
||||||
|
tr46: 0.0.3
|
||||||
|
webidl-conversions: 3.0.1
|
||||||
|
|
||||||
which-typed-array@1.1.20:
|
which-typed-array@1.1.20:
|
||||||
dependencies:
|
dependencies:
|
||||||
available-typed-arrays: 1.0.7
|
available-typed-arrays: 1.0.7
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user