/** * Discord Voice Message Support * * Implements sending voice messages via Discord's API. * Voice messages require: * - OGG/Opus format audio * - Waveform data (base64 encoded, up to 256 samples, 0-255 values) * - Duration in seconds * - Message flag 8192 (IS_VOICE_MESSAGE) * - No other content (text, embeds, etc.)
*/
import crypto from "node:crypto"; import fs from "node:fs/promises"; import path from "node:path"; import { RateLimitError, type RequestClient } from "@buape/carbon"; import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; import {
parseFfprobeCodecAndSampleRate,
runFfmpeg,
runFfprobe,
} from "openclaw/plugin-sdk/media-runtime"; import { MEDIA_FFMPEG_MAX_AUDIO_DURATION_SECS } from "openclaw/plugin-sdk/media-runtime"; import { unlinkIfExists } from "openclaw/plugin-sdk/media-runtime"; import type { RetryRunner } from "openclaw/plugin-sdk/retry-runtime"; import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/temp-path"; import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
// Sample the PCM data to get WAVEFORM_SAMPLES points const step = Math.max(1, Math.floor(samples.length / WAVEFORM_SAMPLES)); const waveform: number[] = [];
for (let i = 0; i < WAVEFORM_SAMPLES && i * step < samples.length; i++) { // Get average absolute amplitude for this segment
let sum = 0;
let count = 0; for (let j = 0; j < step && i * step + j < samples.length; j++) {
sum += Math.abs(samples[i * step + j]);
count++;
} const avg = count > 0 ? sum / count : 0; // Normalize to 0-255 (16-bit signed max is 32767) const normalized = Math.min(255, Math.round((avg / 32767) * 255));
waveform.push(normalized);
}
// Pad with zeros if we don't have enough samples while (waveform.length < WAVEFORM_SAMPLES) {
waveform.push(0);
}
/** * Generate a placeholder waveform (for when audio processing fails)
*/ function generatePlaceholderWaveform(): string { // Generate a simple sine-wave-like pattern const waveform: number[] = []; for (let i = 0; i < WAVEFORM_SAMPLES; i++) { const value = Math.round(128 + 64 * Math.sin((i / WAVEFORM_SAMPLES) * Math.PI * 8));
waveform.push(Math.min(255, Math.max(0, value)));
} return Buffer.from(waveform).toString("base64");
}
/** * Convert audio file to OGG/Opus format if needed * Returns path to the OGG file (may be same as input if already OGG/Opus)
*/
export async function ensureOggOpus(filePath: string): Promise<{ path: string; cleanup: boolean }> { const trimmed = filePath.trim(); // Defense-in-depth: callers should never hand ffmpeg/ffprobe a URL/protocol path. if (/^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed)) { thrownew Error(
`Voice message conversion requires a local file path; received a URL/protocol source: ${trimmed}`,
);
}
// Check if already OGG if (ext === ".ogg") { // Fast-path only when the file is Opus at Discord's expected 48kHz. try { const stdout = await runFfprobe([ "-v", "error", "-select_streams", "a:0", "-show_entries", "stream=codec_name,sample_rate", "-of", "csv=p=0",
filePath,
]); const { codec, sampleRateHz } = parseFfprobeCodecAndSampleRate(stdout); if (codec === "opus" && sampleRateHz === DISCORD_OPUS_SAMPLE_RATE_HZ) { return { path: filePath, cleanup: false };
}
} catch { // If probe fails, convert anyway
}
}
// Convert to OGG/Opus // Always resample to 48kHz to ensure Discord voice messages play at correct speed // (Discord expects 48kHz; lower sample rates like 24kHz from some TTS providers cause 0.5x playback) const tempDir = resolvePreferredOpenClawTmpDir(); const outputPath = path.join(tempDir, `voice-${crypto.randomUUID()}.ogg`);
// Note: Voice messages cannot have content, but can have message_reference for replies if (replyTo) {
messagePayload.message_reference = {
message_id: replyTo,
fail_if_not_exists: false,
};
}
const res = (await request(
() =>
rest.post(`/channels/${channelId}/messages`, {
body: messagePayload,
}) as Promise<{ id: string; channel_id: string }>, "voice-message",
)) as { id: string; channel_id: string };
return res;
}
Messung V0.5 in Prozent
¤ Dauer der Verarbeitung: 0.13 Sekunden
(vorverarbeitet am 2026-06-05)
¤
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.