mison2264 commited on
Commit
4d6a7c1
·
verified ·
1 Parent(s): 7b3fe7c

Update index.html

Browse files
Files changed (1) hide show
  1. 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 from your gallery — upload starts instantly. Our pipeline transcribes, translates, and re-voices it in one Gemini request, then matches the audio to your video's exact length with real-time progress streaming.</p>
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
- abort: null,
 
 
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
- refreshGenerateState();
723
- // Upload starts the instant a video is picked (from the gallery, a file
724
- // browser, or drag-and-drop) — no extra tap needed.
725
- if(!state.running) startDub();
 
 
 
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
- generateBtn.disabled = !state.file || state.running;
 
 
 
 
 
 
 
 
736
  }
737
 
738
  dropbox.addEventListener("click", ()=> fileInput.click());
@@ -812,9 +853,53 @@ function safeJSON(str){
812
  }
813
 
814
  /* ============================================================
815
- Central SSE event handler for the /dub stream
816
  ============================================================ */
817
- function handleSSEEvent(event, data){
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- progressFill.classList.add("active");
829
- progressFill.classList.remove("paused");
830
- setProgress(p.percent, p.message || undefined);
831
  }
832
  break;
833
  }
834
- case "data": {
835
- const d = safeJSON(data);
836
- if(!d) break;
 
 
 
 
 
837
  if(d.source_language) lockSourceLanguage(d.source_language);
838
- if(d.session_id) state.sessionId = d.session_id;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
839
  break;
840
  }
841
  case "error": {
842
  pushLog(String(data), "l-err");
843
- failRun("Backend reported an error.");
 
 
844
  break;
845
  }
846
  case "done": {
@@ -855,50 +970,15 @@ function handleSSEEvent(event, data){
855
  }
856
 
857
  /* ============================================================
858
- Run lifecycle
859
- ============================================================ */
860
- function startRunUI(){
861
- state.running = true;
862
- state.consoleCleared = false;
863
- generateBtn.classList.add("loading");
864
- generateBtn.disabled = true;
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
- async function presentResult(sessionId, meta){
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
- pushLog("[INFO] Fetching dubbed video stream from server…");
919
- const res = await fetch(url, { method:"GET" });
920
- if(!res.ok) throw new Error("HTTP " + res.status);
921
- const blob = await res.blob();
922
-
923
- if(state.resultObjectUrl){ URL.revokeObjectURL(state.resultObjectUrl); }
924
- state.resultObjectUrl = URL.createObjectURL(blob);
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
- pushLog("[ERROR] Could not retrieve the dubbed file: " + err.message, "l-err");
932
- // Fallback: let the browser hit the endpoint directly on click.
933
- resultVideo.src = url;
934
- downloadBtn.href = url;
935
- downloadBtn.setAttribute("download", `dubbed_${sessionId}.mp4`);
936
  }
937
  }
938
 
939
  /* ============================================================
940
- Network: /dub
941
  ============================================================ */
942
  async function startDub(){
943
- if(!state.file || state.running) return;
944
 
945
  state.targetLanguage = targetLang.value;
946
- startRunUI();
947
- pushLog(`[SYS] Uploading "${state.file.name}" (${fmtBytes(state.file.size)})…`, "l-sys");
948
- pushLog(`[INFO] Target language: ${targetLang.value} · Voice: ${voiceSelect.value}`);
 
949
 
950
  const fd = new FormData();
951
- fd.append("file", state.file);
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, handleSSEEvent);
959
- // Stream ended. If we were mid-run without a done/error, note it.
960
- if(state.running){
961
  pushLog("[WARN] Stream closed unexpectedly before completion.", "l-warn");
962
- endRunUI();
963
  }
964
  }catch(err){
965
  if(err.name === "AbortError"){ /* user cancelled */ }
966
  else{
967
  pushLog("[ERROR] Connection failed: " + err.message, "l-err");
968
- failRun("Upload/stream error.");
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 &amp; 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
  }