tnt-group / frontend /src /api.js
titusgiap's picture
fix(sub): preview khớp 100% burn bằng render server (bỏ libass-wasm)
4f9859d
Raw
History Blame Contribute Delete
7.25 kB
// 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();
});
}