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();
  });
}