const API_BASE = '/api' /** * Process a video from URL or file * @param {string|File} urlOrFile - YouTube/TikTok/Instagram URL or File object * @param {object} options - { numClips, language, aspectRatio, rewriteHooks, captionStyle, brandKit } * @returns {Promise<{jobId: string, status: string}>} */ export async function processVideo(urlOrFile, options = {}) { const formData = new FormData() if (typeof urlOrFile === 'string') { formData.append('url', urlOrFile) } else if (urlOrFile instanceof File) { formData.append('file', urlOrFile) } else { throw new Error('urlOrFile must be a string (URL) or File object') } formData.append('num_clips', options.numClips || 5) formData.append('language', options.language || 'en') formData.append('aspect_ratio', options.aspectRatio || '9:16') formData.append('rewrite_hooks', options.rewriteHooks ? 'true' : 'false') // BUG FIX: backend reads 'caption_style' and 'brand_kit' (not _json suffix) if (options.captionStyle) { formData.append('caption_style', JSON.stringify(options.captionStyle)) } if (options.brandKit) { formData.append('brand_kit', JSON.stringify(options.brandKit)) } // Brand logo file upload if (options.brandKit?.logoFile instanceof File) { formData.append('logo_file', options.brandKit.logoFile) } // YouTube cookies.txt for datacenter IP bypass if (options.cookiesFile instanceof File) { formData.append('cookies_file', options.cookiesFile) } const controller = new AbortController() const timeout = setTimeout(() => controller.abort(), 300000) // 5 min timeout try { const response = await fetch(`${API_BASE}/process`, { method: 'POST', body: formData, signal: controller.signal, }) if (!response.ok) { const error = await response.json().catch(() => ({})) throw new Error(error.detail || `HTTP ${response.status}`) } const data = await response.json() // BUG FIX: backend returns snake_case job_id, normalize to camelCase return { jobId: data.job_id || data.jobId, status: data.status || 'processing', progress: data.progress || 0, } } finally { clearTimeout(timeout) } } /** * Get job status and results * @param {string} jobId * @returns {Promise} */ export async function getJob(jobId) { // BUG FIX: backend uses singular /api/job/ not /api/jobs/ const response = await fetch(`${API_BASE}/job/${jobId}`) if (!response.ok) { throw new Error(`Failed to fetch job: ${response.status}`) } return await response.json() } /** * List all jobs * @returns {Promise} */ export async function getJobs() { const response = await fetch(`${API_BASE}/jobs`) if (!response.ok) { throw new Error(`Failed to fetch jobs: ${response.status}`) } return await response.json() } /** * Delete a job * @param {string} jobId * @returns {Promise<{success: boolean}>} */ export async function deleteJob(jobId) { const response = await fetch(`${API_BASE}/job/${jobId}`, { method: 'DELETE', }) if (!response.ok) { throw new Error(`Failed to delete job: ${response.status}`) } return await response.json() } /** * Download a clip * @param {string} jobId * @param {string} clipId * @returns {Promise} */ export async function downloadClip(jobId, clipId) { // BUG FIX: match actual backend route /api/clips/{job_id}/{clip_id}/download const response = await fetch(`${API_BASE}/clips/${jobId}/${clipId}/download`, { method: 'GET', }) if (!response.ok) { throw new Error(`Failed to download clip: ${response.status}`) } return await response.blob() } /** * Generate article reels from a news article URL * @param {string} articleUrl - URL of the article * @param {object} options - { reelType, numPoints, ttsVoice, accentColor, groqApiKey } * @returns {Promise<{jobId: string, status: string}>} */ export async function generateArticleReels(articleUrl, options = {}) { const formData = new FormData() formData.append('article_url', articleUrl) formData.append('reel_type', options.reelType || 'Financial News Reel') formData.append('num_points', options.numPoints || 7) formData.append('tts_voice', options.ttsVoice || 'Guy - News Anchor (Male)') formData.append('accent_hex', options.accentColor || '#E8A020') formData.append('groq_api_key', options.groqApiKey || '') const response = await fetch(`${API_BASE}/article-reels`, { method: 'POST', body: formData, }) if (!response.ok) { const err = await response.json().catch(() => ({})) throw new Error(err.detail || `HTTP ${response.status}`) } const data = await response.json() return { jobId: data.job_id || data.jobId, status: data.status || 'processing', } } /** * Create WebSocket connection for real-time job updates * @param {string} jobId * @param {function} onMessage - Called with JobResult when message arrives * @param {function} onClose - Called when connection closes * @returns {{close: function}} */ export function createWebSocket(jobId, onMessage, onClose) { const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:' const wsUrl = `${protocol}//${window.location.host}/api/ws/${jobId}` let ws = null let reconnectAttempts = 0 const maxReconnectAttempts = 5 const reconnectDelay = 1000 function connect() { try { ws = new WebSocket(wsUrl) ws.onopen = () => { reconnectAttempts = 0 console.log('[WebSocket] Connected to', jobId) } ws.onmessage = (event) => { try { const data = JSON.parse(event.data) if (onMessage) onMessage(data) } catch (err) { console.error('[WebSocket] Failed to parse message:', err) } } ws.onerror = (error) => { console.error('[WebSocket] Error:', error) } ws.onclose = () => { console.log('[WebSocket] Connection closed') if (reconnectAttempts < maxReconnectAttempts) { reconnectAttempts++ setTimeout(connect, reconnectDelay) } else { if (onClose) onClose() } } } catch (err) { console.error('[WebSocket] Connection failed:', err) if (onClose) onClose() } } connect() return { close: () => { if (ws) ws.close() }, } }