File size: 7,248 Bytes
9b53f70 c55c292 9b53f70 b4fd22e 9b53f70 30b7d3a b7ce941 9b53f70 c55c292 9b53f70 e014541 9b53f70 e014541 9b53f70 c55c292 9b53f70 ab852b0 c55989f 4f9859d d908e31 cabddd5 9b53f70 3370310 9b53f70 3370310 9b53f70 3370310 9b53f70 3370310 9b53f70 3370310 9b53f70 3370310 9b53f70 | 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 | // Client gọi backend FastAPI. Dev: Vite proxy /api -> :8000.
const BASE = "";
async function jpost(path, body) {
const r = await fetch(BASE + path, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
if (!r.ok) throw new Error((await r.text()) || r.statusText);
return r.json();
}
async function upload(path, file, fields = {}) {
const fd = new FormData();
fd.append("file", file);
for (const [k, v] of Object.entries(fields)) if (v != null) fd.append(k, v);
const r = await fetch(BASE + path, { method: "POST", body: fd });
if (!r.ok) throw new Error((await r.text()) || r.statusText);
return r.json();
}
// Upload 1 file kèm tiến độ BYTE (fetch không có progress event -> dùng XHR).
// onProgress(loaded, total) gọi liên tục khi bytes đẩy lên.
export function uploadWithProgress(path, file, fields = {}, onProgress) {
return new Promise((resolve, reject) => {
const fd = new FormData();
fd.append("file", file);
for (const [k, v] of Object.entries(fields)) if (v != null) fd.append(k, v);
const xhr = new XMLHttpRequest();
xhr.open("POST", BASE + path);
if (xhr.upload && onProgress) {
xhr.upload.onprogress = (e) => {
if (e.lengthComputable) onProgress(e.loaded, e.total);
};
}
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
try { resolve(JSON.parse(xhr.responseText)); }
catch { resolve({}); }
} else reject(new Error(xhr.responseText || xhr.statusText || `HTTP ${xhr.status}`));
};
xhr.onerror = () => reject(new Error("Lỗi mạng khi tải lên"));
xhr.onabort = () => reject(new Error("Đã huỷ tải lên"));
xhr.send(fd);
});
}
// Chạy nhiều tác vụ song song có GIỚI HẠN luồng (mặc định 4) — nhanh hơn tuần tự
// mà không làm nghẽn trình duyệt (mỗi host chỉ ~6 kết nối đồng thời).
export async function runPool(count, worker, limit = 4) {
let next = 0;
const run = async () => {
while (next < count) {
const i = next++;
await worker(i);
}
};
await Promise.all(Array.from({ length: Math.min(limit, count) }, run));
}
export const fileUrl = (u) => BASE + u;
export const api = {
job: (id) => fetch(BASE + `/api/jobs/${id}`).then((r) => r.json()),
listInputs: (sid) => fetch(BASE + `/api/sessions/${sid}/inputs`).then((r) => r.json()),
cancel: (id) => jpost(`/api/jobs/${id}/cancel`, {}),
dropSession: (sid) => fetch(BASE + `/api/sessions/${sid}`, { method: "DELETE" }).then((r) => r.json()),
// Down list video (không dùng cookie)
parseLinks: (raw) => jpost("/api/download/parse", { raw }),
startDownload: (urls, zip_output, username = "") =>
jpost("/api/download/start", { urls, zip_output, username }),
push: (session, paths, target_tool) =>
jpost("/api/download/push", { session, paths, target_tool }),
// Cut video
cutUpload: (file, session) => upload("/api/cut/upload", file, { session }),
cutPreview: (req) => jpost("/api/cut/preview", req),
cutExecute: (req) => jpost("/api/cut/execute", req),
cutExecuteBatch: (req) => jpost("/api/cut/execute-batch", req),
// segments = mảng nhóm: segments[i] = các khúc thứ i của mọi video -> cùng 1 vị trí Xào
cutPushShuffle: (session, segments) =>
jpost("/api/cut/push-shuffle", { session, segments }),
// Tách video -> video không lời + mp3
cutSplitAudio: (session, file, username = "") =>
jpost("/api/cut/split-audio", { session, file, username }),
// Xào video
shuffleUpload: (file, segment, session) =>
upload("/api/shuffle/upload", file, { segment, session }),
shuffleUploadAudio: (file, session) =>
upload("/api/shuffle/upload-audio", file, { session }),
// Bản có tiến độ byte (dùng cho loader nhanh + chuẩn khi thả clip/thư mục).
shuffleUploadP: (file, segment, session, onProgress) =>
uploadWithProgress("/api/shuffle/upload", file, { segment, session }, onProgress),
shuffleUploadAudioP: (file, session, onProgress) =>
uploadWithProgress("/api/shuffle/upload-audio", file, { session }, onProgress),
shuffleSegments: (sid) => fetch(BASE + `/api/shuffle/segments/${sid}`).then((r) => r.json()),
shuffleEstimate: (req) => jpost("/api/shuffle/estimate", req),
shufflePlan: (req) => jpost("/api/shuffle/plan", req),
shuffleGenerate: (req) => jpost("/api/shuffle/generate", req),
// Dịch / đổi giọng
dubUpload: (file, session) => upload("/api/dub/upload", file, { session }),
dubVoiceSample: (file, session) =>
upload("/api/dub/voice-sample", file, { session }),
dubVoices: (lang) => fetch(BASE + `/api/dub/voices?lang=${lang}`).then((r) => r.json()),
dubStart: (req) => jpost("/api/dub/start", req),
// 2 pha: dịch trước (xem/sửa) rồi mới tạo giọng
dubPrepare: (req) => jpost("/api/dub/prepare", req),
dubSynthesize: (req) => jpost("/api/dub/synthesize", req),
dubTts: (req) => jpost("/api/dub/tts", req),
// Nạp SRT tự tải (thay bản dịch tự động) — trả lines đã bỏ nhãn [tên]
dubSrtImport: (file, session) => upload("/api/dub/subtitle/import", file, { session }),
// Hard subtitle
subtitlePlan: (session, srt) => jpost("/api/dub/subtitle/plan", { session, srt }),
subtitleBurn: (req) => jpost("/api/dub/subtitle", req),
// Xem trước KHỚP 100% bản burn: server render đúng 1 khung tại `time`.
subtitlePreviewFrame: (session, video, segments, mode, time) =>
jpost("/api/dub/subtitle/preview-frame", { session, video, segments, mode, time }),
// Drive
driveStatus: (username) => fetch(BASE + `/api/drive/status?username=${encodeURIComponent(username)}`).then((r) => r.json()),
driveSetup: (username, folder_link) => jpost("/api/drive/setup", { username, folder_link }),
driveRemove: (username) => fetch(BASE + `/api/drive/setup?username=${encodeURIComponent(username)}`, { method: "DELETE" }).then((r) => r.json()),
// History
getHistory: (username) => fetch(BASE + `/api/history?username=${encodeURIComponent(username)}`).then((r) => r.json()),
};
// Poll một job tới khi xong. onTick(job) gọi mỗi lần cập nhật.
// `signal` (AbortSignal, tuỳ chọn): khi component unmount/huỷ, truyền signal đã
// abort để DỪNG vòng poll — tránh rò rỉ setTimeout và setState trên component đã
// gỡ. Khi bị abort, promise KHÔNG resolve (để code đang await không chạy tiếp).
export function pollJob(id, onTick, interval = 1000, signal) {
return new Promise((resolve, reject) => {
let timer = null;
const stop = () => { if (timer) clearTimeout(timer); };
if (signal) signal.addEventListener("abort", stop, { once: true });
const tick = async () => {
if (signal && signal.aborted) return;
try {
const job = await api.job(id);
if (signal && signal.aborted) return;
onTick && onTick(job);
if (["done", "error", "cancelled"].includes(job.status)) {
resolve(job);
} else {
timer = setTimeout(tick, interval);
}
} catch (e) {
if (!(signal && signal.aborted)) reject(e);
}
};
tick();
});
}
|