Spaces:
Running
Running
Update index.html
Browse files- index.html +47 -310
index.html
CHANGED
|
@@ -211,29 +211,6 @@ body{
|
|
| 211 |
@keyframes rot{to{transform:rotate(360deg);}}
|
| 212 |
.src-badge.locked{color:#0a8a4a;background:#eefcf3;border-color:#c7f0d7;}
|
| 213 |
|
| 214 |
-
/* Toggle switch */
|
| 215 |
-
.toggle-line{
|
| 216 |
-
display:flex;align-items:center;justify-content:space-between;gap:14px;
|
| 217 |
-
padding:14px;border-radius:var(--radius-sm);
|
| 218 |
-
background:#f7fbff;border:1px solid #e3f0fb;margin:4px 0 18px;
|
| 219 |
-
}
|
| 220 |
-
.toggle-line .tl-txt{display:flex;flex-direction:column;}
|
| 221 |
-
.toggle-line .tl-txt strong{font-size:14px;font-weight:700;}
|
| 222 |
-
.toggle-line .tl-txt span{font-size:12px;color:var(--ink-soft);}
|
| 223 |
-
.switch{position:relative;width:52px;height:30px;flex-shrink:0;}
|
| 224 |
-
.switch input{opacity:0;width:0;height:0;}
|
| 225 |
-
.slider{
|
| 226 |
-
position:absolute;inset:0;cursor:pointer;border-radius:999px;
|
| 227 |
-
background:#cdd9e8;transition:background .25s ease;
|
| 228 |
-
}
|
| 229 |
-
.slider::before{
|
| 230 |
-
content:"";position:absolute;width:22px;height:22px;left:4px;top:4px;
|
| 231 |
-
border-radius:50%;background:#fff;transition:transform .25s ease;
|
| 232 |
-
box-shadow:0 2px 6px rgba(0,0,0,0.22);
|
| 233 |
-
}
|
| 234 |
-
.switch input:checked + .slider{background:linear-gradient(120deg,var(--sky),var(--sky-soft));box-shadow:0 0 0 4px rgba(0,162,255,0.15);}
|
| 235 |
-
.switch input:checked + .slider::before{transform:translateX(22px);}
|
| 236 |
-
|
| 237 |
/* Primary button */
|
| 238 |
.btn{
|
| 239 |
display:inline-flex;align-items:center;justify-content:center;gap:10px;
|
|
@@ -339,76 +316,6 @@ body{
|
|
| 339 |
}
|
| 340 |
.meta-tag svg{color:var(--sky);}
|
| 341 |
|
| 342 |
-
/* ============================================================
|
| 343 |
-
Modal (speaker confirmation)
|
| 344 |
-
============================================================ */
|
| 345 |
-
.modal-scrim{
|
| 346 |
-
position:fixed;inset:0;z-index:60;display:none;
|
| 347 |
-
align-items:center;justify-content:center;padding:22px;
|
| 348 |
-
background:rgba(10,25,45,0.42);backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);
|
| 349 |
-
opacity:0;transition:opacity .3s ease;
|
| 350 |
-
}
|
| 351 |
-
.modal-scrim.show{display:flex;opacity:1;}
|
| 352 |
-
.modal{
|
| 353 |
-
width:100%;max-width:560px;max-height:88vh;overflow:hidden;
|
| 354 |
-
border-radius:24px;
|
| 355 |
-
background:var(--panel-glass);
|
| 356 |
-
backdrop-filter:blur(26px);-webkit-backdrop-filter:blur(26px);
|
| 357 |
-
border:1px solid rgba(255,255,255,0.7);
|
| 358 |
-
box-shadow:0 40px 90px -30px rgba(0,80,150,0.55);
|
| 359 |
-
transform:translateY(26px) scale(.97);opacity:0;
|
| 360 |
-
transition:transform .4s cubic-bezier(.22,.9,.35,1),opacity .35s ease;
|
| 361 |
-
display:flex;flex-direction:column;
|
| 362 |
-
}
|
| 363 |
-
.modal-scrim.show .modal{transform:translateY(0) scale(1);opacity:1;}
|
| 364 |
-
|
| 365 |
-
.modal-head{padding:24px 26px 18px;border-bottom:1px solid rgba(0,120,200,0.12);}
|
| 366 |
-
.modal-head .mh-top{display:flex;align-items:center;gap:13px;}
|
| 367 |
-
.modal-badge{
|
| 368 |
-
width:44px;height:44px;border-radius:13px;flex-shrink:0;
|
| 369 |
-
display:grid;place-items:center;color:#fff;
|
| 370 |
-
background:linear-gradient(135deg,var(--sky),var(--sky-soft));
|
| 371 |
-
box-shadow:0 10px 22px -8px rgba(0,162,255,0.7);
|
| 372 |
-
}
|
| 373 |
-
.modal-head h2{margin:0;font-size:19px;font-weight:800;letter-spacing:-.02em;}
|
| 374 |
-
.modal-head .mh-sub{margin:2px 0 0;font-size:13px;color:var(--ink-soft);}
|
| 375 |
-
.summary-metric{
|
| 376 |
-
display:flex;align-items:center;gap:12px;margin-top:16px;
|
| 377 |
-
padding:13px 15px;border-radius:14px;
|
| 378 |
-
background:rgba(0,162,255,0.08);border:1px solid rgba(0,162,255,0.18);
|
| 379 |
-
}
|
| 380 |
-
.summary-metric .sm-ico{color:var(--sky-deep);flex-shrink:0;}
|
| 381 |
-
.summary-metric .sm-main{font-weight:750;font-size:15px;}
|
| 382 |
-
.summary-metric .sm-sub{font-size:12.5px;color:var(--ink-soft);}
|
| 383 |
-
|
| 384 |
-
.modal-body{padding:8px 26px 4px;overflow-y:auto;flex:1;}
|
| 385 |
-
.modal-body::-webkit-scrollbar{width:8px;}
|
| 386 |
-
.modal-body::-webkit-scrollbar-thumb{background:#cfe0f2;border-radius:8px;}
|
| 387 |
-
|
| 388 |
-
.spk-row{
|
| 389 |
-
display:flex;align-items:center;gap:14px;padding:14px 2px;
|
| 390 |
-
border-bottom:1px solid rgba(0,120,200,0.10);
|
| 391 |
-
}
|
| 392 |
-
.spk-row:last-child{border-bottom:none;}
|
| 393 |
-
.spk-ava{
|
| 394 |
-
width:42px;height:42px;border-radius:12px;flex-shrink:0;display:grid;place-items:center;color:#fff;
|
| 395 |
-
}
|
| 396 |
-
.spk-ava.male{background:linear-gradient(135deg,#2f7fff,#5aa8ff);}
|
| 397 |
-
.spk-ava.female{background:linear-gradient(135deg,#ff5fa2,#ff9bc4);}
|
| 398 |
-
.spk-ava.unknown{background:linear-gradient(135deg,#8a97ab,#b8c4d6);}
|
| 399 |
-
.spk-info{min-width:0;flex:1;}
|
| 400 |
-
.spk-info .spk-name{font-weight:700;font-size:14px;}
|
| 401 |
-
.spk-info .spk-gender{font-size:12px;color:var(--ink-soft);display:flex;align-items:center;gap:5px;}
|
| 402 |
-
.spk-voice{width:150px;flex-shrink:0;}
|
| 403 |
-
.spk-voice .control select{padding:10px 34px 10px 12px;font-size:13.5px;}
|
| 404 |
-
@media (max-width:480px){
|
| 405 |
-
.spk-row{flex-wrap:wrap;}
|
| 406 |
-
.spk-voice{width:100%;}
|
| 407 |
-
}
|
| 408 |
-
|
| 409 |
-
.modal-foot{padding:18px 26px 24px;display:flex;gap:12px;border-top:1px solid rgba(0,120,200,0.12);}
|
| 410 |
-
.modal-foot .btn{width:auto;flex:1;padding:14px 18px;}
|
| 411 |
-
|
| 412 |
.footer{margin-top:44px;text-align:center;color:#9fb0c4;font-size:12.5px;}
|
| 413 |
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0;}
|
| 414 |
</style>
|
|
@@ -438,7 +345,7 @@ body{
|
|
| 438 |
<!-- ===================== HERO ===================== -->
|
| 439 |
<section class="hero">
|
| 440 |
<h1>Dub any video into <span class="grad">any language</span>, flawlessly.</h1>
|
| 441 |
-
<p>
|
| 442 |
</section>
|
| 443 |
|
| 444 |
<!-- ===================== MAIN GRID ===================== -->
|
|
@@ -542,18 +449,6 @@ body{
|
|
| 542 |
</span>
|
| 543 |
</div>
|
| 544 |
|
| 545 |
-
<!-- Multiple character toggle -->
|
| 546 |
-
<div class="toggle-line">
|
| 547 |
-
<div class="tl-txt">
|
| 548 |
-
<strong>Multiple Characters</strong>
|
| 549 |
-
<span>Diarize speakers & assign a voice to each</span>
|
| 550 |
-
</div>
|
| 551 |
-
<label class="switch" title="Toggle multi-speaker mode">
|
| 552 |
-
<input type="checkbox" id="multiToggle" />
|
| 553 |
-
<span class="slider"></span>
|
| 554 |
-
</label>
|
| 555 |
-
</div>
|
| 556 |
-
|
| 557 |
<button class="btn" id="generateBtn" disabled>
|
| 558 |
<span class="btn-ico" aria-hidden="true">
|
| 559 |
<svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="5 3 19 12 5 21 5 3"/></svg>
|
|
@@ -628,44 +523,6 @@ body{
|
|
| 628 |
</div>
|
| 629 |
</div>
|
| 630 |
|
| 631 |
-
<!-- ===================== MODAL ===================== -->
|
| 632 |
-
<div class="modal-scrim" id="modalScrim" role="dialog" aria-modal="true" aria-labelledby="modalTitle">
|
| 633 |
-
<div class="modal">
|
| 634 |
-
<div class="modal-head">
|
| 635 |
-
<div class="mh-top">
|
| 636 |
-
<div class="modal-badge" aria-hidden="true">
|
| 637 |
-
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
| 638 |
-
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/>
|
| 639 |
-
</svg>
|
| 640 |
-
</div>
|
| 641 |
-
<div>
|
| 642 |
-
<h2 id="modalTitle">Assign Speaker Voices</h2>
|
| 643 |
-
<p class="mh-sub">We detected multiple speakers. Confirm a voice for each before dubbing.</p>
|
| 644 |
-
</div>
|
| 645 |
-
</div>
|
| 646 |
-
<div class="summary-metric">
|
| 647 |
-
<span class="sm-ico" aria-hidden="true">
|
| 648 |
-
<svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round">
|
| 649 |
-
<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/>
|
| 650 |
-
</svg>
|
| 651 |
-
</span>
|
| 652 |
-
<div>
|
| 653 |
-
<div class="sm-main" id="summaryMain">0 Speakers Detected</div>
|
| 654 |
-
<div class="sm-sub" id="summarySub">Source language: —</div>
|
| 655 |
-
</div>
|
| 656 |
-
</div>
|
| 657 |
-
</div>
|
| 658 |
-
<div class="modal-body" id="speakerRows"></div>
|
| 659 |
-
<div class="modal-foot">
|
| 660 |
-
<button class="btn ghost" id="modalCancel">Cancel</button>
|
| 661 |
-
<button class="btn" id="applyDubBtn">
|
| 662 |
-
<span class="btn-ico" aria-hidden="true"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg></span>
|
| 663 |
-
<span>Apply & Dub</span>
|
| 664 |
-
</button>
|
| 665 |
-
</div>
|
| 666 |
-
</div>
|
| 667 |
-
</div>
|
| 668 |
-
|
| 669 |
<script>
|
| 670 |
"use strict";
|
| 671 |
|
|
@@ -674,26 +531,41 @@ body{
|
|
| 674 |
============================================================ */
|
| 675 |
const API_BASE = "https://video-dubbing-api-85ur.onrender.com";
|
| 676 |
|
| 677 |
-
/* Gemini native
|
| 678 |
-
|
| 679 |
const VOICES = [
|
| 680 |
-
{ name: "
|
| 681 |
-
{ name: "Puck",
|
| 682 |
-
{ name: "
|
| 683 |
-
{ name: "
|
| 684 |
-
{ name: "
|
| 685 |
-
{ name: "
|
| 686 |
-
{ name: "
|
| 687 |
-
{ name: "Aoede",
|
| 688 |
-
{ name: "
|
| 689 |
-
{ name: "
|
| 690 |
-
{ name: "
|
| 691 |
-
{ name: "
|
| 692 |
-
{ name: "
|
| 693 |
-
{ name: "
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 694 |
];
|
| 695 |
-
const
|
| 696 |
-
const DEFAULT_FEMALE = "Aoede";
|
| 697 |
|
| 698 |
/* ============================================================
|
| 699 |
Element refs
|
|
@@ -710,7 +582,6 @@ const fileClear = $("fileClear");
|
|
| 710 |
|
| 711 |
const targetLang = $("targetLang");
|
| 712 |
const voiceSelect = $("voiceSelect");
|
| 713 |
-
const multiToggle = $("multiToggle");
|
| 714 |
const generateBtn = $("generateBtn");
|
| 715 |
|
| 716 |
const srcBadge = $("srcBadge");
|
|
@@ -732,13 +603,6 @@ const downloadBtn = $("downloadBtn");
|
|
| 732 |
const serverDot = $("serverDot");
|
| 733 |
const serverStatusText = $("serverStatusText");
|
| 734 |
|
| 735 |
-
const modalScrim = $("modalScrim");
|
| 736 |
-
const speakerRows = $("speakerRows");
|
| 737 |
-
const summaryMain = $("summaryMain");
|
| 738 |
-
const summarySub = $("summarySub");
|
| 739 |
-
const modalCancel = $("modalCancel");
|
| 740 |
-
const applyDubBtn = $("applyDubBtn");
|
| 741 |
-
|
| 742 |
/* ============================================================
|
| 743 |
App state
|
| 744 |
============================================================ */
|
|
@@ -748,7 +612,6 @@ const state = {
|
|
| 748 |
sessionId: null,
|
| 749 |
abort: null,
|
| 750 |
targetLanguage: "English",
|
| 751 |
-
awaitingConfirm: false,
|
| 752 |
resultObjectUrl: null,
|
| 753 |
consoleCleared: false
|
| 754 |
};
|
|
@@ -837,19 +700,10 @@ function buildMainVoiceSelect(){
|
|
| 837 |
VOICES.forEach(v=>{
|
| 838 |
const o = document.createElement("option");
|
| 839 |
o.value = v.name;
|
| 840 |
-
o.textContent = `${v.name} · ${v.
|
| 841 |
voiceSelect.appendChild(o);
|
| 842 |
});
|
| 843 |
-
voiceSelect.value =
|
| 844 |
-
}
|
| 845 |
-
function voiceOptionsHTML(selected){
|
| 846 |
-
return VOICES.map(v=>`<option value="${v.name}" ${v.name===selected?"selected":""}>${v.name} · ${v.profile==="male"?"Masculine":"Feminine"}</option>`).join("");
|
| 847 |
-
}
|
| 848 |
-
function defaultVoiceForGender(gender){
|
| 849 |
-
const g = String(gender||"").toLowerCase();
|
| 850 |
-
if(g === "male") return DEFAULT_MALE;
|
| 851 |
-
if(g === "female") return DEFAULT_FEMALE;
|
| 852 |
-
return voiceSelect.value || DEFAULT_MALE;
|
| 853 |
}
|
| 854 |
|
| 855 |
/* ============================================================
|
|
@@ -866,6 +720,9 @@ function acceptFile(file){
|
|
| 866 |
fileChip.classList.add("show");
|
| 867 |
dropWrap.classList.add("has-file");
|
| 868 |
refreshGenerateState();
|
|
|
|
|
|
|
|
|
|
| 869 |
}
|
| 870 |
function clearFile(){
|
| 871 |
state.file = null;
|
|
@@ -955,7 +812,7 @@ function safeJSON(str){
|
|
| 955 |
}
|
| 956 |
|
| 957 |
/* ============================================================
|
| 958 |
-
Central SSE event handler
|
| 959 |
============================================================ */
|
| 960 |
function handleSSEEvent(event, data){
|
| 961 |
switch(event){
|
|
@@ -979,11 +836,6 @@ function handleSSEEvent(event, data){
|
|
| 979 |
if(!d) break;
|
| 980 |
if(d.source_language) lockSourceLanguage(d.source_language);
|
| 981 |
if(d.session_id) state.sessionId = d.session_id;
|
| 982 |
-
if(d.status === "awaiting_confirmation"){
|
| 983 |
-
pushLog("[SYS] Diarization complete — awaiting voice confirmation.", "l-sys");
|
| 984 |
-
pauseAtSixty();
|
| 985 |
-
openSpeakerModal(d);
|
| 986 |
-
}
|
| 987 |
break;
|
| 988 |
}
|
| 989 |
case "error": {
|
|
@@ -1007,7 +859,6 @@ function handleSSEEvent(event, data){
|
|
| 1007 |
============================================================ */
|
| 1008 |
function startRunUI(){
|
| 1009 |
state.running = true;
|
| 1010 |
-
state.awaitingConfirm = false;
|
| 1011 |
state.consoleCleared = false;
|
| 1012 |
generateBtn.classList.add("loading");
|
| 1013 |
generateBtn.disabled = true;
|
|
@@ -1027,13 +878,6 @@ function endRunUI(){
|
|
| 1027 |
refreshGenerateState();
|
| 1028 |
}
|
| 1029 |
|
| 1030 |
-
function pauseAtSixty(){
|
| 1031 |
-
state.awaitingConfirm = true;
|
| 1032 |
-
progressFill.classList.add("paused");
|
| 1033 |
-
progressFill.classList.remove("active");
|
| 1034 |
-
setProgress(60, "Paused — confirm speaker voices");
|
| 1035 |
-
}
|
| 1036 |
-
|
| 1037 |
function failRun(reason){
|
| 1038 |
pushLog("[WARN] " + (reason || "Run stopped.") + " State reset.", "l-warn");
|
| 1039 |
if(state.abort){ try{ state.abort.abort(); }catch(_){}}
|
|
@@ -1093,78 +937,7 @@ async function presentResult(sessionId, meta){
|
|
| 1093 |
}
|
| 1094 |
|
| 1095 |
/* ============================================================
|
| 1096 |
-
|
| 1097 |
-
============================================================ */
|
| 1098 |
-
function openSpeakerModal(payload){
|
| 1099 |
-
const speakers = Array.isArray(payload.speakers) ? payload.speakers : [];
|
| 1100 |
-
const male = speakers.filter(s=>String(s.gender).toLowerCase()==="male").length;
|
| 1101 |
-
const female = speakers.filter(s=>String(s.gender).toLowerCase()==="female").length;
|
| 1102 |
-
const unknown = speakers.length - male - female;
|
| 1103 |
-
|
| 1104 |
-
const parts = [];
|
| 1105 |
-
if(male) parts.push(`${male} Male`);
|
| 1106 |
-
if(female) parts.push(`${female} Female`);
|
| 1107 |
-
if(unknown) parts.push(`${unknown} Unknown`);
|
| 1108 |
-
summaryMain.textContent = `${speakers.length} Speaker${speakers.length===1?"":"s"} Detected`;
|
| 1109 |
-
summarySub.textContent = (parts.length ? "(" + parts.join(", ") + ")" : "") +
|
| 1110 |
-
(payload.source_language ? ` · Source: ${payload.source_language}` : "");
|
| 1111 |
-
|
| 1112 |
-
// Build rows
|
| 1113 |
-
speakerRows.innerHTML = "";
|
| 1114 |
-
speakers.forEach((sp, i)=>{
|
| 1115 |
-
const id = sp.id || `Speaker_${i}`;
|
| 1116 |
-
const gender = (sp.gender || "Unknown");
|
| 1117 |
-
const g = gender.toLowerCase();
|
| 1118 |
-
const avaClass = g==="male" ? "male" : g==="female" ? "female" : "unknown";
|
| 1119 |
-
const def = defaultVoiceForGender(gender);
|
| 1120 |
-
|
| 1121 |
-
const row = document.createElement("div");
|
| 1122 |
-
row.className = "spk-row";
|
| 1123 |
-
row.innerHTML = `
|
| 1124 |
-
<div class="spk-ava ${avaClass}" aria-hidden="true">
|
| 1125 |
-
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
|
| 1126 |
-
</div>
|
| 1127 |
-
<div class="spk-info">
|
| 1128 |
-
<div class="spk-name">${esc(id)}</div>
|
| 1129 |
-
<div class="spk-gender">${esc(gender)} voice profile</div>
|
| 1130 |
-
</div>
|
| 1131 |
-
<div class="spk-voice">
|
| 1132 |
-
<div class="control">
|
| 1133 |
-
<select data-speaker="${esc(id)}">${voiceOptionsHTML(def)}</select>
|
| 1134 |
-
<span class="chev" aria-hidden="true"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg></span>
|
| 1135 |
-
</div>
|
| 1136 |
-
</div>`;
|
| 1137 |
-
speakerRows.appendChild(row);
|
| 1138 |
-
});
|
| 1139 |
-
|
| 1140 |
-
modalScrim.classList.add("show");
|
| 1141 |
-
}
|
| 1142 |
-
|
| 1143 |
-
function closeModal(){ modalScrim.classList.remove("show"); }
|
| 1144 |
-
|
| 1145 |
-
modalCancel.addEventListener("click", ()=>{
|
| 1146 |
-
closeModal();
|
| 1147 |
-
pushLog("[WARN] Voice confirmation cancelled. Aborting session.", "l-warn");
|
| 1148 |
-
if(state.sessionId){
|
| 1149 |
-
fetch(`${API_BASE}/session/${encodeURIComponent(state.sessionId)}`, { method:"DELETE" }).catch(()=>{});
|
| 1150 |
-
}
|
| 1151 |
-
if(state.abort){ try{ state.abort.abort(); }catch(_){}}
|
| 1152 |
-
endRunUI();
|
| 1153 |
-
setProgress(0, "Cancelled");
|
| 1154 |
-
});
|
| 1155 |
-
|
| 1156 |
-
modalScrim.addEventListener("mousedown",(e)=>{ if(e.target === modalScrim) modalCancel.click(); });
|
| 1157 |
-
|
| 1158 |
-
applyDubBtn.addEventListener("click", async ()=>{
|
| 1159 |
-
const selects = speakerRows.querySelectorAll("select[data-speaker]");
|
| 1160 |
-
const voiceMap = {};
|
| 1161 |
-
selects.forEach(s=>{ voiceMap[s.getAttribute("data-speaker")] = s.value; });
|
| 1162 |
-
closeModal();
|
| 1163 |
-
await confirmDubbing(voiceMap);
|
| 1164 |
-
});
|
| 1165 |
-
|
| 1166 |
-
/* ============================================================
|
| 1167 |
-
Network: /dub and /confirm-dubbing
|
| 1168 |
============================================================ */
|
| 1169 |
async function startDub(){
|
| 1170 |
if(!state.file || state.running) return;
|
|
@@ -1172,20 +945,19 @@ async function startDub(){
|
|
| 1172 |
state.targetLanguage = targetLang.value;
|
| 1173 |
startRunUI();
|
| 1174 |
pushLog(`[SYS] Uploading "${state.file.name}" (${fmtBytes(state.file.size)})…`, "l-sys");
|
| 1175 |
-
pushLog(`[INFO] Target language: ${targetLang.value} · Voice: ${voiceSelect.value}
|
| 1176 |
|
| 1177 |
const fd = new FormData();
|
| 1178 |
fd.append("file", state.file);
|
| 1179 |
fd.append("target_language", targetLang.value);
|
| 1180 |
fd.append("voice_name", voiceSelect.value);
|
| 1181 |
-
fd.append("multiple_characters", multiToggle.checked ? "true" : "false");
|
| 1182 |
|
| 1183 |
state.abort = new AbortController();
|
| 1184 |
try{
|
| 1185 |
const res = await fetch(`${API_BASE}/dub`, { method:"POST", body:fd, signal:state.abort.signal });
|
| 1186 |
await readSSE(res, handleSSEEvent);
|
| 1187 |
-
// Stream ended. If we were mid-run without a done/error
|
| 1188 |
-
if(state.running
|
| 1189 |
pushLog("[WARN] Stream closed unexpectedly before completion.", "l-warn");
|
| 1190 |
endRunUI();
|
| 1191 |
}
|
|
@@ -1198,42 +970,6 @@ async function startDub(){
|
|
| 1198 |
}
|
| 1199 |
}
|
| 1200 |
|
| 1201 |
-
async function confirmDubbing(voiceMap){
|
| 1202 |
-
if(!state.sessionId){
|
| 1203 |
-
pushLog("[ERROR] Missing session id — cannot confirm voices.", "l-err");
|
| 1204 |
-
failRun("No active session.");
|
| 1205 |
-
return;
|
| 1206 |
-
}
|
| 1207 |
-
state.awaitingConfirm = false;
|
| 1208 |
-
state.running = true;
|
| 1209 |
-
generateBtn.classList.add("loading");
|
| 1210 |
-
progressFill.classList.remove("paused");
|
| 1211 |
-
progressFill.classList.add("active");
|
| 1212 |
-
setLive(true);
|
| 1213 |
-
setProgress(61, "Resuming with confirmed voices…");
|
| 1214 |
-
pushLog("[SYS] Voice mappings: " + JSON.stringify(voiceMap), "l-sys");
|
| 1215 |
-
|
| 1216 |
-
const fd = new FormData();
|
| 1217 |
-
fd.append("session_id", state.sessionId);
|
| 1218 |
-
fd.append("voice_mappings", JSON.stringify(voiceMap));
|
| 1219 |
-
|
| 1220 |
-
state.abort = new AbortController();
|
| 1221 |
-
try{
|
| 1222 |
-
const res = await fetch(`${API_BASE}/confirm-dubbing`, { method:"POST", body:fd, signal:state.abort.signal });
|
| 1223 |
-
await readSSE(res, handleSSEEvent);
|
| 1224 |
-
if(state.running){
|
| 1225 |
-
pushLog("[WARN] Confirmation stream closed before completion.", "l-warn");
|
| 1226 |
-
endRunUI();
|
| 1227 |
-
}
|
| 1228 |
-
}catch(err){
|
| 1229 |
-
if(err.name === "AbortError"){ /* cancelled */ }
|
| 1230 |
-
else{
|
| 1231 |
-
pushLog("[ERROR] Confirmation failed: " + err.message, "l-err");
|
| 1232 |
-
failRun("Confirm-dubbing stream error.");
|
| 1233 |
-
}
|
| 1234 |
-
}
|
| 1235 |
-
}
|
| 1236 |
-
|
| 1237 |
generateBtn.addEventListener("click", startDub);
|
| 1238 |
|
| 1239 |
/* ============================================================
|
|
@@ -1244,8 +980,9 @@ async function checkHealth(){
|
|
| 1244 |
const res = await fetch(`${API_BASE}/health`, { method:"GET" });
|
| 1245 |
const j = await res.json();
|
| 1246 |
if(j.status === "ok"){
|
| 1247 |
-
|
| 1248 |
-
|
|
|
|
| 1249 |
}else{
|
| 1250 |
serverDot.style.background = "var(--warn)";
|
| 1251 |
serverStatusText.textContent = "Backend degraded";
|
|
|
|
| 211 |
@keyframes rot{to{transform:rotate(360deg);}}
|
| 212 |
.src-badge.locked{color:#0a8a4a;background:#eefcf3;border-color:#c7f0d7;}
|
| 213 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 214 |
/* Primary button */
|
| 215 |
.btn{
|
| 216 |
display:inline-flex;align-items:center;justify-content:center;gap:10px;
|
|
|
|
| 316 |
}
|
| 317 |
.meta-tag svg{color:var(--sky);}
|
| 318 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 319 |
.footer{margin-top:44px;text-align:center;color:#9fb0c4;font-size:12.5px;}
|
| 320 |
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0;}
|
| 321 |
</style>
|
|
|
|
| 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 ===================== -->
|
|
|
|
| 449 |
</span>
|
| 450 |
</div>
|
| 451 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 452 |
<button class="btn" id="generateBtn" disabled>
|
| 453 |
<span class="btn-ico" aria-hidden="true">
|
| 454 |
<svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="5 3 19 12 5 21 5 3"/></svg>
|
|
|
|
| 523 |
</div>
|
| 524 |
</div>
|
| 525 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 526 |
<script>
|
| 527 |
"use strict";
|
| 528 |
|
|
|
|
| 531 |
============================================================ */
|
| 532 |
const API_BASE = "https://video-dubbing-api-85ur.onrender.com";
|
| 533 |
|
| 534 |
+
/* All 30 official Gemini native-TTS prebuilt voices, with Google's own
|
| 535 |
+
one-word style tag for each (ai.google.dev/gemini-api/docs/speech-generation). */
|
| 536 |
const VOICES = [
|
| 537 |
+
{ name: "Zephyr", style: "Bright" },
|
| 538 |
+
{ name: "Puck", style: "Upbeat" },
|
| 539 |
+
{ name: "Charon", style: "Informative" },
|
| 540 |
+
{ name: "Kore", style: "Firm" },
|
| 541 |
+
{ name: "Fenrir", style: "Excitable" },
|
| 542 |
+
{ name: "Leda", style: "Youthful" },
|
| 543 |
+
{ name: "Orus", style: "Firm" },
|
| 544 |
+
{ name: "Aoede", style: "Breezy" },
|
| 545 |
+
{ name: "Callirrhoe", style: "Easy-going" },
|
| 546 |
+
{ name: "Autonoe", style: "Bright" },
|
| 547 |
+
{ name: "Enceladus", style: "Breathy" },
|
| 548 |
+
{ name: "Iapetus", style: "Clear" },
|
| 549 |
+
{ name: "Umbriel", style: "Easy-going" },
|
| 550 |
+
{ name: "Algieba", style: "Smooth" },
|
| 551 |
+
{ name: "Despina", style: "Smooth" },
|
| 552 |
+
{ name: "Erinome", style: "Clear" },
|
| 553 |
+
{ name: "Algenib", style: "Gravelly" },
|
| 554 |
+
{ name: "Rasalgethi", style: "Informative" },
|
| 555 |
+
{ name: "Laomedeia", style: "Upbeat" },
|
| 556 |
+
{ name: "Achernar", style: "Soft" },
|
| 557 |
+
{ name: "Alnilam", style: "Firm" },
|
| 558 |
+
{ name: "Schedar", style: "Even" },
|
| 559 |
+
{ name: "Gacrux", style: "Mature" },
|
| 560 |
+
{ name: "Pulcherrima", style: "Forward" },
|
| 561 |
+
{ name: "Achird", style: "Friendly" },
|
| 562 |
+
{ name: "Zubenelgenubi", style: "Casual" },
|
| 563 |
+
{ name: "Vindemiatrix", style: "Gentle" },
|
| 564 |
+
{ name: "Sadachbia", style: "Lively" },
|
| 565 |
+
{ name: "Sadaltager", style: "Knowledgeable" },
|
| 566 |
+
{ name: "Sulafat", style: "Warm" }
|
| 567 |
];
|
| 568 |
+
const DEFAULT_VOICE = "Kore";
|
|
|
|
| 569 |
|
| 570 |
/* ============================================================
|
| 571 |
Element refs
|
|
|
|
| 582 |
|
| 583 |
const targetLang = $("targetLang");
|
| 584 |
const voiceSelect = $("voiceSelect");
|
|
|
|
| 585 |
const generateBtn = $("generateBtn");
|
| 586 |
|
| 587 |
const srcBadge = $("srcBadge");
|
|
|
|
| 603 |
const serverDot = $("serverDot");
|
| 604 |
const serverStatusText = $("serverStatusText");
|
| 605 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 606 |
/* ============================================================
|
| 607 |
App state
|
| 608 |
============================================================ */
|
|
|
|
| 612 |
sessionId: null,
|
| 613 |
abort: null,
|
| 614 |
targetLanguage: "English",
|
|
|
|
| 615 |
resultObjectUrl: null,
|
| 616 |
consoleCleared: false
|
| 617 |
};
|
|
|
|
| 700 |
VOICES.forEach(v=>{
|
| 701 |
const o = document.createElement("option");
|
| 702 |
o.value = v.name;
|
| 703 |
+
o.textContent = `${v.name} · ${v.style}`;
|
| 704 |
voiceSelect.appendChild(o);
|
| 705 |
});
|
| 706 |
+
voiceSelect.value = DEFAULT_VOICE;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 707 |
}
|
| 708 |
|
| 709 |
/* ============================================================
|
|
|
|
| 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;
|
|
|
|
| 812 |
}
|
| 813 |
|
| 814 |
/* ============================================================
|
| 815 |
+
Central SSE event handler for the /dub stream
|
| 816 |
============================================================ */
|
| 817 |
function handleSSEEvent(event, data){
|
| 818 |
switch(event){
|
|
|
|
| 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": {
|
|
|
|
| 859 |
============================================================ */
|
| 860 |
function startRunUI(){
|
| 861 |
state.running = true;
|
|
|
|
| 862 |
state.consoleCleared = false;
|
| 863 |
generateBtn.classList.add("loading");
|
| 864 |
generateBtn.disabled = true;
|
|
|
|
| 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(_){}}
|
|
|
|
| 937 |
}
|
| 938 |
|
| 939 |
/* ============================================================
|
| 940 |
+
Network: /dub
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 941 |
============================================================ */
|
| 942 |
async function startDub(){
|
| 943 |
if(!state.file || state.running) return;
|
|
|
|
| 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 |
}
|
|
|
|
| 970 |
}
|
| 971 |
}
|
| 972 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 973 |
generateBtn.addEventListener("click", startDub);
|
| 974 |
|
| 975 |
/* ============================================================
|
|
|
|
| 980 |
const res = await fetch(`${API_BASE}/health`, { method:"GET" });
|
| 981 |
const j = await res.json();
|
| 982 |
if(j.status === "ok"){
|
| 983 |
+
const hasKeys = (j.gemini_keys_configured|0) > 0 && (j.groq_keys_configured|0) > 0;
|
| 984 |
+
serverDot.style.background = hasKeys ? "var(--ok)" : "var(--warn)";
|
| 985 |
+
serverStatusText.textContent = hasKeys ? "Backend online" : "Online · API key missing";
|
| 986 |
}else{
|
| 987 |
serverDot.style.background = "var(--warn)";
|
| 988 |
serverStatusText.textContent = "Backend degraded";
|