feat: joel voice
This commit is contained in:
@@ -27,9 +27,9 @@ const toolHandlers: Record<string, ToolHandler> = {
|
||||
|
||||
let userMemories;
|
||||
if (category) {
|
||||
userMemories = await memoryRepository.findByCategory(userId, category, limit);
|
||||
userMemories = await memoryRepository.findByCategory(userId, context.guildId, category, limit);
|
||||
} else {
|
||||
userMemories = await memoryRepository.findByUserId(userId, limit);
|
||||
userMemories = await memoryRepository.findByUserId(userId, context.guildId, limit);
|
||||
}
|
||||
|
||||
if (userMemories.length === 0) {
|
||||
@@ -61,7 +61,7 @@ const toolHandlers: Record<string, ToolHandler> = {
|
||||
}
|
||||
|
||||
// Check for duplicate memories using new similarity check
|
||||
const similar = await memoryRepository.findSimilar(userId, content);
|
||||
const similar = await memoryRepository.findSimilar(userId, context.guildId, content);
|
||||
if (similar.length > 0) {
|
||||
return "Already knew something similar. Memory not saved (duplicate).";
|
||||
}
|
||||
@@ -89,7 +89,7 @@ const toolHandlers: Record<string, ToolHandler> = {
|
||||
*/
|
||||
async search_memories(args, context): Promise<string> {
|
||||
const query = args.query as string;
|
||||
const guildId = args.guild_id as string | undefined;
|
||||
const guildId = (args.guild_id as string | undefined) ?? context.guildId;
|
||||
const category = args.category as MemoryCategory | undefined;
|
||||
const minImportance = args.min_importance as number | undefined;
|
||||
|
||||
@@ -138,7 +138,7 @@ const toolHandlers: Record<string, ToolHandler> = {
|
||||
|
||||
logger.warn("Forgetting user memories", { userId, requestedBy: context.userId });
|
||||
|
||||
const deleted = await memoryRepository.deleteByUserId(userId);
|
||||
const deleted = await memoryRepository.deleteByUserId(userId, context.guildId);
|
||||
|
||||
return `Deleted ${deleted} memories about user ${userId}.`;
|
||||
},
|
||||
@@ -157,7 +157,7 @@ const toolHandlers: Record<string, ToolHandler> = {
|
||||
}
|
||||
|
||||
// Check for duplicates
|
||||
const similar = await memoryRepository.findSimilar(context.userId, content);
|
||||
const similar = await memoryRepository.findSimilar(context.userId, context.guildId, content);
|
||||
if (similar.length > 0) {
|
||||
return "Similar memory already exists. Skipped.";
|
||||
}
|
||||
@@ -185,7 +185,7 @@ const toolHandlers: Record<string, ToolHandler> = {
|
||||
async get_memory_stats(args, context): Promise<string> {
|
||||
const userId = (args.user_id as string) || context.userId;
|
||||
|
||||
const stats = await memoryRepository.getStats(userId);
|
||||
const stats = await memoryRepository.getStats(userId, context.guildId);
|
||||
|
||||
if (stats.total === 0) {
|
||||
return `No memories stored for this user.`;
|
||||
|
||||
104
src/services/ai/voiceover.ts
Normal file
104
src/services/ai/voiceover.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* ElevenLabs voiceover service
|
||||
*/
|
||||
|
||||
import { config } from "../../core/config";
|
||||
import { createLogger } from "../../core/logger";
|
||||
|
||||
const logger = createLogger("AI:Voiceover");
|
||||
|
||||
const DEFAULT_OUTPUT_FORMAT = "mp3_44100_128" as const;
|
||||
const DEFAULT_STABILITY = 0.1;
|
||||
const DEFAULT_SIMILARITY = 0.90;
|
||||
const DEFAULT_STYLE = 0.25;
|
||||
const DEFAULT_SPEED = 1.20
|
||||
|
||||
function clamp01(value: number): number {
|
||||
return Math.max(0, Math.min(1, value));
|
||||
}
|
||||
|
||||
export interface VoiceoverOptions {
|
||||
text: string;
|
||||
voiceId?: string;
|
||||
modelId?: string;
|
||||
stability?: number;
|
||||
similarityBoost?: number;
|
||||
style?: number;
|
||||
speakerBoost?: boolean;
|
||||
}
|
||||
|
||||
export class VoiceoverService {
|
||||
async generate(options: VoiceoverOptions): Promise<Buffer> {
|
||||
const apiKey = config.elevenlabs.apiKey;
|
||||
if (!apiKey) {
|
||||
throw new Error("Voiceover is not configured (missing ELEVENLABS_API_KEY).");
|
||||
}
|
||||
|
||||
const voiceId = options.voiceId || config.elevenlabs.voiceId;
|
||||
if (!voiceId) {
|
||||
throw new Error("Voiceover is missing a voice ID (set ELEVENLABS_VOICE_ID or pass one).");
|
||||
}
|
||||
|
||||
const text = options.text.trim();
|
||||
if (!text) {
|
||||
throw new Error("Voiceover text is empty.");
|
||||
}
|
||||
|
||||
const modelId = options.modelId || config.elevenlabs.modelId;
|
||||
|
||||
const voiceSettings = {
|
||||
stability: clamp01(options.stability ?? DEFAULT_STABILITY),
|
||||
similarity_boost: clamp01(options.similarityBoost ?? DEFAULT_SIMILARITY),
|
||||
style: clamp01(options.style ?? DEFAULT_STYLE),
|
||||
use_speaker_boost: options.speakerBoost ?? true,
|
||||
};
|
||||
|
||||
const url = new URL(`https://api.elevenlabs.io/v1/text-to-speech/${voiceId}/stream`);
|
||||
url.searchParams.set("output_format", DEFAULT_OUTPUT_FORMAT);
|
||||
|
||||
logger.debug("Generating voiceover", {
|
||||
textLength: text.length,
|
||||
voiceId,
|
||||
modelId,
|
||||
});
|
||||
|
||||
const response = await fetch(url.toString(), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"xi-api-key": apiKey,
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "audio/mpeg",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
text,
|
||||
model_id: modelId,
|
||||
voice_settings: voiceSettings,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorBody = await response.text();
|
||||
logger.error("ElevenLabs API error", {
|
||||
status: response.status,
|
||||
body: errorBody.slice(0, 300),
|
||||
});
|
||||
throw new Error(`ElevenLabs API error (HTTP ${response.status}).`);
|
||||
}
|
||||
|
||||
const audioBuffer = await response.arrayBuffer();
|
||||
return Buffer.from(audioBuffer);
|
||||
}
|
||||
|
||||
async health(): Promise<boolean> {
|
||||
return !!config.elevenlabs.apiKey;
|
||||
}
|
||||
}
|
||||
|
||||
let voiceoverService: VoiceoverService | null = null;
|
||||
|
||||
export function getVoiceoverService(): VoiceoverService {
|
||||
if (!voiceoverService) {
|
||||
voiceoverService = new VoiceoverService();
|
||||
}
|
||||
return voiceoverService;
|
||||
}
|
||||
Reference in New Issue
Block a user