feat: 9:16 render'ı isteğe bağlı yap + Telegram'a video gönderip sunucudan sil (Faz 5)

- auto_render_enabled (varsayılan açık): kapatılınca transkript sonrası
  otomatik render tetiklenmez, aday TRANSCRIBED'de bekler — panelin zaten
  var olan "9:16 Render Et" butonuyla elle tetiklenebilir.
- delete_after_telegram_send (varsayılan KAPALI — geri alınamaz bir
  davranış, bilinçli açılmalı): açıksa render biten video Telegram'a
  gerçek dosya olarak gönderilir (yeni send_telegram_video, sendVideo
  multipart upload, ~50MB bot-upload limiti önceden kontrol ediliyor);
  gönderim başarılıysa dosya diskten silinip ShortVideo.file_path NULL'a
  çekiliyor (DB kaydı/transkript kalıcı kalıyor), başarısızsa dosya
  sunucuda kalıp düz metin bildirimi gidiyor. Kanal sayısı arttıkça VPS
  disk doluluğunu önlemek için.
- Panel: READY + file_path=null durumunda video/indir yerine "Telegram'a
  gönderildi, sunucuda saklanmıyor" notu (channels/[id], segments).

Şema değişikliği yok — ShortVideo.filePath zaten nullable.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-04 01:04:57 +03:00
co-authored by Claude Sonnet 5
parent 63443f3392
commit eb2a9d28ed
8 changed files with 167 additions and 17 deletions
+9
View File
@@ -158,3 +158,12 @@ async def update_short_video(
file_path,
error_message,
)
async def clear_short_video_file(short_id: str) -> None:
"""update_short_video's COALESCE keeps the old file_path when passed None — this explicitly nulls it out once the rendered file has been deleted from disk (e.g. after a successful Telegram delivery)."""
pool = await get_pool()
await pool.execute(
"UPDATE short_videos SET file_path = NULL, updated_at = now() WHERE id = $1",
short_id,
)
+4 -1
View File
@@ -72,7 +72,10 @@ async def process_stt_scoring(job, job_token=None):
f"({candidate['start_sec']}-{candidate['end_sec']}s):\n_{preview}_"
)
await video_render_queue.add("render-short", {"candidateSegmentId": candidate_id})
if await db.get_setting("auto_render_enabled", "true") != "false":
await video_render_queue.add("render-short", {"candidateSegmentId": candidate_id})
else:
print(f"[stt-scoring] auto-render kapalı, {candidate_id} render kuyruğuna eklenmedi (panelden manuel tetiklenebilir)")
print(f"[stt-scoring] candidate {candidate_id} transcribed")
+37
View File
@@ -5,6 +5,11 @@ import httpx
BOT_TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN", "")
CHAT_ID = os.environ.get("TELEGRAM_CHAT_ID", "")
# Telegram's bot-upload limit (no local Bot API server) — checked before
# attempting an upload so an oversized file fails fast instead of burning
# time/bandwidth on a request Telegram will reject anyway.
MAX_VIDEO_BYTES = 50 * 1024 * 1024
async def send_telegram_message(text: str) -> None:
if not BOT_TOKEN or not CHAT_ID:
@@ -18,3 +23,35 @@ async def send_telegram_message(text: str) -> None:
)
if resp.status_code >= 400:
print(f"[telegram] failed to send message: {resp.status_code} {resp.text}")
async def send_telegram_video(video_path: str, caption: str) -> bool:
"""Uploads the actual rendered clip to Telegram. Returns False (without
raising) on any failure — the caller decides what that means for the
local file (e.g. keep it if delivery didn't succeed)."""
if not BOT_TOKEN or not CHAT_ID:
print(f"[telegram] not configured, skipping video: {caption}")
return False
size = os.path.getsize(video_path)
if size > MAX_VIDEO_BYTES:
print(f"[telegram] video {video_path} is {size} bytes, over Telegram's bot-upload limit — skipping")
return False
url = f"https://api.telegram.org/bot{BOT_TOKEN}/sendVideo"
try:
async with httpx.AsyncClient(timeout=120.0) as client:
with open(video_path, "rb") as f:
resp = await client.post(
url,
data={"chat_id": CHAT_ID, "caption": caption, "parse_mode": "Markdown"},
files={"video": (os.path.basename(video_path), f, "video/mp4")},
)
except httpx.HTTPError as exc:
print(f"[telegram] failed to upload video: {exc}")
return False
if resp.status_code >= 400:
print(f"[telegram] failed to send video: {resp.status_code} {resp.text}")
return False
return True
+19 -4
View File
@@ -25,7 +25,7 @@ import mediapipe as mp
from . import db
from .queues import QUEUE_NAMES, REDIS_URL, SHARED_MEDIA_ROOT, VIDEO_RENDER_CONCURRENCY
from .telegram import send_telegram_message
from .telegram import send_telegram_message, send_telegram_video
FACE_MODEL_PATH = os.environ.get(
"FACE_DETECTOR_MODEL_PATH", "/app/models/blaze_face_short_range.tflite"
@@ -245,9 +245,24 @@ async def process_video_render(job, job_token=None):
)
await db.update_short_video(short_id, status="READY", file_path=out_path)
if await db.get_setting("notify_render_done", "true") != "false":
await send_telegram_message(f"🎬 *{candidate['channel_name']}* için 9:16 kısa video hazır!")
print(f"[video-render] short {short_id} ready: {out_path}")
caption = f"🎬 *{candidate['channel_name']}* için 9:16 kısa video hazır!"
if await db.get_setting("delete_after_telegram_send", "false") == "true":
# Video delivery *is* the notification here — it doesn't also
# gate on notify_render_done. Only delete the local file once
# Telegram actually has it; a failed upload leaves it in place
# with a plain-text heads-up instead of silently losing it.
if await send_telegram_video(out_path, caption):
os.remove(out_path)
await db.clear_short_video_file(short_id)
print(f"[video-render] short {short_id} sent to Telegram and removed from disk")
else:
await send_telegram_message(f"{caption}\n(video Telegram'a gönderilemedi, sunucuda kaldı)")
print(f"[video-render] short {short_id} ready: {out_path}")
else:
if await db.get_setting("notify_render_done", "true") != "false":
await send_telegram_message(caption)
print(f"[video-render] short {short_id} ready: {out_path}")
except Exception as exc:
print(f"[video-render] failed for candidate {candidate_id}: {exc}")
await db.update_short_video(short_id, status="FAILED", error_message=str(exc)[:500])