Spaces:
Running
Running
| import type { | |
| OriginalTrackAsset, | |
| OutputFormat, | |
| SourceKind, | |
| StemAsset, | |
| StemResult, | |
| } from "./types"; | |
| export interface SourceImportResponse { | |
| job_id: string; | |
| filename: string; | |
| source_url: string; | |
| resolved_url?: string; | |
| title?: string; | |
| platform: Exclude<SourceKind, "file">; | |
| } | |
| export async function uploadFile( | |
| file: File, | |
| onProgress?: (progress: number) => void | |
| ): Promise<{ job_id: string; filename: string }> { | |
| const formData = new FormData(); | |
| formData.append("file", file); | |
| const xhr = new XMLHttpRequest(); | |
| return new Promise((resolve, reject) => { | |
| xhr.upload.addEventListener("progress", (e) => { | |
| if (e.lengthComputable && onProgress) { | |
| onProgress(e.loaded / e.total); | |
| } | |
| }); | |
| xhr.addEventListener("load", () => { | |
| if (xhr.status >= 200 && xhr.status < 300) { | |
| resolve(JSON.parse(xhr.responseText)); | |
| } else { | |
| try { | |
| const err = JSON.parse(xhr.responseText); | |
| reject(new Error(err.detail || `Upload failed (${xhr.status})`)); | |
| } catch { | |
| reject(new Error(`Upload failed (${xhr.status})`)); | |
| } | |
| } | |
| }); | |
| xhr.addEventListener("error", () => reject(new Error("Upload failed"))); | |
| xhr.open("POST", "/api/upload"); | |
| xhr.send(formData); | |
| }); | |
| } | |
| export async function importUrl( | |
| url: string | |
| ): Promise<SourceImportResponse> { | |
| const res = await fetch("/api/import-url", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ url }), | |
| }); | |
| if (!res.ok) { | |
| const err = await res.json().catch(() => ({ detail: "Request failed" })); | |
| throw new Error(err.detail || `Import failed (${res.status})`); | |
| } | |
| return res.json(); | |
| } | |
| export async function startSeparation( | |
| jobId: string, | |
| stems: string[], | |
| outputFormat: OutputFormat | |
| ): Promise<void> { | |
| const res = await fetch("/api/separate", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ job_id: jobId, stems, output_format: outputFormat }), | |
| }); | |
| if (!res.ok) { | |
| const err = await res.json().catch(() => ({ detail: "Request failed" })); | |
| throw new Error(err.detail || `Separation failed (${res.status})`); | |
| } | |
| } | |
| export interface ProgressEvent { | |
| state: string; | |
| progress: number; | |
| message: string; | |
| stems?: Record<string, string>; | |
| error?: string; | |
| } | |
| export function subscribeProgress( | |
| jobId: string, | |
| onEvent: (event: ProgressEvent) => void, | |
| onDone: (stems: StemResult[]) => void, | |
| onError: (error: string) => void | |
| ): () => void { | |
| const es = new EventSource(`/api/progress/${jobId}`); | |
| let closedByApp = false; | |
| let terminalEventSeen = false; | |
| let errorTimer: number | null = null; | |
| const clearErrorTimer = () => { | |
| if (errorTimer !== null) { | |
| window.clearTimeout(errorTimer); | |
| errorTimer = null; | |
| } | |
| }; | |
| es.onopen = () => { | |
| clearErrorTimer(); | |
| }; | |
| es.onmessage = (e) => { | |
| try { | |
| const data: ProgressEvent = JSON.parse(e.data); | |
| clearErrorTimer(); | |
| onEvent(data); | |
| if (data.state === "done" && data.stems) { | |
| terminalEventSeen = true; | |
| const stemList: StemResult[] = Object.entries(data.stems).map( | |
| ([name, filename]) => ({ name, filename }) | |
| ); | |
| onDone(stemList); | |
| closedByApp = true; | |
| es.close(); | |
| } else if (data.state === "error") { | |
| terminalEventSeen = true; | |
| onError(data.error || "Separation failed"); | |
| closedByApp = true; | |
| es.close(); | |
| } | |
| } catch { | |
| // ignore parse errors | |
| } | |
| }; | |
| es.onerror = () => { | |
| if (closedByApp || terminalEventSeen) { | |
| return; | |
| } | |
| if (es.readyState === EventSource.CLOSED) { | |
| onError("Connection to server lost"); | |
| closedByApp = true; | |
| es.close(); | |
| return; | |
| } | |
| if (errorTimer === null) { | |
| errorTimer = window.setTimeout(() => { | |
| errorTimer = null; | |
| if (!closedByApp && !terminalEventSeen && es.readyState !== EventSource.OPEN) { | |
| onError("Connection to server lost"); | |
| closedByApp = true; | |
| es.close(); | |
| } | |
| }, 5000); | |
| } | |
| }; | |
| return () => { | |
| closedByApp = true; | |
| clearErrorTimer(); | |
| es.close(); | |
| }; | |
| } | |
| export interface ExampleOutputResponse { | |
| song?: string; | |
| original: OriginalTrackAsset; | |
| stems: StemAsset[]; | |
| downloadAllUrl: string; | |
| } | |
| export async function fetchExampleOutput(): Promise<ExampleOutputResponse> { | |
| const res = await fetch("/api/examples/default"); | |
| if (!res.ok) { | |
| const err = await res.json().catch(() => ({ detail: "Request failed" })); | |
| throw new Error(err.detail || `Example load failed (${res.status})`); | |
| } | |
| return res.json(); | |
| } | |
| export function getAudioUrl(jobId: string, filename: string): string { | |
| return `/api/audio/${jobId}/${encodeURIComponent(filename)}`; | |
| } | |
| export function getDownloadUrl(jobId: string, filename: string): string { | |
| return `/api/download/${jobId}/${encodeURIComponent(filename)}`; | |
| } | |
| export function getDownloadAllUrl(jobId: string): string { | |
| return `/api/download/${jobId}/all`; | |
| } | |