feat: analiz zincirini atlayıp ham segmenti doğrudan Telegram'a gönderme modu
Yeni "Ham Kayıt Modu" ayarı (send_raw_segments_to_telegram, varsayılan
kapalı): açıksa her ham segment tamamlanır tamamlanmaz sinyal analizi/
STT/9:16 render zincirine hiç girmeden doğrudan Telegram'a gönderiliyor
ve sunucudan siliniyor. Kullanıcı transkript/9:16 istemediğinde ("sen
bana direk videoyu gönder") tüm analiz pipeline'ını atlayıp sadece
kayıt+gönder+sil yapan basit bir mod.
- RawSegmentStatus'a SENT_TO_TELEGRAM eklendi, RawSegment.filePath
nullable yapıldı (aynı ShortVideo.filePath deseni — gönderim sonrası
null, dosya diskten silinmiş demek).
- api-daemon/src/telegram.ts'e sendTelegramVideo eklendi (native fetch+
FormData+Blob ile multipart upload, ~50MB bot-limiti önceden kontrol
ediliyor) — worker/telegram.py'deki aynı fonksiyonun Node karşılığı.
- streamIngest.ts: segment tamamlanınca, mod açıksa signal-detection
kuyruğuna hiç eklemeden doğrudan gönderiyor (kuyruğa eklemek, aşağıda
dosyayı silmenin sinyal analizini bozmasına sebep olurdu).
- Panel: segment listelerinde filePath=null durumunda video/indir yerine
"Telegram'a gönderildi" notu (segments, channels/[id]).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { spawn, type ChildProcess } from "node:child_process";
|
||||
import { mkdir, readFile } from "node:fs/promises";
|
||||
import { mkdir, readFile, unlink } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { Worker, type Job } from "bullmq";
|
||||
import { prisma } from "@streamclipper/db";
|
||||
@@ -7,6 +7,7 @@ import { env } from "../env";
|
||||
import { QUEUE_NAMES, signalDetectionQueue, type StreamIngestJob } from "../queues";
|
||||
import { redisConnection } from "../redis";
|
||||
import { ytdlpAntiBotArgs } from "../ytdlpCookies";
|
||||
import { sendTelegramVideo } from "../telegram";
|
||||
|
||||
const SEGMENT_LIST_POLL_MS = 5_000;
|
||||
|
||||
@@ -59,13 +60,20 @@ function parseSegmentListLines(raw: string): SegmentListEntry[] {
|
||||
});
|
||||
}
|
||||
|
||||
async function sendRawSegmentsToTelegramEnabled(): Promise<boolean> {
|
||||
const setting = await prisma.appSetting.findUnique({ where: { key: "send_raw_segments_to_telegram" } });
|
||||
return setting?.value === "true";
|
||||
}
|
||||
|
||||
async function processCompletedSegments(
|
||||
entries: SegmentListEntry[],
|
||||
fromIndex: number,
|
||||
outDir: string,
|
||||
sessionId: string,
|
||||
channelName: string,
|
||||
): Promise<number> {
|
||||
let processed = fromIndex;
|
||||
const sendRawToTelegram = await sendRawSegmentsToTelegramEnabled();
|
||||
|
||||
for (let i = fromIndex; i < entries.length; i++) {
|
||||
const entry = entries[i];
|
||||
@@ -82,13 +90,29 @@ async function processCompletedSegments(
|
||||
},
|
||||
});
|
||||
|
||||
await signalDetectionQueue.add("analyze-segment", {
|
||||
rawSegmentId: segment.id,
|
||||
filePath,
|
||||
sessionId,
|
||||
});
|
||||
if (sendRawToTelegram) {
|
||||
// Kullanıcı transkript/9:16 zincirini hiç istemiyor — ham segmenti
|
||||
// analiz kuyruğuna hiç sokmadan doğrudan gönderiyoruz. Kuyruğa
|
||||
// eklemiyoruz çünkü aşağıda dosyayı silmek, o segmenti daha sonra
|
||||
// okumaya çalışacak sinyal analizini bozardı.
|
||||
const caption = `🎬 *${channelName}* — ${duration}sn ham kayıt`;
|
||||
const sent = await sendTelegramVideo(filePath, caption);
|
||||
if (sent) {
|
||||
await unlink(filePath).catch(() => {});
|
||||
await prisma.rawSegment.update({ where: { id: segment.id }, data: { status: "SENT_TO_TELEGRAM", filePath: null } });
|
||||
console.log(`[stream-ingest] segment ${entry.fileName} sent to Telegram and removed from disk`);
|
||||
} else {
|
||||
console.warn(`[stream-ingest] segment ${entry.fileName} failed to send to Telegram, kept on disk`);
|
||||
}
|
||||
} else {
|
||||
await signalDetectionQueue.add("analyze-segment", {
|
||||
rawSegmentId: segment.id,
|
||||
filePath,
|
||||
sessionId,
|
||||
});
|
||||
console.log(`[stream-ingest] segment ready: ${entry.fileName} -> raw_segment ${segment.id}`);
|
||||
}
|
||||
|
||||
console.log(`[stream-ingest] segment ready: ${entry.fileName} -> raw_segment ${segment.id}`);
|
||||
processed = i + 1;
|
||||
}
|
||||
|
||||
@@ -97,6 +121,8 @@ async function processCompletedSegments(
|
||||
|
||||
async function runCapture(job: Job<StreamIngestJob>): Promise<void> {
|
||||
const { sessionId, youtubeUrl, segmentTimeSec, channelDbId } = job.data;
|
||||
const channel = await prisma.channel.findUnique({ where: { id: channelDbId } });
|
||||
const channelName = channel?.name ?? "Bilinmeyen kanal";
|
||||
const outDir = path.join(env.sharedMediaRoot, "raw", sessionId);
|
||||
await mkdir(outDir, { recursive: true });
|
||||
|
||||
@@ -150,7 +176,7 @@ async function runCapture(job: Job<StreamIngestJob>): Promise<void> {
|
||||
if (!raw) return;
|
||||
const entries = parseSegmentListLines(raw);
|
||||
if (entries.length > lastProcessedIndex) {
|
||||
lastProcessedIndex = await processCompletedSegments(entries, lastProcessedIndex, outDir, sessionId);
|
||||
lastProcessedIndex = await processCompletedSegments(entries, lastProcessedIndex, outDir, sessionId, channelName);
|
||||
await prisma.streamSession.update({
|
||||
where: { id: sessionId },
|
||||
data: { totalSegments: lastProcessedIndex },
|
||||
@@ -178,7 +204,7 @@ async function runCapture(job: Job<StreamIngestJob>): Promise<void> {
|
||||
const raw = await readFile(segmentListPath, "utf8").catch(() => "");
|
||||
const entries = parseSegmentListLines(raw);
|
||||
if (entries.length > lastProcessedIndex) {
|
||||
lastProcessedIndex = await processCompletedSegments(entries, lastProcessedIndex, outDir, sessionId);
|
||||
lastProcessedIndex = await processCompletedSegments(entries, lastProcessedIndex, outDir, sessionId, channelName);
|
||||
}
|
||||
|
||||
await prisma.streamSession.update({
|
||||
|
||||
@@ -32,7 +32,7 @@ async function cleanupOnce(): Promise<void> {
|
||||
for (const segment of staleSegments) {
|
||||
await prisma.candidateSegment.deleteMany({ where: { rawSegmentId: segment.id } });
|
||||
await prisma.rawSegment.delete({ where: { id: segment.id } });
|
||||
await unlink(segment.filePath).catch(() => {});
|
||||
if (segment.filePath) await unlink(segment.filePath).catch(() => {});
|
||||
}
|
||||
|
||||
if (staleSegments.length > 0) {
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { readFile, stat } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
const BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN ?? "";
|
||||
const CHAT_ID = process.env.TELEGRAM_CHAT_ID ?? "";
|
||||
|
||||
// Telegram's bot-upload limit (no local Bot API server).
|
||||
const MAX_VIDEO_BYTES = 50 * 1024 * 1024;
|
||||
|
||||
export async function sendTelegramMessage(text: string): Promise<void> {
|
||||
if (!BOT_TOKEN || !CHAT_ID) {
|
||||
console.warn("[telegram] TELEGRAM_BOT_TOKEN/TELEGRAM_CHAT_ID not set, skipping notification:", text);
|
||||
@@ -18,3 +24,40 @@ export async function sendTelegramMessage(text: string): Promise<void> {
|
||||
console.error("[telegram] failed to send message:", res.status, await res.text());
|
||||
}
|
||||
}
|
||||
|
||||
/** Uploads a video file to Telegram. Returns false (never throws) on any failure — the caller decides what that means for the local file. */
|
||||
export async function sendTelegramVideo(filePath: string, caption: string): Promise<boolean> {
|
||||
if (!BOT_TOKEN || !CHAT_ID) {
|
||||
console.warn("[telegram] TELEGRAM_BOT_TOKEN/TELEGRAM_CHAT_ID not set, skipping video:", caption);
|
||||
return false;
|
||||
}
|
||||
|
||||
const { size } = await stat(filePath);
|
||||
if (size > MAX_VIDEO_BYTES) {
|
||||
console.warn(`[telegram] video ${filePath} is ${size} bytes, over Telegram's bot-upload limit — skipping`);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const buffer = await readFile(filePath);
|
||||
const form = new FormData();
|
||||
form.set("chat_id", CHAT_ID);
|
||||
form.set("caption", caption);
|
||||
form.set("parse_mode", "Markdown");
|
||||
form.set("video", new Blob([buffer], { type: "video/mp4" }), path.basename(filePath));
|
||||
|
||||
const res = await fetch(`https://api.telegram.org/bot${BOT_TOKEN}/sendVideo`, {
|
||||
method: "POST",
|
||||
body: form,
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
console.error("[telegram] failed to send video:", res.status, await res.text());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error("[telegram] failed to upload video:", err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user