Spaces:
Running
Running
Update index.html
Browse files- index.html +228 -90
index.html
CHANGED
|
@@ -345,7 +345,7 @@ body{
|
|
| 345 |
<!-- ===================== HERO ===================== -->
|
| 346 |
<section class="hero">
|
| 347 |
<h1>Dub any video into <span class="grad">any language</span>, flawlessly.</h1>
|
| 348 |
-
<p>Pick a video
|
| 349 |
</section>
|
| 350 |
|
| 351 |
<!-- ===================== MAIN GRID ===================== -->
|
|
@@ -608,11 +608,13 @@ const serverStatusText = $("serverStatusText");
|
|
| 608 |
============================================================ */
|
| 609 |
const state = {
|
| 610 |
file: null,
|
| 611 |
-
running: false,
|
|
|
|
| 612 |
sessionId: null,
|
| 613 |
-
|
|
|
|
|
|
|
| 614 |
targetLanguage: "English",
|
| 615 |
-
resultObjectUrl: null,
|
| 616 |
consoleCleared: false
|
| 617 |
};
|
| 618 |
|
|
@@ -714,25 +716,64 @@ function acceptFile(file){
|
|
| 714 |
if(!file.type.startsWith("video/") && !/\.(mp4|mov|mkv|webm|avi|m4v)$/i.test(file.name)){
|
| 715 |
pushLog("[WARN] Selected file does not look like a video. Proceeding anyway.", "l-warn");
|
| 716 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 717 |
state.file = file;
|
| 718 |
fileNameEl.textContent = file.name;
|
| 719 |
fileSizeEl.textContent = fmtBytes(file.size);
|
| 720 |
fileChip.classList.add("show");
|
| 721 |
dropWrap.classList.add("has-file");
|
| 722 |
-
|
| 723 |
-
|
| 724 |
-
|
| 725 |
-
|
|
|
|
|
|
|
|
|
|
| 726 |
}
|
| 727 |
function clearFile(){
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 728 |
state.file = null;
|
| 729 |
fileInput.value = "";
|
| 730 |
fileChip.classList.remove("show");
|
| 731 |
dropWrap.classList.remove("has-file");
|
|
|
|
|
|
|
| 732 |
refreshGenerateState();
|
| 733 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 734 |
function refreshGenerateState(){
|
| 735 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 736 |
}
|
| 737 |
|
| 738 |
dropbox.addEventListener("click", ()=> fileInput.click());
|
|
@@ -812,9 +853,53 @@ function safeJSON(str){
|
|
| 812 |
}
|
| 813 |
|
| 814 |
/* ============================================================
|
| 815 |
-
|
| 816 |
============================================================ */
|
| 817 |
-
function
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 818 |
switch(event){
|
| 819 |
case "log": {
|
| 820 |
pushLog(data);
|
|
@@ -825,22 +910,52 @@ function handleSSEEvent(event, data){
|
|
| 825 |
case "progress": {
|
| 826 |
const p = safeJSON(data);
|
| 827 |
if(p && typeof p.percent === "number"){
|
| 828 |
-
|
| 829 |
-
|
| 830 |
-
setProgress(p.percent, p.message || undefined);
|
| 831 |
}
|
| 832 |
break;
|
| 833 |
}
|
| 834 |
-
case "
|
| 835 |
-
|
| 836 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 837 |
if(d.source_language) lockSourceLanguage(d.source_language);
|
| 838 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 839 |
break;
|
| 840 |
}
|
| 841 |
case "error": {
|
| 842 |
pushLog(String(data), "l-err");
|
| 843 |
-
|
|
|
|
|
|
|
| 844 |
break;
|
| 845 |
}
|
| 846 |
case "done": {
|
|
@@ -855,50 +970,15 @@ function handleSSEEvent(event, data){
|
|
| 855 |
}
|
| 856 |
|
| 857 |
/* ============================================================
|
| 858 |
-
|
| 859 |
-
|
| 860 |
-
|
| 861 |
-
|
| 862 |
-
|
| 863 |
-
|
| 864 |
-
|
| 865 |
-
resultPanel.classList.remove("show");
|
| 866 |
-
progressFill.classList.remove("paused");
|
| 867 |
-
progressFill.classList.add("active");
|
| 868 |
-
setProgress(0, "Initializing…");
|
| 869 |
-
resetSourceLanguage();
|
| 870 |
-
setLive(true);
|
| 871 |
-
}
|
| 872 |
-
|
| 873 |
-
function endRunUI(){
|
| 874 |
-
state.running = false;
|
| 875 |
-
generateBtn.classList.remove("loading");
|
| 876 |
-
progressFill.classList.remove("active");
|
| 877 |
-
setLive(false);
|
| 878 |
-
refreshGenerateState();
|
| 879 |
-
}
|
| 880 |
-
|
| 881 |
-
function failRun(reason){
|
| 882 |
-
pushLog("[WARN] " + (reason || "Run stopped.") + " State reset.", "l-warn");
|
| 883 |
-
if(state.abort){ try{ state.abort.abort(); }catch(_){}}
|
| 884 |
-
endRunUI();
|
| 885 |
-
}
|
| 886 |
-
|
| 887 |
-
function finishRun(d){
|
| 888 |
-
setProgress(100, "Complete");
|
| 889 |
-
pushLog("[SUCCESS] Dubbing finished. Preparing preview…", "l-ok");
|
| 890 |
-
endRunUI();
|
| 891 |
-
const sid = d.session_id || state.sessionId;
|
| 892 |
-
if(sid){ state.sessionId = sid; presentResult(sid, d); }
|
| 893 |
-
}
|
| 894 |
-
|
| 895 |
-
/* ============================================================
|
| 896 |
-
Result presentation + single-shot download
|
| 897 |
-
The backend deletes the session after /download streams, so we fetch
|
| 898 |
-
the file ONCE into an object URL and reuse it for both preview + save.
|
| 899 |
============================================================ */
|
| 900 |
-
|
| 901 |
-
// Build meta tags
|
| 902 |
resultMeta.innerHTML = "";
|
| 903 |
const tags = [];
|
| 904 |
if(meta.source_language) tags.push(["Source", meta.source_language]);
|
|
@@ -913,59 +993,117 @@ async function presentResult(sessionId, meta){
|
|
| 913 |
|
| 914 |
resultPanel.classList.add("show");
|
| 915 |
const url = `${API_BASE}/download/${encodeURIComponent(sessionId)}`;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 916 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 917 |
try{
|
| 918 |
-
|
| 919 |
-
|
| 920 |
-
|
| 921 |
-
|
| 922 |
-
|
| 923 |
-
|
| 924 |
-
|
| 925 |
-
|
| 926 |
-
resultVideo.src = state.resultObjectUrl;
|
| 927 |
-
downloadBtn.href = state.resultObjectUrl;
|
| 928 |
-
downloadBtn.setAttribute("download", `dubbed_${sessionId}.mp4`);
|
| 929 |
-
pushLog("[SUCCESS] Preview ready. Download available.", "l-ok");
|
| 930 |
}catch(err){
|
| 931 |
-
|
| 932 |
-
|
| 933 |
-
|
| 934 |
-
|
| 935 |
-
|
| 936 |
}
|
| 937 |
}
|
| 938 |
|
| 939 |
/* ============================================================
|
| 940 |
-
Network: /dub
|
| 941 |
============================================================ */
|
| 942 |
async function startDub(){
|
| 943 |
-
if(!state.
|
| 944 |
|
| 945 |
state.targetLanguage = targetLang.value;
|
| 946 |
-
|
| 947 |
-
|
| 948 |
-
|
|
|
|
| 949 |
|
| 950 |
const fd = new FormData();
|
| 951 |
-
fd.append("
|
| 952 |
fd.append("target_language", targetLang.value);
|
| 953 |
fd.append("voice_name", voiceSelect.value);
|
| 954 |
|
| 955 |
state.abort = new AbortController();
|
| 956 |
try{
|
| 957 |
const res = await fetch(`${API_BASE}/dub`, { method:"POST", body:fd, signal:state.abort.signal });
|
| 958 |
-
await readSSE(res,
|
| 959 |
-
|
| 960 |
-
if(state.running){
|
| 961 |
pushLog("[WARN] Stream closed unexpectedly before completion.", "l-warn");
|
| 962 |
-
|
| 963 |
}
|
| 964 |
}catch(err){
|
| 965 |
if(err.name === "AbortError"){ /* user cancelled */ }
|
| 966 |
else{
|
| 967 |
pushLog("[ERROR] Connection failed: " + err.message, "l-err");
|
| 968 |
-
failRun("
|
| 969 |
}
|
| 970 |
}
|
| 971 |
}
|
|
|
|
| 345 |
<!-- ===================== HERO ===================== -->
|
| 346 |
<section class="hero">
|
| 347 |
<h1>Dub any video into <span class="grad">any language</span>, flawlessly.</h1>
|
| 348 |
+
<p>Pick a video — upload & transcription start instantly. Choose a language and voice, then press Generate Dub to translate, re-voice, and match the audio to your video's exact length.</p>
|
| 349 |
</section>
|
| 350 |
|
| 351 |
<!-- ===================== MAIN GRID ===================== -->
|
|
|
|
| 608 |
============================================================ */
|
| 609 |
const state = {
|
| 610 |
file: null,
|
| 611 |
+
running: false, // true only while /prepare or /dub is streaming
|
| 612 |
+
phase: "idle", // idle | uploading | preparing | ready | dubbing | done
|
| 613 |
sessionId: null,
|
| 614 |
+
prepared: false,
|
| 615 |
+
uploadXhr: null,
|
| 616 |
+
abort: null, // AbortController for the current SSE fetch
|
| 617 |
targetLanguage: "English",
|
|
|
|
| 618 |
consoleCleared: false
|
| 619 |
};
|
| 620 |
|
|
|
|
| 716 |
if(!file.type.startsWith("video/") && !/\.(mp4|mov|mkv|webm|avi|m4v)$/i.test(file.name)){
|
| 717 |
pushLog("[WARN] Selected file does not look like a video. Proceeding anyway.", "l-warn");
|
| 718 |
}
|
| 719 |
+
// Picking a new file while a previous one is mid-flight (or already
|
| 720 |
+
// uploaded-but-not-yet-dubbed) cleanly cancels/discards that old one.
|
| 721 |
+
abortCurrentOperation();
|
| 722 |
+
if(state.sessionId && !state.running){
|
| 723 |
+
fetch(`${API_BASE}/session/${encodeURIComponent(state.sessionId)}`, { method:"DELETE" }).catch(()=>{});
|
| 724 |
+
}
|
| 725 |
+
resetPipelineState();
|
| 726 |
+
|
| 727 |
state.file = file;
|
| 728 |
fileNameEl.textContent = file.name;
|
| 729 |
fileSizeEl.textContent = fmtBytes(file.size);
|
| 730 |
fileChip.classList.add("show");
|
| 731 |
dropWrap.classList.add("has-file");
|
| 732 |
+
resultPanel.classList.remove("show");
|
| 733 |
+
resetConsole();
|
| 734 |
+
|
| 735 |
+
// Upload starts the instant a video is picked (gallery, file browser, or
|
| 736 |
+
// drag-and-drop) — no extra tap needed. The actual DUBBING only starts
|
| 737 |
+
// when the person presses the Generate Dub button (see startDub()).
|
| 738 |
+
uploadAndPrepare(file);
|
| 739 |
}
|
| 740 |
function clearFile(){
|
| 741 |
+
abortCurrentOperation();
|
| 742 |
+
if(state.sessionId && !state.running){
|
| 743 |
+
fetch(`${API_BASE}/session/${encodeURIComponent(state.sessionId)}`, { method:"DELETE" }).catch(()=>{});
|
| 744 |
+
}
|
| 745 |
+
resetPipelineState();
|
| 746 |
state.file = null;
|
| 747 |
fileInput.value = "";
|
| 748 |
fileChip.classList.remove("show");
|
| 749 |
dropWrap.classList.remove("has-file");
|
| 750 |
+
resultPanel.classList.remove("show");
|
| 751 |
+
resetConsole();
|
| 752 |
refreshGenerateState();
|
| 753 |
}
|
| 754 |
+
function resetPipelineState(){
|
| 755 |
+
state.sessionId = null;
|
| 756 |
+
state.prepared = false;
|
| 757 |
+
state.phase = "idle";
|
| 758 |
+
setProgress(0, "");
|
| 759 |
+
resetSourceLanguage();
|
| 760 |
+
}
|
| 761 |
+
function abortCurrentOperation(){
|
| 762 |
+
if(state.uploadXhr){ try{ state.uploadXhr.abort(); }catch(_){} state.uploadXhr = null; }
|
| 763 |
+
if(state.abort){ try{ state.abort.abort(); }catch(_){} state.abort = null; }
|
| 764 |
+
state.running = false;
|
| 765 |
+
setLive(false);
|
| 766 |
+
}
|
| 767 |
function refreshGenerateState(){
|
| 768 |
+
const canGenerate = !!(state.sessionId && state.prepared) && !state.running;
|
| 769 |
+
generateBtn.disabled = !canGenerate;
|
| 770 |
+
const label = generateBtn.querySelector(".btn-label");
|
| 771 |
+
if(!label) return;
|
| 772 |
+
if(state.phase === "uploading") label.textContent = "Uploading…";
|
| 773 |
+
else if(state.phase === "preparing") label.textContent = "Analyzing…";
|
| 774 |
+
else if(state.phase === "dubbing") label.textContent = "Dubbing…";
|
| 775 |
+
else if(state.phase === "ready" || state.phase === "done") label.textContent = "Generate Dub";
|
| 776 |
+
else label.textContent = "Generate Dub";
|
| 777 |
}
|
| 778 |
|
| 779 |
dropbox.addEventListener("click", ()=> fileInput.click());
|
|
|
|
| 853 |
}
|
| 854 |
|
| 855 |
/* ============================================================
|
| 856 |
+
Run lifecycle
|
| 857 |
============================================================ */
|
| 858 |
+
function beginRun(phase){
|
| 859 |
+
state.running = true;
|
| 860 |
+
state.phase = phase; // "preparing" | "dubbing"
|
| 861 |
+
generateBtn.classList.add("loading");
|
| 862 |
+
refreshGenerateState();
|
| 863 |
+
progressFill.classList.remove("paused");
|
| 864 |
+
progressFill.classList.add("active");
|
| 865 |
+
setLive(true);
|
| 866 |
+
}
|
| 867 |
+
function endRun(nextPhase){
|
| 868 |
+
state.running = false;
|
| 869 |
+
state.phase = nextPhase;
|
| 870 |
+
generateBtn.classList.remove("loading");
|
| 871 |
+
progressFill.classList.remove("active");
|
| 872 |
+
setLive(false);
|
| 873 |
+
refreshGenerateState();
|
| 874 |
+
}
|
| 875 |
+
/* keepSession=true -> the video/transcript are still valid (only
|
| 876 |
+
translate/TTS/mux failed), so leave the session in place and let the
|
| 877 |
+
person just press Generate Dub again without re-uploading. */
|
| 878 |
+
function failRun(reason, keepSession){
|
| 879 |
+
pushLog("[WARN] " + (reason || "Run stopped.") + " State reset.", "l-warn");
|
| 880 |
+
if(state.abort){ try{ state.abort.abort(); }catch(_){} }
|
| 881 |
+
if(state.uploadXhr){ try{ state.uploadXhr.abort(); }catch(_){} }
|
| 882 |
+
if(keepSession && state.sessionId && state.prepared){
|
| 883 |
+
endRun("ready");
|
| 884 |
+
}else{
|
| 885 |
+
resetPipelineState();
|
| 886 |
+
endRun("idle");
|
| 887 |
+
}
|
| 888 |
+
}
|
| 889 |
+
function finishRun(d){
|
| 890 |
+
setProgress(100, "Complete");
|
| 891 |
+
pushLog("[SUCCESS] Dubbing finished. Preparing preview…", "l-ok");
|
| 892 |
+
endRun("done");
|
| 893 |
+
const sid = d.session_id || state.sessionId;
|
| 894 |
+
if(sid){ state.sessionId = sid; presentResult(sid, d); }
|
| 895 |
+
}
|
| 896 |
+
|
| 897 |
+
/* ============================================================
|
| 898 |
+
SSE event handlers — one for the auto "analyze" phase (/prepare),
|
| 899 |
+
one for the manual "dub" phase (/dub). Kept separate because their
|
| 900 |
+
"done"/"error" semantics differ (ready-to-dub vs. finished-with-video).
|
| 901 |
+
============================================================ */
|
| 902 |
+
function handlePrepareEvent(event, data){
|
| 903 |
switch(event){
|
| 904 |
case "log": {
|
| 905 |
pushLog(data);
|
|
|
|
| 910 |
case "progress": {
|
| 911 |
const p = safeJSON(data);
|
| 912 |
if(p && typeof p.percent === "number"){
|
| 913 |
+
// Map the /prepare stream's own 0-100 onto the bar's 48-100 band
|
| 914 |
+
// (0-48 was the raw upload, tracked separately via XHR progress).
|
| 915 |
+
setProgress(48 + p.percent * 0.52, p.message || undefined);
|
| 916 |
}
|
| 917 |
break;
|
| 918 |
}
|
| 919 |
+
case "error": {
|
| 920 |
+
pushLog(String(data), "l-err");
|
| 921 |
+
failRun("Analysis failed — please try re-uploading the video.");
|
| 922 |
+
break;
|
| 923 |
+
}
|
| 924 |
+
case "done": {
|
| 925 |
+
const d = safeJSON(data) || {};
|
| 926 |
+
state.prepared = true;
|
| 927 |
if(d.source_language) lockSourceLanguage(d.source_language);
|
| 928 |
+
setProgress(100, d.line_count ? `Ready — ${d.line_count} line(s) detected` : "Ready");
|
| 929 |
+
pushLog("[SUCCESS] Ready to dub. Pick a language/voice and press Generate Dub.", "l-ok");
|
| 930 |
+
endRun("ready");
|
| 931 |
+
break;
|
| 932 |
+
}
|
| 933 |
+
default: {
|
| 934 |
+
if(data) pushLog(`[${event}] ${data}`);
|
| 935 |
+
}
|
| 936 |
+
}
|
| 937 |
+
}
|
| 938 |
+
|
| 939 |
+
function handleDubEvent(event, data){
|
| 940 |
+
switch(event){
|
| 941 |
+
case "log": {
|
| 942 |
+
pushLog(data);
|
| 943 |
+
const m = /source language detected:\s*(.+)/i.exec(data);
|
| 944 |
+
if(m) lockSourceLanguage(m[1].trim());
|
| 945 |
+
break;
|
| 946 |
+
}
|
| 947 |
+
case "progress": {
|
| 948 |
+
const p = safeJSON(data);
|
| 949 |
+
if(p && typeof p.percent === "number"){
|
| 950 |
+
setProgress(p.percent, p.message || undefined);
|
| 951 |
+
}
|
| 952 |
break;
|
| 953 |
}
|
| 954 |
case "error": {
|
| 955 |
pushLog(String(data), "l-err");
|
| 956 |
+
// Video + transcript are still on the server — no need to re-upload,
|
| 957 |
+
// just let the person press Generate Dub again.
|
| 958 |
+
failRun("Dubbing failed.", true);
|
| 959 |
break;
|
| 960 |
}
|
| 961 |
case "done": {
|
|
|
|
| 970 |
}
|
| 971 |
|
| 972 |
/* ============================================================
|
| 973 |
+
Result presentation
|
| 974 |
+
The result is no longer fetched into a JS blob first (that meant
|
| 975 |
+
waiting for the ENTIRE file to arrive before anything could play —
|
| 976 |
+
very slow on a weak connection). The <video> tag and the download
|
| 977 |
+
link both point straight at /download, so the browser streams it
|
| 978 |
+
natively (progressive playback, native Range/seek support, and the
|
| 979 |
+
OS's own download manager for the Save button).
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 980 |
============================================================ */
|
| 981 |
+
function presentResult(sessionId, meta){
|
|
|
|
| 982 |
resultMeta.innerHTML = "";
|
| 983 |
const tags = [];
|
| 984 |
if(meta.source_language) tags.push(["Source", meta.source_language]);
|
|
|
|
| 993 |
|
| 994 |
resultPanel.classList.add("show");
|
| 995 |
const url = `${API_BASE}/download/${encodeURIComponent(sessionId)}`;
|
| 996 |
+
resultVideo.src = url;
|
| 997 |
+
resultVideo.load();
|
| 998 |
+
downloadBtn.href = url;
|
| 999 |
+
downloadBtn.setAttribute("download", `dubbed_${sessionId}.mp4`);
|
| 1000 |
+
pushLog("[SUCCESS] Preview ready. Download available.", "l-ok");
|
| 1001 |
+
}
|
| 1002 |
+
|
| 1003 |
+
/* ============================================================
|
| 1004 |
+
Network — Phase 1 (automatic): /upload (real XHR progress) -> /prepare
|
| 1005 |
+
============================================================ */
|
| 1006 |
+
function uploadAndPrepare(file){
|
| 1007 |
+
beginRun("uploading");
|
| 1008 |
+
setProgress(0, "Uploading…");
|
| 1009 |
+
pushLog(`[SYS] Uploading "${file.name}" (${fmtBytes(file.size)})…`, "l-sys");
|
| 1010 |
+
|
| 1011 |
+
const xhr = new XMLHttpRequest();
|
| 1012 |
+
state.uploadXhr = xhr;
|
| 1013 |
+
const fd = new FormData();
|
| 1014 |
+
fd.append("file", file);
|
| 1015 |
+
|
| 1016 |
+
xhr.upload.addEventListener("progress", (e)=>{
|
| 1017 |
+
if(!e.lengthComputable) return;
|
| 1018 |
+
const frac = e.loaded / e.total;
|
| 1019 |
+
setProgress(frac * 45, `Uploading… ${Math.round(frac * 100)}%`); // upload = first 45% of the bar
|
| 1020 |
+
});
|
| 1021 |
|
| 1022 |
+
xhr.addEventListener("load", ()=>{
|
| 1023 |
+
state.uploadXhr = null;
|
| 1024 |
+
if(xhr.status < 200 || xhr.status >= 300){
|
| 1025 |
+
let detail = xhr.responseText || `HTTP ${xhr.status}`;
|
| 1026 |
+
try{ const j = JSON.parse(xhr.responseText); detail = j.detail || detail; }catch(_){}
|
| 1027 |
+
pushLog("[ERROR] Upload failed: " + detail, "l-err");
|
| 1028 |
+
failRun("Upload failed.");
|
| 1029 |
+
return;
|
| 1030 |
+
}
|
| 1031 |
+
let j = null;
|
| 1032 |
+
try{ j = JSON.parse(xhr.responseText); }catch(_){}
|
| 1033 |
+
if(!j || !j.session_id){
|
| 1034 |
+
pushLog("[ERROR] Upload succeeded but no session id was returned.", "l-err");
|
| 1035 |
+
failRun("Bad upload response.");
|
| 1036 |
+
return;
|
| 1037 |
+
}
|
| 1038 |
+
state.sessionId = j.session_id;
|
| 1039 |
+
pushLog("[SUCCESS] Upload complete. Analyzing (extracting audio + transcribing)…", "l-ok");
|
| 1040 |
+
setProgress(48, "Analyzing…");
|
| 1041 |
+
prepareSession(j.session_id);
|
| 1042 |
+
});
|
| 1043 |
+
|
| 1044 |
+
xhr.addEventListener("error", ()=>{
|
| 1045 |
+
state.uploadXhr = null;
|
| 1046 |
+
pushLog("[ERROR] Upload connection failed.", "l-err");
|
| 1047 |
+
failRun("Upload connection error.");
|
| 1048 |
+
});
|
| 1049 |
+
xhr.addEventListener("abort", ()=>{ state.uploadXhr = null; });
|
| 1050 |
+
|
| 1051 |
+
xhr.open("POST", `${API_BASE}/upload`);
|
| 1052 |
+
xhr.send(fd);
|
| 1053 |
+
}
|
| 1054 |
+
|
| 1055 |
+
async function prepareSession(sessionId){
|
| 1056 |
+
state.phase = "preparing";
|
| 1057 |
+
refreshGenerateState();
|
| 1058 |
+
state.abort = new AbortController();
|
| 1059 |
try{
|
| 1060 |
+
const res = await fetch(`${API_BASE}/prepare/${encodeURIComponent(sessionId)}`, {
|
| 1061 |
+
method: "GET", signal: state.abort.signal,
|
| 1062 |
+
});
|
| 1063 |
+
await readSSE(res, handlePrepareEvent);
|
| 1064 |
+
if(state.phase === "preparing"){
|
| 1065 |
+
pushLog("[WARN] Analysis stream closed unexpectedly before completion.", "l-warn");
|
| 1066 |
+
failRun("Analysis stream ended early.");
|
| 1067 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1068 |
}catch(err){
|
| 1069 |
+
if(err.name === "AbortError"){ /* cancelled — a new file was picked */ }
|
| 1070 |
+
else{
|
| 1071 |
+
pushLog("[ERROR] Analysis connection failed: " + err.message, "l-err");
|
| 1072 |
+
failRun("Analysis stream error.");
|
| 1073 |
+
}
|
| 1074 |
}
|
| 1075 |
}
|
| 1076 |
|
| 1077 |
/* ============================================================
|
| 1078 |
+
Network — Phase 2 (manual, button press): /dub
|
| 1079 |
============================================================ */
|
| 1080 |
async function startDub(){
|
| 1081 |
+
if(!state.sessionId || !state.prepared || state.running) return;
|
| 1082 |
|
| 1083 |
state.targetLanguage = targetLang.value;
|
| 1084 |
+
beginRun("dubbing");
|
| 1085 |
+
resultPanel.classList.remove("show");
|
| 1086 |
+
setProgress(0, "Starting dub…");
|
| 1087 |
+
pushLog(`[SYS] Dubbing into ${targetLang.value} · Voice: ${voiceSelect.value}`, "l-sys");
|
| 1088 |
|
| 1089 |
const fd = new FormData();
|
| 1090 |
+
fd.append("session_id", state.sessionId);
|
| 1091 |
fd.append("target_language", targetLang.value);
|
| 1092 |
fd.append("voice_name", voiceSelect.value);
|
| 1093 |
|
| 1094 |
state.abort = new AbortController();
|
| 1095 |
try{
|
| 1096 |
const res = await fetch(`${API_BASE}/dub`, { method:"POST", body:fd, signal:state.abort.signal });
|
| 1097 |
+
await readSSE(res, handleDubEvent);
|
| 1098 |
+
if(state.phase === "dubbing"){
|
|
|
|
| 1099 |
pushLog("[WARN] Stream closed unexpectedly before completion.", "l-warn");
|
| 1100 |
+
failRun("Dub stream ended early.", true);
|
| 1101 |
}
|
| 1102 |
}catch(err){
|
| 1103 |
if(err.name === "AbortError"){ /* user cancelled */ }
|
| 1104 |
else{
|
| 1105 |
pushLog("[ERROR] Connection failed: " + err.message, "l-err");
|
| 1106 |
+
failRun("Dub stream error.", true);
|
| 1107 |
}
|
| 1108 |
}
|
| 1109 |
}
|