From 2e23859230785f8c30ea588c317f1604af4c40ac Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 11 Jun 2026 09:56:28 +0000 Subject: [PATCH] Add KapsamKafe Viral Radar & Caption Agent notebook TikTok trend scraping (TikTokApi, no API key required) + viral score calculation, faster-whisper Turkish SRT generation with NLLB-200 auto-translation, FFmpeg subtitle burning, and Groq/Llama pipeline integration helpers. MS_TOKEN defaults to empty for tokenless operation. https://claude.ai/code/session_013TF22qpcBPefLY1117YLdP --- KapsamKafe_ViralRadar_CaptionAgent.ipynb | 594 +++++++++++++++++++++++ 1 file changed, 594 insertions(+) create mode 100644 KapsamKafe_ViralRadar_CaptionAgent.ipynb diff --git a/KapsamKafe_ViralRadar_CaptionAgent.ipynb b/KapsamKafe_ViralRadar_CaptionAgent.ipynb new file mode 100644 index 0000000..f63946d --- /dev/null +++ b/KapsamKafe_ViralRadar_CaptionAgent.ipynb @@ -0,0 +1,594 @@ +{ + "nbformat": 4, + "nbformat_minor": 5, + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.10.0" + }, + "colab": { + "provenance": [], + "gpuType": "T4" + }, + "accelerator": "GPU" + }, + "cells": [ + { + "cell_type": "markdown", + "id": "title-cell", + "metadata": {}, + "source": [ + "# ☕ KapsamKafe — Viral Radar & Caption Agent\n", + "\n", + "**İki modül:**\n", + "- 📡 **Viral Radar** — TikTok'tan hashtag/kullanıcı/trending/arama bazlı video verisi çeker, viral skor hesaplar\n", + "- 🎙️ **Caption Agent** — faster-whisper ile Türkçe SRT üretir, yabancı dilli videolarda NLLB-200 ile otomatik çevirir\n", + "- 🎬 **Bonus** — FFmpeg ile altyazıyı videoya gömer\n", + "\n", + "> **Runtime:** `Runtime → Change runtime type → T4 GPU` seç (Caption Agent için önemli) \n", + "> **Token:** `MS_TOKEN` boş bırakılabilir — sorun çıkarsa aşağıdaki talimatı takip et" + ] + }, + { + "cell_type": "markdown", + "id": "setup-header", + "metadata": {}, + "source": [ + "---\n", + "## 0. Kurulum" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "install-deps", + "metadata": {}, + "outputs": [], + "source": [ + "# Tüm bağımlılıkları yükle\n", + "!pip install -q TikTokApi playwright faster-whisper transformers sentencepiece ffmpeg-python\n", + "!python -m playwright install chromium\n", + "!apt-get install -y -q ffmpeg 2>/dev/null || echo 'ffmpeg zaten yüklü'\n", + "import os\n", + "os.makedirs('output', exist_ok=True)\n", + "print('✅ Kurulum tamamlandı')" + ] + }, + { + "cell_type": "markdown", + "id": "mstoken-header", + "metadata": {}, + "source": [ + "---\n", + "## 1. Yapılandırma\n", + "\n", + "**msToken alma (gerekirse):**\n", + "1. Bilgisayardan `tiktok.com`'a giriş yap\n", + "2. F12 → Application → Cookies → `msToken` değerini kopyala\n", + "3. Aşağıdaki `MS_TOKEN = \"\"` satırına yapıştır\n", + "\n", + "**Not:** Boş bırakılırsa hashtag/kullanıcı modları genellikle çalışır." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "config", + "metadata": {}, + "outputs": [], + "source": [ + "# ─── AYARLAR ───────────────────────────────────────────────────────────────\n", + "MS_TOKEN = \"\" # Boş bırak, hata alırsan tiktok.com'dan kopyala\n", + "\n", + "# Viral Radar varsayılan modu: 'hashtag' | 'user' | 'trending' | 'search'\n", + "DEFAULT_MODE = \"hashtag\"\n", + "DEFAULT_QUERY = \"kahve\" # hashtag/user/search için kullanılır\n", + "VIDEO_COUNT = 30 # çekilecek video sayısı\n", + "\n", + "# Caption Agent\n", + "WHISPER_MODEL = \"medium\" # tiny/base/small/medium/large-v3\n", + "WHISPER_LANG = \"tr\" # None → otomatik algıla\n", + "TRANSLATE_TO_TR = True # Türkçe değilse otomatik çevir\n", + "# ───────────────────────────────────────────────────────────────────────────\n", + "print('✅ Yapılandırma yüklendi')" + ] + }, + { + "cell_type": "markdown", + "id": "viral-header", + "metadata": {}, + "source": [ + "---\n", + "## 2. 📡 Viral Radar" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "viral-functions", + "metadata": {}, + "outputs": [], + "source": [ + "import asyncio, json, time, math\n", + "from datetime import datetime, timezone\n", + "from TikTokApi import TikTokApi\n", + "\n", + "# ─── Viral skor hesaplama ───────────────────────────────────────────────────\n", + "def viral_score(video: dict) -> float:\n", + " \"\"\"\n", + " Skor = (likes + comments*2 + shares*3) / max(views, 1)\n", + " Tazelik bonusu: 24 saatten yeni → 1.5x, 72 saatten yeni → 1.2x\n", + " \"\"\"\n", + " stats = video.get(\"stats\", {})\n", + " likes = stats.get(\"diggCount\", 0)\n", + " comments = stats.get(\"commentCount\", 0)\n", + " shares = stats.get(\"shareCount\", 0)\n", + " views = max(stats.get(\"playCount\", 1), 1)\n", + "\n", + " engagement = (likes + comments * 2 + shares * 3) / views\n", + "\n", + " create_time = video.get(\"createTime\", 0)\n", + " age_hours = (time.time() - create_time) / 3600 if create_time else 999\n", + " if age_hours < 24:\n", + " freshness = 1.5\n", + " elif age_hours < 72:\n", + " freshness = 1.2\n", + " else:\n", + " freshness = 1.0\n", + "\n", + " return round(engagement * freshness * 100, 4)\n", + "\n", + "\n", + "def parse_video(v) -> dict:\n", + " \"\"\"TikTokApi video nesnesinden temiz sözlük üretir.\"\"\"\n", + " raw = v.as_dict if hasattr(v, 'as_dict') else v\n", + " stats = raw.get(\"stats\", {})\n", + " author = raw.get(\"author\", {})\n", + " music = raw.get(\"music\", {})\n", + " return {\n", + " \"id\": raw.get(\"id\"),\n", + " \"desc\": raw.get(\"desc\", \"\"),\n", + " \"createTime\": raw.get(\"createTime\"),\n", + " \"author\": author.get(\"uniqueId\", \"\"),\n", + " \"views\": stats.get(\"playCount\", 0),\n", + " \"likes\": stats.get(\"diggCount\", 0),\n", + " \"comments\": stats.get(\"commentCount\", 0),\n", + " \"shares\": stats.get(\"shareCount\", 0),\n", + " \"hashtags\": [h[\"hashtagName\"] for h in raw.get(\"challenges\", [])],\n", + " \"music_title\": music.get(\"title\", \"\"),\n", + " \"music_author\":music.get(\"authorName\", \"\"),\n", + " \"viral_score\": viral_score(raw),\n", + " }\n", + "\n", + "\n", + "# ─── Ana çekme fonksiyonu ───────────────────────────────────────────────────\n", + "async def fetch_videos(mode: str, query: str = \"\", count: int = 30) -> list:\n", + " ms_token = MS_TOKEN if MS_TOKEN.strip() else None\n", + " kwargs = {\"ms_tokens\": [ms_token]} if ms_token else {}\n", + "\n", + " videos = []\n", + " async with TikTokApi(**kwargs) as api:\n", + " await api.create_sessions(\n", + " ms_tokens=[ms_token] if ms_token else [],\n", + " num_sessions=1,\n", + " sleep_after=3,\n", + " headless=True,\n", + " )\n", + "\n", + " if mode == \"trending\":\n", + " async for v in api.trending.videos(count=count):\n", + " videos.append(parse_video(v))\n", + "\n", + " elif mode == \"hashtag\":\n", + " tag = api.hashtag(name=query)\n", + " async for v in tag.videos(count=count):\n", + " videos.append(parse_video(v))\n", + "\n", + " elif mode == \"user\":\n", + " user = api.user(username=query)\n", + " async for v in user.videos(count=count):\n", + " videos.append(parse_video(v))\n", + "\n", + " elif mode == \"search\":\n", + " async for v in api.search.videos(query, count=count):\n", + " videos.append(parse_video(v))\n", + "\n", + " else:\n", + " raise ValueError(f\"Geçersiz mod: {mode}. Seçenekler: trending, hashtag, user, search\")\n", + "\n", + " return sorted(videos, key=lambda x: x[\"viral_score\"], reverse=True)\n", + "\n", + "\n", + "print('✅ Viral Radar fonksiyonları yüklendi')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "viral-run", + "metadata": {}, + "outputs": [], + "source": [ + "# ─── Çalıştır ──────────────────────────────────────────────────────────────\n", + "print(f'🔍 Mod: {DEFAULT_MODE} | Sorgu: \"{DEFAULT_QUERY}\" | Sayı: {VIDEO_COUNT}')\n", + "\n", + "try:\n", + " results = await fetch_videos(DEFAULT_MODE, DEFAULT_QUERY, VIDEO_COUNT)\n", + "except Exception as e:\n", + " print(f'⚠️ Hata: {e}')\n", + " if 'EmptyResponseException' in str(type(e)) or 'msToken' in str(e).lower():\n", + " print('👉 msToken gerekebilir — tiktok.com\\'dan kopyala ve MS_TOKEN değişkenine yapıştır')\n", + " results = []\n", + "\n", + "if results:\n", + " ts = datetime.now().strftime('%Y%m%d_%H%M%S')\n", + " safe_q = DEFAULT_QUERY.replace(' ', '_')[:30]\n", + " out_path = f'output/viral_radar_{DEFAULT_MODE}_{safe_q}_{ts}.json'\n", + "\n", + " with open(out_path, 'w', encoding='utf-8') as f:\n", + " json.dump(results, f, ensure_ascii=False, indent=2)\n", + "\n", + " print(f'\\n✅ {len(results)} video kaydedildi → {out_path}')\n", + " print(f'\\n🏆 TOP 5 Viral Skor:')\n", + " for i, v in enumerate(results[:5], 1):\n", + " print(f' {i}. [{v[\"viral_score\"]:>8.4f}] @{v[\"author\"]} | 👁 {v[\"views\"]:,} | {v[\"desc\"][:60]}')\nelse:\n", + " print('Sonuç bulunamadı.')" + ] + }, + { + "cell_type": "markdown", + "id": "caption-header", + "metadata": {}, + "source": [ + "---\n", + "## 3. 🎙️ Caption Agent\n", + "\n", + "faster-whisper ile SRT üretir. Kaynak dil Türkçe değilse NLLB-200 ile otomatik çevirir." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "caption-functions", + "metadata": {}, + "outputs": [], + "source": [ + "import torch\n", + "from faster_whisper import WhisperModel\n", + "from transformers import pipeline as hf_pipeline\n", + "from pathlib import Path\n", + "\n", + "# Whisper dil kodu → FLORES-200 kodu eşlemesi\n", + "LANG_TO_FLORES = {\n", + " 'en': 'eng_Latn', 'ar': 'arb_Arab', 'de': 'deu_Latn',\n", + " 'fr': 'fra_Latn', 'es': 'spa_Latn', 'ru': 'rus_Cyrl',\n", + " 'it': 'ita_Latn', 'pt': 'por_Latn', 'ja': 'jpn_Jpan',\n", + " 'ko': 'kor_Hang', 'zh': 'zho_Hans', 'hi': 'hin_Deva',\n", + " 'fa': 'pes_Arab', 'nl': 'nld_Latn', 'tr': 'tur_Latn',\n", + "}\n", + "\n", + "device = 'cuda' if torch.cuda.is_available() else 'cpu'\n", + "compute = 'float16' if device == 'cuda' else 'int8'\n", + "print(f'⚙️ Device: {device} | Compute: {compute}')\n", + "\n", + "# ─── Modelleri yükle ───────────────────────────────────────────────────────\n", + "print(f'📥 Whisper {WHISPER_MODEL} yükleniyor...')\n", + "whisper = WhisperModel(WHISPER_MODEL, device=device, compute_type=compute)\n", + "print('✅ Whisper hazır')\n", + "\n", + "nllb = None\n", + "def get_nllb():\n", + " global nllb\n", + " if nllb is None:\n", + " print('📥 NLLB-200 yükleniyor (ilk kez ~2 dk)...')\n", + " nllb = hf_pipeline(\n", + " 'translation',\n", + " model='facebook/nllb-200-distilled-600M',\n", + " device=0 if device == 'cuda' else -1,\n", + " max_length=512,\n", + " )\n", + " print('✅ NLLB-200 hazır')\n", + " return nllb\n", + "\n", + "\n", + "# ─── Zaman biçimlendirme ────────────────────────────────────────────────────\n", + "def fmt_time(seconds: float) -> str:\n", + " h = int(seconds // 3600)\n", + " m = int((seconds % 3600) // 60)\n", + " s = int(seconds % 60)\n", + " ms = int((seconds - int(seconds)) * 1000)\n", + " return f'{h:02d}:{m:02d}:{s:02d},{ms:03d}'\n", + "\n", + "\n", + "# ─── Segment çevirisi ───────────────────────────────────────────────────────\n", + "def translate_segment(text: str, src_lang: str) -> str:\n", + " src_flores = LANG_TO_FLORES.get(src_lang)\n", + " if not src_flores or src_lang == 'tr':\n", + " return text\n", + " translator = get_nllb()\n", + " out = translator(\n", + " text,\n", + " src_lang=src_flores,\n", + " tgt_lang='tur_Latn',\n", + " )\n", + " return out[0]['translation_text']\n", + "\n", + "\n", + "# ─── Ana fonksiyon: video → SRT ────────────────────────────────────────────\n", + "def transcribe_to_srt(\n", + " video_path: str,\n", + " output_srt: str = None,\n", + " language: str = None,\n", + " translate: bool = True,\n", + ") -> str:\n", + " \"\"\"\n", + " video_path : yerel video/ses dosyası\n", + " output_srt : çıktı SRT yolu (None → otomatik)\n", + " language : 'tr', 'en' vb. (None → otomatik algıla)\n", + " translate : Türkçe değilse NLLB-200 ile çevir\n", + " \"\"\"\n", + " if output_srt is None:\n", + " stem = Path(video_path).stem\n", + " output_srt = f'output/{stem}_tr.srt'\n", + "\n", + " print(f'🎙️ Transkripsiyon: {video_path}')\n", + " segments, info = whisper.transcribe(\n", + " video_path,\n", + " language=language,\n", + " word_timestamps=True,\n", + " beam_size=5,\n", + " )\n", + " detected_lang = info.language\n", + " print(f' Tespit edilen dil: {detected_lang} (güven: {info.language_probability:.2%})')\n", + "\n", + " lines = []\n", + " seg_idx = 0\n", + " for seg in segments:\n", + " seg_idx += 1\n", + " text = seg.text.strip()\n", + " if translate and detected_lang != 'tr':\n", + " text = translate_segment(text, detected_lang)\n", + "\n", + " lines.append(str(seg_idx))\n", + " lines.append(f'{fmt_time(seg.start)} --> {fmt_time(seg.end)}')\n", + " lines.append(text)\n", + " lines.append('')\n", + "\n", + " srt_content = '\\n'.join(lines)\n", + " with open(output_srt, 'w', encoding='utf-8') as f:\n", + " f.write(srt_content)\n", + "\n", + " print(f'✅ SRT kaydedildi → {output_srt} ({seg_idx} segment)')\n", + " return output_srt\n", + "\n", + "\n", + "print('✅ Caption Agent fonksiyonları yüklendi')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "caption-run", + "metadata": {}, + "outputs": [], + "source": [ + "# ─── Kullanım ──────────────────────────────────────────────────────────────\n", + "# VIDEO_PATH değişkenini kendi video dosyana göre düzenle\n", + "VIDEO_PATH = '/content/sample_video.mp4' # ← buraya kendi dosyanı yaz\n", + "\n", + "if not Path(VIDEO_PATH).exists():\n", + " print(f'⚠️ Dosya bulunamadı: {VIDEO_PATH}')\n", + " print(' Colab sol panelinden dosya yükle veya yolu düzelt')\n", + "else:\n", + " srt_path = transcribe_to_srt(\n", + " video_path=VIDEO_PATH,\n", + " language=WHISPER_LANG if WHISPER_LANG else None,\n", + " translate=TRANSLATE_TO_TR,\n", + " )\n", + " print(f'\\n📄 SRT önizleme (ilk 5 segment):')\n", + " with open(srt_path, encoding='utf-8') as f:\n", + " lines = f.readlines()\n", + " print(''.join(lines[:20]))" + ] + }, + { + "cell_type": "markdown", + "id": "ffmpeg-header", + "metadata": {}, + "source": [ + "---\n", + "## 4. 🎬 Bonus — FFmpeg ile Altyazı Gömme\n", + "\n", + "Bebas Neue font, beyaz metin + gold outline (Mozalp tasarım sistemine uygun)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ffmpeg-setup", + "metadata": {}, + "outputs": [], + "source": [ + "# Bebas Neue font indir\n", + "!wget -q -O /tmp/BebasNeue-Regular.ttf \\\n", + " 'https://github.com/dharmatype/Bebas-Neue/raw/master/fonts/BebasNeue(2019)ByDhamraType/ttf/BebasNeue-Regular.ttf' \\\n", + " && echo '✅ Bebas Neue hazır' || echo 'Font indirilemedi, sistem fontuna geçiliyor'" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ffmpeg-functions", + "metadata": {}, + "outputs": [], + "source": [ + "import subprocess\n", + "\n", + "FONT_PATH = '/tmp/BebasNeue-Regular.ttf'\n", + "if not Path(FONT_PATH).exists():\n", + " FONT_PATH = '/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf'\n", + "\n", + "def burn_subtitles(\n", + " video_path: str,\n", + " srt_path: str,\n", + " output_path: str = None,\n", + " font_size: int = 22,\n", + ") -> str:\n", + " \"\"\"\n", + " SRT'yi video üzerine yazar.\n", + " Beyaz metin, gold (#FFD700) outline — Mozalp stil.\n", + " \"\"\"\n", + " if output_path is None:\n", + " stem = Path(video_path).stem\n", + " output_path = f'output/{stem}_subtitled.mp4'\n", + "\n", + " style = (\n", + " f\"FontName=Bebas Neue,\"\n", + " f\"FontSize={font_size},\"\n", + " f\"PrimaryColour=&H00FFFFFF,\"\n", + " f\"OutlineColour=&H0000D7FF,\"\n", + " f\"BackColour=&H80000000,\"\n", + " f\"Bold=1,\"\n", + " f\"Outline=2,\"\n", + " f\"Shadow=1,\"\n", + " f\"Alignment=2,\"\n", + " f\"MarginV=30\"\n", + " )\n", + "\n", + " cmd = [\n", + " 'ffmpeg', '-y',\n", + " '-i', video_path,\n", + " '-vf', f\"subtitles={srt_path}:force_style='{style}':fontsdir=/tmp\",\n", + " '-c:a', 'copy',\n", + " '-c:v', 'libx264',\n", + " '-crf', '23',\n", + " '-preset', 'fast',\n", + " output_path\n", + " ]\n", + "\n", + " print(f'🎬 Altyazı gömülüyor...')\n", + " result = subprocess.run(cmd, capture_output=True, text=True)\n", + " if result.returncode != 0:\n", + " print('FFmpeg hata:', result.stderr[-500:])\n", + " else:\n", + " print(f'✅ Tamamlandı → {output_path}')\n", + " return output_path\n", + "\n", + "\n", + "print('✅ burn_subtitles fonksiyonu hazır')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ffmpeg-run", + "metadata": {}, + "outputs": [], + "source": [ + "# ─── Kullanım ──────────────────────────────────────────────────────────────\n", + "# srt_path ve VIDEO_PATH önceki hücreden geliyor\n", + "# İkisi de tanımlıysa çalıştır\n", + "try:\n", + " if Path(VIDEO_PATH).exists() and Path(srt_path).exists():\n", + " final_video = burn_subtitles(VIDEO_PATH, srt_path)\n", + " else:\n", + " print('⚠️ Önce Caption Agent hücresini çalıştır')\n", + "except NameError:\n", + " print('⚠️ Önce Caption Agent hücresini çalıştır (srt_path tanımlı değil)')" + ] + }, + { + "cell_type": "markdown", + "id": "pipeline-header", + "metadata": {}, + "source": [ + "---\n", + "## 5. 🔗 Groq/Llama Pipeline Entegrasyonu\n", + "\n", + "Viral Radar çıktısını veya SRT içeriğini Groq'a gönder." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "groq-integration", + "metadata": {}, + "outputs": [], + "source": [ + "# Groq ile KapsamKafe Agent entegrasyonu\n", + "# pip install -q groq → ihtiyaç olursa hücreyi çalıştır\n", + "\n", + "def viral_data_to_prompt(viral_json_path: str) -> str:\n", + " \"\"\"JSON viral raporu → Groq/Llama için sistem promptu\"\"\"\n", + " with open(viral_json_path, encoding='utf-8') as f:\n", + " data = json.load(f)\n", + "\n", + " top10 = data[:10]\n", + " lines = []\n", + " for i, v in enumerate(top10, 1):\n", + " lines.append(\n", + " f\"{i}. @{v['author']} | skor:{v['viral_score']} | \"\n", + " f\"görüntülenme:{v['views']:,} | \"\n", + " f\"açıklama: {v['desc'][:80]} | \"\n", + " f\"etiketler: {', '.join(v['hashtags'][:5])}\"\n", + " )\n", + "\n", + " return (\n", + " \"Aşağıdaki TikTok trend verileri KapsamKafe için Viral Radar tarafından toplandı.\\n\"\n", + " \"TOP 10 viral video:\\n\" + \"\\n\".join(lines) +\n", + " \"\\n\\nBu veriye dayanarak KapsamKafe için içerik fikirleri öner.\"\n", + " )\n", + "\n", + "\n", + "def srt_to_prompt(srt_path: str, max_chars: int = 3000) -> str:\n", + " \"\"\"SRT dosyasını Groq/Llama için temiz metin bloğuna çevirir\"\"\"\n", + " with open(srt_path, encoding='utf-8') as f:\n", + " content = f.read()\n", + " # Timestamp ve index satırlarını temizle\n", + " lines = [\n", + " l.strip() for l in content.splitlines()\n", + " if l.strip() and not l.strip().isdigit() and '-->' not in l\n", + " ]\n", + " text = ' '.join(lines)[:max_chars]\n", + " return f\"Aşağıdaki TikTok video transkripti için başlık ve içerik önerileri yap:\\n\\n{text}\"\n", + "\n", + "\n", + "# Örnek kullanım (Groq API key gerektirir)\n", + "# from groq import Groq\n", + "# client = Groq(api_key=\"GROQ_API_KEY\")\n", + "# prompt = viral_data_to_prompt('output/viral_radar_hashtag_kahve_....json')\n", + "# response = client.chat.completions.create(\n", + "# model='llama3-70b-8192',\n", + "# messages=[{'role': 'user', 'content': prompt}]\n", + "# )\n", + "# print(response.choices[0].message.content)\n", + "\n", + "print('✅ Pipeline entegrasyon fonksiyonları hazır')\n", + "print(' viral_data_to_prompt(json_path) → Groq prompt')\n", + "print(' srt_to_prompt(srt_path) → Groq prompt')" + ] + }, + { + "cell_type": "markdown", + "id": "notes-header", + "metadata": {}, + "source": [ + "---\n", + "## Notlar\n", + "\n", + "| Konu | Detay |\n", + "|------|-------|\n", + "| TikTok token | msToken ~1-2 saat geçerli; sorun çıkarsa tazele |\n", + "| GPU | T4 önerilir; CPU'da medium Whisper yavaş |\n", + "| NLLB-200 | İlk yükleme ~2 dk, sonraki çalıştırmalarda önbellekte |\n", + "| SRT format | Groq/Llama'ya `srt_to_prompt()` ile temiz metin olarak gönder |\n", + "| Çıktı klasörü | Tüm dosyalar `output/` altına kaydedilir |" + ] + } + ] +}