feat: transcribe Discord audio attachments via OpenAI Whisper API
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
Attachment,
|
||||||
Client,
|
Client,
|
||||||
Events,
|
Events,
|
||||||
GatewayIntentBits,
|
GatewayIntentBits,
|
||||||
@@ -9,6 +10,48 @@ import {
|
|||||||
import { ASSISTANT_NAME, TRIGGER_PATTERN } from '../config.js';
|
import { ASSISTANT_NAME, TRIGGER_PATTERN } from '../config.js';
|
||||||
import { readEnvFile } from '../env.js';
|
import { readEnvFile } from '../env.js';
|
||||||
import { logger } from '../logger.js';
|
import { logger } from '../logger.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transcribe an audio attachment via OpenAI Whisper API.
|
||||||
|
* Returns the transcribed text, or a fallback placeholder on failure.
|
||||||
|
*/
|
||||||
|
async function transcribeAudio(
|
||||||
|
att: Attachment,
|
||||||
|
apiKey: string,
|
||||||
|
): Promise<string> {
|
||||||
|
try {
|
||||||
|
const res = await fetch(att.url);
|
||||||
|
if (!res.ok) throw new Error(`Download failed: ${res.status}`);
|
||||||
|
const buffer = Buffer.from(await res.arrayBuffer());
|
||||||
|
|
||||||
|
const form = new FormData();
|
||||||
|
const filename = att.name || 'audio.ogg';
|
||||||
|
form.append('file', new Blob([buffer]), filename);
|
||||||
|
form.append('model', 'whisper-1');
|
||||||
|
|
||||||
|
const whisperRes = await fetch(
|
||||||
|
'https://api.openai.com/v1/audio/transcriptions',
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: { Authorization: `Bearer ${apiKey}` },
|
||||||
|
body: form,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (!whisperRes.ok) {
|
||||||
|
const errText = await whisperRes.text();
|
||||||
|
throw new Error(`Whisper API ${whisperRes.status}: ${errText}`);
|
||||||
|
}
|
||||||
|
const data = (await whisperRes.json()) as { text: string };
|
||||||
|
logger.info(
|
||||||
|
{ file: filename, length: data.text.length },
|
||||||
|
'Audio transcribed',
|
||||||
|
);
|
||||||
|
return `[Voice message transcription]: ${data.text}`;
|
||||||
|
} catch (err) {
|
||||||
|
logger.error({ err, file: att.name }, 'Audio transcription failed');
|
||||||
|
return `[Audio: ${att.name || 'audio'} (transcription failed)]`;
|
||||||
|
}
|
||||||
|
}
|
||||||
import { registerChannel, ChannelOpts } from './registry.js';
|
import { registerChannel, ChannelOpts } from './registry.js';
|
||||||
import {
|
import {
|
||||||
AgentType,
|
AgentType,
|
||||||
@@ -103,12 +146,18 @@ export class DiscordChannel implements Channel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle attachments — store placeholders so the agent knows something was sent
|
// Handle attachments — transcribe audio, placeholder for others
|
||||||
if (message.attachments.size > 0) {
|
if (message.attachments.size > 0) {
|
||||||
const attachmentDescriptions = [...message.attachments.values()].map(
|
const openaiKey =
|
||||||
(att) => {
|
process.env.OPENAI_API_KEY ||
|
||||||
|
readEnvFile(['OPENAI_API_KEY']).OPENAI_API_KEY ||
|
||||||
|
'';
|
||||||
|
const attachmentDescriptions = await Promise.all(
|
||||||
|
[...message.attachments.values()].map(async (att) => {
|
||||||
const contentType = att.contentType || '';
|
const contentType = att.contentType || '';
|
||||||
if (contentType.startsWith('image/')) {
|
if (contentType.startsWith('audio/') && openaiKey) {
|
||||||
|
return transcribeAudio(att, openaiKey);
|
||||||
|
} else if (contentType.startsWith('image/')) {
|
||||||
return `[Image: ${att.name || 'image'}]`;
|
return `[Image: ${att.name || 'image'}]`;
|
||||||
} else if (contentType.startsWith('video/')) {
|
} else if (contentType.startsWith('video/')) {
|
||||||
return `[Video: ${att.name || 'video'}]`;
|
return `[Video: ${att.name || 'video'}]`;
|
||||||
@@ -117,7 +166,7 @@ export class DiscordChannel implements Channel {
|
|||||||
} else {
|
} else {
|
||||||
return `[File: ${att.name || 'file'}]`;
|
return `[File: ${att.name || 'file'}]`;
|
||||||
}
|
}
|
||||||
},
|
}),
|
||||||
);
|
);
|
||||||
if (content) {
|
if (content) {
|
||||||
content = `${content}\n${attachmentDescriptions.join('\n')}`;
|
content = `${content}\n${attachmentDescriptions.join('\n')}`;
|
||||||
|
|||||||
Reference in New Issue
Block a user