Local branch had diverged from origin/master (sibling commits on the same base). Rewrote local history linearly on top of origin/master, folding in all local content: exa/hot-platform discovery hunters, leaks ledger, nightly sweep orchestrator, updated .gitignore and skill docs, plus the latest hunt output and vault state. Remote-only files (vault keys, channel scripts) were restored rather than dropped, so the resulting tree is a full union of both sides.
1 line
22 KiB
JSON
1 line
22 KiB
JSON
{"repo": "chandra-pp/clone-opusclip-", "files": {"server/.env.example": {"2c9ca2338520c71f3341cd78b7fddeba511b13d0": {"t": 1785747095.266215, "n": 5675, "h": "8ad150c070f3fb20", "c": "# API key Anthropic untuk deteksi highlight + skor virality.\n# Tanpa key ini aplikasi tetap jalan memakai analisis heuristik (kualitas lebih rendah).\nANTHROPIC_API_KEY=\n\n# Model Claude (opsional)\nCLAUDE_MODEL=claude-opus-4-8\n\n# ── Provider AI ───────────────────────────────────────────────────────────────\n# Pilih provider analisis highlight: auto | claude | openai | heuristic\n# auto = pakai router lokal bila OPENAI_BASE_URL+OPENAI_MODEL terisi, lalu Claude, lalu heuristik\nAI_PROVIDER=auto\n\n# Router lokal OpenAI-compatible di PC Anda (9router/Ollama/LM Studio/LocalAI/OpenRouter).\n# Base URL HARUS berakhiran /v1, mis. http://localhost:5555/v1\n# OPENAI_BASE_URL=http://localhost:5555/v1\n# OPENAI_API_KEY=\n# OPENAI_MODEL=llama-3.1-8b-instruct\n# OPENAI_JSON_MODE=true\n\n# DeepSeek (OpenAI-compatible). Tempel API key untuk mengaktifkan; set AI_PROVIDER=deepseek\n# untuk memaksa pakai DeepSeek. Model: deepseek-chat (V3) | deepseek-reasoner (R1).\n# DEEPSEEK_API_KEY=\n# DEEPSEEK_MODEL=deepseek-chat\n\n# Ukuran model Whisper lokal: tiny | base | small | medium | large-v3\n# Makin besar makin akurat tapi makin lambat. \"small\" seimbang untuk bahasa Indonesia.\nWHISPER_MODEL=small\n\n# Kunci bahasa video (kode ISO: id, en, ...) atau \"auto\" untuk deteksi otomatis.\n# Mengunci ke \"id\" mengurangi salah dengar fonem (z->j, k->p) pada video berbahasa Indonesia.\nWHISPER_LANGUAGE=id\n\n# Path python.exe yang punya faster-whisper (opsional, dideteksi otomatis)\n# PYTHON_PATH=C:\\Users\\NAMA\\AppData\\Local\\Programs\\Python\\Python312\\python.exe\n\n# ── Kamera presisi Gemini Vision (mode Klip Fokus) ────────────────────────────\n# Gemini \"melihat\" frame klip lalu melacak posisi bola agar kamera mengarah presisi.\n# Dapatkan key di https://aistudio.google.com/apikey , lalu isi di bawah.\n# GEMINI_API_KEY=\n# GEMINI_MODEL=gemini-2.5-flash\n# Frame/detik yang dikirim ke Gemini, batas frame per klip, dan lebar frame (px):\n# FOCUS_VISION_FPS=1\n# FOCUS_VISION_MAX_FRAMES=90\n# FOCUS_VISION_FRAME_WIDTH=512\n\n# Gaya caption (opsional). Warna = hex RGB tanpa '#', bergilir per baris.\n# CAPTION_FONT_SIZE=92\n# CAPTION_UPPERCASE=true\n# CAPTION_COLORS=FFD400,3DFF7E,00E5FF,FF4D8D\n\n# Port API server\nPORT=4000\n\n# ── Auto-pantau akun (X / TikTok) ─────────────────────────────────────────────\n# Menu \"Auto-pantau\" memeriksa akun terdaftar secara berkala lalu otomatis\n# mengunduh klip baru dan menjalankannya lewat pipeline \"Get clips in 1 click\".\n# MONITOR_AUTOSTART = jalankan penjadwal otomatis saat server menyala (true/false).\n# MONITOR_INTERVAL_MINUTES = selang waktu antar pemeriksaan (menit).\n# MONITOR_MAX_PER_CHECK = jumlah klip terbaru per akun yang diambil tiap pemeriksaan.\n# MONITOR_AUTOSTART=true\n# MONITOR_INTERVAL_MINUTES=10\n# MONITOR_MAX_PER_CHECK=3\n\n# ── Cookies yt-dlp (untuk X & opsional TikTok) ────────────────────────────────\n# Mengunduh video X (Twitter) HAMPIR SELALU butuh login. Isi salah satu:\n# 1) File cookies format Netscape (ekspor pakai ekstensi \"Get cookies.txt\"):\n# X_COOKIES_FILE=C:\\Users\\NAMA\\x-cookies.txt\n# TIKTOK_COOKIES_FILE=C:\\Users\\NAMA\\tiktok-cookies.txt\n# 2) ATAU baca cookies langsung dari browser tempat Anda sudah login:\n# nilai: chrome | edge | firefox | brave\n# YTDLP_COOKIES_FROM_BROWSER=edge\n# Catatan: TikTok publik umumnya bisa tanpa cookies, dan diunduh TANPA watermark.\n\n# ── Auto-rekam pertandingan (instant replay) ──────────────────────────────────\n# Bot merekam layar/jendela terus-menerus ke buffer bergulir, mengawasi momen\n# heboh (suara/papan skor/API), lalu memotong klip otomatis di sekitar momen itu.\n#\n# Perekaman layar (ffmpeg gdigrab):\n# CAP_FPS=20\n# CAP_PRESET=veryfast\n# CAP_CRF=23\n# CAP_SEGMENT_SECONDS=2\n# CAP_BUFFER_SECONDS=150\n# Perangkat audio LOOPBACK untuk merekam suara sistem (WAJIB untuk deteksi suara).\n# Lihat daftar: ffmpeg -f dshow -list_devices true -i dummy\n# Contoh: \"Stereo Mix (Realtek(R) Audio)\" atau \"CABLE Output (VB-Audio Virtual Cable)\"\n# Kosong = rekam tanpa audio (deteksi suara nonaktif).\n# CAP_AUDIO_DEVICE=\n\n# Jendela klip di sekitar momen (detik) + jeda antar pemicu:\n# CLIP_PRE_SECONDS=25\n# CLIP_POST_SECONDS=20\n# TRIGGER_COOLDOWN_SECONDS=25\n\n# Detektor suara (lonjakan loudness ebur128):\n# AUDIO_THRESHOLD_DB=8\n# AUDIO_SUSTAIN_MS=700\n# AUDIO_BASELINE_SECONDS=20\n\n# Detektor API skor langsung (default: API-Football / api-sports.io v3).\n# Daftar di https://www.api-football.com/ lalu isi key; ID fixture diisi per-sesi di UI.\n# SCORE_API_URL=https://v3.football.api-sports.io\n# SCORE_API_KEY=\n# SCORE_API_KEY_HEADER=x-apisports-key\n# SCORE_API_POLL_SECONDS=20\n\n# Detektor OCR papan skor (butuh tesseract terpasang: winget install UB-Mannheim.TesseractOCR)\n# OCR_POLL_SECONDS=4\n# TESSERACT_PATH=tesseract\n\n# Jumlah maksimum klip per video\nMAX_CLIPS=6\n\n# ── Mode \"Split → Portrait\" ───────────────────────────────────────────────────\n# Membagi video panjang menjadi N klip berurutan, tiap klip dijadikan 9:16 dengan\n# video di tengah + background blur/gambar (tanpa AI, tanpa deteksi wajah).\n# REELS_DEFAULT_CLIPS = jumlah klip default bila tidak diisi di form.\n# REELS_MAX_CLIPS = batas atas jumlah klip yang boleh diminta.\n# REELS_FG_HEIGHT = tinggi (px) video utama di kanvas 1080x1920 (harus genap).\n# Makin besar -> video makin besar, sisi makin ter-crop, background makin tipis.\n# REELS_BLUR = kekuatan blur background (boxblur luma_radius).\n# REELS_DEFAULT_CLIPS=18\n# REELS_MAX_CLIPS=40\n# REELS_FG_HEIGHT=1080\n# REELS_BLUR=24\n# Overlay teks (badge judul atas, badge \"Part N\" bawah, watermark kiri-atas):\n# REELS_BADGE_COLOR=red\n# REELS_TITLE_FONTSIZE=68\n# REELS_PART_FONTSIZE=58\n# REELS_WATERMARK_FONTSIZE=40\n# REELS_WATERMARK_LOGO_WIDTH=220\n"}}, "server/src/config.js": {"2c9ca2338520c71f3341cd78b7fddeba511b13d0": {"t": 1786068490.8528373, "n": 15170, "h": "90d09c5953c2b803", "c": "import path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport dotenv from \"dotenv\";\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\ndotenv.config({ path: path.join(__dirname, \"..\", \".env\") });\n\nexport const SERVER_ROOT = path.join(__dirname, \"..\");\nexport const STORAGE_DIR = process.env.STORAGE_DIR || path.join(SERVER_ROOT, \"storage\");\nexport const PROJECTS_DIR = path.join(STORAGE_DIR, \"projects\");\nexport const UPLOADS_DIR = path.join(STORAGE_DIR, \"uploads\");\nexport const SESSIONS_DIR = path.join(STORAGE_DIR, \"sessions\");\n\nexport const PORT = Number(process.env.PORT || 4000);\n\nexport const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY || \"\";\nexport const CLAUDE_MODEL = process.env.CLAUDE_MODEL || \"claude-opus-4-8\";\n\n// Pemilihan provider analisis AI: \"auto\" | \"claude\" | \"openai\" | \"heuristic\".\n// \"auto\" = utamakan router lokal (OpenAI-compatible) bila ada, lalu Claude, lalu heuristik.\nexport const AI_PROVIDER = (process.env.AI_PROVIDER || \"auto\").toLowerCase();\n\n// Provider OpenAI-compatible (router lokal di PC Anda: 9router, Ollama, LM Studio, LocalAI, OpenRouter, dll).\n// Arahkan base URL ke endpoint router Anda (harus berakhiran /v1), mis. http://localhost:5555/v1\nexport const OPENAI_BASE_URL = (process.env.OPENAI_BASE_URL || \"\").trim().replace(/\\/+$/, \"\");\nexport const OPENAI_API_KEY = process.env.OPENAI_API_KEY || \"\";\nexport const OPENAI_MODEL = process.env.OPENAI_MODEL || \"\";\n// Kirim response_format json_object (set OPENAI_JSON_MODE=false bila router Anda menolaknya).\nexport const OPENAI_JSON_MODE = (process.env.OPENAI_JSON_MODE ?? \"true\") !== \"false\";\n\n// DeepSeek (juga OpenAI-compatible). Cukup isi DEEPSEEK_API_KEY untuk mengaktifkan.\n// Model valid: \"deepseek-chat\" (V3) atau \"deepseek-reasoner\" (R1).\nexport const DEEPSEEK_API_KEY = process.env.DEEPSEEK_API_KEY || \"\";\nexport const DEEPSEEK_MODEL = process.env.DEEPSEEK_MODEL || \"deepseek-chat\";\nexport const DEEPSEEK_BASE_URL = (process.env.DEEPSEEK_BASE_URL || \"https://api.deepseek.com/v1\")\n .trim()\n .replace(/\\/+$/, \"\");\nexport const DEEPSEEK_JSON_MODE = (process.env.DEEPSEEK_JSON_MODE ?? \"true\") !== \"false\";\n\n// Whisper model size: tiny | base | small | medium | large-v3\nexport const WHISPER_MODEL = process.env.WHISPER_MODEL || \"base\";\n\n// Kode bahasa ISO (id, en, ...) atau \"auto\" untuk deteksi otomatis.\n// Mengunci bahasa meningkatkan akurasi transkripsi secara signifikan.\nexport const WHISPER_LANGUAGE = process.env.WHISPER_LANGUAGE || \"auto\";\n\n// Gaya caption yang dibakar ke video\nexport const CAPTION_FONT_SIZE = Number(process.env.CAPTION_FONT_SIZE || 92);\nexport const CAPTION_UPPERCASE = (process.env.CAPTION_UPPERCASE ?? \"true\") !== \"false\";\n// Warna highlight kata aktif (hex RGB tanpa '#'), bergilir per baris\nexport const CAPTION_COLORS = (process.env.CAPTION_COLORS || \"FFD400,3DFF7E,00E5FF,FF4D8D\")\n .split(\",\")\n .map((c) => c.trim())\n .filter(Boolean);\n\n// Batas jumlah klip yang diminta ke AI\nexport const MAX_CLIPS = Number(process.env.MAX_CLIPS || 6);\nexport const MIN_CLIP_SECONDS = Number(process.env.MIN_CLIP_SECONDS || 15);\nexport const MAX_CLIP_SECONDS = Number(process.env.MAX_CLIP_SECONDS || 75);\n\n// Lokal target penilaian viral:\n// \"id\" = Mode Indonesia (rubrik & gaya FYP Indonesia: relatable, hook 3 detik,\n// topik yang naik di ID, hashtag fyp, rambu SARA).\n// \"global\" = rubrik umum (hook/retensi/nilai konten).\n// Default \"id\" karena app ini menyasar pasar Indonesia. Bisa dipaksa per video nanti\n// lewat field project.viralLocale.\nexport const VIRAL_LOCALE = (process.env.VIRAL_LOCALE || \"id\").toLowerCase();\n\n// ===== Tren nyata (Tuas B) =====\n// Menyuntik daftar topik yang SEDANG NAIK di Indonesia ke prompt analisis, agar AI\n// memberi bonus skor untuk klip yang nyambung dengan tren hari ini.\n// - Google Trends (RSS \"trending now\", geo=ID): TANPA API key.\n// - YouTube Trending (Data API v3, chart=mostPopular, regionCode=ID): BUTUH YOUTUBE_API_KEY.\n// Bila keduanya gagal/ tak ada key, analisis tetap jalan (tanpa blok tren).\nexport const TRENDS_ENABLED = (process.env.TRENDS_ENABLED ?? \"true\") !== \"false\";\nexport const TRENDS_REGION = (process.env.TRENDS_REGION || \"ID\").trim().toUpperCase();\n// Cache hasil tren (menit) agar tak memanggil sumber tiap analisis. Tren berubah lambat.\nexport const TRENDS_TTL_MINUTES = Math.max(5, Number(process.env.TRENDS_TTL_MINUTES || 120));\n// Batas jumlah topik yang dimasukkan ke prompt.\nexport const TRENDS_MAX_ITEMS = Math.max(3, Number(process.env.TRENDS_MAX_ITEMS || 20));\n// YouTube Data API v3 key (opsional). Tanpa ini, hanya Google Trends yang dipakai.\nexport const YOUTUBE_API_KEY = (process.env.YOUTUBE_API_KEY || \"\").trim();\n// Sumber tren via AI: minta provider AI (DeepSeek/Claude/router lokal) memperluas tren\n// Google Trends menjadi topik/hashtag yang sedang naik di TikTok/Reels/Shorts Indonesia.\n// TANPA perlu API key sosial media. Set TRENDS_AI=false untuk mematikan.\nexport const TRENDS_AI = (process.env.TRENDS_AI ?? \"true\") !== \"false\";\n\n// ===== Thumbnail klip berjudul =====\n// Bakar judul/momen klip ke thumbnail agar lebih eye-catching (gaya thumbnail FYP).\nexport const THUMB_TITLE = (process.env.THUMB_TITLE ?? \"true\") !== \"false\";\n// Warna teks judul thumbnail (hex RGB tanpa '#'). Default kuning terang (ikut warna caption pertama).\nexport const THUMB_ACCENT = (process.env.THUMB_ACCENT || CAPTION_COLORS[0] || \"FFD400\").replace(/^#/, \"\");\n\n// ===== Judul dibakar ke VIDEO (hook) =====\n// Tampilkan judul sebagai banner di beberapa detik awal video, supaya ikut terlihat\n// saat video diputar/diunduh (bukan cuma di thumbnail). Set VIDEO_TITLE=false untuk mematikan.\nexport const VIDEO_TITLE = (process.env.VIDEO_TITLE ?? \"true\") !== \"false\";\n// Berapa detik judul tampil di awal video.\nexport const VIDEO_TITLE_SECONDS = Math.max(1, Number(process.env.VIDEO_TITLE_SECONDS || 3.5));\n\n// ===== Mode \"Split → Portrait\" =====\n// Membagi video panjang menjadi N klip berurutan, lalu tiap klip dijadikan 9:16\n// dengan video utama di tengah dan background blur (atau gambar) di atas-bawah.\n// Jumlah klip default bila pengguna tidak mengisi.\nexport const REELS_DEFAULT_CLIPS = Number(process.env.REELS_DEFAULT_CLIPS || 18);\n// Batas atas jumlah klip yang boleh diminta.\nexport const REELS_MAX_CLIPS = Number(process.env.REELS_MAX_CLIPS || 40);\n// Tinggi (px) video utama di kanvas 1080x1920. Makin besar = video makin besar,\n// sisi kiri-kanan makin terpotong, background atas-bawah makin tipis. Harus genap.\nexport const REELS_FG_HEIGHT = Number(process.env.REELS_FG_HEIGHT || 1080);\n// Kekuatan blur background (boxblur luma_radius).\nexport const REELS_BLUR = Number(process.env.REELS_BLUR || 24);\n\n// Overlay teks pada mode \"Split → Portrait\" (badge judul, badge \"Part N\", watermark).\nexport const REELS_BADGE_COLOR = process.env.REELS_BADGE_COLOR || \"red\"; // warna kotak badge judul & Part N\nexport const REELS_TITLE_FONTSIZE = Number(process.env.REELS_TITLE_FONTSIZE || 68);\nexport const REELS_PART_FONTSIZE = Number(process.env.REELS_PART_FONTSIZE || 58);\nexport const REELS_WATERMARK_FONTSIZE = Number(process.env.REELS_WATERMARK_FONTSIZE || 40);\nexport const REELS_WATERMARK_LOGO_WIDTH = Number(process.env.REELS_WATERMARK_LOGO_WIDTH || 220);\n\n// ===== Auto-pantau akun (X / TikTok) =====\n// Penjadwal yang memeriksa akun terdaftar secara berkala lalu mengunduh klip baru\n// dan menjalankannya lewat pipeline \"Get clips in 1 click\".\n// MONITOR_AUTOSTART = jalankan penjadwal otomatis saat server menyala.\nexport const MONITOR_AUTOSTART = (process.env.MONITOR_AUTOSTART ?? \"true\") !== \"false\";\n// Selang waktu antar pemeriksaan otomatis (menit).\nexport const MONITOR_INTERVAL_MINUTES = Math.max(\n 1,\n Number(process.env.MONITOR_INTERVAL_MINUTES || 10)\n);\n// Berapa klip terbaru yang diambil per akun pada tiap pemeriksaan (batas default form).\nexport const MONITOR_MAX_PER_CHECK = Math.max(1, Number(process.env.MONITOR_MAX_PER_CHECK || 3));\n\n// ===== Cookies yt-dlp =====\n// Mengunduh video X (Twitter) hampir selalu butuh login. Isi salah satu:\n// - X_COOKIES_FILE: path file cookies format Netscape (mis. hasil ekspor ekstensi browser).\n// - YTDLP_COOKIES_FROM_BROWSER: nama browser (chrome|edge|firefox|brave) agar yt-dlp\n// membaca cookies langsung dari browser tempat Anda login ke X/TikTok.\n// TikTok umumnya bisa tanpa cookies untuk video publik (dan diunduh TANPA watermark).\nexport const X_COOKIES_FILE = (process.env.X_COOKIES_FILE || \"\").trim();\nexport const TIKTOK_COOKIES_FILE = (process.env.TIKTOK_COOKIES_FILE || \"\").trim();\nexport const YTDLP_COOKIES_FROM_BROWSER = (process.env.YTDLP_COOKIES_FROM_BROWSER || \"\").trim();\n\n// ===== Auto-rekam pertandingan (instant replay) =====\n// Bot merekam layar/jendela secara terus-menerus ke buffer bergulir, mengawasi\n// momen heboh (suara/papan skor/API), lalu memotong jendela klip otomatis.\n//\n// -- Perekaman layar (ffmpeg gdigrab) --\nexport const CAP_FPS = Number(process.env.CAP_FPS || 20);\nexport const CAP_PRESET = process.env.CAP_PRESET || \"veryfast\"; // preset x264 (ultrafast..medium)\nexport const CAP_CRF = Number(process.env.CAP_CRF || 23);\nexport const CAP_SEGMENT_SECONDS = Math.max(1, Number(process.env.CAP_SEGMENT_SECONDS || 2));\n// Berapa detik riwayat (buffer bergulir) yang disimpan sebelum dibuang janitor.\nexport const CAP_BUFFER_SECONDS = Math.max(30, Number(process.env.CAP_BUFFER_SECONDS || 150));\n// Perangkat audio dshow untuk LOOPBACK suara sistem (mis. \"Stereo Mix (Realtek(R) Audio)\"\n// atau \"CABLE Output (VB-Audio Virtual Cable)\"). Kosong = rekam tanpa audio\n// (deteksi suara nonaktif; andalkan OCR/API).\nexport const CAP_AUDIO_DEVICE = (process.env.CAP_AUDIO_DEVICE || \"\").trim();\n\n// -- Jendela klip di sekitar momen terdeteksi --\nexport const CLIP_PRE_SECONDS = Math.max(3, Number(process.env.CLIP_PRE_SECONDS || 25));\nexport const CLIP_POST_SECONDS = Math.max(2, Number(process.env.CLIP_POST_SECONDS || 20));\n// Jeda minimum antar pemicu agar satu gol tak jadi banyak klip (detik).\nexport const TRIGGER_COOLDOWN_SECONDS = Math.max(5, Number(process.env.TRIGGER_COOLDOWN_SECONDS || 25));\n\n// -- Detektor suara (lonjakan loudness ebur128) --\n// Picu bila loudness sesaat (M, LUFS) melampaui baseline + ambang ini selama sustain.\nexport const AUDIO_THRESHOLD_DB = Number(process.env.AUDIO_THRESHOLD_DB || 8);\nexport const AUDIO_SUSTAIN_MS = Number(process.env.AUDIO_SUSTAIN_MS || 700);\nexport const AUDIO_BASELINE_SECONDS = Number(process.env.AUDIO_BASELINE_SECONDS || 20);\n\n// -- Detektor API skor langsung (default: API-Football / api-sports.io v3) --\nexport const SCORE_API_URL = (process.env.SCORE_API_URL || \"https://v3.football.api-sports.io\")\n .trim()\n .replace(/\\/+$/, \"\");\nexport const SCORE_API_KEY = (process.env.SCORE_API_KEY || \"\").trim();\nexport const SCORE_API_KEY_HEADER = (process.env.SCORE_API_KEY_HEADER || \"x-apisports-key\").trim();\nexport const SCORE_API_POLL_SECONDS = Math.max(10, Number(process.env.SCORE_API_POLL_SECONDS || 20));\n\n// -- Detektor OCR papan skor (butuh tesseract terpasang di PATH) --\nexport const OCR_POLL_SECONDS = Math.max(2, Number(process.env.OCR_POLL_SECONDS || 4));\nexport const TESSERACT_PATH = (process.env.TESSERACT_PATH || \"tesseract\").trim();\n\n// ===== Klip Highlight Fokus (auto-follow bola & pemain) =====\n// Menganalisis di mana gerakan terkonsentrasi tiap saat (di sekitar gol = bola +\n// pemain yang mencetak + kiper), lalu memotong/menggeser bingkai 9:16 mengikutinya.\n// -- Pass analisis gerakan (frame mini grayscale) --\nexport const FOCUS_ANALYZE_FPS = Math.max(2, Number(process.env.FOCUS_ANALYZE_FPS || 6));\nexport const FOCUS_ANALYZE_WIDTH = Math.max(16, Number(process.env.FOCUS_ANALYZE_WIDTH || 64));\nexport const FOCUS_ANALYZE_HEIGHT = Math.max(9, Number(process.env.FOCUS_ANALYZE_HEIGHT || 36));\n// Penghalusan jalur kamera (detik). Memakai rata-rata TERPUSAT (centered) sehingga\n// mulus TANPA lag/telat. Jangan terlalu besar agar kamera tetap responsif ke bola.\nexport const FOCUS_SMOOTH_SECONDS = Math.max(0.2, Number(process.env.FOCUS_SMOOTH_SECONDS || 1.4));\n// Pengaman kecepatan geser kamera (fraksi lebar layar per detik). Dibuat longgar agar\n// kamera bisa mengejar sprint bola; hanya mencegah lonjakan ekstrem akibat noise.\nexport const FOCUS_MAX_PAN_PER_SECOND = Math.max(0.05, Number(process.env.FOCUS_MAX_PAN_PER_SECOND || 0.8));\n// Bila input lebih panjang dari ini, ambil hanya jendela di sekitar momen puncak (detik).\nexport const FOCUS_MAX_SECONDS = Math.max(8, Number(process.env.FOCUS_MAX_SECONDS || 60));\n// Berapa detik sebelum momen puncak untuk mulai jendela klip (bila input dipotong).\nexport const FOCUS_PRE_SECONDS = Math.max(2, Number(process.env.FOCUS_PRE_SECONDS || 18));\n// Input lebih pendek dari ini diproses utuh sebagai 1 klip; lebih panjang → deteksi\n// beberapa momen (gol) dan buat 1 klip per momen.\nexport const FOCUS_SINGLE_MAX_SECONDS = Math.max(20, Number(process.env.FOCUS_SINGLE_MAX_SECONDS || 70));\n// Jumlah maksimum momen (gol) yang dideteksi dari satu video panjang (sebelum digabung).\nexport const FOCUS_MAX_CLIPS = Math.max(1, Number(process.env.FOCUS_MAX_CLIPS || 8));\n// Jendela tiap klip momen: sebelum & sesudah puncak (detik). Post dibuat panjang agar\n// proses gol + selebrasi tertangkap sampai kickoff berjalan lagi (total ~1,5 menit).\nexport const FOCUS_CLIP_PRE_SECONDS = Math.max(2, Number(process.env.FOCUS_CLIP_PRE_SECONDS || 20));\nexport const FOCUS_CLIP_POST_SECONDS = Math.max(2, Number(process.env.FOCUS_CLIP_POST_SECONDS || 80));\n// Jarak minimum antar PUNCAK terdeteksi (detik) — sekadar agar satu momen tak terhitung\n// ganda. Tak lagi terikat panjang klip: jendela yang tumpang tindih akan DIGABUNG.\nexport const FOCUS_MIN_GAP_SECONDS = Math.max(10, Number(process.env.FOCUS_MIN_GAP_SECONDS || 30));\n// Batas atas panjang satu klip hasil penggabungan (detik) agar tak membengkak.\nexport const FOCUS_MAX_CLIP_SECONDS = Math.max(30, Number(process.env.FOCUS_MAX_CLIP_SECONDS || 180));\n// Tingkat zoom default (1 = ikuti permainan; >1 = lebih dekat ke pemain). Batas 1..2.\nexport const FOCUS_DEFAULT_ZOOM = Math.min(2, Math.max(1, Number(process.env.FOCUS_DEFAULT_ZOOM || 1.15)));\n\n// ===== Kamera presisi via Gemini Vision =====\n// Gemini \"melihat\" frame-frame klip lalu mengembalikan posisi bola/aksi tiap waktu,\n// dipakai sebagai jalur kamera (lebih presisi daripada heuristik gerakan).\nexport const GEMINI_API_KEY = (process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY || \"\").trim();\nexport const GEMINI_MODEL = (process.env.GEMINI_MODEL || \"gemini-2.5-flash\").trim();\nexport const GEMINI_BASE_URL = (process.env.GEMINI_BASE_URL || \"https://generativelanguage.googleapis.com/v1beta\")\n .trim()\n .replace(/\\/+$/, \"\");\n// Berapa frame per detik yang diambil & dikirim ke Gemini untuk melacak bola.\nexport const FOCUS_VISION_FPS = Math.max(0.25, Number(process.env.FOCUS_VISION_FPS || 1));\n// Batas jumlah frame per klip (mengontrol biaya & ukuran request).\nexport const FOCUS_VISION_MAX_FRAMES = Math.max(10, Number(process.env.FOCUS_VISION_MAX_FRAMES || 90));\n// Lebar frame (px) yang dikirim ke Gemini (kecil = murah & cepat).\nexport const FOCUS_VISION_FRAME_WIDTH = Math.max(256, Number(process.env.FOCUS_VISION_FRAME_WIDTH || 512));\n"}}}} |