File size: 6,378 Bytes
edb7d3e b5d56b9 edb7d3e b5d56b9 31dce00 edb7d3e b5d56b9 edb7d3e b5d56b9 edb7d3e b5d56b9 edb7d3e b5d56b9 edb7d3e b5d56b9 edb7d3e b5d56b9 a72d248 b5d56b9 edb7d3e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 | 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<JobResult>}
*/
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<Array>}
*/
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<Blob>}
*/
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()
},
}
}
|