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>
162 lines
7.7 KiB
TypeScript
162 lines
7.7 KiB
TypeScript
import { prisma } from "@streamclipper/db";
|
||
import { deleteRawSegment, deleteCandidateSegment, retryRender, cancelRender } from "../actions";
|
||
import { formatTr } from "../../lib/formatDate";
|
||
import { ConfirmButton } from "../components/ConfirmButton";
|
||
import { Button } from "@/components/ui/button";
|
||
import { Badge, type badgeVariants } from "@/components/ui/badge";
|
||
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||
import type { VariantProps } from "class-variance-authority";
|
||
|
||
export const dynamic = "force-dynamic";
|
||
|
||
const STATUS_BADGE: Record<string, VariantProps<typeof badgeVariants>["variant"]> = {
|
||
PENDING: "muted",
|
||
PROCESSED: "ok",
|
||
DISCARDED: "err",
|
||
SENT_TO_TELEGRAM: "ok",
|
||
PENDING_STT: "warn",
|
||
TRANSCRIBED: "ok",
|
||
RENDERING: "warn",
|
||
READY: "ok",
|
||
FAILED: "err",
|
||
};
|
||
|
||
function transcriptPreview(transcriptJson: unknown): string | null {
|
||
if (!transcriptJson || typeof transcriptJson !== "object") return null;
|
||
const text = (transcriptJson as { text?: string }).text;
|
||
return text ? text.slice(0, 240) : null;
|
||
}
|
||
|
||
export default async function SegmentsPage() {
|
||
const segments = await prisma.rawSegment.findMany({
|
||
orderBy: { createdAt: "desc" },
|
||
take: 50,
|
||
include: { candidates: { orderBy: { createdAt: "desc" }, include: { short: true } } },
|
||
});
|
||
|
||
return (
|
||
<div className="flex flex-col gap-6">
|
||
<h1 className="text-2xl font-semibold tracking-tight">Segment & Aday Klip Kütüphanesi</h1>
|
||
|
||
{segments.length === 0 ? (
|
||
<p className="text-sm text-muted-foreground">Henüz işlenmiş segment yok.</p>
|
||
) : (
|
||
<div className="flex flex-col gap-4">
|
||
{segments.map((segment) => (
|
||
<Card key={segment.id}>
|
||
<CardHeader className="flex-row items-center justify-between">
|
||
<div>
|
||
<span className="font-mono-num text-xs text-muted-foreground">{segment.filePath}</span>
|
||
<p className="font-mono-num text-xs text-muted-foreground">
|
||
{segment.duration}sn · {formatTr(segment.startedAt)}
|
||
</p>
|
||
</div>
|
||
<Badge variant={STATUS_BADGE[segment.status] ?? "muted"}>{segment.status}</Badge>
|
||
</CardHeader>
|
||
<CardContent className="flex flex-col gap-3">
|
||
{segment.filePath ? (
|
||
<>
|
||
<video controls preload="metadata" className="w-full max-w-md rounded-md border border-border">
|
||
<source src={`/api/segments/${segment.id}/video`} type="video/mp4" />
|
||
</video>
|
||
<div className="flex items-center gap-2">
|
||
<Button asChild variant="outline" size="sm">
|
||
<a href={`/api/segments/${segment.id}/video?download=1`}>İndir</a>
|
||
</Button>
|
||
<form action={deleteRawSegment.bind(null, segment.id)}>
|
||
<ConfirmButton
|
||
confirmText="Bu segmenti ve içindeki tüm aday klipleri silmek istediğine emin misin? Geri alınamaz."
|
||
label="Sil"
|
||
/>
|
||
</form>
|
||
</div>
|
||
</>
|
||
) : (
|
||
<p className="text-xs text-muted-foreground">✅ Telegram'a gönderildi, sunucuda saklanmıyor.</p>
|
||
)}
|
||
|
||
{segment.status === "SENT_TO_TELEGRAM" ? (
|
||
<p className="text-sm text-muted-foreground">
|
||
Bu segment analiz edilmeden (transkript/9:16 atlanarak) doğrudan Telegram'a gönderildi.
|
||
</p>
|
||
) : segment.candidates.length === 0 ? (
|
||
<p className="text-sm text-muted-foreground">Bu segmentte aday klip bulunamadı.</p>
|
||
) : (
|
||
segment.candidates.map((c) => (
|
||
<div key={c.id} className="border-l-2 border-border pl-4">
|
||
<div className="flex items-center justify-between gap-2">
|
||
<span className="font-mono-num text-xs text-muted-foreground">
|
||
{c.startSec}s – {c.endSec}s
|
||
{c.audioPeakScore != null && ` · peak ${c.audioPeakScore.toFixed(1)}dB`}
|
||
{c.chatVelocityScore != null && " · chat spike"}
|
||
</span>
|
||
<Badge variant={STATUS_BADGE[c.status] ?? "muted"}>{c.status}</Badge>
|
||
</div>
|
||
{transcriptPreview(c.transcriptJson) && (
|
||
<p className="mt-1.5 text-sm leading-relaxed">{transcriptPreview(c.transcriptJson)}</p>
|
||
)}
|
||
|
||
{c.short && (
|
||
<div className="mt-2 flex flex-col items-start gap-2">
|
||
<Badge variant={STATUS_BADGE[c.short.status] ?? "muted"}>9:16: {c.short.status}</Badge>
|
||
{c.short.status === "READY" && c.short.filePath && (
|
||
<>
|
||
<video controls preload="metadata" className="w-[220px] rounded-md border border-border">
|
||
<source src={`/api/shorts/${c.short.id}/video`} type="video/mp4" />
|
||
</video>
|
||
<Button asChild variant="outline" size="sm">
|
||
<a href={`/api/shorts/${c.short.id}/video?download=1`}>İndir</a>
|
||
</Button>
|
||
</>
|
||
)}
|
||
{c.short.status === "READY" && !c.short.filePath && (
|
||
<p className="text-xs text-muted-foreground">
|
||
✅ Telegram'a gönderildi, sunucuda saklanmıyor.
|
||
</p>
|
||
)}
|
||
{c.short.status === "FAILED" && (
|
||
<>
|
||
{c.short.errorMessage && (
|
||
<p className="font-mono-num text-xs text-muted-foreground">{c.short.errorMessage}</p>
|
||
)}
|
||
<form action={retryRender.bind(null, c.id)}>
|
||
<Button type="submit" size="sm">
|
||
Tekrar Dene
|
||
</Button>
|
||
</form>
|
||
</>
|
||
)}
|
||
{c.short.status === "RENDERING" && (
|
||
<form action={cancelRender.bind(null, c.id)}>
|
||
<ConfirmButton
|
||
confirmText="Render'ı başarısız say ve kilidi aç? (Çalışan process'i öldürmez, sadece durumu sıfırlar)"
|
||
label="Takıldıysa İptal Et"
|
||
variant="outline"
|
||
/>
|
||
</form>
|
||
)}
|
||
</div>
|
||
)}
|
||
{!c.short && c.status === "TRANSCRIBED" && (
|
||
<form action={retryRender.bind(null, c.id)} className="mt-2">
|
||
<Button type="submit" size="sm">
|
||
9:16 Render Et
|
||
</Button>
|
||
</form>
|
||
)}
|
||
|
||
<form action={deleteCandidateSegment.bind(null, c.id)} className="mt-2">
|
||
<ConfirmButton confirmText="Bu aday klibi silmek istediğine emin misin?" label="Sil" />
|
||
</form>
|
||
</div>
|
||
))
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|