Viral Short Analyzer — API Integration Guide
Dokumen lengkap untuk integrasi aplikasi desktop (Windows) ke server analisa video viral. Server berbasis TanStack Start + Gemini multimodal (AI menonton video langsung — bukan sekadar transcript).
1. Base URL
Gunakan URL di mana aplikasi web ini di-deploy. Halaman /docs di browser akan otomatis menampilkan Base URL terdeteksi dari domain saat ini.
https://your-domain.com
Contoh production: https://clipmaxs.lovable.app
2. Autentikasi
Semua request ke /api/public/* WAJIB membawa header:
X-App-Key: <APP_API_KEY>
APP_API_KEY di-generate / di-set di halaman /admin → Settings →
App API Key. Aplikasi desktop harus menyimpan key ini di config lokal.
Tanpa header / key salah → 401 Unauthorized.
3. Endpoint Ringkasan
| Method | Path | Tujuan |
|---|---|---|
| POST | /api/public/upload-file | Upload file video lokal → dapat file_uri |
| POST | /api/public/analyze | Jalankan analisa (URL / hasil upload) → JSON viral |
| GET | /api/public/progress | Pantau progress real-time berdasarkan clientJobId |
Flow:
[URL YouTube/TikTok/IG] ──► POST /api/public/analyze ──► JSON
[File lokal .mp4 dst] ──► POST /api/public/upload-file ──► file_uri
│
└─► POST /api/public/analyze (pakai uri) ──► JSON
Untuk URL YouTube/TikTok/Instagram bisa langsung pakai /analyze
tanpa upload — server yang menangani download/forward ke Gemini.
4. POST /api/public/upload-file
Upload file video lokal user ke Gemini File API melalui server kami (supaya API key Gemini tidak bocor ke client).
Request
POST https://your-domain.com/api/public/upload-file
Headers:
X-App-Key: <APP_API_KEY>
Content-Type: multipart/form-data
Body (form-data):
file: <binary> ← required, video (mp4/mov/webm/mkv) max ~1GB
Response 200
{
"file_uri": "https://generativelanguage.googleapis.com/v1beta/files/abc123",
"mime_type": "video/mp4",
"duration_sec": 1234
}
Simpan file_uri dan mime_type untuk dipakai di langkah berikutnya.
duration_sec bisa 0 kalau Gemini belum mengembalikan metadata durasi.
Response error
{ "error": "pesan error" } // status 4xx/5xx
5. POST /api/public/analyze
Endpoint utama. Menerima salah satu dari:
url→ link YouTube / TikTok / InstagramgeminiFileUri→ hasil dari/upload-file
Plus objek options yang merefleksikan setting di UI aplikasi desktop.
5.1 Request (JSON — direkomendasikan)
POST https://your-domain.com/api/public/analyze
Headers:
X-App-Key: <APP_API_KEY>
Content-Type: application/json
Body:
{
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
"clientJobId": "job_<guid>",
"geminiFileUri": null,
"geminiFileMime": null,
"geminiFileDurationSec": null,
"options": {
"language": "auto",
"clipCount": "5-10",
"clipDuration": "30-60",
"contentFocus": "auto",
"visualAi": false,
"fullVoNarrative": false,
"chunkMinutes": 20,
"autoChunk": true
}
}
Kirim salah satu: url ATAU (geminiFileUri + geminiFileMime).
clientJobId opsional tapi sangat direkomendasikan kalau mau polling
progress (lihat section 6).
5.2 Request (multipart — kalau mau upload + analyze dalam 1 call)
POST https://your-domain.com/api/public/analyze
Headers:
X-App-Key: <APP_API_KEY>
Content-Type: multipart/form-data
Body:
file: <binary> (atau pakai field "url" sebagai gantinya)
url: https://...
clientJobId: job_<guid>
language: auto
clipCount: 5-10
clipDuration: 30-60
contentFocus: auto
visualAi: true (string "true"/"false" atau "1"/"0")
fullVoNarrative: false
chunkMinutes: 20
autoChunk: true
options: {"language":"id", ...} ← opsional, JSON string (override field di atas)
5.3 Field options — Mapping ke UI
| UI Field | Key JSON | Tipe | Nilai yang diterima |
|---|---|---|---|
| AI LANGUAGE | language | string | "auto", "id", "en" |
| CLIP COUNT | clipCount | string | bebas, contoh: "3", "5-10", "10+" |
| CLIP DURATION | clipDuration | string | bebas (detik), contoh: "15-30", "30-60", "60-90" |
| CONTENT FOCUS | contentFocus | string | "auto", "podcast", "comedy", "motivation", "news", "education", "business", "gaming", "tech", "health", "beauty" |
| Visual AI (toggle) | visualAi | boolean | true / false |
| Full VO Narrative | fullVoNarrative | boolean | true / false |
| Minutes per chunk | chunkMinutes | number | 5 – 60, default 20 (menit) |
| Auto chunking (toggle) | autoChunk | boolean | true / false, default true |
Semua field di options opsional. Jika tidak dikirim:
language→"auto"(ikut bahasa pembicara)contentFocus→"auto"clipCount/clipDuration→ tidak ada batasan ketat, AI bebas memilihvisualAi/fullVoNarrative→falsechunkMinutes→20autoChunk→true
5.4 Efek tiap opsi terhadap output
- language=id → transcript, title, summary, hook, caption, reason, visual_notes WAJIB Bahasa Indonesia.
- language=en → idem dalam English.
- language=auto → AI mendeteksi bahasa dominan pembicara.
- clipCount="5-10" → AI menghasilkan 5–10 entri
viral_moments. - clipDuration="30-60" →
end - startsetiap klip berada di rentang 30–60 detik. - contentFocus=podcast dll → AI memprioritaskan jenis momen sesuai kategori.
- visualAi=true → AI memperhatikan ekspresi/gesture/teks di layar;
visual_noteslebih detail. - fullVoNarrative=true →
captiondiisi naskah voice-over lengkap siap dibacakan (bukan caption sosmed pendek). - chunkMinutes + autoChunk → video panjang diproses per chunk tanpa dipotong fisik; Gemini diberi
start_offset/end_offset. Hasil tiap chunk digabung server-side.
5.5 Chunking (video panjang)
Server otomatis membagi video panjang menjadi beberapa segmen virtual:
- Hanya aktif kalau
autoChunk=truedan durasi video >chunkMinutes * 60 + 300detik. - Default
chunkMinutes=20, jadi video > ~25 menit akan di-chunk. - Setiap chunk diproses dengan key rotation; bila satu key kena timeout (524), server otomatis pindah ke key berikutnya.
- Output tetap 1 JSON final;
chunkeddanchunk_countmemberitahui apakah proses terjadi per chunk.
6. Response Sukses (200)
{
"jobId": "f0a9b1cc-...-uuid",
"result": {
"source": {
"type": "youtube",
"url": "https://www.youtube.com/watch?v=...",
"duration_sec": 0
},
"chunked": false,
"chunk_count": 1,
"language": "id",
"title_suggestion": "Rick Astley - Never Gonna Give You Up",
"summary": "Video musik klasik ...",
"transcript": [
{ "start": 0.0, "end": 4.2, "text": "We're no strangers to love" },
{ "start": 4.2, "end": 8.5, "text": "You know the rules and so do I" }
],
"viral_moments": [
{
"start": 12.0,
"end": 45.0,
"score": 0.87,
"reason": "Hook lirik ikonik + visual khas 80an",
"hook": "We're no strangers to love",
"title": "Hook Lirik Rickroll",
"caption": "POV: ketika kamu di-rickroll lagi 😂 #rickroll",
"hashtags": ["#rickroll", "#viral", "#fyp", "#80s", "#classic"],
"visual_notes": "Rick Astley menari di depan panggung dengan jas hitam"
}
]
}
}
Catatan struktur
- Semua
start/enddalam detik (float), terurutstart < end. - Server otomatis menukar bila Gemini mengembalikan terbalik dan memastikan urutan key
startlebih dulu sebelumend. viral_moments[].hashtagsselalu array (boleh kosong).sourceselalu ada (diinjeksi server, bukan dari Gemini).chunked/chunk_countmemberitahui apakah video panjang di-chunk.jobIdboleh disimpan untuk lookup ulang di/admin → Jobs.
7. Progress Tracking (polling)
Analisa bisa berjalan menit-an. Aplikasi desktop bisa memantau progress
real-time dengan polling endpoint /api/public/progress.
7.1 Kirim clientJobId saat memulai analisa
Generate ID unik di sisi client (mis. GUID / job_<timestamp>_<rand>),
lalu sertakan di body request /api/public/analyze:
{
"url": "https://...",
"clientJobId": "job_lz9k_8h3a2f",
"options": { ... }
}
Atau di multipart sebagai field clientJobId.
7.2 Polling progress
GET https://your-domain.com/api/public/progress?clientJobId=<id>
Headers:
X-App-Key: <APP_API_KEY>
Response 200:
{
"jobId": "uuid-server",
"status": "processing", // processing | done | failed
"progress": {
"step": "analyze", // init | source | metadata | download | upload | prepare | analyze | finalize | done | failed
"message": "Analisa chunk 2/4 (20.0–40.0 mnt)...",
"percent": 65,
"chunk_index": 2,
"chunk_total": 4,
"updated_at": "2026-06-29T10:11:12.000Z"
},
"error": null,
"result": null, // hanya terisi bila status == "done"
"created_at": "...",
"finished_at": null
}
Response 404: { "status": "not_found" } — job belum tercatat (tunggu 1–2 detik).
7.3 Pola pemakaian
1. Generate clientJobId
2. POST /api/public/analyze (async di thread/Task)
3. Loop tiap 1–2 detik: GET /api/public/progress?clientJobId=...
- update UI progress bar dari progress.percent + progress.message
- stop loop bila status == "done" atau "failed"
4. Ambil hasil final dari response POST /analyze ATAU dari result di /progress
7.4 Daftar step
| step | arti |
|---|---|
init | Job baru dibuat |
source | Deteksi sumber (YouTube/TikTok/IG/upload) |
metadata | Ambil metadata (durasi) dari YouTube |
download | Unduh video dari TikTok/Instagram |
upload | Upload ke Gemini File API |
prepare | Siapkan prompt + chunking |
analyze | Gemini menganalisa (per chunk) |
finalize | Gabungkan hasil semua chunk |
done | Selesai, result tersedia |
failed | Gagal, lihat error |
8. Response Error
| HTTP | error (contoh) | Penyebab |
|---|---|---|
| 401 | Unauthorized | X-App-Key salah / belum di-set di admin |
| 400 | URL tidak dikenali (YouTube/TikTok/Instagram). | Format URL tidak valid |
| 400 | URL YouTube tidak valid. ... | ID YouTube < 11 karakter / link rusak |
| 500 | Gagal mengunduh video dari tiktok. ... | Scraper gagal; tambahkan cookies di /admin |
| 500 | Output Gemini bukan JSON valid: ... | Model meleset; retry biasanya berhasil |
| 500 | All Gemini keys failed | Pool API key habis / rate limited; tambah key di admin |
| 524 | timeout / Cloudflare (rotasi ke key berikutnya) | Video panjang / Gemini lambat; coba chunk lebih kecil |
| 500 | pesan lain | Lihat tab Jobs di /admin untuk detail |
Format selalu:
{ "error": "pesan" }
9. Contoh Implementasi (C# / .NET — aplikasi Windows)
using System.Net.Http.Json;
using System.Text.Json;
public record AnalyzeOptions(
string language = "auto",
string clipCount = "5-10",
string clipDuration = "30-60",
string contentFocus = "auto",
bool visualAi = false,
bool fullVoNarrative = false,
int chunkMinutes = 20,
bool autoChunk = true
);
public record AnalyzeRequest(
string? url,
string? geminiFileUri,
string? geminiFileMime,
string? clientJobId,
AnalyzeOptions options
);
public class ViralClient
{
private readonly HttpClient _http;
private readonly string _baseUrl;
public ViralClient(string baseUrl, string appApiKey)
{
_baseUrl = baseUrl.TrimEnd('/');
_http = new HttpClient { BaseAddress = new Uri(_baseUrl) };
_http.DefaultRequestHeaders.Add("X-App-Key", appApiKey);
_http.Timeout = TimeSpan.FromMinutes(15); // analisa bisa lama
}
public async Task<JsonDocument> AnalyzeUrlAsync(string url, AnalyzeOptions opts, string? clientJobId = null)
{
var req = new AnalyzeRequest(url, null, null, clientJobId ?? Guid.NewGuid().ToString("N"), opts);
var res = await _http.PostAsJsonAsync("/api/public/analyze", req);
var body = await res.Content.ReadAsStringAsync();
if (!res.IsSuccessStatusCode) throw new Exception(body);
return JsonDocument.Parse(body);
}
public async Task<JsonDocument> AnalyzeFileAsync(string path, AnalyzeOptions opts, string? clientJobId = null)
{
// 1) upload
using var form = new MultipartFormDataContent();
var fs = File.OpenRead(path);
form.Add(new StreamContent(fs), "file", Path.GetFileName(path));
var up = await _http.PostAsync("/api/public/upload-file", form);
var upBody = await up.Content.ReadAsStringAsync();
if (!up.IsSuccessStatusCode) throw new Exception(upBody);
var upJson = JsonDocument.Parse(upBody).RootElement;
var uri = upJson.GetProperty("file_uri").GetString();
var mime = upJson.GetProperty("mime_type").GetString();
// 2) analyze
var req = new AnalyzeRequest(null, uri, mime, clientJobId ?? Guid.NewGuid().ToString("N"), opts);
var res = await _http.PostAsJsonAsync("/api/public/analyze", req);
var body = await res.Content.ReadAsStringAsync();
if (!res.IsSuccessStatusCode) throw new Exception(body);
return JsonDocument.Parse(body);
}
public async Task<JsonElement> GetProgressAsync(string clientJobId)
{
var res = await _http.GetAsync($"/api/public/progress?clientJobId={clientJobId}");
var body = await res.Content.ReadAsStringAsync();
return JsonDocument.Parse(body).RootElement;
}
}
Cara pakai lengkap dengan polling progress:
var client = new ViralClient("https://clipmaxs.lovable.app", "APP_API_KEY_DARI_ADMIN");
var opts = new AnalyzeOptions(
language: "id",
clipCount: "5-10",
clipDuration: "30-60",
contentFocus: "podcast",
visualAi: true,
fullVoNarrative: false,
chunkMinutes: 20,
autoChunk: true);
var clientJobId = $"job_{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}";
// Jalankan analisa di background
var analyzeTask = Task.Run(async () =>
{
var json = await client.AnalyzeUrlAsync(
"https://www.youtube.com/watch?v=dQw4w9WgXcQ", opts, clientJobId);
return json;
});
// Poll progress setiap 1.5 detik
while (true)
{
var p = await client.GetProgressAsync(clientJobId);
var status = p.GetProperty("status").GetString();
var progress = p.GetProperty("progress");
var msg = progress.GetProperty("message").GetString();
var pct = progress.GetProperty("percent").GetInt32();
Console.WriteLine($"[{status}] {pct}% - {msg}");
if (status == "done" || status == "failed") break;
await Task.Delay(1500);
}
var result = await analyzeTask;
Console.WriteLine(result.RootElement.GetProperty("result"));
10. Contoh cURL
URL analyze:
curl -X POST https://your-domain.com/api/public/analyze \
-H "X-App-Key: APP_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
"clientJobId": "job_001",
"options": {
"language": "id",
"clipCount": "5-10",
"clipDuration": "30-60",
"contentFocus": "podcast",
"visualAi": true,
"fullVoNarrative": false,
"chunkMinutes": 20,
"autoChunk": true
}
}'
Poll progress:
curl -X GET 'https://your-domain.com/api/public/progress?clientJobId=job_001' \
-H "X-App-Key: APP_API_KEY"
Upload file + analyze:
# 1) upload
RESP=$(curl -s -X POST https://your-domain.com/api/public/upload-file \
-H "X-App-Key: APP_API_KEY" \
-F "file=@video.mp4")
URI=$(echo $RESP | jq -r .file_uri)
MIME=$(echo $RESP | jq -r .mime_type)
# 2) analyze
curl -X POST https://your-domain.com/api/public/analyze \
-H "X-App-Key: APP_API_KEY" \
-H "Content-Type: application/json" \
-d "{
"geminiFileUri": "$URI",
"geminiFileMime": "$MIME",
"clientJobId": "job_002",
"options": { "language":"auto", "clipCount":"5-10",
"clipDuration":"30-60", "contentFocus":"auto",
"visualAi": true, "fullVoNarrative": false,
"chunkMinutes": 20, "autoChunk": true }
}"
11. Timeout, Retry & Concurrency
- Analisa video panjang bisa 1–10 menit. Set HTTP client timeout min 15 menit.
- Server otomatis melakukan key rotation kalau satu Gemini key kena rate-limit (429) atau error 5xx/timeout (524/503/504). Aplikasi desktop tidak perlu retry manual.
- Untuk batch (bulk links), kirim paralel max 2–3 request agar pool API tidak langsung jenuh.
- Setiap call membuat 1 row di tabel
analysis_jobs. Status:processing→done|failed. Hasil JSON tersimpan di kolomresult_jsondan bisa di-review di/admin → Jobs → Lihat Hasil.
12. Sumber video yang didukung
- YouTube:
youtube.com/watch?v=...,youtu.be/...,youtube.com/shorts/...→ Dikirim langsung ke Gemini sebagaifile_data.file_uri(Gemini native). - TikTok:
tiktok.com/@user/video/...,vm.tiktok.com/...→ Server mendownload via TikWM/OG tag, lalu upload ke Gemini File API. - Instagram: Reels / Post
instagram.com/reel/...,/p/...→ Server mendownload via OG tag (butuh cookies untuk konten private/age-gated). - Upload langsung: mp4 / mov / webm / mkv.
Cookies (Netscape cookies.txt) bisa di-paste di /admin → Cookies untuk membantu scraper Instagram/TikTok ketika konten butuh login.
13. Checklist Antigrafiti (atau dev lain)
Untuk men-generate kode klien, cukup beri tahu:
- Base URL dari domain deploy (section 1).
- Header auth:
X-App-Key: <key>. - 3 endpoint:
/api/public/upload-file,/api/public/analyze,/api/public/progress. - Schema request
options(section 5.3) — jangan lupachunkMinutesdanautoChunk. - Schema response (section 6) — JSON dengan
jobId,result.viral_moments[],chunked,chunk_count. - Progress tracking via
clientJobId+GET /progress(section 7). - Timeout minimum 15 menit; key rotation otomatis untuk 429/5xx.
- Error berformat
{ "error": "..." }.
Dokumen ini sudah self-contained — bisa langsung di-share ke developer aplikasi desktop untuk implementasi.