Spaces:
Running
Running
File size: 13,515 Bytes
00d2c12 9b53f70 5b27e49 07d3e14 5b27e49 07d3e14 5b27e49 a04fadb d94104e a04fadb 5b27e49 07d3e14 d94104e 5b27e49 d94104e 9b53f70 5b27e49 9b53f70 d94104e 9b53f70 c55c292 00d2c12 c55c292 5b27e49 c55c292 5b27e49 c55c292 9b53f70 5b27e49 9b53f70 d94104e 9b53f70 b4fd22e 9b53f70 30b7d3a b7ce941 9b53f70 c55c292 5b27e49 9b53f70 e014541 9b53f70 5b27e49 9b53f70 c55c292 9b53f70 ab852b0 788bc93 c55989f 4f9859d d908e31 cabddd5 5b27e49 cabddd5 d94104e cabddd5 5b27e49 9b53f70 3370310 5b27e49 3370310 9b53f70 3370310 5b27e49 9b53f70 3370310 9b53f70 3370310 5b27e49 9b53f70 3370310 9b53f70 5b27e49 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 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 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 | import { currentCtx as trackCtx } from "./lib/track";
// Client gọi backend FastAPI. Dev: Vite proxy /api -> :8000.
const BASE = "";
// Máy chủ KHÔNG phải lúc nào cũng trả JSON: Space bận, vừa khởi động lại hoặc
// cổng vào HF hết giờ chờ thì nó chen vào một trang HTML ("<!DOCTYPE html>...").
// Gọi thẳng r.json() lúc đó ném "Unexpected token '<'" — câu người dùng không
// hiểu, lại che mất chuyện đang thật sự xảy ra. Mọi lời gọi đi qua đây: đọc body
// MỘT lần, tự nhận ra HTML/không-JSON, và gắn cờ `transient` cho những lỗi chỉ
// là hụt hơi tạm thời (mạng chớp, 5xx, 429, 408) để chỗ gọi biết nên thử lại.
const BUSY_MSG = "Máy chủ đang bận hoặc vừa khởi động lại, chưa trả về dữ liệu.";
function tagged(msg, { status = 0, transient = false } = {}) {
const e = new Error(msg);
e.status = status;
e.transient = transient;
return e;
}
// Lỗi FastAPI trả về dạng {"detail": "..."} — lấy ra câu chữ cho người đọc,
// đừng ném nguyên cục JSON (hay nguyên trang HTML) vào alert.
function httpError(r, text, data) {
const d = data?.detail ?? data?.error ?? data?.message;
let msg = "";
if (typeof d === "string" && d) msg = d;
else if (Array.isArray(d) && d.length) msg = d.map((x) => x.msg || "").join("; ");
else if (text && !/^\s*</.test(text) && text.trim().length <= 300) msg = text.trim();
else msg = `${BUSY_MSG} (lỗi ${r.status}${r.statusText ? " " + r.statusText : ""})`;
return tagged(msg, {
status: r.status,
transient: r.status >= 500 || r.status === 429 || r.status === 408,
});
}
async function readJson(r) {
const text = await r.text().catch(() => "");
let data = null, isJson = false;
if (text) { try { data = JSON.parse(text); isJson = true; } catch { /* không phải JSON */ } }
if (!r.ok) throw httpError(r, text, data);
if (!isJson) throw tagged(`${BUSY_MSG} Thử lại sau giây lát.`, { status: r.status, transient: true });
return data;
}
const sleep = (ms) => new Promise((ok) => setTimeout(ok, ms));
// Mở ra cho các trang gọi thẳng (Listing, Flux2, Đăng nhập, Quản trị...) —
// đừng dùng fetch trần rồi r.json() nữa, lỗi "Unexpected token '<'" sinh ra từ đó.
//
// Lời gọi CHỈ ĐỌC (GET/HEAD) gặp trục trặc tạm thời thì tự thử lại vài nhịp:
// đọc lại thì cùng lắm tốn thêm một vòng, không đẻ ra việc mới. Lệnh GHI
// (POST/DELETE) KHÔNG tự thử lại — bấm một lần mà chạy hai job thì tai hại hơn
// là báo lỗi để người dùng tự bấm lại.
export async function request(path, init, { retries = 2, backoff = 600 } = {}) {
const method = (init?.method || "GET").toUpperCase();
const canRetry = method === "GET" || method === "HEAD";
for (let attempt = 0; ; attempt++) {
try {
let r;
try {
r = await fetch(BASE + path, init);
} catch (e) {
// Huỷ có chủ đích (AbortController hết giờ chờ) không phải trục trặc
// mạng — trả nguyên lỗi để chỗ gọi tự phân biệt.
if (e?.name === "AbortError") throw e;
// fetch chỉ ném khi mạng đứt / máy chủ chưa nghe — luôn là tạm thời.
throw tagged("Mất kết nối tới máy chủ. Kiểm tra mạng rồi thử lại.", { transient: true });
}
return await readJson(r);
} catch (e) {
if (!canRetry || !e?.transient || attempt >= retries || init?.signal?.aborted) throw e;
await sleep(backoff * (attempt + 1));
}
}
}
export const jget = (path) => request(path);
export function jpost(path, body) {
return request(path, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
}
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);
return request(path, { method: "POST", body: fd });
}
// 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);
// Upload đi bằng XHR (để có % tiến độ) nên KHÔNG qua bản vá fetch trong
// lib/track.js -> gắn tay ngữ cảnh theo dõi để backend vẫn quy được về
// đúng task của đúng người.
try {
const c = trackCtx();
if (c.user_id) {
xhr.setRequestHeader("X-TNT-User", c.user_id);
xhr.setRequestHeader("X-TNT-Session", c.session_id);
if (c.task_id) xhr.setRequestHeader("X-TNT-Task", c.task_id);
if (c.feature) xhr.setRequestHeader("X-TNT-Feature", c.feature);
}
} catch { /* thiếu header theo dõi không được chặn upload */ }
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) {
// 200 mà thân là HTML (cổng vào chen vào) thì coi như hỏng tạm thời,
// đừng resolve {} rồi để chỗ gọi vấp vào chỗ thiếu trường.
try { resolve(JSON.parse(xhr.responseText)); }
catch { reject(tagged(`${BUSY_MSG} Thử lại sau giây lát.`, { status: xhr.status, transient: true })); }
} else {
// Non-2xx cũng có thể là trang HTML của cổng vào, không phải JSON.
let msg = "";
try {
const j = JSON.parse(xhr.responseText);
const d = j?.detail ?? j?.error ?? j?.message;
if (typeof d === "string") msg = d;
} catch { /* không phải JSON */ }
if (!msg) {
const t = (xhr.responseText || "").trim();
msg = t && !t.startsWith("<") && t.length <= 300
? t
: `${BUSY_MSG} (lỗi ${xhr.status}${xhr.statusText ? " " + xhr.statusText : ""})`;
}
reject(tagged(msg, { status: xhr.status, transient: xhr.status >= 500 }));
}
};
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) => jget(`/api/jobs/${id}`),
listInputs: (sid) => jget(`/api/sessions/${sid}/inputs`),
cancel: (id) => jpost(`/api/jobs/${id}/cancel`, {}),
dropSession: (sid) => request(`/api/sessions/${sid}`, { method: "DELETE" }),
// 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) => jget(`/api/shuffle/segments/${sid}`),
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) => jget(`/api/dub/voices?lang=${lang}`),
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 }),
// Nhận dạng lời gốc trong video (ASR, không dịch) — cho tab "Chèn chữ vào video"
dubTranscribe: (req) => jpost("/api/dub/subtitle/transcribe", req),
// 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) => jget(`/api/drive/status?username=${encodeURIComponent(username)}`),
driveSetup: (username, folder_link) => jpost("/api/drive/setup", { username, folder_link }),
driveRemove: (username) => request(`/api/drive/setup?username=${encodeURIComponent(username)}`, { method: "DELETE" }),
// History
getHistory: (username) => jget(`/api/history?username=${encodeURIComponent(username)}`),
};
// 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).
// Chịu đựng máy chủ mất tiếng tối đa chừng này rồi mới báo hỏng.
const POLL_GRACE_MS = 90_000;
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 });
// Job chạy TRÊN MÁY CHỦ, vòng này chỉ đứng ngoài hỏi thăm. Một cú 502 của
// cổng vào Space hay mạng chớp giữa chừng KHÔNG có nghĩa là job hỏng — nên
// hỏi lại, chỉ bỏ cuộc khi mất liên lạc quá lâu. (Trước đây lỗi đầu tiên là
// văng luôn: người dùng mất việc đang chạy dở ở 60%.)
let failedSince = 0;
const tick = async () => {
if (signal && signal.aborted) return;
try {
const job = await api.job(id);
if (signal && signal.aborted) return;
failedSince = 0;
onTick && onTick(job);
if (["done", "error", "cancelled"].includes(job.status)) {
resolve(job);
} else {
timer = setTimeout(tick, interval);
}
} catch (e) {
if (signal && signal.aborted) return;
// Máy chủ trả lời rành mạch "không có job này" -> nó vừa khởi động lại,
// tiến trình trong RAM mất rồi, hỏi thêm cũng vô ích.
if (e?.status === 404) {
return reject(new Error("Máy chủ vừa khởi động lại nên không còn tiến trình này. Hãy chạy lại."));
}
if (!e?.transient) return reject(e);
const now = Date.now();
if (!failedSince) failedSince = now;
if (now - failedSince > POLL_GRACE_MS) {
return reject(new Error(`${e.message} (mất liên lạc với máy chủ quá lâu)`));
}
// Máy chủ đang hụt hơi -> giãn nhịp hỏi ra, đừng dồn thêm.
timer = setTimeout(tick, Math.min(5000, Math.max(interval, interval * 2)));
}
};
tick();
});
}
|