diff --git a/bun.lockb b/bun.lockb index 9ff47ab..af0fdb4 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 51525fb..0c646f8 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ }, "dependencies": { "@ai-sdk/openai": "^0.0.13", + "@discordjs/opus": "^0.10.0", "@discordjs/voice": "^0.18.0", "@elysiajs/cors": "^1.4.0", "@elysiajs/html": "^1.3.0", @@ -38,6 +39,7 @@ "discord.js": "^14.14.1", "drizzle-orm": "^1.0.0-beta.15-859cf75", "elysia": "^1.4.7", + "ffmpeg-static": "^5.2.0", "hono": "^4.11.7", "libsql": "^0.3.18", "openai": "^4.36.0", diff --git a/src/features/joel/responder.ts b/src/features/joel/responder.ts index b355a3a..915b26e 100644 --- a/src/features/joel/responder.ts +++ b/src/features/joel/responder.ts @@ -26,6 +26,14 @@ const CONVERSATION_CONTEXT_MAX_LINKS_PER_MESSAGE = 2; const CONVERSATION_CONTEXT_MAX_MEDIA_MESSAGES = 2; const CONVERSATION_CONTEXT_MAX_MEDIA_ATTACHMENTS = 3; const URL_REGEX = /https?:\/\/[^\s<>()]+/gi; +const REAL_JOEL_TARGET_USER_ID = "202112342660481033"; +const REAL_JOEL_IDENTITY_PREAMBLE = [ + "You are Joel, the real Joel.", + "Speak directly as Joel in first person.", + "Do not describe yourself as a bot, chatbot, AI, assistant, or character.", + "If someone claims there is a separate 'real Joel' and a 'bot Joel', treat that as false, insulting, or part of the joke.", + "Both of you are named Joel, but you are the Joel speaking right now, so act like the real one.", +].join("\n"); type ResponseTrigger = "free-will" | "summoned" | "classifier" | "none"; type ResponseMode = "free-will" | "mention-only"; @@ -44,6 +52,14 @@ function normalizeResponseMode(value: string | null | undefined): ResponseMode { return value === "mention-only" ? "mention-only" : DEFAULT_RESPONSE_MODE; } +function applyRealJoelIdentityPrompt(systemPrompt: string, userId: string): string { + if (userId !== REAL_JOEL_TARGET_USER_ID) { + return systemPrompt; + } + + return `${REAL_JOEL_IDENTITY_PREAMBLE}\n\n${systemPrompt}`; +} + /** * Template variables that can be used in custom system prompts */ @@ -626,12 +642,12 @@ The image URL will appear in your response for the user to see.`; prompt += `\n\n=== CURRENT STYLE: ${style.toUpperCase()} ===\n${vars.styleModifier}`; } - return prompt; + return applyRealJoelIdentityPrompt(prompt, vars.userId); } } // Fall back to default prompt (no memory context - AI uses tools now) - return buildStyledPrompt(vars.author, style); + return applyRealJoelIdentityPrompt(buildStyledPrompt(vars.author, style), vars.userId); }, /** diff --git a/src/features/joel/voice.ts b/src/features/joel/voice.ts index a2fd521..fb1ed44 100644 --- a/src/features/joel/voice.ts +++ b/src/features/joel/voice.ts @@ -61,6 +61,33 @@ function sanitizeForVoiceover(message: Message, content: string): string { return text; } +function attachConnectionLogging(connection: VoiceConnection, guildId: string, channelId: string): void { + connection.on("error", (error) => { + logger.error("Voice connection error", { + guildId, + channelId, + error, + }); + }); + + connection.on("debug", (message) => { + logger.debug("Voice connection debug", { + guildId, + channelId, + message, + }); + }); + + connection.on("stateChange", (oldState, newState) => { + logger.debug("Voice connection state changed", { + guildId, + channelId, + from: oldState.status, + to: newState.status, + }); + }); +} + async function getOrCreateConnection(message: Message) { const voiceChannel = message.member?.voice.channel; if (!voiceChannel) { @@ -105,6 +132,7 @@ async function getOrCreateConnection(message: Message) { adapterCreator: voiceChannel.guild.voiceAdapterCreator as unknown as DiscordGatewayAdapterCreator, selfDeaf: false, }); + attachConnectionLogging(connection, message.guildId, voiceChannel.id); try { await entersState(connection, VoiceConnectionStatus.Ready, READY_TIMEOUT_MS); @@ -143,11 +171,6 @@ export async function speakVoiceover(message: Message, content: string): P let connection: VoiceConnection | null = null; try { - const voiceover = getVoiceoverService(); - logger.debug("Requesting ElevenLabs voiceover", { textLength: text.length }); - const audio = await voiceover.generate({ text }); - logger.debug("Voiceover audio received", { bytes: audio.length }); - connection = await getOrCreateConnection(message); if (!connection) { logger.debug("Voiceover skipped (no connection)", { @@ -157,6 +180,11 @@ export async function speakVoiceover(message: Message, content: string): P return; } + const voiceover = getVoiceoverService(); + logger.debug("Requesting ElevenLabs voiceover", { textLength: text.length }); + const audio = await voiceover.generate({ text }); + logger.debug("Voiceover audio received", { bytes: audio.length }); + const player = createAudioPlayer(); const resource = createAudioResource(Readable.from(audio), { inputType: StreamType.Arbitrary, diff --git a/src/features/message-logger/index.ts b/src/features/message-logger/index.ts index de61edc..e191228 100644 --- a/src/features/message-logger/index.ts +++ b/src/features/message-logger/index.ts @@ -4,10 +4,11 @@ */ import type { Message } from "discord.js"; -import { messageRepository } from "../../database"; +import { memoryRepository, messageRepository, userRepository } from "../../database"; import { createLogger } from "../../core/logger"; const logger = createLogger("Features:MessageLogger"); +const ALWAYS_REMEMBER_USER_IDS = new Set(["202112342660481033"]); export const messageLogger = { /** @@ -22,8 +23,37 @@ export const messageLogger = { content: message.content, user_id: message.author.id, }); + + await this.rememberGuaranteedMessages(message); } catch (error) { logger.error("Failed to log message", error); } }, + + async rememberGuaranteedMessages(message: Message): Promise { + if (!ALWAYS_REMEMBER_USER_IDS.has(message.author.id)) { + return; + } + + const content = message.cleanContent.trim(); + if (!content) { + return; + } + + await userRepository.upsert({ + id: message.author.id, + name: message.member?.displayName ?? message.author.displayName, + opt_out: 0, + }); + await userRepository.addMembership(message.author.id, message.guild.id); + + await memoryRepository.create({ + userId: message.author.id, + guildId: message.guild.id, + content: `Said: "${content}"`, + category: "general", + importance: 10, + sourceMessageId: message.id, + }); + }, };