Address remaining review items (queue, selfbot v6 API, ldconfig, resample)
Some checks failed
Release / semantic-release (push) Successful in 22s
tests / Unit tests (Linux, Python 3.11) (push) Successful in 9m56s
Release / build-linux (push) Failing after 7m15s
Release / build-windows (push) Has been cancelled
Release / build-macos (arm64, macos-latest) (push) Has been cancelled
Release / build-macos (x64, macos-15-intel) (push) Has been cancelled
Release / release-main (push) Has been cancelled
Release / release-develop (push) Has been cancelled

- voice.ts: reply playback is now a FIFO queue (AudioPlayerStatus.Idle drains
  it) so concurrent speakers no longer cut each other's replies off.
- selfbot.ts: rewritten against the REAL @dank074/discord-video-stream v6 API
  (verified from its d.ts): prepareStream(input, opts, signal)->{command,output},
  playStream(output, streamer, {type:"go-live"}, signal), Streamer.joinVoice.
  x11grab via customInputOptions; optional NVENC encode (RTX 5050) via exported
  `nvenc`. package.json pinned to ^6.0.0 (was a wrong ^4.2.1).
- Dockerfile: dropped the hardcoded python3.12 LD_LIBRARY_PATH. faster-whisper
  >=1.1 self-locates the pip CUDA libs; ldconfig (full path, glob) registers
  them as a robust fallback. Verified: ld.so cache lists libcublas/libcudnn and
  GPU whisper works with LD_LIBRARY_PATH empty.
- bridge: STT resample 48k->16k upgraded from nearest-neighbor to linear
  (np.interp).

Verified: tsc clean, image builds, GPU whisper OK via ldconfig, compose valid.
This commit is contained in:
javis-bot
2026-06-09 18:47:25 +09:00
parent 964123682f
commit b56c9c7721
7 changed files with 417 additions and 50 deletions

View File

@@ -6,53 +6,51 @@
* requires a USER account token (a "selfbot"), which violates Discord ToS and
* can get the account banned. Use a throwaway/burner account, never your main.
*
* Dependencies are optional (native): install with
* Dependencies are optional (native) and dynamically imported so the core bot
* installs/runs without them:
* bun add discord.js-selfbot-v13 @dank074/discord-video-stream
* They are dynamically imported so the core bot installs/runs without them.
* bun pm trust @dank074/node-av node-datachannel # build native deps
*
* Library API targets @dank074/discord-video-stream v6 (Streamer / prepareStream
* / playStream). If a different major is installed, the import guard below will
* point you at the docs rather than crash cryptically.
* API targets @dank074/discord-video-stream v6 (verified against its d.ts):
* new Streamer(client) -> joinVoice(guildId, channelId)
* prepareStream(input, opts, signal) -> { command, output }
* playStream(output, streamer, { type: "go-live" }, signal)
*/
import type { AppConfig } from "../config.ts";
import type { ScreenStreamer, StreamContext } from "./index.ts";
export class SelfbotStreamer implements ScreenStreamer {
readonly kind = "selfbot" as const;
private config: AppConfig;
private streamer: any = null;
private controller: AbortController | null = null;
private active = false;
constructor(config: AppConfig) {
this.config = config;
}
constructor(private config: AppConfig) {}
isActive() {
return this.active;
}
private async loadLib() {
let selfbot: any, videoStream: any;
let selfbot: any, vs: any;
try {
selfbot = await import("discord.js-selfbot-v13");
// Optional native dep; resolved at runtime only. Version/name can vary by
// upstream release, so we don't hard-bind its types at compile time.
// Optional native dep; resolved at runtime only.
// @ts-ignore - optional dependency, may be absent until `bun add`ed
videoStream = await import("@dank074/discord-video-stream");
vs = await import("@dank074/discord-video-stream");
} catch (e) {
throw new Error(
"셀프봇 송출 의존성이 없습니다. 설치: bun add discord.js-selfbot-v13 @dank074/discord-video-stream\n" +
`원본 오류: ${(e as Error).message}`,
);
}
if (!videoStream.Streamer || !videoStream.prepareStream || !videoStream.playStream) {
if (!vs.Streamer || !vs.prepareStream || !vs.playStream) {
throw new Error(
"@dank074/discord-video-stream v6 API(Streamer/prepareStream/playStream)를 찾지 못했습니다. " +
"package.json 버전을 ^4.2.1(=v6 npm 태그)로 맞추거나 docs를 확인하세요.",
"package.json 버전을 ^6.0.0으로 맞추세요.",
);
}
return { selfbot, videoStream };
return { selfbot, vs };
}
async start(ctx: StreamContext): Promise<string> {
@@ -60,39 +58,50 @@ export class SelfbotStreamer implements ScreenStreamer {
if (!this.config.selfbotToken) {
return "DISCORD_SELFBOT_TOKEN이 설정되지 않았습니다 (.env). 버너 계정 토큰을 넣어주세요.";
}
const { selfbot, videoStream } = await this.loadLib();
const { Streamer, prepareStream, playStream, Utils } = videoStream;
if (!ctx.voiceChannelId) {
return "셀프봇 송출은 음성 채널 안에서 호출해야 합니다.";
}
const { selfbot, vs } = await this.loadLib();
const { Streamer, prepareStream, playStream, nvenc } = vs;
this.streamer = new Streamer(new selfbot.Client());
await this.streamer.client.login(this.config.selfbotToken);
await this.streamer.joinVoice(ctx.guildId, ctx.voiceChannelId);
// Grab the VNC X display with ffmpeg's x11grab and let the library
// encode/transport it. NVENC (RTX 5050) is used if available.
const input = `x11grab:${this.config.vncDisplay}`;
const { command, output } = prepareStream(
input,
{
width: parseInt(this.config.vncResolution.split("x")[0] ?? "1920", 10),
height: parseInt(this.config.vncResolution.split("x")[1] ?? "1080", 10),
frameRate: this.config.vncFramerate,
bitrateVideo: this.config.vncBitrateKbps,
videoCodec: Utils?.normalizeVideoCodec ? Utils.normalizeVideoCodec("H264") : "H264",
// x11grab needs to be set as the input format for ffmpeg
customHeaders: undefined,
inputFormat: "x11grab",
inputSize: this.config.vncResolution,
},
(this.controller = new AbortController()).signal,
);
const [w, h] = this.config.vncResolution.split("x").map((n) => parseInt(n, 10));
this.controller = new AbortController();
// Grab the VNC X display with ffmpeg's x11grab. customInputOptions are
// placed before `-i <display>`, exactly as x11grab requires.
const options: any = {
width: w || 1920,
height: h || 1080,
frameRate: this.config.vncFramerate,
videoCodec: "H264",
bitrateVideo: this.config.vncBitrateKbps,
bitrateVideoMax: Math.round(this.config.vncBitrateKbps * 1.5),
hardwareAcceleratedDecoding: this.config.streamHw,
minimizeLatency: true,
customInputOptions: [
"-f", "x11grab",
"-video_size", this.config.vncResolution,
"-framerate", String(this.config.vncFramerate),
],
};
// NVENC hardware encode on the RTX 5050 when enabled.
if (this.config.streamHw && nvenc) options.encoder = nvenc;
const { command, output } = prepareStream(
this.config.vncDisplay,
options,
this.controller.signal,
);
command.on("error", (err: Error) => {
if (!this.controller?.signal.aborted) console.error("[selfbot] ffmpeg error:", err);
});
this.active = true;
// Fire-and-forget; resolves when the stream ends.
playStream(output, this.streamer, { type: "go-live" })
playStream(output, this.streamer, { type: "go-live" }, this.controller.signal)
.catch((err: Error) => console.error("[selfbot] playStream:", err))
.finally(() => {
this.active = false;