diff --git a/apps/api-daemon/src/youtubePolling.ts b/apps/api-daemon/src/youtubePolling.ts index e05188e..c1f96f6 100644 --- a/apps/api-daemon/src/youtubePolling.ts +++ b/apps/api-daemon/src/youtubePolling.ts @@ -10,9 +10,18 @@ const execFileAsync = promisify(execFile); /** * Live detection failures are kept per channel and surfaced via GET /status - * for debugging. + * for debugging. `at` lets the panel show how stale an error is — a message + * on its own doesn't say whether it's from the last poll or from hours ago + * before the channel started passing again. */ -export const lastPollErrors = new Map(); +export interface PollError { + message: string; + at: string; +} +export const lastPollErrors = new Map(); + +/** Non-error outcomes yt-dlp still reports as a non-zero exit — not worth surfacing as a scary error badge in the panel. */ +const BENIGN_POLL_MESSAGE = /is not currently live|will begin in/; /** * A few approaches were tried before landing here: @@ -57,11 +66,10 @@ async function findLiveVideoId(channelId: string): Promise { return videoId || null; } catch (err) { const message = err instanceof Error ? err.message : String(err); - // yt-dlp reports "not live" as a non-zero exit too — that's a normal, - // expected outcome on every check where the channel isn't broadcasting, - // not a failure worth surfacing as an error in the panel. - if (!/is not currently live/.test(message)) { - lastPollErrors.set(channelId, message.slice(0, 500)); + // yt-dlp reports "not live" / "starts in N minutes" as a non-zero exit + // too — normal, expected outcomes, not failures worth a red badge. + if (!BENIGN_POLL_MESSAGE.test(message)) { + lastPollErrors.set(channelId, { message: message.slice(0, 500), at: new Date().toISOString() }); } else { lastPollErrors.delete(channelId); } @@ -107,7 +115,7 @@ export async function startCaptureSession( /** Runs the live-check for a single channel and applies its result — the unit both `pollOnce` and the panel's manual "şimdi kontrol et" action reuse. */ export async function checkChannel(channel: { id: string; name: string; channelId: string }): Promise<{ liveVideoId: string | null; - error: string | null; + error: PollError | null; }> { const liveVideoId = await findLiveVideoId(channel.channelId); await prisma.channel.update({ diff --git a/apps/frontend/app/actions.ts b/apps/frontend/app/actions.ts index f90371c..57af7f3 100644 --- a/apps/frontend/app/actions.ts +++ b/apps/frontend/app/actions.ts @@ -136,6 +136,16 @@ export async function updateSystemSettings(formData: FormData) { revalidatePath("/settings"); } +export async function updateSttEnabled(formData: FormData) { + const enabled = formData.get("sttEnabled") === "on"; + await prisma.appSetting.upsert({ + where: { key: "stt_enabled" }, + create: { key: "stt_enabled", value: String(enabled) }, + update: { value: String(enabled) }, + }); + revalidatePath("/settings"); +} + export async function updateNotificationPrefs(formData: FormData) { const streamStart = formData.get("notifyStreamStart") === "on"; const renderDone = formData.get("notifyRenderDone") === "on"; diff --git a/apps/frontend/app/channels/[id]/page.tsx b/apps/frontend/app/channels/[id]/page.tsx index bca0aba..2fc4131 100644 --- a/apps/frontend/app/channels/[id]/page.tsx +++ b/apps/frontend/app/channels/[id]/page.tsx @@ -122,11 +122,16 @@ export default async function ChannelDetailPage({ params }: { params: Promise<{ {status?.lastPollError && (
- - son poll hatası - +
+ + son poll hatası + + + {new Date(status.lastPollError.at).toLocaleString("tr-TR")} + +
-                {status.lastPollError}
+                {status.lastPollError.message}
               
)} diff --git a/apps/frontend/app/page.tsx b/apps/frontend/app/page.tsx index 1217410..cb0e1a9 100644 --- a/apps/frontend/app/page.tsx +++ b/apps/frontend/app/page.tsx @@ -91,7 +91,14 @@ export default async function DashboardPage() {
{recording ? "🔴 Kayıtta" : "—"} - {status?.lastPollError && hata} + {status?.lastPollError && ( + + hata + + )}
diff --git a/apps/frontend/app/settings/page.tsx b/apps/frontend/app/settings/page.tsx index 05ad413..bf93d09 100644 --- a/apps/frontend/app/settings/page.tsx +++ b/apps/frontend/app/settings/page.tsx @@ -1,5 +1,5 @@ import { prisma } from "@streamclipper/db"; -import { updateYtdlpCookies, updateSystemSettings, updateNotificationPrefs } from "../actions"; +import { updateYtdlpCookies, updateSystemSettings, updateNotificationPrefs, updateSttEnabled } from "../actions"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; @@ -13,12 +13,13 @@ export const dynamic = "force-dynamic"; const STALE_COOKIE_HOURS = 4; export default async function SettingsPage() { - const [setting, pollSetting, ttlSetting, notifyStart, notifyRender] = await Promise.all([ + const [setting, pollSetting, ttlSetting, notifyStart, notifyRender, sttSetting] = await Promise.all([ prisma.appSetting.findUnique({ where: { key: "ytdlp_cookies" } }), prisma.appSetting.findUnique({ where: { key: "poll_interval_ms" } }), prisma.appSetting.findUnique({ where: { key: "raw_segment_ttl_hours" } }), prisma.appSetting.findUnique({ where: { key: "notify_stream_start" } }), prisma.appSetting.findUnique({ where: { key: "notify_render_done" } }), + prisma.appSetting.findUnique({ where: { key: "stt_enabled" } }), ]); const cookieAgeHours = setting ? (Date.now() - setting.updatedAt.getTime()) / 3_600_000 : null; @@ -27,6 +28,7 @@ export default async function SettingsPage() { const ttlHours = ttlSetting ? Number(ttlSetting.value) : 24; const notifyStreamStart = notifyStart?.value !== "false"; const notifyRenderDone = notifyRender?.value !== "false"; + const sttEnabled = sttSetting?.value !== "false"; return (
@@ -95,6 +97,28 @@ export default async function SettingsPage() { + + + STT (Transkripsiyon) + + Kapatırsan yeni aday klipler transkribe edilmez (PENDING_STT'de bekler) — OpenAI Whisper çağrısı + yapılmaz, render de tetiklenmez. Boş/konuşmasız yayınlarda (ör. sadece ambiyans müzik) gereksiz API + maliyetini önlemek için kapatabilirsin. + + +
+ + + + + + +
+
+ Bildirimler diff --git a/apps/frontend/lib/apiDaemon.ts b/apps/frontend/lib/apiDaemon.ts index 4618039..a39feab 100644 --- a/apps/frontend/lib/apiDaemon.ts +++ b/apps/frontend/lib/apiDaemon.ts @@ -5,7 +5,7 @@ export interface ChannelStatus { lastCheckedAt: string | null; recording: boolean; activeSessionId: string | null; - lastPollError: string | null; + lastPollError: { message: string; at: string } | null; } export const API_DAEMON_URL = process.env.API_DAEMON_URL ?? "http://localhost:4001"; diff --git a/apps/worker/worker/stt_scoring.py b/apps/worker/worker/stt_scoring.py index 9fc132f..7407964 100644 --- a/apps/worker/worker/stt_scoring.py +++ b/apps/worker/worker/stt_scoring.py @@ -39,6 +39,10 @@ async def process_stt_scoring(job, job_token=None): print(f"[stt-scoring] candidate {candidate_id} not found, skipping") return + if await db.get_setting("stt_enabled", "true") == "false": + print(f"[stt-scoring] STT panelden kapatılmış, {candidate_id} atlanıyor (PENDING_STT'de kalır)") + return + with tempfile.NamedTemporaryFile(suffix=".mp3") as clip: subprocess.run( [