{"id":7167,"date":"2026-10-16T03:19:55","date_gmt":"2026-10-16T03:19:55","guid":{"rendered":"https:\/\/schoolict.net\/tools\/?page_id=7167"},"modified":"2026-10-16T04:58:20","modified_gmt":"2026-10-16T04:58:20","slug":"video-dub-studio","status":"publish","type":"page","link":"https:\/\/schoolict.net\/tools\/video-dub-studio\/","title":{"rendered":"Video Dub Studio | Translate and Dub Videos Free"},"content":{"rendered":"<div class=\"pth-hero-section\">\n<div class=\"pth-hero-content\">\n<h2>Translate and Dub Videos Free<\/h2>\n<p>Free video dubbing in your browser: transcribe, translate, add a voice and export SRT, WAV or MP4. Your video never leaves your device.<\/p>\n<div id=\"pth-toc-placeholder\"><\/div>\n<\/p><\/div>\n<div class=\"pth-hero-image\">\n    <img data-no-lazy=\"1\"\n         src=\"https:\/\/schoolict.net\/tools\/wp-content\/uploads\/2026\/10\/Video-Dub-Studio-800x447.jpg\"\n         width=\"800\"\n         height=\"447\"\n         alt=\"Video Dub Studio\"\n         fetchpriority=\"high\"\n         loading=\"eager\"\n         decoding=\"async\"\n         style=\"width:100%; height:auto; display:block;\">\n  <\/div>\n<\/div>\n\n\n<div class=\"wp-block-rank-math-toc-block\" id=\"rank-math-toc\"><h2>Table of Contents<\/h2><nav><ul><li><a href=\"#\ud83d\udd34-who-gets-real-value-from-a-video-dubbing-tool-in-school\">\ud83d\udd34 Who gets real value from a video dubbing tool in school?<\/a><\/li><li><a href=\"#\ud83d\udfe1-one-sentence-from-input-to-output\">\ud83d\udfe1 One sentence, from input to output<\/a><\/li><li><a href=\"#\ud83d\udfe2-mistakes-that-waste-your-time\">\ud83d\udfe2 Mistakes that waste your time<\/a><\/li><li><a href=\"#\ud83d\udd35-honest-limits-you-should-know\">\ud83d\udd35 Honest limits you should know<\/a><\/li><li><a href=\"#\ud83d\udfe3-use-only-what-you-have-the-right-to-dub\">\ud83d\udfe3 Use only what you have the right to dub<\/a><\/li><\/ul><\/nav><\/div>\n\n\n<!-- ==========================================================================\r\n  SCHOOLICT.NET | Video Dub Studio | slug: video-dub-studio | version 2.6\r\n  Browser-only video dubbing: Whisper transcript OR direct SBV\/SRT\/VTT upload\r\n  -> Context-aware Gemini translation (BYOK) -> segment editor\r\n  -> voice (browser TTS or cloud TTS) -> SRT\/VTT\/WAV\/MP3\/MP4.\r\n  Standards: ES5 only, IIFE, pure ASCII JS, no backticks, no raw closing tags\r\n  in JS, no bare single-letter \"v\" variable, DOM built with createElement.\r\n=========================================================================== -->\r\n<div id=\"sict-videodub-wrap\">\r\n  <div class=\"swd-head\">\r\n    <div class=\"swd-badges\">\r\n      <span class=\"swd-ver\">v2.6<\/span>\r\n      <span class=\"swd-status\" data-i18n=\"status\">Pro Edition &middot; runs in your browser<\/span>\r\n    <\/div>\r\n    <h2 class=\"swd-title\" data-i18n=\"title\">Video Dub Studio<\/h2>\r\n    <p class=\"swd-sub\" data-i18n=\"sub\">Transcribe, upload SBV\/SRT subtitles, translate with real meaning, and dub audio tracks or full videos.<\/p>\r\n    <div class=\"swd-actions\">\r\n      <label class=\"swd-btn swd-ghost\" style=\"cursor:pointer;margin:0;display:inline-flex;align-items:center;gap:6px;\">\r\n        <span>&#127760;<\/span>\r\n        <select id=\"swd-ui-lang\" aria-label=\"Interface language\" style=\"background:transparent;border:0;color:inherit;font:inherit;font-weight:700;cursor:pointer;\">\r\n          <option value=\"en\">English<\/option>\r\n          <option value=\"si\">&#3523;&#3538;&#3458;&#3524;&#3517;<\/option>\r\n          <option value=\"ta\">&#2980;&#2990;&#3007;&#2996;&#3021;<\/option>\r\n          <option value=\"hi\">&#2361;&#2367;&#2344;&#2381;&#2342;&#2368;<\/option>\r\n          <option value=\"es\">Espa&ntilde;ol<\/option>\r\n          <option value=\"fr\">Fran&ccedil;ais<\/option>\r\n          <option value=\"ar\">&#1575;&#1604;&#1593;&#1585;&#1576;&#1610;&#1577;<\/option>\r\n          <option value=\"zh\">&#20013;&#25991;<\/option>\r\n          <option value=\"ja\">&#26085;&#26412;&#35486;<\/option>\r\n          <option value=\"ko\">&#54620;&#44397;&#50612;<\/option>\r\n        <\/select>\r\n      <\/label>\r\n      <button type=\"button\" class=\"swd-btn swd-ghost\" id=\"swd-full\" data-i18n=\"fullWidth\">&#8596; Full Width<\/button>\r\n      <button type=\"button\" class=\"swd-btn swd-ghost\" id=\"swd-restore\" data-i18n=\"restoreSession\">&#128190; Restore Last Session<\/button>\r\n      <button type=\"button\" class=\"swd-btn swd-ghost\" id=\"swd-clear-tr-top\" data-i18n=\"clearTrShort\">&#129529; Clear Translations<\/button>\r\n      <button type=\"button\" class=\"swd-btn swd-ghost\" id=\"swd-save-proj\" data-i18n=\"saveProj\">&#128190; Save Project<\/button>\r\n      <label class=\"swd-btn swd-ghost\" style=\"cursor:pointer;margin:0;\">\r\n        <span data-i18n=\"loadProj\">&#128194; Load Project<\/span>\r\n        <input type=\"file\" id=\"swd-load-proj\" class=\"swd-hidden\" accept=\".json\" \/>\r\n      <\/label>\r\n      <button type=\"button\" class=\"swd-btn swd-ghost\" id=\"swd-clear\" data-i18n=\"clearAll\">&#128465;&#65039; Clear All<\/button>\r\n    <\/div>\r\n  <\/div>\r\n\r\n  <details class=\"swd-keys\" open>\r\n    <summary data-i18n=\"keysSummary\">&#128273; API keys - one place for translation and voices<\/summary>\r\n    <div class=\"swd-keygrid\">\r\n      <div><label class=\"swd-lbl\" for=\"swd-gkey\" data-i18n=\"geminiKey\">Gemini key (translation + Gemini voice)<\/label>\r\n        <input type=\"password\" id=\"swd-gkey\" class=\"swd-inp\" placeholder=\"Paste Gemini key\" aria-label=\"Gemini API key\" autocomplete=\"off\" \/><\/div>\r\n      <div><label class=\"swd-lbl\" for=\"swd-okey\" data-i18n=\"openaiKey\">OpenAI key (optional voice)<\/label>\r\n        <input type=\"password\" id=\"swd-okey\" class=\"swd-inp\" placeholder=\"Paste OpenAI key\" aria-label=\"OpenAI API key\" autocomplete=\"off\" \/><\/div>\r\n      <div><label class=\"swd-lbl\" for=\"swd-ekey\" data-i18n=\"elevenKey\">ElevenLabs key (optional voice)<\/label>\r\n        <input type=\"password\" id=\"swd-ekey\" class=\"swd-inp\" placeholder=\"Paste ElevenLabs key\" aria-label=\"ElevenLabs API key\" autocomplete=\"off\" \/><\/div>\r\n    <\/div>\r\n    <label class=\"swd-check\" style=\"margin-top:8px\"><input type=\"checkbox\" id=\"swd-remember\" \/> <span data-i18n=\"rememberKeys\">Remember keys on this device (browser storage - never use on a shared computer)<\/span><\/label>\r\n  <\/details>\r\n\r\n  <div class=\"swd-tabs\" role=\"tablist\">\r\n    <button type=\"button\" class=\"swd-tab swd-tab-on\" data-tab=\"load\" role=\"tab\" data-i18n=\"tab1\">1. Load &amp; Subtitles<\/button>\r\n    <button type=\"button\" class=\"swd-tab\" data-tab=\"tr\" role=\"tab\" data-i18n=\"tab2\">2. Translate &amp; Edit<\/button>\r\n    <button type=\"button\" class=\"swd-tab\" data-tab=\"voice\" role=\"tab\" data-i18n=\"tab3\">3. Voice &amp; Sync<\/button>\r\n    <button type=\"button\" class=\"swd-tab\" data-tab=\"export\" role=\"tab\" data-i18n=\"tab4\">4. Export<\/button>\r\n  <\/div>\r\n\r\n  <!-- TAB 1 -->\r\n  <section class=\"swd-panel swd-panel-on\" data-panel=\"load\">\r\n    <div class=\"swd-grid\">\r\n      <div>\r\n        <label class=\"swd-drop\" for=\"swd-file\">\r\n          <span class=\"swd-drop-icon\">&#127916;<\/span>\r\n          <span class=\"swd-drop-t\" data-i18n=\"dropT\">Drop a video or audio file here, or click to choose<\/span>\r\n          <span class=\"swd-drop-s\" data-i18n=\"dropS\">mp4, webm, mov, mp3, wav, m4a &middot; optional if uploading subtitles only<\/span>\r\n        <\/label>\r\n        <input type=\"file\" id=\"swd-file\" class=\"swd-hidden\" accept=\"video\/*,audio\/*\" aria-label=\"Choose a video or audio file\" \/>\r\n        <div class=\"swd-info\" id=\"swd-info\" aria-live=\"polite\"><\/div>\r\n        <video id=\"swd-video\" class=\"swd-video\" controls playsinline preload=\"metadata\" style=\"display:none\"><\/video>\r\n\r\n        <!-- Direct Subtitle Upload Box -->\r\n        <div class=\"swd-card\" style=\"margin-top:12px;border-style:dashed;border-width:2px;border-color:#0369a1;\">\r\n          <h3 class=\"swd-h3\" data-i18n=\"subUploadTitle\">&#128196; Direct Subtitle Upload (.sbv, .srt, .vtt, .ass)<\/h3>\r\n          <p class=\"swd-hint\" data-i18n=\"subUploadHint\" style=\"margin:0 0 8px;\">Already have a subtitle file (like YouTube .sbv or .srt)? Upload it directly to translate its real meaning and generate a dubbed audio track without needing a video.<\/p>\r\n          <label class=\"swd-btn swd-primary swd-block\" style=\"text-align:center;cursor:pointer;margin-top:6px;\">\r\n            <span data-i18n=\"subUploadBtn\">&#128194; Upload Subtitle File (.sbv \/ .srt \/ .vtt)<\/span>\r\n            <input type=\"file\" id=\"swd-subfile\" class=\"swd-hidden\" accept=\".sbv,.srt,.vtt,.sub,.ass,.ssa,.txt\" \/>\r\n          <\/label>\r\n          <div class=\"swd-info\" id=\"swd-subinfo\" aria-live=\"polite\" style=\"margin:6px 0 0;color:#0369a1;\"><\/div>\r\n        <\/div>\r\n\r\n        <p class=\"swd-note\" data-i18n=\"legalNote\">Use only videos or subtitles you own or have permission to dub.<\/p>\r\n      <\/div>\r\n      <div class=\"swd-card\">\r\n        <h3 class=\"swd-h3\" data-i18n=\"transcribeTitle\">Transcription (Whisper, in your browser)<\/h3>\r\n        <label class=\"swd-lbl\" for=\"swd-model\" data-i18n=\"modelLbl\">Model<\/label>\r\n        <select id=\"swd-model\" class=\"swd-inp\" aria-label=\"Whisper model\">\r\n          <option value=\"Xenova\/whisper-base\">Base (multilingual) - recommended<\/option>\r\n          <option value=\"Xenova\/whisper-tiny\">Tiny (multilingual) - fastest<\/option>\r\n          <option value=\"Xenova\/whisper-base.en\">Base (English only)<\/option>\r\n        <\/select>\r\n        <label class=\"swd-lbl\" for=\"swd-srclang\" data-i18n=\"spokenLbl\">Spoken language in the video<\/label>\r\n        <select id=\"swd-srclang\" class=\"swd-inp\" aria-label=\"Source language\">\r\n          <option value=\"\" data-i18n=\"autoDetect\">Auto-detect<\/option>\r\n          <option value=\"english\">English<\/option>\r\n          <option value=\"sinhala\">Sinhala<\/option>\r\n          <option value=\"tamil\">Tamil<\/option>\r\n          <option value=\"hindi\">Hindi<\/option>\r\n          <option value=\"bengali\">Bengali<\/option>\r\n          <option value=\"urdu\">Urdu<\/option>\r\n          <option value=\"nepali\">Nepali<\/option>\r\n          <option value=\"arabic\">Arabic<\/option>\r\n          <option value=\"persian\">Persian<\/option>\r\n          <option value=\"hebrew\">Hebrew<\/option>\r\n          <option value=\"turkish\">Turkish<\/option>\r\n          <option value=\"spanish\">Spanish<\/option>\r\n          <option value=\"french\">French<\/option>\r\n          <option value=\"german\">German<\/option>\r\n          <option value=\"italian\">Italian<\/option>\r\n          <option value=\"portuguese\">Portuguese<\/option>\r\n          <option value=\"dutch\">Dutch<\/option>\r\n          <option value=\"swedish\">Swedish<\/option>\r\n          <option value=\"danish\">Danish<\/option>\r\n          <option value=\"norwegian\">Norwegian<\/option>\r\n          <option value=\"finnish\">Finnish<\/option>\r\n          <option value=\"polish\">Polish<\/option>\r\n          <option value=\"czech\">Czech<\/option>\r\n          <option value=\"hungarian\">Hungarian<\/option>\r\n          <option value=\"romanian\">Romanian<\/option>\r\n          <option value=\"greek\">Greek<\/option>\r\n          <option value=\"russian\">Russian<\/option>\r\n          <option value=\"ukrainian\">Ukrainian<\/option>\r\n          <option value=\"chinese\">Chinese<\/option>\r\n          <option value=\"japanese\">Japanese<\/option>\r\n          <option value=\"korean\">Korean<\/option>\r\n          <option value=\"thai\">Thai<\/option>\r\n          <option value=\"vietnamese\">Vietnamese<\/option>\r\n          <option value=\"indonesian\">Indonesian<\/option>\r\n          <option value=\"malay\">Malay<\/option>\r\n          <option value=\"tagalog\">Tagalog<\/option>\r\n          <option value=\"swahili\">Swahili<\/option>\r\n          <option value=\"telugu\">Telugu<\/option>\r\n          <option value=\"malayalam\">Malayalam<\/option>\r\n          <option value=\"kannada\">Kannada<\/option>\r\n          <option value=\"marathi\">Marathi<\/option>\r\n          <option value=\"gujarati\">Gujarati<\/option>\r\n          <option value=\"punjabi\">Punjabi<\/option>\r\n          <option value=\"burmese\">Burmese<\/option>\r\n          <option value=\"khmer\">Khmer<\/option>\r\n          <option value=\"lao\">Lao<\/option>\r\n        <\/select>\r\n        <button type=\"button\" class=\"swd-btn swd-primary swd-block\" id=\"swd-transcribe\" data-i18n=\"transcribe\">&#128172; Transcribe Video\/Audio<\/button>\r\n        <div class=\"swd-progress\" id=\"swd-tr-prog\" style=\"display:none\"><span class=\"swd-bar\"><\/span><\/div>\r\n        <p class=\"swd-hint\" id=\"swd-tr-hint\" data-i18n=\"trHint\">The first run downloads the model once, then it is cached by your browser.<\/p>\r\n      <\/div>\r\n    <\/div>\r\n  <\/section>\r\n\r\n  <!-- TAB 2 -->\r\n  <section class=\"swd-panel\" data-panel=\"tr\">\r\n    <div class=\"swd-grid\">\r\n      <div class=\"swd-card\">\r\n        <h3 class=\"swd-h3\" data-i18n=\"translateTitle\">Translate with Gemini (Real Meaning)<\/h3>\r\n        <label class=\"swd-lbl\" for=\"swd-target\" data-i18n=\"targetLbl\">Target language<\/label>\r\n        <select id=\"swd-target\" class=\"swd-inp\" aria-label=\"Target language\">\r\n          <option value=\"si|Sinhala|si-LK\">Sinhala<\/option>\r\n          <option value=\"ta|Tamil|ta-IN\">Tamil<\/option>\r\n          <option value=\"en|English|en-US\">English<\/option>\r\n          <option value=\"hi|Hindi|hi-IN\">Hindi<\/option>\r\n          <option value=\"bn|Bengali|bn-BD\">Bengali<\/option>\r\n          <option value=\"ur|Urdu|ur-PK\">Urdu<\/option>\r\n          <option value=\"ne|Nepali|ne-NP\">Nepali<\/option>\r\n          <option value=\"ar|Arabic|ar-SA\">Arabic<\/option>\r\n          <option value=\"fa|Persian|fa-IR\">Persian<\/option>\r\n          <option value=\"he|Hebrew|he-IL\">Hebrew<\/option>\r\n          <option value=\"tr|Turkish|tr-TR\">Turkish<\/option>\r\n          <option value=\"es|Spanish|es-ES\">Spanish<\/option>\r\n          <option value=\"fr|French|fr-FR\">French<\/option>\r\n          <option value=\"de|German|de-DE\">German<\/option>\r\n          <option value=\"it|Italian|it-IT\">Italian<\/option>\r\n          <option value=\"pt|Portuguese|pt-BR\">Portuguese<\/option>\r\n          <option value=\"nl|Dutch|nl-NL\">Dutch<\/option>\r\n          <option value=\"sv|Swedish|sv-SE\">Swedish<\/option>\r\n          <option value=\"da|Danish|da-DK\">Danish<\/option>\r\n          <option value=\"nb|Norwegian|nb-NO\">Norwegian<\/option>\r\n          <option value=\"fi|Finnish|fi-FI\">Finnish<\/option>\r\n          <option value=\"pl|Polish|pl-PL\">Polish<\/option>\r\n          <option value=\"cs|Czech|cs-CZ\">Czech<\/option>\r\n          <option value=\"hu|Hungarian|hu-HU\">Hungarian<\/option>\r\n          <option value=\"ro|Romanian|ro-RO\">Romanian<\/option>\r\n          <option value=\"el|Greek|el-GR\">Greek<\/option>\r\n          <option value=\"ru|Russian|ru-RU\">Russian<\/option>\r\n          <option value=\"uk|Ukrainian|uk-UA\">Ukrainian<\/option>\r\n          <option value=\"zh|Chinese|zh-CN\">Chinese<\/option>\r\n          <option value=\"ja|Japanese|ja-JP\">Japanese<\/option>\r\n          <option value=\"ko|Korean|ko-KR\">Korean<\/option>\r\n          <option value=\"th|Thai|th-TH\">Thai<\/option>\r\n          <option value=\"vi|Vietnamese|vi-VN\">Vietnamese<\/option>\r\n          <option value=\"id|Indonesian|id-ID\">Indonesian<\/option>\r\n          <option value=\"ms|Malay|ms-MY\">Malay<\/option>\r\n          <option value=\"fil|Filipino|fil-PH\">Filipino<\/option>\r\n          <option value=\"sw|Swahili|sw-KE\">Swahili<\/option>\r\n          <option value=\"te|Telugu|te-IN\">Telugu<\/option>\r\n          <option value=\"ml|Malayalam|ml-IN\">Malayalam<\/option>\r\n          <option value=\"kn|Kannada|kn-IN\">Kannada<\/option>\r\n          <option value=\"mr|Marathi|mr-IN\">Marathi<\/option>\r\n          <option value=\"gu|Gujarati|gu-IN\">Gujarati<\/option>\r\n          <option value=\"pa|Punjabi|pa-IN\">Punjabi<\/option>\r\n          <option value=\"my|Burmese|my-MM\">Burmese<\/option>\r\n          <option value=\"km|Khmer|km-KH\">Khmer<\/option>\r\n          <option value=\"lo|Lao|lo-LA\">Lao<\/option>\r\n        <\/select>\r\n\r\n        <label class=\"swd-lbl\" for=\"swd-tr-style\" data-i18n=\"trStyleLbl\">Translation Style &amp; Context<\/label>\r\n        <select id=\"swd-tr-style\" class=\"swd-inp\" aria-label=\"Translation style\">\r\n          <option value=\"meaning\" data-i18n=\"styleMeaning\">Real Meaning &amp; Natural Dubbing (Idiomatic, Spoken Flow)<\/option>\r\n          <option value=\"story\" data-i18n=\"styleStory\">Storytelling \/ Narrative (Expressive, Emotional)<\/option>\r\n          <option value=\"educational\" data-i18n=\"styleEdu\">Educational \/ Tutorial (Clear, Accurate Terms)<\/option>\r\n          <option value=\"concise\" data-i18n=\"styleConcise\">Short &amp; Lip-Sync Friendly (Compact Phrasing)<\/option>\r\n        <\/select>\r\n\r\n        <label class=\"swd-lbl\" for=\"swd-gmodel\" data-i18n=\"textModelLbl\">Text model<\/label>\r\n        <input type=\"text\" id=\"swd-gmodel\" class=\"swd-inp\" value=\"gemini-2.5-flash\" aria-label=\"Gemini text model\" \/>\r\n        <button type=\"button\" class=\"swd-btn swd-primary swd-block\" id=\"swd-optimize\" data-i18n=\"optimizeBtn\">&#10024; Optimize Segments (Auto-Merge broken sentences)<\/button>\r\n        <button type=\"button\" class=\"swd-btn swd-primary swd-block\" id=\"swd-translate\" data-i18n=\"translateAll\">&#127760; Translate pending segments (Real Meaning)<\/button>\r\n        <button type=\"button\" class=\"swd-btn swd-ghost swd-block\" id=\"swd-shorten\" data-i18n=\"shorten\">&#9986;&#65039; Shorten lines that were too long<\/button>\r\n        <button type=\"button\" class=\"swd-btn swd-ghost swd-block\" id=\"swd-cleartr\" data-i18n=\"clearTr\">&#129529; Clear translations only<\/button>\r\n        <div class=\"swd-progress\" id=\"swd-tl-prog\" style=\"display:none\"><span class=\"swd-bar\"><\/span><\/div>\r\n        <p class=\"swd-hint\" id=\"swd-tl-hint\" data-i18n=\"tlHint\">Get a free key at aistudio.google.com\/apikey. Translates using surrounding sentence context so idioms and real meaning are preserved.<\/p>\r\n      <\/div>\r\n      <div>\r\n        <div class=\"swd-seghead\">\r\n          <strong id=\"swd-segcount\">0 segments<\/strong>\r\n          <span class=\"swd-hint\" data-i18n=\"segHeadHint\">Adjust times, add custom voices, or split.<\/span>\r\n          <span class=\"swd-hint\" id=\"swd-saved\"><\/span>\r\n        <\/div>\r\n        <div class=\"swd-segtools\">\r\n          <input type=\"text\" id=\"swd-segsearch\" class=\"swd-inp\" placeholder=\"Search segments...\" data-i18n-attr=\"placeholder:searchPh\" aria-label=\"Search segments\" \/>\r\n          <button type=\"button\" class=\"swd-btn swd-ghost swd-btn-sm\" id=\"swd-undo\" title=\"Undo\" aria-label=\"Undo\">&#8630; Undo<\/button>\r\n          <button type=\"button\" class=\"swd-btn swd-ghost swd-btn-sm\" id=\"swd-redo\" title=\"Redo\" aria-label=\"Redo\">&#8631; Redo<\/button>\r\n          <span class=\"swd-hint\" id=\"swd-segstat\"><\/span>\r\n        <\/div>\r\n        <div class=\"swd-segs\" id=\"swd-segs\"><\/div>\r\n      <\/div>\r\n    <\/div>\r\n  <\/section>\r\n\r\n  <!-- TAB 3 -->\r\n  <section class=\"swd-panel\" data-panel=\"voice\">\r\n    <div class=\"swd-grid\">\r\n      <div class=\"swd-card\">\r\n        <h3 class=\"swd-h3\" data-i18n=\"voiceTitle\">Voice<\/h3>\r\n        <label class=\"swd-lbl\" for=\"swd-provider\" data-i18n=\"voiceEngine\">Voice engine<\/label>\r\n        <select id=\"swd-provider\" class=\"swd-inp\" aria-label=\"Voice engine\">\r\n          <option value=\"gemini\">Gemini TTS (your key, builds downloadable audio)<\/option>\r\n          <option value=\"openai\">OpenAI TTS (your key, builds downloadable audio)<\/option>\r\n          <option value=\"elevenlabs\">ElevenLabs (your key, builds downloadable audio)<\/option>\r\n          <option value=\"browser\" data-i18n=\"browserVoiceOpt\">Browser voice (free, live preview only)<\/option>\r\n        <\/select>\r\n        <div id=\"swd-keybox\">\r\n          <label class=\"swd-lbl\" for=\"swd-tts-model\">Gemini voice model<\/label>\r\n          <input type=\"text\" id=\"swd-tts-model\" class=\"swd-inp\" value=\"gemini-2.5-flash-preview-tts\" aria-label=\"Gemini voice model\" \/>\r\n          <label class=\"swd-lbl\" for=\"swd-voice\" data-i18n=\"defaultVoiceLbl\">Default Voice name or ID<\/label>\r\n          <input type=\"text\" id=\"swd-voice\" class=\"swd-inp\" placeholder=\"Gemini: Kore, Puck, Zephyr | OpenAI: alloy\" aria-label=\"Voice name\" \/>\r\n        <\/div>\r\n        <div class=\"swd-voicestat\" id=\"swd-voicestat\"><\/div>\r\n        <div id=\"swd-bvbox\" style=\"display:none\"><label class=\"swd-lbl\" for=\"swd-bvoice\" data-i18n=\"browserVoiceFree\">Browser voice (free)<\/label><select id=\"swd-bvoice\" class=\"swd-inp\" aria-label=\"Browser voice\"><\/select><\/div>\r\n\r\n        <div class=\"swd-row2\">\r\n          <div>\r\n            <label class=\"swd-lbl\" for=\"swd-rate\">Voice rate: <span id=\"swd-rate-out\">1.00<\/span>x<\/label>\r\n            <input type=\"range\" id=\"swd-rate\" min=\"50\" max=\"200\" value=\"100\" aria-label=\"Voice rate\" \/>\r\n          <\/div>\r\n          <div>\r\n            <label class=\"swd-lbl\" for=\"swd-pitch\">Voice pitch: <span id=\"swd-pitch-out\">1.00<\/span><\/label>\r\n            <input type=\"range\" id=\"swd-pitch\" min=\"50\" max=\"200\" value=\"100\" aria-label=\"Voice pitch\" \/>\r\n          <\/div>\r\n        <\/div>\r\n\r\n        <label class=\"swd-check\" style=\"margin-top:10px\"><input type=\"checkbox\" id=\"swd-muteorig\" checked \/> <span data-i18n=\"muteOrig\">Mute the original voice completely (dub voice only)<\/span><\/label>\r\n        <label class=\"swd-check\" style=\"margin-top:6px\"><input type=\"checkbox\" id=\"swd-hardmute\" checked \/> <span data-i18n=\"hardMute\">Silence original while dub voice is speaking (fixes mixed languages)<\/span><\/label>\r\n\r\n        <label class=\"swd-lbl\" for=\"swd-duck\">Original audio level when not muted: <span id=\"swd-duck-val\">15<\/span>%<\/label>\r\n        <input type=\"range\" id=\"swd-duck\" min=\"0\" max=\"100\" value=\"15\" aria-label=\"Original volume\" \/>\r\n\r\n        <label class=\"swd-lbl\" for=\"swd-maxrate\">Max speed-up to fit a sentence: <span id=\"swd-rate-val\">1.35<\/span>x<\/label>\r\n        <input type=\"range\" id=\"swd-maxrate\" min=\"100\" max=\"200\" value=\"135\" aria-label=\"Max speed-up\" \/>\r\n\r\n        <label class=\"swd-lbl\" for=\"swd-quick-sec\" data-i18n=\"quickLenLbl\">Quick preview length (seconds, 10-600)<\/label>\r\n        <input type=\"number\" id=\"swd-quick-sec\" class=\"swd-inp\" min=\"10\" max=\"600\" step=\"5\" value=\"60\" aria-label=\"Quick preview length in seconds\" \/>\r\n\r\n        <button type=\"button\" class=\"swd-btn swd-ghost swd-block\" id=\"swd-quick\" data-i18n=\"quickPreview\">&#9889; Quick preview (cloud voice, then plays)<\/button>\r\n        <button type=\"button\" class=\"swd-btn swd-primary swd-block\" id=\"swd-build\" data-i18n=\"buildDub\">&#127908; Build dubbed audio track<\/button>\r\n        <div class=\"swd-progress\" id=\"swd-bd-prog\" style=\"display:none\"><span class=\"swd-bar\"><\/span><\/div>\r\n        <div class=\"swd-btnrow\"><button type=\"button\" class=\"swd-btn swd-ghost\" id=\"swd-stop\" style=\"display:none\" data-i18n=\"stop\">&#9209; Stop<\/button><button type=\"button\" class=\"swd-btn swd-ghost\" id=\"swd-retry\" style=\"display:none\" data-i18n=\"retry\">&#128257; Retry failed<\/button><\/div>\r\n        <p class=\"swd-hint\" id=\"swd-bd-hint\" data-i18n=\"bdHint\">Works with or without a video file! If you uploaded an .sbv\/.srt subtitle file only, this builds a synced standalone audio track ready to download.<\/p>\r\n\r\n        <!-- Direct Audio Player & Quick Download inside Tab 3 -->\r\n        <div id=\"swd-dubplayer-wrap\" class=\"swd-card\" style=\"display:none;margin-top:12px;background:#e0f2fe;border-color:#7dd3fc;\">\r\n          <strong style=\"font-size:13px;display:block;margin-bottom:6px;\">&#127911; <span data-i18n=\"dubReadyTitle\">Dubbed Audio Track Ready:<\/span><\/strong>\r\n          <audio id=\"swd-dubaudio\" controls preload=\"auto\" style=\"width:100%;height:38px;\"><\/audio>\r\n          <div class=\"swd-btnrow\" style=\"margin-top:8px;\">\r\n            <button type=\"button\" class=\"swd-btn swd-primary swd-btn-sm\" id=\"swd-dl-wav-fast\" data-i18n=\"dlWav\">&#127925; Download .wav<\/button>\r\n            <button type=\"button\" class=\"swd-btn swd-ghost swd-btn-sm\" id=\"swd-dl-mp3-fast\" data-i18n=\"dlMp3\">&#127925; Download .mp3<\/button>\r\n          <\/div>\r\n        <\/div>\r\n      <\/div>\r\n      <div>\r\n        <label class=\"swd-check\"><input type=\"checkbox\" id=\"swd-live\" \/> <span data-i18n=\"livePreview\">Live dub preview (plays with the video above)<\/span><\/label>\r\n        <button type=\"button\" class=\"swd-btn swd-primary swd-block\" id=\"swd-bpreview\" data-i18n=\"previewDubOnly\">&#9654; Preview dub only - browser voice, no recording (use Edge for best voices)<\/button>\r\n        <div class=\"swd-card swd-reccard\">\r\n          <h3 class=\"swd-h3\">&#128308; <span data-i18n=\"recordTitle\">Record live dub (no API key)<\/span><\/h3>\r\n          <p class=\"swd-hint\" data-i18n=\"recordHint\">Plays the subtitle timeline or video with the browser voice and records the tab audio into a dubbed track you can export. Use desktop Chrome or Edge. Choose This Tab and tick Share tab audio.<\/p>\r\n          <button type=\"button\" class=\"swd-btn swd-primary swd-block\" id=\"swd-rec\" data-i18n=\"recStart\">&#128308; Start recording (Edge \/ Chrome)<\/button>\r\n          <button type=\"button\" class=\"swd-btn swd-ghost swd-block\" id=\"swd-recstop\" style=\"display:none\" data-i18n=\"recStop\">&#9209; Stop and save<\/button>\r\n          <div class=\"swd-progress\" id=\"swd-rec-prog\" style=\"display:none\"><span class=\"swd-bar\"><\/span><\/div>\r\n          <p class=\"swd-hint\" id=\"swd-rec-hint\"><\/p>\r\n        <\/div>\r\n        <label class=\"swd-lbl\" for=\"swd-capmode\" data-i18n=\"capOnVideo\">Captions on the video<\/label>\r\n        <select id=\"swd-capmode\" class=\"swd-inp\" aria-label=\"Caption language\">\r\n          <option value=\"dub\" data-i18n=\"capDub\">Dub language (translation)<\/option>\r\n          <option value=\"src\" data-i18n=\"capSrc\">Original language<\/option>\r\n          <option value=\"both\" data-i18n=\"capBoth\">Both<\/option>\r\n          <option value=\"off\" data-i18n=\"capOff\">Off<\/option>\r\n        <\/select>\r\n        <div class=\"swd-vwrap\"><video id=\"swd-video2\" class=\"swd-video\" controls playsinline preload=\"metadata\" style=\"display:none\"><\/video><div class=\"swd-cap\" id=\"swd-cap\"><\/div><\/div>\r\n      <\/div>\r\n    <\/div>\r\n  <\/section>\r\n\r\n  <!-- TAB 4 -->\r\n  <section class=\"swd-panel\" data-panel=\"export\">\r\n    <div class=\"swd-cards\">\r\n      <div class=\"swd-card\"><h3 class=\"swd-h3\" data-i18n=\"subsTitle\">Subtitles<\/h3>\r\n        <button type=\"button\" class=\"swd-btn swd-primary swd-block\" id=\"swd-dl-srt\" data-i18n=\"dlSrt\">&#128196; Download .srt<\/button>\r\n        <button type=\"button\" class=\"swd-btn swd-ghost swd-block\" id=\"swd-dl-vtt\" data-i18n=\"dlVtt\">&#128196; Download .vtt<\/button>\r\n        <button type=\"button\" class=\"swd-btn swd-ghost swd-block\" id=\"swd-dl-sbv\" data-i18n=\"dlSbv\">&#128196; Download .sbv<\/button><\/div>\r\n      <div class=\"swd-card\"><h3 class=\"swd-h3\" data-i18n=\"dubAudio\">Dubbed audio<\/h3>\r\n        <button type=\"button\" class=\"swd-btn swd-primary swd-block\" id=\"swd-dl-wav\" data-i18n=\"dlWav\">&#127925; Download dubbed .wav<\/button>\r\n        <button type=\"button\" class=\"swd-btn swd-ghost swd-block\" id=\"swd-dl-mp3\" data-i18n=\"dlMp3\">&#127925; Download dubbed .mp3<\/button><\/div>\r\n      <div class=\"swd-card\"><h3 class=\"swd-h3\" data-i18n=\"dubVideo\">Dubbed video<\/h3>\r\n        <label class=\"swd-lbl\" for=\"swd-sub-mode\" data-i18n=\"subModeLbl\">Subtitles inside the video<\/label>\r\n        <select id=\"swd-sub-mode\" class=\"swd-inp\" aria-label=\"Subtitles inside the video\">\r\n          <option value=\"none\" data-i18n=\"subNone\">None<\/option>\r\n          <option value=\"soft\" data-i18n=\"subSoft\">Soft subtitles (selectable track, fast)<\/option>\r\n          <option value=\"burn\" data-i18n=\"subBurn\">Burn-in (experimental, slow)<\/option>\r\n        <\/select>\r\n        <button type=\"button\" class=\"swd-btn swd-primary swd-block\" id=\"swd-dl-mp4\" data-i18n=\"dlMp4\">&#127902;&#65039; Download dubbed .mp4<\/button>\r\n        <div class=\"swd-progress\" id=\"swd-mux-prog\" style=\"display:none\"><span class=\"swd-bar\"><\/span><\/div>\r\n        <p class=\"swd-hint\" id=\"swd-mux-hint\"><\/p><\/div>\r\n    <\/div>\r\n  <\/section>\r\n\r\n  <div class=\"swd-toast\" id=\"swd-toast\" role=\"status\" aria-live=\"polite\"><\/div>\r\n<\/div>\r\n\r\n<style>\r\n#sict-videodub-wrap{--swd-ink:#0f172a;--swd-mut:#64748b;--swd-line:#e2e8f0;--swd-acc:#0369a1;--swd-bg:#f8fafc;max-width:1100px;margin:0 auto;padding:16px;color:var(--swd-ink);font-family:system-ui,-apple-system,\"Segoe UI\",Roboto,sans-serif;line-height:1.5;box-sizing:border-box}\r\n#sict-videodub-wrap *{box-sizing:border-box}\r\n.swd-head{background:linear-gradient(135deg,#0c4a6e,#0369a1);color:#fff;border-radius:16px;padding:20px;margin-bottom:14px}\r\n.swd-badges{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:6px}\r\n.swd-ver,.swd-status{background:rgba(255,255,255,.18);border-radius:999px;padding:2px 10px;font-size:12px;font-weight:700}\r\n.swd-title{margin:0 0 4px;font-size:26px;color:#fff}\r\n.swd-sub{margin:0 0 10px;opacity:.92;font-size:15px}\r\n.swd-actions{display:flex;gap:8px;flex-wrap:wrap}\r\n.swd-tabs{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:12px}\r\n.swd-tab{flex:1 1 140px;border:1px solid var(--swd-line);background:#fff;color:var(--swd-ink);border-radius:10px;padding:10px 12px;font-weight:700;font-size:14px;cursor:pointer}\r\n.swd-tab-on{background:var(--swd-acc);color:#fff;border-color:var(--swd-acc)}\r\n.swd-panel{display:none;background:#fff;border:1px solid var(--swd-line);border-radius:14px;padding:16px}\r\n.swd-panel-on{display:block}\r\n.swd-grid{display:grid;grid-template-columns:minmax(0,1.3fr) minmax(0,1fr);gap:16px}\r\n.swd-cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:14px}\r\n@media(max-width:760px){.swd-grid{grid-template-columns:1fr}}\r\n.swd-card{background:var(--swd-bg);border:1px solid var(--swd-line);border-radius:12px;padding:14px}\r\n.swd-h3{margin:0 0 8px;font-size:16px}\r\n.swd-lbl{display:block;margin:10px 0 4px;font-size:13px;font-weight:700}\r\n.swd-inp{width:100%;padding:9px 10px;border:1px solid #cbd5e1;border-radius:8px;font-size:14px;background:#fff;color:var(--swd-ink)}\r\n.swd-inp-sm{padding:4px 6px;font-size:12px;display:inline-block;width:70px}\r\n#sict-videodub-wrap .swd-seg-t input[type=number]{width:96px}\r\n.swd-keys{background:#fff;border:1px solid var(--swd-line);border-radius:12px;padding:10px 14px;margin-bottom:12px}\r\n.swd-keys summary{font-weight:800;cursor:pointer}\r\n.swd-keygrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:10px}\r\n.swd-ta{resize:vertical;min-height:54px;font-family:inherit}\r\n.swd-btn{border:0;border-radius:10px;padding:10px 14px;font-weight:700;font-size:14px;cursor:pointer}\r\n.swd-btn-sm{padding:4px 8px;font-size:12px}\r\n.swd-primary{background:var(--swd-acc);color:#fff}\r\n.swd-ghost{background:rgba(148,163,184,.2);color:inherit;border:1px solid var(--swd-line)}\r\n.swd-head .swd-ghost{background:rgba(255,255,255,.16);color:#fff;border-color:rgba(255,255,255,.3)}\r\n.swd-head .swd-ghost select option{color:#0f172a}\r\n.swd-block{display:block;width:100%;margin-top:12px}\r\n.swd-btn:disabled{opacity:.5;cursor:not-allowed}\r\n.swd-hidden{position:absolute;left:-9999px;width:1px;height:1px;opacity:0}\r\n.swd-drop{display:flex;flex-direction:column;align-items:center;gap:4px;padding:22px 14px;border:2px dashed #94a3b8;border-radius:14px;text-align:center;cursor:pointer;background:var(--swd-bg)}\r\n.swd-drop-icon{font-size:34px}.swd-drop-t{font-weight:700}.swd-drop-s,.swd-hint,.swd-note{font-size:12.5px;color:var(--swd-mut)}\r\n.swd-info{margin:10px 0;font-size:14px;font-weight:600}\r\n.swd-video{width:100%;max-height:420px;background:#000;border-radius:10px;margin-top:8px}\r\n.swd-progress{height:6px;background:var(--swd-line);border-radius:99px;overflow:hidden;margin-top:10px}\r\n.swd-bar{display:block;height:100%;width:0;background:var(--swd-acc);transition:width .25s}\r\n.swd-indet .swd-bar{width:40%;animation:swdmove 1.1s infinite ease-in-out;transition:none}\r\n@keyframes swdmove{0%{margin-left:-40%}100%{margin-left:100%}}\r\n.swd-seghead{display:flex;justify-content:space-between;gap:8px;align-items:center;margin-bottom:8px;flex-wrap:wrap}\r\n.swd-segtools{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-bottom:8px}\r\n.swd-segtools .swd-inp{flex:1 1 140px;width:auto}\r\n.swd-segs{max-height:560px;overflow-y:auto;display:flex;flex-direction:column;gap:8px;padding-right:4px}\r\n.swd-seg{border:1px solid var(--swd-line);border-radius:10px;padding:8px 10px;background:#fff}\r\n.swd-seg-t{display:flex;gap:6px;align-items:center;font-size:12px;color:var(--swd-acc);font-weight:700;margin-bottom:6px;flex-wrap:wrap}\r\n.swd-seg-src{font-size:13.5px;color:var(--swd-mut);margin:2px 0 6px}\r\n.swd-voicestat{margin-top:10px;font-size:13px;font-weight:600}\r\n.swd-check{display:flex;gap:8px;align-items:center;font-weight:700;font-size:14px}\r\n.swd-row2{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:6px}\r\n#sict-videodub-wrap input[type=range]{width:100%}\r\n.swd-toast{position:fixed;right:18px;bottom:18px;background:#0f172a;color:#fff;padding:12px 16px;border-radius:10px;font-size:14px;font-weight:600;box-shadow:0 10px 30px rgba(0,0,0,.2);opacity:0;transform:translateY(10px);transition:opacity .2s,transform .2s;z-index:99999;max-width:340px;pointer-events:none}\r\n.swd-toast.swd-toast-on{opacity:1;transform:translateY(0)}\r\n.swd-reccard{margin-top:12px}\r\n.swd-vwrap{position:relative}\r\n.swd-cap{position:absolute;left:6%;right:6%;bottom:58px;text-align:center;color:#fff;background:rgba(0,0,0,.68);padding:4px 10px;border-radius:6px;font-size:18px;line-height:1.35;display:none;pointer-events:none}\r\n.swd-warn{font-size:12.5px;font-weight:700;color:#b91c1c;margin:0 0 4px}\r\n.swd-btnrow{display:flex;gap:8px;margin-top:8px;flex-wrap:wrap}\r\n#sict-videodub-wrap .swd-tab{background:#fff;color:#0f172a}\r\n#sict-videodub-wrap .swd-tab:hover,#sict-videodub-wrap .swd-tab:focus{background:#e0f2fe;color:#0c4a6e}\r\n#sict-videodub-wrap .swd-tab.swd-tab-on,#sict-videodub-wrap .swd-tab.swd-tab-on:hover,#sict-videodub-wrap .swd-tab.swd-tab-on:focus{background:#0369a1;color:#fff}\r\n#sict-videodub-wrap .swd-primary,#sict-videodub-wrap .swd-primary:hover,#sict-videodub-wrap .swd-primary:focus{background:#0369a1;color:#fff}\r\n#sict-videodub-wrap.swd-wide{max-width:none}\r\n.swd-badge-count{display:inline-block;background:rgba(3,105,161,.12);color:#0369a1;padding:1px 8px;border-radius:999px;font-size:12px;font-weight:700;margin-left:6px}\r\n<\/style>\r\n\r\n<script data-no-optimize=\"1\" data-no-minify=\"1\" data-cfasync=\"false\">\r\n(function () {\r\n  \"use strict\";\r\n\r\n  var WRAP_ID = \"sict-videodub-wrap\";\r\n  var booted = false;\r\n\r\n  \/* ============================================================\r\n     GLOBAL I18N DICTIONARY\r\n     ============================================================ *\/\r\n  var I18N = {\r\n    en: { status:\"Pro Edition - runs in your browser\", title:\"Video Dub Studio\",\r\n      sub:\"Transcribe, upload SBV\/SRT subtitles, translate with real meaning, and dub audio tracks or full videos.\",\r\n      fullWidth:\"Full Width\", stdWidth:\"Standard Width\", clearAll:\"Clear All\", saveProj:\"Save Project\",\r\n      loadProj:\"Load Project\", restoreSession:\"Restore Last Session\", clearTrShort:\"Clear Translations\",\r\n      keysSummary:\"API keys - one place for translation and voices\",\r\n      geminiKey:\"Gemini key (translation + Gemini voice)\", openaiKey:\"OpenAI key (optional voice)\",\r\n      elevenKey:\"ElevenLabs key (optional voice)\",\r\n      rememberKeys:\"Remember keys on this device (browser storage - never use on a shared computer)\",\r\n      tab1:\"1. Load & Subtitles\", tab2:\"2. Translate & Edit\", tab3:\"3. Voice & Sync\", tab4:\"4. Export\",\r\n      dropT:\"Drop a video or audio file here, or click to choose\",\r\n      dropS:\"mp4, webm, mov, mp3, wav, m4a - optional if uploading subtitles only\",\r\n      subUploadTitle:\"Direct Subtitle Upload (.sbv, .srt, .vtt, .ass)\",\r\n      subUploadHint:\"Already have a subtitle file (like YouTube .sbv or .srt)? Upload it directly to translate its real meaning and generate a dubbed audio track without needing a video.\",\r\n      subUploadBtn:\"Upload Subtitle File (.sbv \/ .srt \/ .vtt)\",\r\n      legalNote:\"Use only videos or subtitles you own or have permission to dub.\",\r\n      transcribeTitle:\"Transcription (Whisper, in your browser)\", modelLbl:\"Model\",\r\n      spokenLbl:\"Spoken language in the video\", autoDetect:\"Auto-detect\", transcribe:\"Transcribe Video\/Audio\",\r\n      trHint:\"The first run downloads the model once, then it is cached by your browser.\",\r\n      translateTitle:\"Translate with Gemini (Real Meaning)\", targetLbl:\"Target language\",\r\n      trStyleLbl:\"Translation Style & Context\",\r\n      styleMeaning:\"Real Meaning & Natural Dubbing (Idiomatic, Spoken Flow)\",\r\n      styleStory:\"Storytelling \/ Narrative (Expressive, Emotional)\",\r\n      styleEdu:\"Educational \/ Tutorial (Clear, Accurate Terms)\",\r\n      styleConcise:\"Short & Lip-Sync Friendly (Compact Phrasing)\",\r\n      textModelLbl:\"Text model\", translateAll:\"Translate pending segments (Real Meaning)\", optimizeBtn:\"\\u2728 Optimize Segments (Auto-Merge broken sentences)\",\r\n      shorten:\"Shorten lines that were too long\", clearTr:\"Clear translations only\",\r\n      tlHint:\"Get a free key at aistudio.google.com\/apikey. Translates using surrounding sentence context so idioms and real meaning are preserved.\",\r\n      segHeadHint:\"Adjust times, add custom voices, or split.\", searchPh:\"Search segments...\",\r\n      voiceTitle:\"Voice\", voiceEngine:\"Voice engine\",\r\n      browserVoiceOpt:\"Browser voice (free, live preview only)\",\r\n      defaultVoiceLbl:\"Default Voice name or ID\", browserVoiceFree:\"Browser voice (free)\",\r\n      muteOrig:\"Mute the original voice completely (dub voice only)\",\r\n      hardMute:\"Silence original while dub voice is speaking (fixes mixed languages)\",\r\n      quickLenLbl:\"Quick preview length (seconds, 10-600)\",\r\n      quickPreview:\"Quick preview (cloud voice, then plays)\", buildDub:\"Build dubbed audio track\",\r\n      dubReadyTitle:\"Dubbed Audio Track Ready:\",\r\n      stop:\"Stop\", retry:\"Retry failed\",\r\n      bdHint:\"Works with or without a video file! If you uploaded an .sbv\/.srt subtitle file only, this builds a synced standalone audio track ready to download.\",\r\n      livePreview:\"Live dub preview (plays with the video above)\",\r\n      previewDubOnly:\"Preview dub only - browser voice, no recording (use Edge for best voices)\",\r\n      recordTitle:\"Record live dub (no API key)\",\r\n      recordHint:\"Plays the video with the browser voice and records the tab audio into a dubbed track you can export in Tab 4. Use desktop Chrome or Edge. In the share dialog, choose This Tab and tick Share tab audio. Do not pause while recording.\",\r\n      recStart:\"Start recording (Edge \/ Chrome)\", recStop:\"Stop and save\",\r\n      capOnVideo:\"Captions on the video\", capDub:\"Dub language (translation)\",\r\n      capSrc:\"Original language\", capBoth:\"Both\", capOff:\"Off\",\r\n      subsTitle:\"Subtitles\", dubAudio:\"Dubbed audio\", dubVideo:\"Dubbed video\",\r\n      subModeLbl:\"Subtitles inside the video\", subNone:\"None\",\r\n      subSoft:\"Soft subtitles (selectable track, fast)\", subBurn:\"Burn-in (experimental, slow)\",\r\n      dlSrt:\"Download .srt\", dlVtt:\"Download .vtt\", dlSbv:\"Download .sbv\", dlWav:\"Download dubbed .wav\", dlMp4:\"Download dubbed .mp4\", dlMp3:\"Download dubbed .mp3\",\r\n      msgLoadFirst:\"Load a video\/audio file first, or upload an .sbv\/.srt subtitle file.\", msgNoSegs:\"No segments to save.\",\r\n      msgProjLoaded:\"Project loaded. Load the same video in Tab 1 if needed.\",\r\n      msgProjBad:\"Invalid project file.\",\r\n      msgSubLoaded:\"Loaded {n} subtitle segments ({d}s timeline). Ready to translate!\",\r\n      msgSubBad:\"Could not find valid timestamps in this subtitle file.\",\r\n      msgSessionRestored:\"Session restored ({n} segments).\",\r\n      msgCleared:\"Cleared everything, including the saved session.\",\r\n      msgTranscribed:\"Transcribed {n} segments.\",\r\n      msgTransFail:\"Transcription failed.\",\r\n      msgNeedKey:\"Add your Gemini key in the API keys panel at the top.\",\r\n      msgTranslateDone:\"Context-aware translation complete.\",\r\n      msgTranslateFail:\"Translate failed: {m}\",\r\n      msgNeedTrans:\"Transcribe a video or upload a subtitle file (.sbv\/.srt) first (Tab 1).\",\r\n      msgNoTr:\"Translate the segments first (Tab 2).\",\r\n      msgBuiltDone:\"Dubbed audio track built! Listen or download below.\", msgPreviewDone:\"Preview ready.\",\r\n      msgBuildFail:\"Build failed.\", msgStopped:\"Stopped.\",\r\n      msgNothingExport:\"Nothing to export yet.\",\r\n      msgBuildFirst:\"Build the dubbed audio first (Tab 3).\",\r\n      msgPartial:\"This is only the quick preview. Press Build dubbed audio track for the full audio.\",\r\n      msgMuxDone:\"Dubbed video ready.\", msgMuxFail:\"Mux failed.\",\r\n      msgClearTr:\"Translations cleared. Press Translate to redo them.\",\r\n      msgShortenDone:\"Shortened {n} lines. Rebuild the audio.\",\r\n      msgShortenNoFlag:\"No flagged lines yet. Build the audio first; lines that do not fit get flagged.\",\r\n      msgShortenFail:\"Shorten failed: {m}\",\r\n      msgRecordSaved:\"Live dub saved. Go to Tab 4 to export.\",\r\n      msgRecordEmpty:\"Recording was empty.\",\r\n      msgRecHint:\"Recording... the video plays from the start. It stops automatically at the end.\",\r\n      msgNoVoice:\"This browser has no voice for {l}. Use Quick preview with a Gemini voice instead.\",\r\n      msgCloudOnly:\"Browser voice is live-only. Pick Gemini, OpenAI or ElevenLabs to build downloadable audio.\",\r\n      msgNoFile:\"Load a video or subtitle file first.\", msgBadFile:\"Could not read the audio of this file.\",\r\n      msgVideoOnly:\"Load a video file in Tab 1 to export MP4 (for audio-only, download WAV or MP3).\", msgPickCloud:\"Pick Gemini, OpenAI or ElevenLabs to build audio.\",\r\n      msgTranslateFirst:\"Translate the segments first (Tab 2).\",\r\n      msgDubbing:\"Dubbed track ready.\", msgRecording:\"Recording...\",\r\n      msgRestoreNone:\"No saved session was found in this browser.\",\r\n      doneWord:\"Done\", leftWord:\"about {t} left\", etaSec:\"{n} s\", etaMin:\"{m} min {s} s\",\r\n      doneOf:\"{d} \/ {t} ({p}%)\", doneOfLabel:\"{l} {d} \/ {t} ({p}%)\",\r\n      segsWord:\"segments\", undoneWord:\"untranslated\", undoneBadge:\"{n} pending\",\r\n      msgMergedOpt:\"Optimized and combined {n} broken segments.\",\r\n      msgMergedClear:\"Merged. Click Translate to translate the new combined segment.\",\r\n      msgNoOptNeeded:\"All segments look grammatically fine. No auto-merges needed.\",\r\n      msgAllTranslated:\"All segments are already translated. Clear translations first if you want to redo them.\",\r\n      msgLastSeg:\"This is the last segment.\" },\r\n\r\n    si: { status:\"Pro Edition - \\u0d94\\u0db6\\u0dda \\u0db6\\u0dca\\u200d\\u0dbb\\u0dc0\\u0dd4\\u0dc3\\u0dbb\\u0dba\\u0dda \\u0d9a\\u0dca\\u200d\\u0dbb\\u0dd2\\u0dba\\u0dcf\\u0dad\\u0dca\\u0db8\\u0d9a \\u0dc0\\u0dda\", title:\"\\u0dc0\\u0dd3\\u0da9\\u0dd2\\u0dba\\u0ddd \\u0da9\\u0db6\\u0dca \\u0dc3\\u0dca\\u0da7\\u0dd4\\u0da9\\u0dd2\\u0dba\\u0ddd\",\r\n      sub:\"SBV\/SRT \\u0d8b\\u0db4\\u0dc3\\u0dd2\\u0dbb\\u0dd0\\u0dc3\\u0dd2 \\u0d9c\\u0ddc\\u0db1\\u0dd4 \\u0d95\\u0db1\\u0dd1\\u0db8 \\u0db7\\u0dcf\\u0dc2\\u0dcf\\u0dc0\\u0d9a\\u0da7 \\u0db1\\u0dd2\\u0dc0\\u0dd0\\u0dbb\\u0daf\\u0dd2 \\u0d85\\u0dbb\\u0dca\\u0dae\\u0dba\\u0dd9\\u0db1\\u0dca \\u0db4\\u0dbb\\u0dd2\\u0dc0\\u0dbb\\u0dca\\u0dad\\u0db1\\u0dba \\u0d9a\\u0dbb \\u0da9\\u0db6\\u0dca \\u0dc4\\u0dac \\u0db4\\u0da7\\u0dba (WAV\/MP3) \\u0db6\\u0dcf\\u0d9c\\u0dad \\u0d9a\\u0dbb\\u0d9c\\u0db1\\u0dca\\u0db1.\",\r\n      fullWidth:\"\\u0db4\\u0dd6\\u0dbb\\u0dca\\u0dab \\u0db4\\u0dc5\\u0dbd\", stdWidth:\"\\u0dc3\\u0dcf\\u0db8\\u0dcf\\u0db1\\u0dca\\u200d\\u0dba \\u0db4\\u0dc5\\u0dbd\", clearAll:\"\\u0dc3\\u0dd2\\u0dba\\u0dbd\\u0dca\\u0dbd \\u0db8\\u0d9a\\u0db1\\u0dca\\u0db1\", saveProj:\"\\u0dc0\\u0dca\\u200d\\u0dba\\u0dcf\\u0db4\\u0dd8\\u0dad\\u0dd2\\u0dba \\u0dc3\\u0dd4\\u0dbb\\u0d9a\\u0dd2\\u0db1\\u0dca\\u0db1\",\r\n      loadProj:\"\\u0dc0\\u0dca\\u200d\\u0dba\\u0dcf\\u0db4\\u0dd8\\u0dad\\u0dd2\\u0dba \\u0db4\\u0dd6\\u0dbb\\u0dab\\u0dba\", restoreSession:\"\\u0db4\\u0dd9\\u0dbb \\u0dc3\\u0dd0\\u0dc3\\u0dd2\\u0dba \\u0db1\\u0dd0\\u0dc0\\u0dad \\u0dbd\\u0db6\\u0dcf\\u0d9c\\u0db1\\u0dca\\u0db1\", clearTrShort:\"\\u0db4\\u0dbb\\u0dd2\\u0dc0\\u0dbb\\u0dca\\u0dad\\u0db1 \\u0db8\\u0d9a\\u0db1\\u0dca\\u0db1\",\r\n      keysSummary:\"API \\u0dba\\u0dad\\u0dd4\\u0dbb\\u0dd4 - \\u0db4\\u0dbb\\u0dd2\\u0dc0\\u0dbb\\u0dca\\u0dad\\u0db1\\u0dba \\u0dc3\\u0dc4 \\u0dc4\\u0dac \\u0dc3\\u0db3\\u0dc4\\u0dcf \\u0d91\\u0d9a\\u0db8 \\u0dad\\u0dd0\\u0db1\\u0d9a\",\r\n      geminiKey:\"Gemini \\u0dba\\u0dad\\u0dd4\\u0dbb (\\u0db4\\u0dbb\\u0dd2\\u0dc0\\u0dbb\\u0dca\\u0dad\\u0db1\\u0dba + Gemini \\u0dc4\\u0dac)\", openaiKey:\"OpenAI \\u0dba\\u0dad\\u0dd4\\u0dbb (\\u0dc0\\u0dd2\\u0d9a\\u0dbd\\u0dca\\u0db4 \\u0dc4\\u0dac)\",\r\n      elevenKey:\"ElevenLabs \\u0dba\\u0dad\\u0dd4\\u0dbb (\\u0dc0\\u0dd2\\u0d9a\\u0dbd\\u0dca\\u0db4 \\u0dc4\\u0dac)\",\r\n      rememberKeys:\"\\u0db8\\u0dd9\\u0db8 \\u0d8b\\u0db4\\u0dcf\\u0d82\\u0d9c\\u0dba\\u0dda \\u0dba\\u0dad\\u0dd4\\u0dbb\\u0dd4 \\u0db8\\u0dad\\u0d9a \\u0dad\\u0db6\\u0dcf \\u0d9c\\u0db1\\u0dca\\u0db1\",\r\n      tab1:\"1. \\u0db4\\u0dd6\\u0dbb\\u0dab\\u0dba \\u0dc3\\u0dc4 \\u0d8b\\u0db4\\u0dc3\\u0dd2\\u0dbb\\u0dd0\\u0dc3\\u0dd2\", tab2:\"2. \\u0db4\\u0dbb\\u0dd2\\u0dc0\\u0dbb\\u0dca\\u0dad\\u0db1\\u0dba \\u0dc3\\u0dc4 \\u0dc3\\u0d82\\u0dc3\\u0dca\\u0d9a\\u0dbb\\u0dab\\u0dba\", tab3:\"3. \\u0dc4\\u0dac \\u0dc3\\u0dc4 \\u0dc3\\u0db8\\u0db8\\u0dd4\\u0dc4\\u0dd4\\u0dbb\\u0dca\\u0dad\", tab4:\"4. \\u0d85\\u0db4\\u0db1\\u0dba\\u0db1\\u0dba\",\r\n      dropT:\"\\u0dc0\\u0dd3\\u0da9\\u0dd2\\u0dba\\u0ddd \\u0dc4\\u0ddd \\u0dc1\\u0dca\\u200d\\u0dbb\\u0dc0\\u0dca\\u200d\\u0dba \\u0d9c\\u0ddc\\u0db1\\u0dd4\\u0dc0\\u0d9a\\u0dca \\u0db8\\u0dd9\\u0dc4\\u0dd2 \\u0d85\\u0daf\\u0dd2\\u0db1\\u0dca\\u0db1, \\u0dc4\\u0ddd \\u0dad\\u0ddd\\u0dbb\\u0db1\\u0dca\\u0db1\",\r\n      dropS:\"mp4, webm, mov, mp3, wav, m4a - \\u0d8b\\u0db4\\u0dc3\\u0dd2\\u0dbb\\u0dd0\\u0dc3\\u0dd2 \\u0db4\\u0db8\\u0dab\\u0d9a\\u0dca \\u0db1\\u0db8\\u0dca \\u0dc0\\u0dd3\\u0da9\\u0dd2\\u0dba\\u0ddd \\u0d85\\u0dc0\\u0dc1\\u0dca\\u200d\\u0dba \\u0db1\\u0dd0\\u0dad\",\r\n      subUploadTitle:\"\\u0d8b\\u0db4\\u0dc3\\u0dd2\\u0dbb\\u0dd0\\u0dc3\\u0dd2 \\u0d9c\\u0ddc\\u0db1\\u0dd4\\u0dc0 \\u0d9a\\u0dd9\\u0dbd\\u0dd2\\u0db1\\u0dca\\u0db8 \\u0d87\\u0dad\\u0dd4\\u0dc5\\u0dad\\u0dca \\u0d9a\\u0dbb\\u0db1\\u0dca\\u0db1 (.sbv, .srt, .vtt)\",\r\n      subUploadHint:\"YouTube .sbv \\u0dc4\\u0ddd .srt \\u0d8b\\u0db4\\u0dc3\\u0dd2\\u0dbb\\u0dd0\\u0dc3\\u0dd2 \\u0d9c\\u0ddc\\u0db1\\u0dd4\\u0dc0\\u0d9a\\u0dca \\u0dad\\u0dd2\\u0db6\\u0dda\\u0daf? \\u0dc0\\u0dd3\\u0da9\\u0dd2\\u0dba\\u0ddd\\u0dc0\\u0d9a\\u0dca \\u0db1\\u0ddc\\u0db8\\u0dd0\\u0dad\\u0dd2\\u0dc0 \\u0dc0\\u0dd4\\u0dc0\\u0daf \\u0d91\\u0dba \\u0db1\\u0dd2\\u0dc0\\u0dd0\\u0dbb\\u0daf\\u0dd2 \\u0d85\\u0dbb\\u0dca\\u0dae\\u0dba\\u0dd9\\u0db1\\u0dca \\u0db4\\u0dbb\\u0dd2\\u0dc0\\u0dbb\\u0dca\\u0dad\\u0db1\\u0dba \\u0d9a\\u0dbb \\u0da9\\u0db6\\u0dca \\u0dc4\\u0dac \\u0db4\\u0da7\\u0dba \\u0dc3\\u0dcf\\u0daf\\u0dcf \\u0d9c\\u0db1\\u0dca\\u0db1.\",\r\n      subUploadBtn:\"Subtitle \\u0d9c\\u0ddc\\u0db1\\u0dd4\\u0dc0 \\u0dad\\u0ddd\\u0dbb\\u0db1\\u0dca\\u0db1 (.sbv \/ .srt \/ .vtt)\",\r\n      legalNote:\"\\u0d94\\u0db6\\u0da7 \\u0dc4\\u0dd2\\u0db8\\u0dd2 \\u0dc4\\u0ddd \\u0d85\\u0dc0\\u0dc3\\u0dbb \\u0d87\\u0dad\\u0dd2 \\u0dc0\\u0dd3\\u0da9\\u0dd2\\u0dba\\u0ddd \\u0db4\\u0db8\\u0dab\\u0d9a\\u0dca \\u0db7\\u0dcf\\u0dc0\\u0dd2\\u0dad\\u0dcf \\u0d9a\\u0dbb\\u0db1\\u0dca\\u0db1.\",\r\n      transcribeTitle:\"\\u0db4\\u0dd2\\u0da7\\u0db4\\u0dad\\u0dca \\u0d9a\\u0dd2\\u0dbb\\u0dd3\\u0db8 (Whisper, \\u0d94\\u0db6\\u0dda \\u0db6\\u0dca\\u200d\\u0dbb\\u0dc0\\u0dd4\\u0dc3\\u0dbb\\u0dba\\u0dda)\", modelLbl:\"\\u0db8\\u0dcf\\u0daf\\u0dd2\\u0dbd\\u0dd2\\u0dba\",\r\n      spokenLbl:\"\\u0dc0\\u0dd3\\u0da9\\u0dd2\\u0dba\\u0ddd\\u0dc0\\u0dda \\u0d9a\\u0dae\\u0db1 \\u0db7\\u0dcf\\u0dc2\\u0dcf\\u0dc0\", autoDetect:\"\\u0dc3\\u0dca\\u0dc0\\u0dba\\u0d82\\u0d9a\\u0dca\\u200d\\u0dbb\\u0dd3\\u0dba\\u0dc0 \\u0dc4\\u0db3\\u0dd4\\u0db1\\u0dcf\\u0d9c\\u0db1\\u0dca\\u0db1\", transcribe:\"\\u0db4\\u0dd2\\u0da7\\u0db4\\u0dad\\u0dca \\u0d9a\\u0dbb\\u0db1\\u0dca\\u0db1\",\r\n      trHint:\"\\u0db4\\u0dc5\\u0db8\\u0dd4 \\u0dc0\\u0dbb\\u0da7 \\u0db8\\u0dcf\\u0daf\\u0dd2\\u0dbd\\u0dd2\\u0dba \\u0db6\\u0dcf\\u0d9c\\u0dad \\u0dc0\\u0dda, \\u0db4\\u0dc3\\u0dd4\\u0dc0 \\u0db6\\u0dca\\u200d\\u0dbb\\u0dc0\\u0dd4\\u0dc3\\u0dbb\\u0dba \\u0db8\\u0d9c\\u0dd2\\u0db1\\u0dca \\u0dc4\\u0dd0\\u0db9\\u0dd2\\u0dbd\\u0dd2\\u0d9c\\u0dad \\u0dc0\\u0dda.\",\r\n      translateTitle:\"Gemini \\u0dc3\\u0db8\\u0d9f \\u0dc3\\u0dd0\\u0db6\\u0dd1 \\u0d85\\u0dbb\\u0dca\\u0dae\\u0dba\\u0dd9\\u0db1\\u0dca \\u0db4\\u0dbb\\u0dd2\\u0dc0\\u0dbb\\u0dca\\u0dad\\u0db1\\u0dba\", targetLbl:\"\\u0d89\\u0dbd\\u0d9a\\u0dca\\u0d9a \\u0db7\\u0dcf\\u0dc2\\u0dcf\\u0dc0\",\r\n      trStyleLbl:\"\\u0db4\\u0dbb\\u0dd2\\u0dc0\\u0dbb\\u0dca\\u0dad\\u0db1 \\u0dc1\\u0ddb\\u0dbd\\u0dd2\\u0dba\",\r\n      styleMeaning:\"\\u0dc3\\u0dd0\\u0db6\\u0dd1 \\u0d85\\u0dbb\\u0dca\\u0dae\\u0dba \\u0dc3\\u0dc4 \\u0dc3\\u0dca\\u0dc0\\u0dcf\\u0db7\\u0dcf\\u0dc0\\u0dd2\\u0d9a \\u0d9a\\u0dae\\u0db1 \\u0db7\\u0dcf\\u0dc2\\u0dcf\\u0dc0 (Natural Dubbing)\",\r\n      styleStory:\"\\u0d9a\\u0dad\\u0dcf\\u0db1\\u0dca\\u0daf\\u0dbb \/ \\u0db1\\u0dcf\\u0da7\\u0dca\\u200d\\u0dba\\u0db8\\u0dba \\u0dc1\\u0ddb\\u0dbd\\u0dd2\\u0dba (Storytelling)\",\r\n      styleEdu:\"\\u0d85\\u0db0\\u0dca\\u200d\\u0dba\\u0dcf\\u0db4\\u0db1\\u0dd2\\u0d9a \/ \\u0db4\\u0dcf\\u0da9\\u0db8\\u0dca (Educational)\",\r\n      styleConcise:\"\\u0d9a\\u0dd9\\u0da7\\u0dd2 \\u0dc3\\u0dc4 \\u0dc4\\u0dac \\u0d9a\\u0dcf\\u0dbd\\u0dba\\u0da7 \\u0d9c\\u0dd0\\u0dbd\\u0db4\\u0dd9\\u0db1 (Concise)\",\r\n      textModelLbl:\"\\u0db4\\u0dd9\\u0dc5 \\u0db8\\u0dcf\\u0daf\\u0dd2\\u0dbd\\u0dd2\\u0dba\", translateAll:\"\\u0dc3\\u0dd0\\u0db6\\u0dd1 \\u0d85\\u0dbb\\u0dca\\u0dae\\u0dba\\u0dd9\\u0db1\\u0dca \\u0db4\\u0dbb\\u0dd2\\u0dc0\\u0dbb\\u0dca\\u0dad\\u0db1\\u0dba \\u0d9a\\u0dbb\\u0db1\\u0dca\\u0db1\", optimizeBtn:\"\\u2728 \\u0d9a\\u0ddc\\u0da7\\u0dc3\\u0dca \\u0db4\\u0dca\\u200d\\u0dbb\\u0dc1\\u0dc3\\u0dca\\u0dad \\u0d9a\\u0dbb\\u0db1\\u0dca\\u0db1 (\\u0d9a\\u0dd0\\u0da9\\u0dd4\\u0dab\\u0dd4 \\u0dc0\\u0dcf\\u0d9a\\u0dca\\u200d\\u0dba \\u0d91\\u0d9a\\u0dca \\u0d9a\\u0dbb\\u0db1\\u0dca\\u0db1)\",\r\n      shorten:\"\\u0daf\\u0dd2\\u0d9c\\u0dd4 \\u0db4\\u0dda\\u0dc5\\u0dd2 \\u0d9a\\u0dd9\\u0da7\\u0dd2 \\u0d9a\\u0dbb\\u0db1\\u0dca\\u0db1\", clearTr:\"\\u0db4\\u0dbb\\u0dd2\\u0dc0\\u0dbb\\u0dca\\u0dad\\u0db1 \\u0db4\\u0db8\\u0dab\\u0d9a\\u0dca \\u0db8\\u0d9a\\u0db1\\u0dca\\u0db1\",\r\n      tlHint:\"aistudio.google.com\/apikey \\u0dc0\\u0dd9\\u0dad\\u0dd2\\u0db1\\u0dca \\u0db1\\u0ddc\\u0db8\\u0dd2\\u0dbd\\u0dda \\u0dba\\u0dad\\u0dd4\\u0dbb\\u0d9a\\u0dca \\u0dbd\\u0db6\\u0dcf\\u0d9c\\u0db1\\u0dca\\u0db1.\",\r\n      segHeadHint:\"\\u0dc0\\u0dda\\u0dbd\\u0dcf\\u0dc0\\u0db1\\u0dca \\u0dc3\\u0d9a\\u0dc3\\u0db1\\u0dca\\u0db1, \\u0d85\\u0db7\\u0dd2\\u0dbb\\u0dd4\\u0da0\\u0dd2 \\u0dc4\\u0dac \\u0d91\\u0d9a\\u0dca \\u0d9a\\u0dbb\\u0db1\\u0dca\\u0db1, \\u0dc4\\u0ddd \\u0db6\\u0dd9\\u0daf\\u0db1\\u0dca\\u0db1.\", searchPh:\"\\u0d9a\\u0ddc\\u0da7\\u0dc3\\u0dca \\u0dc3\\u0ddc\\u0dba\\u0db1\\u0dca\\u0db1...\",\r\n      voiceTitle:\"\\u0dc4\\u0dac\", voiceEngine:\"\\u0dc4\\u0dac \\u0d91\\u0db1\\u0dca\\u0da2\\u0dd2\\u0db8\",\r\n      browserVoiceOpt:\"\\u0db6\\u0dca\\u200d\\u0dbb\\u0dc0\\u0dd4\\u0dc3\\u0dbb \\u0dc4\\u0dac (\\u0db1\\u0ddc\\u0db8\\u0dd2\\u0dbd\\u0dda, \\u0dc3\\u0da2\\u0dd3\\u0dc0\\u0dd3 \\u0db4\\u0dd9\\u0dbb\\u0daf\\u0dc3\\u0dd4\\u0db1 \\u0db4\\u0db8\\u0dab\\u0dd2)\",\r\n      defaultVoiceLbl:\"\\u0db4\\u0dd9\\u0dbb\\u0db1\\u0dd2\\u0db8\\u0dd2 \\u0dc4\\u0dac \\u0db1\\u0db8 \\u0dc4\\u0ddd ID\", browserVoiceFree:\"\\u0db6\\u0dca\\u200d\\u0dbb\\u0dc0\\u0dd4\\u0dc3\\u0dbb \\u0dc4\\u0dac (\\u0db1\\u0ddc\\u0db8\\u0dd2\\u0dbd\\u0dda)\",\r\n      muteOrig:\"\\u0db8\\u0dd4\\u0dbd\\u0dca \\u0dc4\\u0dac \\u0dc3\\u0db8\\u0dca\\u0db4\\u0dd6\\u0dbb\\u0dca\\u0dab\\u0dba\\u0dd9\\u0db1\\u0dca\\u0db8 \\u0db1\\u0dd2\\u0dc1\\u0dca\\u0dc1\\u0db6\\u0dca\\u0daf \\u0d9a\\u0dbb\\u0db1\\u0dca\\u0db1\",\r\n      hardMute:\"\\u0da9\\u0db6\\u0dca \\u0dc4\\u0dac \\u0d9a\\u0dae\\u0dcf \\u0d9a\\u0dbb\\u0db1 \\u0dc0\\u0dd2\\u0da7 \\u0db8\\u0dd4\\u0dbd\\u0dca \\u0dc4\\u0dac \\u0db1\\u0dd2\\u0dc1\\u0dca\\u0dc1\\u0db6\\u0dca\\u0daf \\u0d9a\\u0dbb\\u0db1\\u0dca\\u0db1 (\\u0db8\\u0dd2\\u0dc1\\u0dca\\u200d\\u0dbb \\u0db7\\u0dcf\\u0dc2\\u0dcf \\u0db1\\u0dd2\\u0dc0\\u0dd0\\u0dbb\\u0daf\\u0dd2 \\u0d9a\\u0dbb\\u0dba\\u0dd2)\",\r\n      quickLenLbl:\"\\u0d89\\u0d9a\\u0dca\\u0db8\\u0db1\\u0dca \\u0db4\\u0dd9\\u0dbb\\u0daf\\u0dc3\\u0dd4\\u0db1\\u0dca \\u0daf\\u0dd2\\u0d9c (\\u0dad\\u0dad\\u0dca\\u0db4\\u0dbb, 10-600)\",\r\n      quickPreview:\"\\u0d89\\u0d9a\\u0dca\\u0db8\\u0db1\\u0dca \\u0db4\\u0dd9\\u0dbb\\u0daf\\u0dc3\\u0dd4\\u0db1\", buildDub:\"\\u0da9\\u0db6\\u0dca \\u0dc4\\u0dac \\u0db4\\u0da7\\u0dba \\u0dc3\\u0dcf\\u0daf\\u0db1\\u0dca\\u0db1 (Build Dub Audio)\",\r\n      dubReadyTitle:\"\\u0da9\\u0db6\\u0dca \\u0dc4\\u0dac \\u0db4\\u0da7\\u0dba \\u0dc3\\u0dd6\\u0daf\\u0dcf\\u0db1\\u0db8\\u0dca:\",\r\n      stop:\"\\u0db1\\u0dc0\\u0dad\\u0dca\\u0dc0\\u0db1\\u0dca\\u0db1\", retry:\"\\u0d85\\u0dc3\\u0dcf\\u0dbb\\u0dca\\u0dae\\u0d9a \\u0db1\\u0dd0\\u0dc0\\u0dad \\u0d8b\\u0dad\\u0dca\\u0dc3\\u0dcf\\u0dc4 \\u0d9a\\u0dbb\\u0db1\\u0dca\\u0db1\",\r\n      bdHint:\"\\u0dc0\\u0dd3\\u0da9\\u0dd2\\u0dba\\u0ddd\\u0dc0\\u0d9a\\u0dca \\u0db1\\u0ddc\\u0db8\\u0dd0\\u0dad\\u0dd2\\u0dc0 \\u0dc0\\u0dd4\\u0dc0\\u0daf .sbv\/.srt \\u0d8b\\u0db4\\u0dc3\\u0dd2\\u0dbb\\u0dd0\\u0dc3\\u0dd2 \\u0d9c\\u0ddc\\u0db1\\u0dd4\\u0dc0\\u0da7 \\u0d85\\u0daf\\u0dcf\\u0dc5 \\u0da9\\u0db6\\u0dca \\u0dc4\\u0dac \\u0db4\\u0da7\\u0dba \\u0dc3\\u0dcf\\u0daf\\u0dcf \\u0db6\\u0dcf\\u0d9c\\u0dad \\u0d9a\\u0dc5 \\u0dc4\\u0dd0\\u0d9a.\",\r\n      livePreview:\"\\u0dc3\\u0da2\\u0dd3\\u0dc0\\u0dd3 \\u0da9\\u0db6\\u0dca \\u0db4\\u0dd9\\u0dbb\\u0daf\\u0dc3\\u0dd4\\u0db1 (\\u0d89\\u0dc4\\u0dad \\u0dc0\\u0dd3\\u0da9\\u0dd2\\u0dba\\u0ddd\\u0dc0 \\u0dc3\\u0db8\\u0d9f \\u0dc0\\u0dcf\\u0daf\\u0db1\\u0dba \\u0dc0\\u0dda)\",\r\n      previewDubOnly:\"\\u0da9\\u0db6\\u0dca \\u0db4\\u0db8\\u0dab\\u0d9a\\u0dca \\u0db4\\u0dd9\\u0dbb\\u0daf\\u0dc3\\u0dd4\\u0db1\\u0dca - \\u0db6\\u0dca\\u200d\\u0dbb\\u0dc0\\u0dd4\\u0dc3\\u0dbb \\u0dc4\\u0dac, \\u0db4\\u0da7\\u0dd2\\u0d9c\\u0dad \\u0d9a\\u0dd2\\u0dbb\\u0dd3\\u0db8\\u0d9a\\u0dca \\u0db1\\u0dd0\\u0dad\",\r\n      recordTitle:\"\\u0dc3\\u0da2\\u0dd3\\u0dc0\\u0dd3 \\u0da9\\u0db6\\u0dca \\u0db4\\u0da7\\u0dd2\\u0d9c\\u0dad \\u0d9a\\u0dbb\\u0db1\\u0dca\\u0db1 (API \\u0dba\\u0dad\\u0dd4\\u0dbb\\u0d9a\\u0dca \\u0db1\\u0dd0\\u0dad)\",\r\n      recordHint:\"Edge \\u0dc4\\u0ddd Chrome \\u0db7\\u0dcf\\u0dc0\\u0dd2\\u0dad\\u0dcf \\u0d9a\\u0dbb\\u0db1\\u0dca\\u0db1. share dialog \\u0d91\\u0d9a\\u0dda\\u0daf\\u0dd3 This Tab \\u0dad\\u0ddd\\u0dbb\\u0dcf Share tab audio \\u0dc3\\u0dbd\\u0d9a\\u0dd4\\u0dab\\u0dd4 \\u0d9a\\u0dbb\\u0db1\\u0dca\\u0db1.\",\r\n      recStart:\"\\u0db4\\u0da7\\u0dd2\\u0d9c\\u0dad \\u0d9a\\u0dd2\\u0dbb\\u0dd3\\u0db8 \\u0d86\\u0dbb\\u0db8\\u0dca\\u0db7 \\u0d9a\\u0dbb\\u0db1\\u0dca\\u0db1\", recStop:\"\\u0db1\\u0dc0\\u0dad\\u0dca\\u0dc0\\u0dcf \\u0dc3\\u0dd4\\u0dbb\\u0d9a\\u0dd2\\u0db1\\u0dca\\u0db1\",\r\n      capOnVideo:\"\\u0dc0\\u0dd3\\u0da9\\u0dd2\\u0dba\\u0ddd\\u0dc0\\u0dda \\u0d8b\\u0db4\\u0dc3\\u0dd2\\u0dbb\\u0dd0\\u0dc3\\u0dd2\", capDub:\"\\u0da9\\u0db6\\u0dca \\u0db7\\u0dcf\\u0dc2\\u0dcf\\u0dc0\", capSrc:\"\\u0db8\\u0dd4\\u0dbd\\u0dca \\u0db7\\u0dcf\\u0dc2\\u0dcf\\u0dc0\", capBoth:\"\\u0daf\\u0dd9\\u0d9a\\u0db8\", capOff:\"\\u0db1\\u0dd2\\u0dc0\\u0dcf\",\r\n      subsTitle:\"\\u0d8b\\u0db4\\u0dc3\\u0dd2\\u0dbb\\u0dd0\\u0dc3\\u0dd2\", dubAudio:\"\\u0da9\\u0db6\\u0dca \\u0dc1\\u0dca\\u200d\\u0dbb\\u0dc0\\u0dca\\u200d\\u0dba\", dubVideo:\"\\u0da9\\u0db6\\u0dca \\u0dc0\\u0dd3\\u0da9\\u0dd2\\u0dba\\u0ddd\",\r\n      subModeLbl:\"\\u0dc0\\u0dd3\\u0da9\\u0dd2\\u0dba\\u0ddd\\u0dc0 \\u0dad\\u0dd4\\u0dc5 \\u0d8b\\u0db4\\u0dc3\\u0dd2\\u0dbb\\u0dd0\\u0dc3\\u0dd2\", subNone:\"\\u0d9a\\u0dd2\\u0dc3\\u0dd2\\u0dc0\\u0d9a\\u0dca \\u0db1\\u0dd0\\u0dad\",\r\n      subSoft:\"\\u0db8\\u0dd8\\u0daf\\u0dd4 \\u0d8b\\u0db4\\u0dc3\\u0dd2\\u0dbb\\u0dd0\\u0dc3\\u0dd2\", subBurn:\"\\u0db4\\u0dd2\\u0dc5\\u0dd2\\u0dc3\\u0dca\\u0dc3\\u0dd3\\u0db8 (\\u0db4\\u0dbb\\u0dd3\\u0d9a\\u0dca\\u0dc2\\u0dab\\u0dcf\\u0dad\\u0dca\\u0db8\\u0d9a)\",\r\n      dlSrt:\"Download .srt\", dlVtt:\"Download .vtt\", dlSbv:\"Download .sbv\", dlWav:\"Download dubbed .wav\", dlMp4:\"Download dubbed .mp4\", dlMp3:\"Download .mp3 (\\u0d89\\u0dad\\u0dcf \\u0d9a\\u0dd4\\u0da9\\u0dcf\\u0dba\\u0dd2)\",\r\n      segsWord:\"\\u0d9a\\u0ddc\\u0da7\\u0dc3\\u0dca\", undoneWord:\"\\u0db4\\u0dbb\\u0dd2\\u0dc0\\u0dbb\\u0dca\\u0dad\\u0db1\\u0dba \\u0db1\\u0ddc\\u0d9a\\u0dc5\", undoneBadge:\"{n} \\u0d89\\u0dad\\u0dd2\\u0dbb\\u0dd2\",\r\n      msgSubLoaded:\"\\u0d8b\\u0db4\\u0dc3\\u0dd2\\u0dbb\\u0dd0\\u0dc3\\u0dd2 \\u0d9a\\u0ddc\\u0da7\\u0dc3\\u0dca {n} \\u0d9a\\u0dca \\u0db4\\u0dd6\\u0dbb\\u0dab\\u0dba \\u0dc0\\u0dd2\\u0dba (\\u0dad\\u0dad\\u0dca\\u0db4\\u0dbb {d}). \\u0daf\\u0dd0\\u0db1\\u0dca \\u0db4\\u0dbb\\u0dd2\\u0dc0\\u0dbb\\u0dca\\u0dad\\u0db1\\u0dba \\u0d9a\\u0dbb\\u0db1\\u0dca\\u0db1!\",\r\n      msgSubBad:\"\\u0db8\\u0dd9\\u0db8 \\u0d8b\\u0db4\\u0dc3\\u0dd2\\u0dbb\\u0dd0\\u0dc3\\u0dd2 \\u0d9c\\u0ddc\\u0db1\\u0dd4\\u0dc0 \\u0d9a\\u0dd2\\u0dba\\u0dc0\\u0dd2\\u0dba \\u0db1\\u0ddc\\u0dc4\\u0dd0\\u0d9a.\",\r\n      msgMergedOpt:\"\\u0d9a\\u0dd0\\u0da9\\u0dd4\\u0dab\\u0dd4 \\u0d9a\\u0ddc\\u0da7\\u0dc3\\u0dca {n} \\u0d9a\\u0dca \\u0dc3\\u0dcf\\u0dbb\\u0dca\\u0dae\\u0d9a\\u0dc0 \\u0d91\\u0d9a\\u0dca \\u0d9a\\u0dbb\\u0db1 \\u0dbd\\u0daf\\u0dd2.\",\r\n      msgMergedClear:\"\\u0d91\\u0d9a\\u0dca \\u0d9a\\u0dbb\\u0db1 \\u0dbd\\u0daf\\u0dd2. \\u0db4\\u0dbb\\u0dd2\\u0dc0\\u0dbb\\u0dca\\u0dad\\u0db1\\u0dba \\u0dc3\\u0db3\\u0dc4\\u0dcf \\u0db1\\u0dd0\\u0dc0\\u0dad Translate \\u0d94\\u0db6\\u0db1\\u0dca\\u0db1.\",\r\n      msgNoOptNeeded:\"\\u0dc3\\u0dd2\\u0dba\\u0dbd\\u0dd4\\u0db8 \\u0d9a\\u0ddc\\u0da7\\u0dc3\\u0dca \\u0dc0\\u0dca\\u200d\\u0dba\\u0dcf\\u0d9a\\u0dbb\\u0dab\\u0dcf\\u0db1\\u0dd4\\u0d9a\\u0dd6\\u0dbd\\u0dc0 \\u0db1\\u0dd2\\u0dc0\\u0dd0\\u0dbb\\u0daf\\u0dd2\\u0dba\\u0dd2. \\u0d91\\u0d9a\\u0dad\\u0dd4 \\u0d9a\\u0dd2\\u0dbb\\u0dd3\\u0db8\\u0dca \\u0d85\\u0dc0\\u0dc1\\u0dca\\u200d\\u0dba \\u0db1\\u0ddc\\u0dc0\\u0dda.\",\r\n      msgAllTranslated:\"\\u0dc3\\u0dd2\\u0dba\\u0dbd\\u0dd4\\u0db8 \\u0d9a\\u0ddc\\u0da7\\u0dc3\\u0dca \\u0daf\\u0dd0\\u0db1\\u0da7\\u0db8\\u0dad\\u0dca \\u0db4\\u0dbb\\u0dd2\\u0dc0\\u0dbb\\u0dca\\u0dad\\u0db1\\u0dba \\u0dc0\\u0dd3 \\u0d87\\u0dad. \\u0db1\\u0dd0\\u0dc0\\u0dad \\u0d9a\\u0dd2\\u0dbb\\u0dd3\\u0db8\\u0da7 \\u0d85\\u0dc0\\u0dc1\\u0dca\\u200d\\u0dba \\u0db1\\u0db8\\u0dca Clear Translations \\u0d94\\u0db6\\u0db1\\u0dca\\u0db1.\",\r\n      msgLastSeg:\"\\u0db8\\u0dd9\\u0dba \\u0d85\\u0dc0\\u0dc3\\u0dcf\\u0db1 \\u0d9a\\u0ddc\\u0da7\\u0dc3\\u0dba\\u0dd2.\" },\r\n\r\n    ta: { status:\"Pro Edition - \\u0b89\\u0b99\\u0bcd\\u0b95\\u0bb3\\u0bcd \\u0b89\\u0bb2\\u0bbe\\u0bb5\\u0bbf\\u0baf\\u0bbf\\u0bb2\\u0bcd \\u0b87\\u0baf\\u0b99\\u0bcd\\u0b95\\u0bc1\\u0b95\\u0bbf\\u0bb1\\u0ba4\\u0bc1\", title:\"\\u0bb5\\u0bc0\\u0b9f\\u0bbf\\u0baf\\u0bcb \\u0b9f\\u0baa\\u0bcd \\u0bb8\\u0bcd\\u0b9f\\u0bc1\\u0b9f\\u0bbf\\u0baf\\u0bcb\" },\r\n    hi: { status:\"Pro Edition - \\u0906\\u092a\\u0915\\u0947 \\u092c\\u094d\\u0930\\u093e\\u0909\\u091c\\u093c\\u0930 \\u092e\\u0947\\u0902 \\u091a\\u0932\\u0924\\u093e \\u0939\\u0948\", title:\"\\u0935\\u0940\\u0921\\u093f\\u092f\\u094b \\u0921\\u092c \\u0938\\u094d\\u091f\\u0942\\u0921\\u093f\\u092f\\u094b\" },\r\n    es: { status:\"Edici\\u00f3n Pro - funciona en tu navegador\", title:\"Estudio de Doblaje de Video\" },\r\n    fr: { status:\"\\u00c9dition Pro - dans votre navigateur\", title:\"Studio de Doublage Vid\\u00e9o\" },\r\n    ar: { status:\"\\u0625\\u0635\\u062f\\u0627\\u0631 \\u0627\\u062d\\u062a\\u0631\\u0627\\u0641\\u064a - \\u064a\\u0639\\u0645\\u0644 \\u0641\\u064a \\u0645\\u062a\\u0635\\u0641\\u062d\\u0643\", title:\"\\u0627\\u0633\\u062a\\u0648\\u062f\\u064a\\u0648 \\u062f\\u0628\\u0644\\u062c\\u0629 \\u0627\\u0644\\u0641\\u064a\\u062f\\u064a\\u0648\" },\r\n    zh: { status:\"\\u4e13\\u4e1a\\u7248 - \\u5728\\u60a8\\u7684\\u6d4f\\u89c8\\u5668\\u4e2d\\u8fd0\\u884c\", title:\"\\u89c6\\u9891\\u914d\\u97f3\\u5de5\\u4f5c\\u5ba4\" },\r\n    ja: { status:\"Pro \\u7248 - \\u30d6\\u30e9\\u30a6\\u30b6\\u3067\\u52d5\\u4f5c\", title:\"\\u30d3\\u30c7\\u30aa\\u5439\\u304d\\u66ff\\u3048\\u30b9\\u30bf\\u30b8\\u30aa\" },\r\n    ko: { status:\"Pro \\uc5d0\\ub514\\uc158 - \\ube0c\\ub77c\\uc6b0\\uc800\\uc5d0\\uc11c \\uc2e4\\ud589\", title:\"\\ube44\\ub514\\uc624 \\ub354\\ube59 \\uc2a4\\ud29c\\ub514\\uc624\" }\r\n  };\r\n\r\n  \/\/ Populate fallback keys from English\r\n  [\"si\",\"ta\",\"hi\",\"es\",\"fr\",\"ar\",\"zh\",\"ja\",\"ko\"].forEach(function(l) {\r\n      if(!I18N[l]) return;\r\n      var e = I18N.en;\r\n      for(var k in e) { if(I18N[l][k] === undefined) I18N[l][k] = e[k]; }\r\n  });\r\n\r\n  var CUR_LANG = \"en\";\r\n\r\n  function t(key) {\r\n    var dict = I18N[CUR_LANG] || I18N.en;\r\n    var s = dict[key];\r\n    if (s === undefined) { s = I18N.en[key]; }\r\n    if (s === undefined) { return key; }\r\n    if (arguments.length > 1) {\r\n      var i;\r\n      for (i = 1; i < arguments.length; i++) {\r\n        s = s.replace(\"{\" + (i - 1) + \"}\", arguments[i]);\r\n      }\r\n    }\r\n    return s;\r\n  }\r\n  function tf(key, map) {\r\n    var s = t(key);\r\n    if (!map) { return s; }\r\n    for (var k in map) { if (map.hasOwnProperty(k)) { s = s.split(\"{\" + k + \"}\").join(map[k]); } }\r\n    return s;\r\n  }\r\n\r\n  function applyI18n(wrap) {\r\n    var els = wrap.querySelectorAll(\"[data-i18n]\"), i;\r\n    for (i = 0; i < els.length; i++) { els[i].textContent = t(els[i].getAttribute(\"data-i18n\")); }\r\n    var aels = wrap.querySelectorAll(\"[data-i18n-attr]\");\r\n    for (i = 0; i < aels.length; i++) {\r\n      var spec = aels[i].getAttribute(\"data-i18n-attr\").split(\":\");\r\n      if (spec.length === 2) { aels[i].setAttribute(spec[0], t(spec[1])); }\r\n    }\r\n    var langSel = document.getElementById(\"swd-ui-lang\");\r\n    if (langSel) { langSel.value = CUR_LANG; }\r\n  }\r\n\r\n  function boot() {\r\n    if (booted) { return; }\r\n    var wrap = document.getElementById(WRAP_ID);\r\n    if (!wrap) { return; }\r\n    booted = true;\r\n    try { init(wrap); } catch (err) { if (window.console) { console.error(\"Video Dub Studio failed to start\", err); } }\r\n  }\r\n\r\n  function init(wrap) {\r\n    var SR = 44100;\r\n\r\n    \/\/ ---------- utilities ----------\r\n    function $(id) { return document.getElementById(id); }\r\n    function on(el, ev, fn) { if (el) { el.addEventListener(ev, fn); } }\r\n    function show(el, yes) { if (el) { el.style.display = yes ? \"\" : \"none\"; } }\r\n    function ce(tag, cls, txt) {\r\n      var el = document.createElement(tag);\r\n      if (cls) { el.className = cls; }\r\n      if (txt !== undefined && txt !== null) { el.textContent = txt; }\r\n      return el;\r\n    }\r\n    var toastEl = $(\"swd-toast\");\r\n    var toastTimer = null;\r\n    function toast(msg) {\r\n      if (!toastEl) { return; }\r\n      toastEl.textContent = msg;\r\n      toastEl.classList.add(\"swd-toast-on\");\r\n      if (toastTimer) { clearTimeout(toastTimer); }\r\n      toastTimer = setTimeout(function () { toastEl.classList.remove(\"swd-toast-on\"); }, 3600);\r\n    }\r\n    function pad(num, len) { var s = String(num); while (s.length < len) { s = \"0\" + s; } return s; }\r\n    function fmtTime(sec, sep) {\r\n      var ms = Math.round(sec * 1000);\r\n      var h = Math.floor(ms \/ 3600000); ms -= h * 3600000;\r\n      var m = Math.floor(ms \/ 60000); ms -= m * 60000;\r\n      var s = Math.floor(ms \/ 1000); ms -= s * 1000;\r\n      return pad(h, 2) + \":\" + pad(m, 2) + \":\" + pad(s, 2) + sep + pad(ms, 3);\r\n    }\r\n    function fmtSbvTime(sec) {\r\n      var ms = Math.round(sec * 1000);\r\n      var h = Math.floor(ms \/ 3600000); ms -= h * 3600000;\r\n      var m = Math.floor(ms \/ 60000); ms -= m * 60000;\r\n      var s = Math.floor(ms \/ 1000); ms -= s * 1000;\r\n      return h + \":\" + pad(m, 2) + \":\" + pad(s, 2) + \".\" + pad(ms, 3);\r\n    }\r\n    function download(blob, name) {\r\n      var url = URL.createObjectURL(blob);\r\n      var a = document.createElement(\"a\");\r\n      a.href = url; a.download = name;\r\n      document.body.appendChild(a); a.click(); document.body.removeChild(a);\r\n      setTimeout(function () { URL.revokeObjectURL(url); }, 4000);\r\n    }\r\n    function lang() {\r\n      var parts = $(\"swd-target\").value.split(\"|\");\r\n      return { code: parts[0], name: parts[1], bcp: parts[2] };\r\n    }\r\n    function strToU8(str) {\r\n      var utf8 = unescape(encodeURIComponent(str));\r\n      var arr = new Uint8Array(utf8.length), i;\r\n      for (i = 0; i < utf8.length; i++) { arr[i] = utf8.charCodeAt(i); }\r\n      return arr;\r\n    }\r\n\r\n    \/\/ ---------- Universal Subtitle Parser (.sbv, .srt, .vtt, .sub, .ass) ----------\r\n    function parseTimecode(str) {\r\n      if (!str) { return NaN; }\r\n      var clean = str.trim().replace(\",\", \".\");\r\n      var parts = clean.split(\":\");\r\n      if (parts.length === 3) {\r\n        return parseInt(parts[0], 10) * 3600 + parseInt(parts[1], 10) * 60 + parseFloat(parts[2]);\r\n      }\r\n      if (parts.length === 2) {\r\n        return parseInt(parts[0], 10) * 60 + parseFloat(parts[1]);\r\n      }\r\n      return NaN;\r\n    }\r\n\r\n    function parseSubtitleText(rawText) {\r\n      var text = (rawText || \"\").replace(\/\\r\\n\/g, \"\\n\").replace(\/\\r\/g, \"\\n\").replace(\/^\\uFEFF\/, \"\");\r\n      var out = [];\r\n\r\n      \/\/ Check for ASS\/SSA format (\"Dialogue: ...\")\r\n      if (\/^Dialogue:\\s*\/m.test(text)) {\r\n        var lines = text.split(\"\\n\"), li;\r\n        for (li = 0; li < lines.length; li++) {\r\n          var line = lines[li].trim();\r\n          if (line.indexOf(\"Dialogue:\") === 0) {\r\n            var p = line.split(\",\");\r\n            if (p.length >= 10) {\r\n              var sAss = parseTimecode(p[1]), eAss = parseTimecode(p[2]);\r\n              var contentAss = p.slice(9).join(\",\").replace(\/\\{[^}]*\\}\/g, \"\").replace(\/\\\\N\/gi, \" \").trim();\r\n              if (!isNaN(sAss) && !isNaN(eAss) && contentAss) {\r\n                out.push({ start: sAss, end: Math.max(eAss, sAss + 0.3), src: contentAss, tr: \"\", voice: \"\", over: 0 });\r\n              }\r\n            }\r\n          }\r\n        }\r\n        if (out.length) { return out; }\r\n      }\r\n\r\n      \/\/ Standard blocks for .sbv, .srt, .vtt\r\n      var blocks = text.split(\/\\n\\s*\\n\/), b;\r\n      var arrowRe = \/(\\d{1,2}:\\d{2}:\\d{2}[.,]\\d{1,3}|\\d{1,2}:\\d{2}[.,]\\d{1,3})\\s*-->\\s*(\\d{1,2}:\\d{2}:\\d{2}[.,]\\d{1,3}|\\d{1,2}:\\d{2}[.,]\\d{1,3})\/;\r\n      var sbvRe = \/^(\\d{1,2}:\\d{2}:\\d{2}[.,]\\d{1,3}|\\d{1,2}:\\d{2}[.,]\\d{1,3})\\s*,\\s*(\\d{1,2}:\\d{2}:\\d{2}[.,]\\d{1,3}|\\d{1,2}:\\d{2}[.,]\\d{1,3})$\/;\r\n\r\n      for (b = 0; b < blocks.length; b++) {\r\n        var bLines = blocks[b].trim().split(\"\\n\");\r\n        if (!bLines.length) { continue; }\r\n        var tIdx = -1, s0 = NaN, e0 = NaN, k;\r\n\r\n        for (k = 0; k < Math.min(3, bLines.length); k++) {\r\n          var cand = bLines[k].trim();\r\n          var mArrow = arrowRe.exec(cand);\r\n          if (mArrow) {\r\n            s0 = parseTimecode(mArrow[1]);\r\n            e0 = parseTimecode(mArrow[2]);\r\n            tIdx = k;\r\n            break;\r\n          }\r\n          var mSbv = sbvRe.exec(cand);\r\n          if (mSbv) {\r\n            s0 = parseTimecode(mSbv[1]);\r\n            e0 = parseTimecode(mSbv[2]);\r\n            tIdx = k;\r\n            break;\r\n          }\r\n        }\r\n\r\n        if (tIdx >= 0 && !isNaN(s0) && !isNaN(e0)) {\r\n          var txtLines = [];\r\n          for (k = tIdx + 1; k < bLines.length; k++) {\r\n            var cleanLine = bLines[k].replace(\/<[^>]+>\/g, \"\").replace(\/\\{[^}]*\\}\/g, \"\").trim();\r\n            if (cleanLine) { txtLines.push(cleanLine); }\r\n          }\r\n          var joined = txtLines.join(\" \").trim();\r\n          if (joined) {\r\n            out.push({ start: s0, end: Math.max(e0, s0 + 0.3), src: joined, tr: \"\", voice: \"\", over: 0 });\r\n          }\r\n        }\r\n      }\r\n      return out;\r\n    }\r\n\r\n    \/\/ ---------- progress bars: done \/ total (pct%) - ETA left ----------\r\n    var progT0 = {};\r\n    function fmtEta(sec) {\r\n      if (!isFinite(sec) || sec < 0) { return \"\"; }\r\n      if (sec < 5) { return \"a few seconds\"; }\r\n      if (sec < 60) { return Math.round(sec) + \" s\"; }\r\n      var mins = Math.floor(sec \/ 60);\r\n      return mins + \" min \" + Math.round(sec - mins * 60) + \" s\";\r\n    }\r\n    function progStart(barId) {\r\n      progT0[barId] = Date.now();\r\n      var el = $(barId);\r\n      if (!el) { return; }\r\n      el.classList.add(\"swd-indet\");\r\n      var b = el.querySelector(\".swd-bar\"); if (b) { b.style.width = \"\"; }\r\n      show(el, true);\r\n    }\r\n    function progIndet(barId, hintId, text) {\r\n      var el = $(barId); if (!el) { return; }\r\n      el.classList.add(\"swd-indet\"); show(el, true);\r\n      if (hintId && $(hintId)) { $(hintId).textContent = text; }\r\n    }\r\n    function progSet(barId, hintId, done, total, label) {\r\n      var el = $(barId); if (!el) { return; }\r\n      var bar = el.querySelector(\".swd-bar\");\r\n      var pct = total > 0 ? Math.min(100, Math.round(done \/ total * 100)) : 0;\r\n      var secs = (Date.now() - (progT0[barId] || Date.now())) \/ 1000;\r\n      var eta = done > 0 ? secs \/ done * (total - done) : -1;\r\n      el.classList.remove(\"swd-indet\"); show(el, true);\r\n      bar.style.width = pct + \"%\";\r\n      var tail = (eta >= 0 && done < total) ? (\" - \" + tf(\"leftWord\", { t: fmtEta(eta) })) : \"\";\r\n      if (done >= total && total > 0) { tail = \" - \" + t(\"doneWord\"); }\r\n      $(hintId).textContent = (label ? label + \" \" : \"\") + tf(\"doneOf\", { d: done, t: total, p: pct }) + tail;\r\n    }\r\n    function progEnd(barId) { var el = $(barId); if (el) { show(el, false); } }\r\n\r\n    \/\/ ---------- state ----------\r\n    var st = { file: null, subFileName: \"\", isVideo: false, url: null, ac: null, buf: null, dur: 0,\r\n               segs: [], dubPcm: null, dubBlob: null, dubUrl: null, spoken: {},\r\n               clips: {}, abort: false, fatal: \"\", lastLimit: 0, partial: false, forceBrowser: false };\r\n    var videoEl = $(\"swd-video\");\r\n    var dubAudio = $(\"swd-dubaudio\");\r\n\r\n    function ac() {\r\n      if (!st.ac) { var C = window.AudioContext || window.webkitAudioContext; st.ac = new C(); }\r\n      return st.ac;\r\n    }\r\n\r\n    function getEffectiveDuration() {\r\n      if (st.dur && st.dur > 0) { return st.dur; }\r\n      var maxEnd = 0, i;\r\n      for (i = 0; i < st.segs.length; i++) {\r\n        if (st.segs[i].end > maxEnd) { maxEnd = st.segs[i].end; }\r\n      }\r\n      return maxEnd > 0 ? (maxEnd + 1.5) : 0;\r\n    }\r\n\r\n    \/\/ ---------- tabs ----------\r\n    var tabs = wrap.querySelectorAll(\".swd-tab\");\r\n    var panels = wrap.querySelectorAll(\".swd-panel\");\r\n    function selectTab(name) {\r\n      var i;\r\n      for (i = 0; i < tabs.length; i++) { tabs[i].classList.toggle(\"swd-tab-on\", tabs[i].getAttribute(\"data-tab\") === name); }\r\n      for (i = 0; i < panels.length; i++) { panels[i].classList.toggle(\"swd-panel-on\", panels[i].getAttribute(\"data-panel\") === name); }\r\n    }\r\n    (function () {\r\n      var i;\r\n      for (i = 0; i < tabs.length; i++) {\r\n        (function (tabEl) { on(tabEl, \"click\", function () { selectTab(tabEl.getAttribute(\"data-tab\")); }); })(tabs[i]);\r\n      }\r\n    })();\r\n    on($(\"swd-full\"), \"click\", function () {\r\n      var isWide = wrap.classList.toggle(\"swd-wide\");\r\n      $(\"swd-full\").textContent = isWide ? \"\\u2194 \" + t(\"stdWidth\") : \"\\u2194 \" + t(\"fullWidth\");\r\n    });\r\n\r\n    \/\/ ---------- UI language ----------\r\n    var UI_LANG_KEY = \"sict_vd_uilang\";\r\n    try { var sl = localStorage.getItem(UI_LANG_KEY); if (sl && I18N[sl]) { CUR_LANG = sl; } } catch (eL) {}\r\n    applyI18n(wrap);\r\n    on($(\"swd-ui-lang\"), \"change\", function () {\r\n      CUR_LANG = $(\"swd-ui-lang\").value || \"en\";\r\n      try { localStorage.setItem(UI_LANG_KEY, CUR_LANG); } catch (eL2) {}\r\n      applyI18n(wrap);\r\n      refreshVoices();\r\n      renderSegs();\r\n    });\r\n\r\n    \/\/ ---------- session \/ project ----------\r\n    var SETTING_IDS = [\"swd-target\", \"swd-tr-style\", \"swd-srclang\", \"swd-provider\", \"swd-voice\", \"swd-tts-model\", \"swd-duck\",\r\n                       \"swd-maxrate\", \"swd-gmodel\", \"swd-quick-sec\", \"swd-muteorig\", \"swd-hardmute\",\r\n                       \"swd-capmode\", \"swd-rate\", \"swd-pitch\"];\r\n    var SESSION_KEY = \"sict_vd_session_v1\", saveTimer = null;\r\n    var undoStack = [], redoStack = [];\r\n    function snapshot() {\r\n      undoStack.push(JSON.stringify(st.segs));\r\n      if (undoStack.length > 50) { undoStack.shift(); }\r\n      redoStack.length = 0;\r\n    }\r\n    function collectSettings() {\r\n      var o = {};\r\n      SETTING_IDS.forEach(function (id) { var el = $(id); if (el) { o[id] = (el.type === \"checkbox\") ? el.checked : el.value; } });\r\n      return o;\r\n    }\r\n    function normalizeSegs(arr) {\r\n      var list = arr.map(function (item) {\r\n        return { start: Number(item.start) || 0, end: Number(item.end) || 0,\r\n                 src: String(item.src || \"\"), tr: String(item.tr || \"\"),\r\n                 voice: String(item.voice || \"\"), over: Number(item.over) || 0 };\r\n      });\r\n      list.sort(function (x, y) { return x.start - y.start; });\r\n      return list;\r\n    }\r\n    function applySettings(o) {\r\n      SETTING_IDS.forEach(function (id) {\r\n        var el = $(id), ok = true;\r\n        if (!el || o[id] === undefined) { return; }\r\n        if (el.type === \"checkbox\") { el.checked = !!o[id]; return; }\r\n        if (el.tagName === \"SELECT\") { ok = Array.prototype.some.call(el.options, function (op) { return op.value === String(o[id]); }); }\r\n        if (ok) { el.value = o[id]; }\r\n      });\r\n      $(\"swd-duck-val\").textContent = $(\"swd-duck\").value;\r\n      $(\"swd-rate-val\").textContent = (parseInt($(\"swd-maxrate\").value, 10) \/ 100).toFixed(2);\r\n      $(\"swd-rate-out\").textContent = (parseInt($(\"swd-rate\").value, 10) \/ 100).toFixed(2);\r\n      $(\"swd-pitch-out\").textContent = (parseInt($(\"swd-pitch\").value, 10) \/ 100).toFixed(2);\r\n      show($(\"swd-keybox\"), $(\"swd-provider\").value !== \"browser\");\r\n      refreshVoices();\r\n    }\r\n    function scheduleSave() {\r\n      if (saveTimer) { clearTimeout(saveTimer); }\r\n      saveTimer = setTimeout(function () {\r\n        try {\r\n          if (!st.segs.length) { $(\"swd-saved\").textContent = \"\"; return; }\r\n          localStorage.setItem(SESSION_KEY, JSON.stringify({ segs: st.segs, settings: collectSettings(), file: st.file ? st.file.name : st.subFileName }));\r\n          var now = new Date();\r\n          $(\"swd-saved\").textContent = \" Auto-saved \" + pad(now.getHours(), 2) + \":\" + pad(now.getMinutes(), 2);\r\n        } catch (e6) { \/* storage may be full or blocked *\/ }\r\n      }, 500);\r\n    }\r\n    function restoreSession() {\r\n      try {\r\n        var raw = localStorage.getItem(SESSION_KEY);\r\n        if (!raw) { toast(t(\"msgRestoreNone\")); return; }\r\n        var saved = JSON.parse(raw);\r\n        if (!saved || !Array.isArray(saved.segs) || !saved.segs.length) { toast(t(\"msgRestoreNone\")); return; }\r\n        st.segs = normalizeSegs(saved.segs);\r\n        if (saved.settings) { applySettings(saved.settings); }\r\n        renderSegs();\r\n        toast(tf(\"msgSessionRestored\", { n: st.segs.length }));\r\n      } catch (e7) { toast(t(\"msgRestoreNone\")); }\r\n    }\r\n    SETTING_IDS.forEach(function (id) { on($(id), \"change\", scheduleSave); });\r\n    on($(\"swd-save-proj\"), \"click\", function () {\r\n      if (!st.segs.length) { toast(t(\"msgNoSegs\")); return; }\r\n      var data = JSON.stringify({ app: \"video-dub-studio\", version: 5, segs: st.segs, settings: collectSettings() });\r\n      download(new Blob([data], { type: \"application\/json\" }), \"video-dub-project.json\");\r\n    });\r\n    on($(\"swd-load-proj\"), \"change\", function (ev) {\r\n      var f = ev.target.files && ev.target.files[0];\r\n      if (!f) { return; }\r\n      var reader = new FileReader();\r\n      reader.onload = function (e2) {\r\n        try {\r\n          var data = JSON.parse(e2.target.result);\r\n          if (!data || !Array.isArray(data.segs)) { throw new Error(\"bad project\"); }\r\n          snapshot();\r\n          st.segs = normalizeSegs(data.segs);\r\n          if (data.settings) { applySettings(data.settings); }\r\n          renderSegs(); selectTab(\"tr\");\r\n          toast(t(\"msgProjLoaded\"));\r\n        } catch (err) { toast(t(\"msgProjBad\")); }\r\n      };\r\n      reader.readAsText(f);\r\n      $(\"swd-load-proj\").value = \"\";\r\n    });\r\n    on($(\"swd-restore\"), \"click\", restoreSession);\r\n\r\n    function clearAll() {\r\n      snapshot();\r\n      st.segs = []; st.buf = null; st.file = null; st.subFileName = \"\"; st.dur = 0; st.dubPcm = null; st.dubBlob = null;\r\n      st.clips = {}; st.partial = false; st.forceBrowser = false;\r\n      try { localStorage.removeItem(SESSION_KEY); } catch (e8) { \/* ignore *\/ }\r\n      videoEl.removeAttribute(\"src\"); show(videoEl, false);\r\n      var v2 = $(\"swd-video2\"); if (v2) { v2.removeAttribute(\"src\"); show(v2, false); }\r\n      dubAudio.removeAttribute(\"src\"); show($(\"swd-dubplayer-wrap\"), false);\r\n      $(\"swd-info\").textContent = \"\"; $(\"swd-subinfo\").textContent = \"\";\r\n      $(\"swd-file\").value = \"\"; if ($(\"swd-subfile\")) { $(\"swd-subfile\").value = \"\"; }\r\n      renderSegs();\r\n      toast(t(\"msgCleared\"));\r\n    }\r\n    on($(\"swd-clear\"), \"click\", clearAll);\r\n    on($(\"swd-clear-tr-top\"), \"click\", clearTranslations);\r\n    function clearTranslations() {\r\n      snapshot();\r\n      for (var ci = 0; ci < st.segs.length; ci++) { st.segs[ci].tr = \"\"; st.segs[ci].over = 0; }\r\n      st.dubBlob = null; st.clips = {}; show($(\"swd-dubplayer-wrap\"), false); renderSegs();\r\n      toast(t(\"msgClearTr\"));\r\n    }\r\n    on($(\"swd-cleartr\"), \"click\", clearTranslations);\r\n\r\n    \/\/ undo \/ redo\r\n    on($(\"swd-undo\"), \"click\", function () {\r\n      if (!undoStack.length) { return; }\r\n      redoStack.push(JSON.stringify(st.segs));\r\n      st.segs = normalizeSegs(JSON.parse(undoStack.pop()));\r\n      renderSegs();\r\n    });\r\n    on($(\"swd-redo\"), \"click\", function () {\r\n      if (!redoStack.length) { return; }\r\n      undoStack.push(JSON.stringify(st.segs));\r\n      st.segs = normalizeSegs(JSON.parse(redoStack.pop()));\r\n      renderSegs();\r\n    });\r\n\r\n    \/\/ ---------- audio helpers ----------\r\n    function resampleLinear(inp, inRate, outRate) {\r\n      var ratio = inRate \/ outRate;\r\n      var outLen = Math.max(1, Math.floor(inp.length \/ ratio));\r\n      var out = new Float32Array(outLen);\r\n      var i, pos, i0, frac, a, b;\r\n      for (i = 0; i < outLen; i++) {\r\n        pos = i * ratio; i0 = Math.floor(pos); frac = pos - i0;\r\n        a = inp[i0] || 0; b = inp[i0 + 1] || a;\r\n        out[i] = a + (b - a) * frac;\r\n      }\r\n      return out;\r\n    }\r\n    function toMono(buffer, outRate) {\r\n      var len = buffer.length, ch = buffer.numberOfChannels, mono = new Float32Array(len), c, i, d;\r\n      for (c = 0; c < ch; c++) {\r\n        d = buffer.getChannelData(c);\r\n        for (i = 0; i < len; i++) { mono[i] += d[i] \/ ch; }\r\n      }\r\n      return buffer.sampleRate === outRate ? mono : resampleLinear(mono, buffer.sampleRate, outRate);\r\n    }\r\n    function speedUp(arr, rate) { return rate <= 1.001 ? arr : resampleLinear(arr, rate, 1); }\r\n    function writeStr(dv, off, s) { var i; for (i = 0; i < s.length; i++) { dv.setUint8(off + i, s.charCodeAt(i)); } }\r\n    function encodeWAV(mono, rate) {\r\n      var n = mono.length, out = new ArrayBuffer(44 + n * 2), dv = new DataView(out), i, x;\r\n      writeStr(dv, 0, \"RIFF\"); dv.setUint32(4, 36 + n * 2, true); writeStr(dv, 8, \"WAVE\"); writeStr(dv, 12, \"fmt \");\r\n      dv.setUint32(16, 16, true); dv.setUint16(20, 1, true); dv.setUint16(22, 1, true);\r\n      dv.setUint32(24, rate, true); dv.setUint32(28, rate * 2, true); dv.setUint16(32, 2, true); dv.setUint16(34, 16, true);\r\n      writeStr(dv, 36, \"data\"); dv.setUint32(40, n * 2, true);\r\n      for (i = 0; i < n; i++) { x = Math.max(-1, Math.min(1, mono[i])); dv.setInt16(44 + i * 2, x < 0 ? x * 32768 : x * 32767, true); }\r\n      return new Blob([out], { type: \"audio\/wav\" });\r\n    }\r\n    function decodeBytes(arrayBuffer) { return ac().decodeAudioData(arrayBuffer.slice(0)); }\r\n\r\n    \/\/ ---------- FFmpeg ----------\r\n    var ffmpeg = null, ffLoaded = false;\r\n    function loadFFmpegLib() {\r\n      if (window.FFmpeg && window.FFmpeg.createFFmpeg) { return Promise.resolve(); }\r\n      return new Promise(function (resolve, reject) {\r\n        var s = document.createElement(\"script\");\r\n        s.src = \"https:\/\/unpkg.com\/@ffmpeg\/ffmpeg@0.11.6\/dist\/ffmpeg.min.js\";\r\n        s.onload = function () { resolve(); };\r\n        s.onerror = function () { reject(new Error(\"Could not load the video engine.\")); };\r\n        document.head.appendChild(s);\r\n      });\r\n    }\r\n    function getFFmpeg() {\r\n      if (ffLoaded && ffmpeg) { return Promise.resolve(ffmpeg); }\r\n      return loadFFmpegLib().then(function () {\r\n        ffmpeg = window.FFmpeg.createFFmpeg({ log: false, mainName: \"main\",\r\n          corePath: \"https:\/\/unpkg.com\/@ffmpeg\/core-st@0.11.1\/dist\/ffmpeg-core.js\" });\r\n        return ffmpeg.load();\r\n      }).then(function () { ffLoaded = true; return ffmpeg; });\r\n    }\r\n    function fileToU8(file) { return file.arrayBuffer().then(function (ab) { return new Uint8Array(ab); }); }\r\n    function extractAudioFFmpeg(file) {\r\n      return getFFmpeg().then(function (ff) {\r\n        return fileToU8(file).then(function (u8) {\r\n          ff.FS(\"writeFile\", \"in_media\", u8);\r\n          return ff.run(\"-i\", \"in_media\", \"-vn\", \"-ac\", \"2\", \"-ar\", \"44100\", \"out_extract.wav\");\r\n        }).then(function () { return decodeBytes(ff.FS(\"readFile\", \"out_extract.wav\").buffer); });\r\n      });\r\n    }\r\n\r\n    \/\/ ---------- Tab 1: Video\/Audio File & Direct Subtitle File (.sbv\/.srt\/.vtt) ----------\r\n    function setMedia(buf) {\r\n      st.buf = buf; st.dur = buf.duration;\r\n      $(\"swd-info\").textContent = (st.file ? st.file.name : \"Media\") + \" - \" + st.dur.toFixed(1) + \" s - ready.\";\r\n    }\r\n    function handleFile(file) {\r\n      if (!file) { return; }\r\n      st.file = file; st.isVideo = (file.type || \"\").indexOf(\"video\") === 0;\r\n      if (st.url) { URL.revokeObjectURL(st.url); }\r\n      st.url = URL.createObjectURL(file);\r\n      videoEl.src = st.url; show(videoEl, true);\r\n      var v2 = $(\"swd-video2\"); v2.src = st.url; show(v2, true);\r\n      $(\"swd-info\").textContent = \"Reading audio...\";\r\n      file.arrayBuffer().then(decodeBytes).then(setMedia).catch(function () {\r\n        $(\"swd-info\").textContent = \"Extracting audio with FFmpeg (first use downloads the engine)...\";\r\n        return extractAudioFFmpeg(file).then(setMedia);\r\n      }).catch(function (err) {\r\n        $(\"swd-info\").textContent = t(\"msgBadFile\");\r\n        if (window.console) { console.error(err); }\r\n      });\r\n    }\r\n    on($(\"swd-file\"), \"change\", function () { handleFile($(\"swd-file\").files && $(\"swd-file\").files[0]); });\r\n\r\n    \/\/ Direct Subtitle Upload Handler (.sbv, .srt, .vtt, .sub, .ass)\r\n    on($(\"swd-subfile\"), \"change\", function (ev) {\r\n      var f = ev.target.files && ev.target.files[0];\r\n      if (!f) { return; }\r\n      var reader = new FileReader();\r\n      reader.onload = function (e2) {\r\n        var parsed = parseSubtitleText(String(e2.target.result || \"\"));\r\n        if (!parsed.length) {\r\n          $(\"swd-subinfo\").textContent = t(\"msgSubBad\");\r\n          toast(t(\"msgSubBad\"));\r\n          return;\r\n        }\r\n        snapshot();\r\n        st.subFileName = f.name;\r\n        st.segs = normalizeSegs(parsed);\r\n        st.dubBlob = null; st.partial = false;\r\n        show($(\"swd-dubplayer-wrap\"), false);\r\n        var effDur = getEffectiveDuration().toFixed(1);\r\n        $(\"swd-subinfo\").textContent = f.name + \" - \" + st.segs.length + \" segments (\" + effDur + \"s)\";\r\n        renderSegs();\r\n        toast(tf(\"msgSubLoaded\", { n: st.segs.length, d: effDur }));\r\n        selectTab(\"tr\");\r\n      };\r\n      reader.readAsText(f);\r\n      $(\"swd-subfile\").value = \"\";\r\n    });\r\n\r\n    \/\/ ---------- API keys ----------\r\n    var KEY_STORE = \"sict_vd_keys_v1\";\r\n    function keyFor(provider) {\r\n      var id = provider === \"openai\" ? \"swd-okey\" : (provider === \"elevenlabs\" ? \"swd-ekey\" : \"swd-gkey\");\r\n      return ($(id).value || \"\").trim();\r\n    }\r\n    function saveKeys() {\r\n      try {\r\n        if ($(\"swd-remember\").checked) {\r\n          localStorage.setItem(KEY_STORE, JSON.stringify({ g: $(\"swd-gkey\").value, o: $(\"swd-okey\").value, e: $(\"swd-ekey\").value }));\r\n        } else { localStorage.removeItem(KEY_STORE); }\r\n      } catch (e4) { \/* storage may be blocked *\/ }\r\n    }\r\n    (function () {\r\n      try {\r\n        var raw = localStorage.getItem(KEY_STORE);\r\n        if (raw) {\r\n          var saved = JSON.parse(raw);\r\n          $(\"swd-gkey\").value = saved.g || \"\"; $(\"swd-okey\").value = saved.o || \"\"; $(\"swd-ekey\").value = saved.e || \"\";\r\n          $(\"swd-remember\").checked = true;\r\n        }\r\n      } catch (e5) { \/* ignore *\/ }\r\n    })();\r\n    [\"swd-gkey\", \"swd-okey\", \"swd-ekey\"].forEach(function (kid) { on($(kid), \"input\", saveKeys); });\r\n    on($(\"swd-remember\"), \"change\", saveKeys);\r\n\r\n    \/\/ ---------- Whisper ----------\r\n    var tfMod = null, asrPipe = null, asrName = \"\";\r\n    function loadTransformers() {\r\n      if (tfMod) { return Promise.resolve(tfMod); }\r\n      return import(\"https:\/\/cdn.jsdelivr.net\/npm\/@huggingface\/transformers@3.0.0\").then(function (mod) {\r\n        tfMod = mod; if (mod.env) { mod.env.allowLocalModels = false; } return mod;\r\n      });\r\n    }\r\n    function getPipe(name, onDl) {\r\n      if (asrPipe && asrName === name) { return Promise.resolve(asrPipe); }\r\n      return loadTransformers().then(function (mod) { return mod.pipeline(\"automatic-speech-recognition\", name, { progress_callback: onDl }); })\r\n        .then(function (p) { asrPipe = p; asrName = name; return p; });\r\n    }\r\n    on($(\"swd-transcribe\"), \"click\", function () {\r\n      if (!st.buf) { toast(t(\"msgLoadFirst\")); return; }\r\n      var model = $(\"swd-model\").value, srcLang = $(\"swd-srclang\").value, opts = { return_timestamps: true };\r\n      if (model.indexOf(\".en\") < 0) { opts.task = \"transcribe\"; if (srcLang) { opts.language = srcLang; } }\r\n      var audio = toMono(st.buf, 16000), WINSEC = 30, WIN = WINSEC * 16000, total = Math.max(1, Math.ceil(audio.length \/ WIN));\r\n      var files = {}, newSegs = [];\r\n      function onDl(info) {\r\n        var kf, loaded = 0, size = 0;\r\n        if (!info || info.status !== \"progress\") { return; }\r\n        files[info.file] = { l: info.loaded || 0, t: info.total || 0 };\r\n        for (kf in files) { loaded += files[kf].l; size += files[kf].t; }\r\n        if (size > 0) { progSet(\"swd-tr-prog\", \"swd-tr-hint\", Math.round(loaded \/ size * 100), 100, \"Downloading model\"); }\r\n      }\r\n      $(\"swd-transcribe\").disabled = true; progStart(\"swd-tr-prog\"); $(\"swd-tr-hint\").textContent = \"Loading the speech model...\";\r\n      getPipe(model, onDl).then(function (pipe) {\r\n        progStart(\"swd-tr-prog\");\r\n        var chain = Promise.resolve(), w;\r\n        for (w = 0; w < total; w++) {\r\n          (function (wi) {\r\n            chain = chain.then(function () {\r\n              var off = wi * WIN, slice = audio.subarray(off, Math.min(audio.length, off + WIN)), base = wi * WINSEC;\r\n              if (slice.length < 8000) { return null; }\r\n              return pipe(slice, opts).then(function (out) {\r\n                var chunks = (out && out.chunks) ? out.chunks : [];\r\n                chunks.forEach(function (ck) {\r\n                  var ts = ck.timestamp || [0, null], txt = (ck.text || \"\").trim();\r\n                  if (!txt) { return; }\r\n                  var s0 = base + (ts[0] || 0), e0 = (ts[1] === null || ts[1] === undefined) ? Math.min(base + WINSEC, st.dur) : base + ts[1];\r\n                  newSegs.push({ start: s0, end: Math.max(e0, s0 + 0.3), src: txt, tr: \"\", voice: \"\", over: 0 });\r\n                });\r\n                progSet(\"swd-tr-prog\", \"swd-tr-hint\", wi + 1, total, \"Transcribed part\");\r\n              });\r\n            });\r\n          })(w);\r\n        }\r\n        return chain;\r\n      }).then(function () {\r\n        st.segs = newSegs; st.dubBlob = null; st.partial = false;\r\n        $(\"swd-transcribe\").disabled = false; renderSegs(); progEnd(\"swd-tr-prog\");\r\n        $(\"swd-tr-hint\").textContent = \"Done - \" + st.segs.length + \" segments.\";\r\n        toast(tf(\"msgTranscribed\", { n: st.segs.length }));\r\n        selectTab(\"tr\");\r\n      }).catch(function (err) {\r\n        $(\"swd-transcribe\").disabled = false; progEnd(\"swd-tr-prog\");\r\n        $(\"swd-tr-hint\").textContent = \"Transcription failed. Check your connection and try again.\";\r\n        if (window.console) { console.error(\"ASR error\", err); }\r\n        toast(t(\"msgTransFail\"));\r\n      });\r\n    });\r\n\r\n    \/\/ ---------- Tab 2: segment editor ----------\r\n    function sortSegs() { st.segs.sort(function (x, y) { return x.start - y.start; }); }\r\n    function splitText(str, ratio) {\r\n      var text = str || \"\", target = Math.floor(text.length * ratio), best = -1, dist = 1e9, i;\r\n      for (i = 0; i < text.length; i++) {\r\n        if (text.charAt(i) === \" \" && Math.abs(i - target) < dist) { dist = Math.abs(i - target); best = i; }\r\n      }\r\n      if (best < 0) { return [text, \"\"]; }\r\n      return [text.slice(0, best).trim(), text.slice(best + 1).trim()];\r\n    }\r\n\r\n    \/\/ Auto-Merge by Punctuation\r\n    on($(\"swd-optimize\"), \"click\", function () {\r\n      if (!st.segs.length) return;\r\n      snapshot();\r\n      var i = 0, count = 0;\r\n      while (i < st.segs.length - 1) {\r\n         var s = (st.segs[i].src || \"\").trim();\r\n         if (s && !\/[.?!]$\/.test(s)) {\r\n            st.segs[i].end = Math.max(st.segs[i].end, st.segs[i+1].end);\r\n            st.segs[i].src = s + \" \" + (st.segs[i+1].src || \"\").trim();\r\n            st.segs[i].tr = \"\";\r\n            st.segs[i].over = 0;\r\n            st.segs.splice(i+1, 1);\r\n            count++;\r\n         } else {\r\n            i++;\r\n         }\r\n      }\r\n      renderSegs();\r\n      toast(count > 0 ? tf(\"msgMergedOpt\", {n: count}) : t(\"msgNoOptNeeded\"));\r\n    });\r\n\r\n    var segTimer = null;\r\n    function playSeg(sg) {\r\n      if (st.file) {\r\n        var vid = $(\"swd-video2\");\r\n        $(\"swd-live\").checked = true; applyVolume(); st.spoken = {};\r\n        vid.currentTime = Math.max(0, sg.start - 0.05); vid.play();\r\n        if (segTimer) { clearTimeout(segTimer); }\r\n        segTimer = setTimeout(function () { vid.pause(); }, Math.max(300, (sg.end - sg.start) * 1000));\r\n        return;\r\n      }\r\n      \/\/ If subtitle-only (no video loaded), speak the segment with browser TTS or play built dub audio slice\r\n      if (st.dubBlob && dubAudio.src) {\r\n        dubAudio.currentTime = Math.max(0, sg.start);\r\n        dubAudio.play();\r\n        if (segTimer) { clearTimeout(segTimer); }\r\n        segTimer = setTimeout(function () { dubAudio.pause(); }, Math.max(300, (sg.end - sg.start) * 1000));\r\n        return;\r\n      }\r\n      if (window.speechSynthesis && (sg.tr || sg.src)) {\r\n        window.speechSynthesis.cancel();\r\n        speakSeg({ start: sg.start, end: sg.end, tr: sg.tr || sg.src });\r\n      } else {\r\n        toast(t(\"msgNoFile\"));\r\n      }\r\n    }\r\n\r\n    function renderSegs() {\r\n      var box = $(\"swd-segs\"), i, query = ($(\"swd-segsearch\") && $(\"swd-segsearch\").value || \"\").toLowerCase();\r\n      while (box.firstChild) { box.removeChild(box.firstChild); }\r\n      var shown = 0, translated = 0;\r\n      for (i = 0; i < st.segs.length; i++) {\r\n        if (st.segs[i].tr && st.segs[i].tr.trim()) { translated++; }\r\n        if (query && (st.segs[i].src || \"\").toLowerCase().indexOf(query) < 0 && (st.segs[i].tr || \"\").toLowerCase().indexOf(query) < 0) { continue; }\r\n        shown++;\r\n        (function (idx) {\r\n          var sg = st.segs[idx], row = ce(\"div\", \"swd-seg\"), head = ce(\"div\", \"swd-seg-t\");\r\n          head.appendChild(ce(\"span\", \"\", \"#\" + (idx + 1) + \" \"));\r\n          var sInp = ce(\"input\", \"swd-inp swd-inp-sm\"); sInp.type = \"number\"; sInp.step = \"0.05\"; sInp.min = \"0\"; sInp.value = sg.start.toFixed(2);\r\n          var eInp = ce(\"input\", \"swd-inp swd-inp-sm\"); eInp.type = \"number\"; eInp.step = \"0.05\"; eInp.min = \"0\"; eInp.value = sg.end.toFixed(2);\r\n          var vInp = ce(\"input\", \"swd-inp swd-inp-sm\"); vInp.style.width = \"110px\"; vInp.placeholder = \"Custom voice\"; vInp.value = sg.voice || \"\";\r\n          sInp.setAttribute(\"aria-label\", \"Start seconds, segment \" + (idx + 1));\r\n          eInp.setAttribute(\"aria-label\", \"End seconds, segment \" + (idx + 1));\r\n          vInp.setAttribute(\"aria-label\", \"Custom voice, segment \" + (idx + 1));\r\n          var playBtn = ce(\"button\", \"swd-btn swd-ghost swd-btn-sm\", \"Play\");\r\n          var splBtn = ce(\"button\", \"swd-btn swd-ghost swd-btn-sm\", \"Split\");\r\n          var mrgBtn = ce(\"button\", \"swd-btn swd-ghost swd-btn-sm\", \"Merge\");\r\n          var delBtn = ce(\"button\", \"swd-btn swd-ghost swd-btn-sm\", \"Del\");\r\n          playBtn.type = \"button\"; splBtn.type = \"button\"; mrgBtn.type = \"button\"; delBtn.type = \"button\";\r\n          on(sInp, \"change\", function () {\r\n            snapshot();\r\n            sg.start = Math.max(0, parseFloat(sInp.value) || 0);\r\n            if (sg.end <= sg.start) { sg.end = sg.start + 0.3; }\r\n            sortSegs(); renderSegs();\r\n          });\r\n          on(eInp, \"change\", function () {\r\n            snapshot();\r\n            sg.end = Math.max(sg.start + 0.1, parseFloat(eInp.value) || 0);\r\n            eInp.value = sg.end.toFixed(2); scheduleSave();\r\n          });\r\n          on(vInp, \"change\", function () { sg.voice = vInp.value; scheduleSave(); });\r\n          on(playBtn, \"click\", function () { playSeg(sg); });\r\n          on(splBtn, \"click\", function () {\r\n            snapshot();\r\n            var mid = sg.start + (sg.end - sg.start) \/ 2, a1 = splitText(sg.src, 0.5), b1 = splitText(sg.tr, 0.5);\r\n            var newSg = { start: mid, end: sg.end, src: a1[1], tr: b1[1], voice: sg.voice, over: 0 };\r\n            sg.end = mid; sg.src = a1[0]; sg.tr = b1[0];\r\n            st.segs.splice(idx + 1, 0, newSg); renderSegs();\r\n          });\r\n\r\n          \/\/ Smart Merge Button\r\n          on(mrgBtn, \"click\", function () {\r\n            var nx = st.segs[idx + 1];\r\n            if (!nx) { toast(t(\"msgLastSeg\") || \"This is the last segment.\"); return; }\r\n            snapshot();\r\n            sg.end = Math.max(sg.end, nx.end);\r\n            sg.src = (sg.src + \" \" + nx.src).trim();\r\n            sg.tr = \"\";\r\n            sg.over = 0;\r\n            st.segs.splice(idx + 1, 1);\r\n            renderSegs();\r\n            toast(t(\"msgMergedClear\") || \"Merged. Click Translate to translate the new combined segment.\");\r\n          });\r\n\r\n          on(delBtn, \"click\", function () { snapshot(); st.segs.splice(idx, 1); renderSegs(); });\r\n          head.appendChild(sInp); head.appendChild(ce(\"span\", \"\", \"-\")); head.appendChild(eInp);\r\n          head.appendChild(vInp); head.appendChild(playBtn); head.appendChild(splBtn); head.appendChild(mrgBtn); head.appendChild(delBtn);\r\n          row.appendChild(head);\r\n          if (sg.over > 0) { row.appendChild(ce(\"div\", \"swd-warn\", \"Voice is \" + sg.over.toFixed(2) + \"x longer than its slot. Shorten this line or widen the time.\")); }\r\n          row.appendChild(ce(\"div\", \"swd-seg-src\", sg.src));\r\n          var ta = ce(\"textarea\", \"swd-inp swd-ta\");\r\n          ta.rows = 2; ta.value = sg.tr; ta.setAttribute(\"aria-label\", \"Translation for segment \" + (idx + 1));\r\n          ta.placeholder = \"Translation appears here (you can type or fix it)\";\r\n          on(ta, \"input\", function () { sg.tr = ta.value; sg.over = 0; scheduleSave(); });\r\n          row.appendChild(ta); box.appendChild(row);\r\n        })(i);\r\n      }\r\n      $(\"swd-segcount\").textContent = st.segs.length + \" \" + t(\"segsWord\") + (shown !== st.segs.length ? (\" (\" + shown + \" shown)\") : \"\");\r\n      var pend = st.segs.length - translated;\r\n      $(\"swd-segstat\").innerHTML = \"\";\r\n      if (pend > 0) { $(\"swd-segstat\").appendChild(ce(\"span\", \"swd-badge-count\", tf(\"undoneBadge\", { n: pend }))); }\r\n      scheduleSave();\r\n    }\r\n    on($(\"swd-segsearch\"), \"input\", renderSegs);\r\n\r\n    \/\/ ---------- Context-Aware Real Meaning Translation ----------\r\n    function geminiTranslateBatch(itemsWithCtx, key, model, langName, styleMode) {\r\n      var styleInstruction = \"Translate by capturing the TRUE MEANING, idioms, cultural nuances, and natural spoken flow in \" + langName + \" (never do awkward word-for-word literal translation). \";\r\n      if (styleMode === \"story\") {\r\n        styleInstruction += \"Use an expressive, immersive storytelling and narrative voice that sounds natural when spoken aloud. \";\r\n      } else if (styleMode === \"educational\") {\r\n        styleInstruction += \"Use a clear, engaging educational tone that explains concepts naturally while preserving technical accuracy. \";\r\n      } else if (styleMode === \"concise\") {\r\n        styleInstruction += \"Keep phrases compact and punchy so the spoken dub fits comfortably inside each time slot Duration_Sec. \";\r\n      } else {\r\n        styleInstruction += \"Use conversational, everyday spoken \" + langName + \" suitable for professional voice dubbing, matching the approximate speaking duration (Duration_Sec). \";\r\n      }\r\n\r\n      var prompt = \"You are an expert localization & dubbing translator for \" + langName + \".\\n\" +\r\n        styleInstruction + \"\\n\" +\r\n        \"Each item in the input JSON array has:\\n\" +\r\n        \"- 'line': the subtitle segment to translate\\n\" +\r\n        \"- 'prev_context': the immediately preceding line (for context\/pronouns\/continuity only - do NOT include in output)\\n\" +\r\n        \"- 'next_context': the immediately following line (for context only - do NOT include in output)\\n\" +\r\n        \"- 'duration_sec': time slot in seconds\\n\\n\" +\r\n        \"Return ONLY a JSON array of strings containing the translated 'line' for each item, withlarla EXACTLY \" + itemsWithCtx.length + \" strings in the exact same order.\\n\" +\r\n        JSON.stringify(itemsWithCtx);\r\n\r\n      var url = \"https:\/\/generativelanguage.googleapis.com\/v1beta\/models\/\" + encodeURIComponent(model) + \":generateContent?key=\" + encodeURIComponent(key);\r\n\r\n      var generationConfig = {\r\n        responseMimeType: \"application\/json\",\r\n        responseSchema: { type: \"ARRAY\", items: { type: \"STRING\" } }\r\n      };\r\n\r\n      return fetch(url, { method: \"POST\", headers: { \"Content-Type\": \"application\/json\" },\r\n        body: JSON.stringify({ contents: [{ parts: [{ text: prompt }] }], generationConfig: generationConfig }) })\r\n        .then(readJson).then(function (j) {\r\n          var part = j.candidates && j.candidates[0] && j.candidates[0].content && j.candidates[0].content.parts[0];\r\n          var raw = part && part.text ? part.text : \"\";\r\n          var a = raw.indexOf(\"[\"), b = raw.lastIndexOf(\"]\");\r\n          var arr = JSON.parse(raw.slice(a, b + 1));\r\n          if (!arr || arr.length !== itemsWithCtx.length) { throw new Error(\"Translation count mismatch.\"); }\r\n          return arr;\r\n        });\r\n    }\r\n    function readJson(r) {\r\n      if (!r.ok) { return r.text().then(function (t2) { throw new Error(t2 || (\"HTTP \" + r.status)); }); }\r\n      return r.json();\r\n    }\r\n    on($(\"swd-translate\"), \"click\", function () {\r\n      var key = ($(\"swd-gkey\").value || \"\").trim(), model = ($(\"swd-gmodel\").value || \"\").trim() || \"gemini-2.5-flash\";\r\n      var styleMode = $(\"swd-tr-style\") ? $(\"swd-tr-style\").value : \"meaning\";\r\n      if (!st.segs.length) { toast(t(\"msgNeedTrans\")); return; }\r\n      if (!key) { toast(t(\"msgNeedKey\")); return; }\r\n\r\n      var emptyIdxs = [];\r\n      for (var i = 0; i < st.segs.length; i++) {\r\n          if (!(st.segs[i].tr || \"\").trim()) { emptyIdxs.push(i); }\r\n      }\r\n      if (!emptyIdxs.length) { toast(t(\"msgAllTranslated\")); return; }\r\n\r\n      snapshot();\r\n      var L = lang(), BATCH = 15, chain = Promise.resolve(), done = 0;\r\n      $(\"swd-translate\").disabled = true; progStart(\"swd-tl-prog\"); $(\"swd-tl-hint\").textContent = \"Translating real meaning with context...\";\r\n\r\n      var _loop_1 = function (from) {\r\n          chain = chain.then(function () {\r\n             var chunkIdxs = emptyIdxs.slice(from, from + BATCH);\r\n             var itemsWithCtx = chunkIdxs.map(function(k) {\r\n                var cur = st.segs[k];\r\n                var prev = k > 0 ? st.segs[k - 1].src : \"\";\r\n                var next = k + 1 < st.segs.length ? st.segs[k + 1].src : \"\";\r\n                return {\r\n                  line: cur.src,\r\n                  prev_context: prev,\r\n                  next_context: next,\r\n                  duration_sec: Number(Math.max(0.5, cur.end - cur.start).toFixed(2))\r\n                };\r\n             });\r\n             return geminiTranslateBatch(itemsWithCtx, key, model, L.name, styleMode).then(function (arr) {\r\n                arr.forEach(function (t2, k) { st.segs[chunkIdxs[k]].tr = String(t2); });\r\n                done += chunkIdxs.length;\r\n                progSet(\"swd-tl-prog\", \"swd-tl-hint\", done, emptyIdxs.length, \"Translated\");\r\n                renderSegs();\r\n             });\r\n          });\r\n      };\r\n\r\n      for (var from = 0; from < emptyIdxs.length; from += BATCH) {\r\n          _loop_1(from);\r\n      }\r\n\r\n      chain.then(function () {\r\n        $(\"swd-translate\").disabled = false;\r\n        progEnd(\"swd-tl-prog\"); $(\"swd-tl-hint\").textContent = \"Done. Review the translation, then go to Tab 3 to build the dub audio track.\";\r\n        toast(t(\"msgTranslateDone\"));\r\n      }).catch(function (err) {\r\n        $(\"swd-translate\").disabled = false;\r\n        progEnd(\"swd-tl-prog\");\r\n        if (window.console) { console.error(\"Translate error\", err); }\r\n        toast(tf(\"msgTranslateFail\", { m: (err && err.message ? err.message.slice(0, 110) : \"error\") }));\r\n      });\r\n    });\r\n\r\n    \/\/ Context-Aware Shorten\r\n    on($(\"swd-shorten\"), \"click\", function () {\r\n      var key = keyFor(\"gemini\"), model = ($(\"swd-gmodel\").value || \"\").trim() || \"gemini-2.5-flash\", idxs = [], i2;\r\n      for (i2 = 0; i2 < st.segs.length; i2++) { if (st.segs[i2].over > 0) { idxs.push(i2); } }\r\n      if (!idxs.length) { toast(t(\"msgShortenNoFlag\")); return; }\r\n      if (!key) { toast(t(\"msgNeedKey\")); return; }\r\n\r\n      var L = lang();\r\n      var pairs = idxs.map(function (k2) { return { source: st.segs[k2].src, translation_to_shorten: st.segs[k2].tr }; });\r\n      var prompt = \"Here is a JSON array of objects containing an original 'source' and its \" + L.name + \" 'translation_to_shorten'. For each object, rewrite the translation in \" + L.name + \" so it is about 70 percent as long, keeping the exact real meaning of the source in a natural spoken style. Return ONLY a JSON array of strings containing the shortened translations, with exactly \" + pairs.length + \" items in the same order.\\n\" + JSON.stringify(pairs);\r\n\r\n      var url = \"https:\/\/generativelanguage.googleapis.com\/v1beta\/models\/\" + encodeURIComponent(model) + \":generateContent?key=\" + encodeURIComponent(key);\r\n      $(\"swd-shorten\").disabled = true;\r\n      fetch(url, { method: \"POST\", headers: { \"Content-Type\": \"application\/json\" },\r\n        body: JSON.stringify({ contents: [{ parts: [{ text: prompt }] }], generationConfig: { responseMimeType: \"application\/json\", responseSchema: { type: \"ARRAY\", items: { type: \"STRING\" } } } }) })\r\n        .then(readJson).then(function (j) {\r\n          var part = j.candidates && j.candidates[0] && j.candidates[0].content && j.candidates[0].content.parts[0];\r\n          var raw = part && part.text ? part.text : \"\", arr = JSON.parse(raw.slice(raw.indexOf(\"[\"), raw.lastIndexOf(\"]\") + 1));\r\n          if (!arr || arr.length !== idxs.length) { throw new Error(\"Count mismatch.\"); }\r\n          snapshot();\r\n          arr.forEach(function (t2, k3) { st.segs[idxs[k3]].tr = String(t2); st.segs[idxs[k3]].over = 0; });\r\n          renderSegs(); toast(tf(\"msgShortenDone\", { n: idxs.length }));\r\n        }).catch(function (err) {\r\n          toast(tf(\"msgShortenFail\", { m: (err && err.message ? err.message.slice(0, 90) : \"error\") }));\r\n        }).then(function () { $(\"swd-shorten\").disabled = false; });\r\n    });\r\n    on($(\"swd-target\"), \"change\", refreshVoices);\r\n\r\n    \/\/ ---------- Voices ----------\r\n    function matchedVoices() {\r\n      var L = lang(), list = (window.speechSynthesis && window.speechSynthesis.getVoices()) || [], res = [], q;\r\n      for (q = 0; q < list.length; q++) { if ((list[q].lang || \"\").toLowerCase().indexOf(L.code) === 0) { res.push(list[q]); } }\r\n      return res;\r\n    }\r\n    function pickVoice() {\r\n      var all = matchedVoices(), want = $(\"swd-bvoice\").value, q;\r\n      for (q = 0; q < all.length; q++) { if (all[q].voiceURI === want) { return all[q]; } }\r\n      for (q = 0; q < all.length; q++) { if (\/natural|online\/i.test(all[q].name)) { return all[q]; } }\r\n      return all[0] || null;\r\n    }\r\n    function refreshVoices() {\r\n      var box = $(\"swd-voicestat\"), L = lang(), sel = $(\"swd-bvoice\"), keep = sel.value, hits, q, opt;\r\n      if (!window.speechSynthesis) { box.textContent = \"This browser has no built-in speech. Use a cloud engine or subtitles.\"; return; }\r\n      hits = matchedVoices();\r\n      while (sel.firstChild) { sel.removeChild(sel.firstChild); }\r\n      for (q = 0; q < hits.length; q++) { opt = ce(\"option\", \"\", hits[q].name); opt.value = hits[q].voiceURI; sel.appendChild(opt); }\r\n      if (keep) { sel.value = keep; }\r\n      show($(\"swd-bvbox\"), hits.length > 0);\r\n      box.textContent = hits.length ? (\"Browser voices found for \" + L.name + \": \" + hits.length + \".\")\r\n        : (\"No free browser voice for \" + L.name + \" on this device. Use Gemini \/ OpenAI \/ ElevenLabs TTS above to build the dubbed audio track.\");\r\n    }\r\n    if (window.speechSynthesis) { window.speechSynthesis.onvoiceschanged = refreshVoices; }\r\n    refreshVoices();\r\n    on($(\"swd-provider\"), \"change\", function () { show($(\"swd-keybox\"), $(\"swd-provider\").value !== \"browser\"); });\r\n    on($(\"swd-duck\"), \"input\", function () { $(\"swd-duck-val\").textContent = $(\"swd-duck\").value; applyVolume(); });\r\n    on($(\"swd-maxrate\"), \"input\", function () { $(\"swd-rate-val\").textContent = (parseInt($(\"swd-maxrate\").value, 10) \/ 100).toFixed(2); });\r\n    on($(\"swd-rate\"), \"input\", function () { $(\"swd-rate-out\").textContent = (parseInt($(\"swd-rate\").value, 10) \/ 100).toFixed(2); });\r\n    on($(\"swd-pitch\"), \"input\", function () { $(\"swd-pitch-out\").textContent = (parseInt($(\"swd-pitch\").value, 10) \/ 100).toFixed(2); });\r\n\r\n    function pcm16ToWav(pcm, rate) {\r\n      var out = new ArrayBuffer(44 + pcm.byteLength), dv = new DataView(out);\r\n      writeStr(dv, 0, \"RIFF\"); dv.setUint32(4, 36 + pcm.byteLength, true); writeStr(dv, 8, \"WAVE\"); writeStr(dv, 12, \"fmt \");\r\n      dv.setUint32(16, 16, true); dv.setUint16(20, 1, true); dv.setUint16(22, 1, true);\r\n      dv.setUint32(24, rate, true); dv.setUint32(28, rate * 2, true); dv.setUint16(32, 2, true); dv.setUint16(34, 16, true);\r\n      writeStr(dv, 36, \"data\"); dv.setUint32(40, pcm.byteLength, true);\r\n      new Uint8Array(out, 44).set(new Uint8Array(pcm));\r\n      return out;\r\n    }\r\n    function b64ToBytes(b64) {\r\n      var bin = atob(b64), n = bin.length, u = new Uint8Array(n), i;\r\n      for (i = 0; i < n; i++) { u[i] = bin.charCodeAt(i); }\r\n      return u.buffer;\r\n    }\r\n    function ttsFail(name) { return function (r) { if (!r.ok) { return r.text().then(function (t2) { throw new Error(name + \": \" + t2); }); } return r.arrayBuffer(); }; }\r\n    function generateTTS(provider, text, key, voice) {\r\n      if (provider === \"gemini\") {\r\n        var gModel = ($(\"swd-tts-model\").value || \"\").trim() || \"gemini-2.5-flash-preview-tts\";\r\n        var gUrl = \"https:\/\/generativelanguage.googleapis.com\/v1beta\/models\/\" + encodeURIComponent(gModel) + \":generateContent?key=\" + encodeURIComponent(key);\r\n        var gBody = { contents: [{ parts: [{ text: text }] }], generationConfig: { responseModalities: [\"AUDIO\"],\r\n          speechConfig: { voiceConfig: { prebuiltVoiceConfig: { voiceName: voice || \"Kore\" } } } } };\r\n        return fetch(gUrl, { method: \"POST\", headers: { \"Content-Type\": \"application\/json\" }, body: JSON.stringify(gBody) })\r\n          .then(readJson).then(function (j) {\r\n            var part = j.candidates && j.candidates[0] && j.candidates[0].content && j.candidates[0].content.parts[0];\r\n            if (!part || !part.inlineData) { throw new Error(\"No audio returned by Gemini.\"); }\r\n            var mime = part.inlineData.mimeType || \"\";\r\n            if (mime.indexOf(\"wav\") >= 0) { return decodeBytes(b64ToBytes(part.inlineData.data)); }\r\n            var rate = 24000, m = \/rate=(\\d+)\/.exec(mime);\r\n            if (m) { rate = parseInt(m[1], 10); }\r\n            return decodeBytes(pcm16ToWav(b64ToBytes(part.inlineData.data), rate));\r\n          });\r\n      }\r\n      if (provider === \"openai\") {\r\n        return fetch(\"https:\/\/api.openai.com\/v1\/audio\/speech\", { method: \"POST\",\r\n          headers: { \"Content-Type\": \"application\/json\", \"Authorization\": \"Bearer \" + key },\r\n          body: JSON.stringify({ model: \"gpt-4o-mini-tts\", input: text, voice: voice || \"alloy\", response_format: \"wav\" }) })\r\n          .then(ttsFail(\"OpenAI\")).then(decodeBytes);\r\n      }\r\n      if (provider === \"elevenlabs\") {\r\n        return fetch(\"https:\/\/api.elevenlabs.io\/v1\/text-to-speech\/\" + encodeURIComponent(voice || \"21m00Tcm4TlvDq8ikWAM\"), {\r\n          method: \"POST\", headers: { \"Content-Type\": \"application\/json\", \"xi-api-key\": key },\r\n          body: JSON.stringify({ text: text, model_id: \"eleven_multilingual_v2\" }) })\r\n          .then(ttsFail(\"ElevenLabs\")).then(decodeBytes);\r\n      }\r\n      return Promise.reject(new Error(\"Unknown provider\"));\r\n    }\r\n    function sleep(ms) { return new Promise(function (resolve) { setTimeout(resolve, ms); }); }\r\n    function ttsRetry(provider, text, key, voice, tries) {\r\n      if (st.abort) { return Promise.reject(new Error(\"Stopped\")); }\r\n      return generateTTS(provider, text, key, voice).catch(function (err) {\r\n        var msg = String(err && err.message ? err.message : err);\r\n        if (st.abort) { throw err; }\r\n        if (\/404|400|403|NOT_FOUND|INVALID_ARGUMENT|PERMISSION_DENIED|API key\/i.test(msg)) { st.fatal = msg; st.abort = true; throw err; }\r\n        if (tries < 4 && \/429|RESOURCE_EXHAUSTED|500|503|overloaded|Failed to fetch|network\/i.test(msg)) {\r\n          return sleep(1500 * Math.pow(2, tries)).then(function () { return ttsRetry(provider, text, key, voice, tries + 1); });\r\n        }\r\n        throw err;\r\n      });\r\n    }\r\n    function getClipPcm(provider, text, key, voice) {\r\n      var ck = provider + \"|\" + voice + \"|\" + text;\r\n      if (!st.clips[ck]) {\r\n        st.clips[ck] = ttsRetry(provider, text, key, voice, 0).then(function (buf) { return toMono(buf, SR); });\r\n        st.clips[ck].catch(function () { delete st.clips[ck]; });\r\n      }\r\n      return st.clips[ck];\r\n    }\r\n    function runPool(count, limit, worker) {\r\n      return new Promise(function (resolve) {\r\n        var nextIdx = 0, active = 0;\r\n        function pump() {\r\n          if (count === 0) { resolve(); return; }\r\n          while (active < limit && nextIdx < count) {\r\n            active++;\r\n            worker(nextIdx++).then(finish, finish);\r\n          }\r\n        }\r\n        function finish() {\r\n          active--;\r\n          if (nextIdx >= count && active === 0) { resolve(); } else { pump(); }\r\n        }\r\n        pump();\r\n      });\r\n    }\r\n    function fitClip(arr, idx, maxRate, totalDur) {\r\n      var sg = st.segs[idx], nx = st.segs[idx + 1];\r\n      var uRate = parseInt($(\"swd-rate\").value, 10) \/ 100;\r\n      if (Math.abs(uRate - 1) > 0.02) { arr = speedUp(arr, uRate); }\r\n      var avail = Math.max(0.4, (nx ? nx.start : totalDur) - sg.start), need = arr.length \/ SR;\r\n      sg.over = (need \/ avail > maxRate) ? need \/ avail : 0;\r\n      if (need > avail) { arr = speedUp(arr, Math.min(maxRate, need \/ avail)); }\r\n      var maxLen = Math.floor(avail * SR);\r\n      if (arr.length > maxLen) { arr = arr.subarray(0, maxLen); }\r\n      return { startSamp: Math.floor(sg.start * SR), arr: arr };\r\n    }\r\n    function setBuilding(active) {\r\n      $(\"swd-build\").disabled = active; $(\"swd-quick\").disabled = active;\r\n      if (active) { progStart(\"swd-bd-prog\"); } else { progEnd(\"swd-bd-prog\"); }\r\n      show($(\"swd-stop\"), active);\r\n      if (active) { show($(\"swd-retry\"), false); }\r\n    }\r\n\r\n    \/\/ Build Dubbed Audio (works WITH or WITHOUT a video\/audio file!)\r\n    function buildDub(limitSec) {\r\n      var provider = $(\"swd-provider\").value;\r\n      if (provider === \"browser\") { toast(t(\"msgCloudOnly\")); return; }\r\n      if (!st.segs.length) { toast(t(\"msgNoFile\")); return; }\r\n      var key = keyFor(provider);\r\n      if (!key) { toast(t(\"msgNeedKey\")); return; }\r\n      var defVoice = ($(\"swd-voice\").value || \"\").trim(), maxRate = parseInt($(\"swd-maxrate\").value, 10) \/ 100;\r\n      var duck = parseInt($(\"swd-duck\").value, 10) \/ 100;\r\n      sortSegs();\r\n      var totalDur = getEffectiveDuration();\r\n      var targets = [], clips = [], done = 0, failed = 0, firstErr = \"\", n;\r\n      for (n = 0; n < st.segs.length; n++) {\r\n        var txtToVoice = (st.segs[n].tr || \"\").trim() || (st.segs[n].src || \"\").trim();\r\n        if (txtToVoice && (!limitSec || st.segs[n].start < limitSec)) { targets.push(n); }\r\n      }\r\n      if (!targets.length) { toast(\"No segments to voice yet.\"); return; }\r\n      st.abort = false; st.fatal = \"\"; st.lastLimit = limitSec; setBuilding(true);\r\n      var t0 = Date.now();\r\n      runPool(targets.length, 3, function (tIdx) {\r\n        if (st.abort) { return Promise.resolve(); }\r\n        var si = targets[tIdx], sg = st.segs[si];\r\n        var lineText = (sg.tr || \"\").trim() || (sg.src || \"\").trim();\r\n        return getClipPcm(provider, lineText, key, (sg.voice || \"\").trim() || defVoice).then(function (arr) {\r\n          clips.push(fitClip(arr, si, maxRate, totalDur));\r\n        }).catch(function (err) {\r\n          failed++; if (!firstErr) { firstErr = String(err && err.message ? err.message : err); }\r\n        }).then(function () {\r\n          done++;\r\n          var lbl = \"Voiced\" + (failed ? (\" (\" + failed + \" failed)\") : \"\");\r\n          progSet(\"swd-bd-prog\", \"swd-bd-hint\", done, targets.length, lbl);\r\n        });\r\n      }).then(function () {\r\n        if (st.fatal) { throw new Error(st.fatal); }\r\n        if (st.abort) {\r\n          setBuilding(false); show($(\"swd-retry\"), true);\r\n          $(\"swd-bd-hint\").textContent = \"Stopped. \" + clips.length + \" sentence(s) are cached. Press Retry to continue.\";\r\n          toast(t(\"msgStopped\")); return;\r\n        }\r\n        if (!clips.length) { throw new Error(firstErr || \"No audio was produced.\"); }\r\n\r\n        \/\/ Determine output buffer length (works even when st.buf is null for subtitle-only uploads)\r\n        var maxSample = Math.ceil(totalDur * SR);\r\n        clips.forEach(function (c) {\r\n          if (c.startSamp + c.arr.length > maxSample) { maxSample = c.startSamp + c.arr.length; }\r\n        });\r\n\r\n        var out, j, fadeSamps = Math.floor(0.4 * SR);\r\n        if (st.buf && !muteOrigOn()) {\r\n          out = toMono(st.buf, SR);\r\n          if (out.length < maxSample) {\r\n            var ext = new Float32Array(maxSample);\r\n            ext.set(out);\r\n            out = ext;\r\n          }\r\n          var mask = new Float32Array(out.length);\r\n          for (j = 0; j < out.length; j++) { mask[j] = 1; }\r\n          clips.forEach(function (c) {\r\n            var s0 = c.startSamp, e0 = c.startSamp + c.arr.length, k, f;\r\n            for (k = Math.max(0, s0 - fadeSamps); k <= Math.min(out.length - 1, e0 + fadeSamps); k++) {\r\n              f = duck;\r\n              if (k < s0) { f = duck + (1 - duck) * (s0 - k) \/ fadeSamps; }\r\n              else if (k > e0) { f = duck + (1 - duck) * (k - e0) \/ fadeSamps; }\r\n              if (f < mask[k]) { mask[k] = f; }\r\n            }\r\n          });\r\n          for (j = 0; j < out.length; j++) { out[j] *= mask[j]; }\r\n        } else {\r\n          out = new Float32Array(maxSample);\r\n        }\r\n\r\n        clips.forEach(function (c) {\r\n          var k, fade;\r\n          for (k = 0; k < c.arr.length && c.startSamp + k < out.length; k++) {\r\n            fade = (c.arr.length - k < 800) ? (c.arr.length - k) \/ 800 : 1;\r\n            out[c.startSamp + k] += c.arr[k] * fade;\r\n          }\r\n        });\r\n\r\n        var peak = 0;\r\n        for (j = 0; j < out.length; j++) { if (Math.abs(out[j]) > peak) { peak = Math.abs(out[j]); } }\r\n        if (peak > 0 && peak !== 1) {\r\n          var factor = Math.min(10, 0.95 \/ peak);\r\n          for (j = 0; j < out.length; j++) { out[j] *= factor; }\r\n        }\r\n\r\n        st.partial = !!limitSec; st.forceBrowser = false; st.dubPcm = out; st.dubBlob = encodeWAV(out, SR);\r\n        if (st.dubUrl) { URL.revokeObjectURL(st.dubUrl); }\r\n        st.dubUrl = URL.createObjectURL(st.dubBlob);\r\n        dubAudio.src = st.dubUrl;\r\n        show($(\"swd-dubplayer-wrap\"), true);\r\n\r\n        setBuilding(false); show($(\"swd-retry\"), failed > 0);\r\n        var longCount = 0, q;\r\n        for (q = 0; q < st.segs.length; q++) { if (st.segs[q].over > 0) { longCount++; } }\r\n        renderSegs();\r\n        var secs = ((Date.now() - t0) \/ 1000).toFixed(0);\r\n        $(\"swd-bd-hint\").textContent = (failed\r\n          ? (\"Built in \" + secs + \" s, but \" + failed + \" segment(s) failed: \" + firstErr.slice(0, 110) + \" Press Retry failed to redo only those.\")\r\n          : ((limitSec ? \"Quick preview ready in \" : \"Dubbed audio track ready in \") + secs + \" s.\" + (limitSec ? \" Press Build dubbed audio track for the full audio.\" : \" Listen or download below, or go to Tab 4.\")))\r\n          + (longCount ? (\" \" + longCount + \" line(s) are too long for their time slot: see Tab 2 and use Shorten.\") : \"\");\r\n        toast(limitSec ? t(\"msgPreviewDone\") : t(\"msgBuiltDone\"));\r\n        if (limitSec) {\r\n          if (st.file) {\r\n            $(\"swd-live\").checked = true; applyVolume();\r\n            var pv = $(\"swd-video2\"); pv.currentTime = 0;\r\n            var pp = pv.play(); if (pp && pp.catch) { pp.catch(function () { \/* autoplay blocked *\/ }); }\r\n          } else {\r\n            dubAudio.currentTime = 0;\r\n            var ap = dubAudio.play(); if (ap && ap.catch) { ap.catch(function () {}); }\r\n          }\r\n        }\r\n      }).catch(function (err) {\r\n        setBuilding(false); show($(\"swd-retry\"), true);\r\n        var msg = String(err && err.message ? err.message : err);\r\n        $(\"swd-bd-hint\").textContent = \"Build stopped: \" + msg.slice(0, 160) + \" Check the API key and the voice model name, then press Retry.\";\r\n        if (window.console) { console.error(\"Dub build error\", err); }\r\n        toast(t(\"msgBuildFail\"));\r\n      });\r\n    }\r\n    function browserPreview() {\r\n      var hasTr = false, h;\r\n      for (h = 0; h < st.segs.length; h++) { if ((st.segs[h].tr || \"\").trim()) { hasTr = true; break; } }\r\n      if (!st.segs.length) { toast(t(\"msgNoFile\")); return; }\r\n      if (!hasTr) { toast(t(\"msgNoTr\")); return; }\r\n      if (!voiceCount()) {\r\n        $(\"swd-bd-hint\").textContent = tf(\"msgNoVoice\", { l: lang().name });\r\n        toast(tf(\"msgNoVoice\", { l: lang().name })); return;\r\n      }\r\n      if (st.file) {\r\n        $(\"swd-live\").checked = true; st.spoken = {}; applyVolume();\r\n        var bv = $(\"swd-video2\"); bv.currentTime = 0;\r\n        var bp = bv.play(); if (bp && bp.catch) { bp.catch(function () { \/* autoplay blocked *\/ }); }\r\n      } else {\r\n        \/\/ Speak first translated segment if no video is loaded\r\n        for (h = 0; h < st.segs.length; h++) {\r\n          if ((st.segs[h].tr || \"\").trim()) { speakSeg(st.segs[h]); break; }\r\n        }\r\n      }\r\n    }\r\n    on($(\"swd-build\"), \"click\", function () { buildDub(0); });\r\n    function quickSecs() { var qs = parseInt($(\"swd-quick-sec\").value, 10) || 60; return Math.max(10, Math.min(600, qs)); }\r\n    on($(\"swd-quick\"), \"click\", function () { if ($(\"swd-provider\").value === \"browser\") { st.forceBrowser = true; browserPreview(); } else { buildDub(quickSecs()); } });\r\n    on($(\"swd-bpreview\"), \"click\", function () { st.forceBrowser = true; dubAudio.pause(); browserPreview(); });\r\n    on($(\"swd-muteorig\"), \"change\", function () { applyVolume(); });\r\n    on($(\"swd-hardmute\"), \"change\", function () { applyVolume(); });\r\n    on($(\"swd-stop\"), \"click\", function () { st.abort = true; $(\"swd-bd-hint\").textContent = \"Stopping after the current requests...\"; });\r\n    on($(\"swd-retry\"), \"click\", function () { buildDub(st.lastLimit || 0); });\r\n\r\n    \/\/ ---------- live preview \/ ducking ----------\r\n    var videos = [videoEl, $(\"swd-video2\")];\r\n    function liveOn() { return $(\"swd-live\").checked; }\r\n    function dubActive() { return !!st.dubBlob && !st.forceBrowser; }\r\n    function muteOrigOn() { return $(\"swd-muteorig\").checked; }\r\n    function hardMuteOn() { return $(\"swd-hardmute\").checked; }\r\n    function duckLevel() { return parseInt($(\"swd-duck\").value, 10) \/ 100; }\r\n\r\n    function applyVolume() {\r\n      var i;\r\n      for (i = 0; i < videos.length; i++) {\r\n        if (!videos[i]) { continue; }\r\n        if (!liveOn()) { videos[i].muted = false; videos[i].volume = 1; }\r\n        else if (dubActive() || muteOrigOn()) { videos[i].muted = true; }\r\n        else { videos[i].muted = false; videos[i].volume = 1; }\r\n      }\r\n    }\r\n    function setOrigVolume(level) {\r\n      var i;\r\n      for (i = 0; i < videos.length; i++) {\r\n        if (!videos[i]) { continue; }\r\n        if (dubActive() || muteOrigOn()) { videos[i].muted = true; }\r\n        else { videos[i].muted = false; videos[i].volume = Math.max(0, Math.min(1, level)); }\r\n      }\r\n    }\r\n    function speakSeg(sg) {\r\n      if (!window.speechSynthesis || !sg.tr) { return; }\r\n      var L = lang(), u = new SpeechSynthesisUtterance(sg.tr), chosen = pickVoice();\r\n      u.lang = L.bcp; if (chosen) { u.voice = chosen; }\r\n      var gRate = parseInt($(\"swd-rate\").value, 10) \/ 100;\r\n      var gPitch = parseInt($(\"swd-pitch\").value, 10) \/ 100;\r\n      u.rate = Math.max(0.5, Math.min(2.2, gRate));\r\n      u.pitch = Math.max(0.5, Math.min(2, gPitch));\r\n      var estimate = sg.tr.length \/ 14, slot = Math.max(0.5, sg.end - sg.start);\r\n      var fitRate = Math.max(0.8, Math.min(2, estimate \/ slot));\r\n      u.rate = Math.max(0.5, Math.min(2.2, (u.rate + fitRate) \/ 2));\r\n      window.speechSynthesis.speak(u);\r\n    }\r\n    function syncAudio(src) {\r\n      if (!dubActive() || !liveOn()) { return; }\r\n      if (Math.abs(dubAudio.currentTime - src.currentTime) > 0.3) { dubAudio.currentTime = src.currentTime; }\r\n    }\r\n\r\n    function updateDuckForTime(t2) {\r\n      if (!liveOn() || dubActive()) { return; }\r\n      var i, active = false, n = st.segs.length;\r\n      for (i = 0; i < n; i++) {\r\n        if (t2 < st.segs[i].start - 0.05) { break; }\r\n        if (t2 >= st.segs[i].start - 0.05 && t2 < st.segs[i].end + 0.4 && (st.segs[i].tr || \"\").trim()) { active = true; break; }\r\n      }\r\n      if (active) { setOrigVolume(hardMuteOn() ? 0 : duckLevel()); }\r\n      else { setOrigVolume(1); }\r\n    }\r\n\r\n    function bindVideo(el) {\r\n      on(el, \"play\", function () {\r\n        var oi;\r\n        for (oi = 0; oi < videos.length; oi++) { if (videos[oi] && videos[oi] !== el) { videos[oi].pause(); } }\r\n        applyVolume();\r\n        if (liveOn() && dubActive()) { dubAudio.currentTime = el.currentTime; dubAudio.play(); }\r\n      });\r\n      on(el, \"pause\", function () {\r\n        dubAudio.pause();\r\n        if (window.speechSynthesis) { window.speechSynthesis.cancel(); }\r\n        setOrigVolume(1);\r\n      });\r\n      on(el, \"seeked\", function () {\r\n        var i; st.spoken = {};\r\n        for (i = 0; i < st.segs.length; i++) { if (st.segs[i].start < el.currentTime) { st.spoken[i] = true; } }\r\n        if (window.speechSynthesis) { window.speechSynthesis.cancel(); }\r\n        syncAudio(el);\r\n      });\r\n      on(el, \"timeupdate\", function () {\r\n        if (!liveOn()) { return; }\r\n        if (dubActive()) { syncAudio(el); return; }\r\n        var i, t2 = el.currentTime, n = st.segs.length;\r\n        updateDuckForTime(t2);\r\n        for (i = 0; i < n; i++) {\r\n          if (t2 < st.segs[i].start) { break; }\r\n          if (!st.spoken[i] && t2 >= st.segs[i].start && t2 < st.segs[i].end + 0.6 && (st.segs[i].tr || \"\").trim()) {\r\n            st.spoken[i] = true; speakSeg(st.segs[i]);\r\n          }\r\n        }\r\n      });\r\n    }\r\n    (function () { var i; for (i = 0; i < videos.length; i++) { bindVideo(videos[i]); } })();\r\n    function voiceCount() { return matchedVoices().length; }\r\n    on($(\"swd-live\"), \"change\", function () {\r\n      st.forceBrowser = false;\r\n      if (liveOn() && $(\"swd-provider\").value === \"browser\" && !dubActive() && !voiceCount()) {\r\n        toast(tf(\"msgNoVoice\", { l: lang().name }));\r\n      }\r\n      st.spoken = {}; applyVolume();\r\n      if (!liveOn()) { dubAudio.pause(); if (window.speechSynthesis) { window.speechSynthesis.cancel(); } }\r\n    });\r\n\r\n    \/\/ ---------- record live dub ----------\r\n    var rec = { mr: null, stream: null, chunks: [], prev: null, active: false };\r\n    function recUi(active) {\r\n      show($(\"swd-rec\"), !active); show($(\"swd-recstop\"), active);\r\n    }\r\n    function stopRec() { if (rec.mr && rec.mr.state !== \"inactive\") { rec.mr.stop(); } }\r\n    function restorePrev() {\r\n      if (rec.prev) { st.dubBlob = rec.prev.blob; st.dubPcm = rec.prev.pcm; st.dubUrl = rec.prev.url; rec.prev = null; }\r\n    }\r\n    function onRecStop() {\r\n      rec.active = false; recUi(false); progEnd(\"swd-rec-prog\");\r\n      if (window.speechSynthesis) { window.speechSynthesis.cancel(); }\r\n      if (rec.stream) { rec.stream.getTracks().forEach(function (tk) { tk.stop(); }); rec.stream = null; }\r\n      var blob = new Blob(rec.chunks, { type: (rec.mr && rec.mr.mimeType) || \"audio\/webm\" });\r\n      if (blob.size < 2000) { restorePrev(); $(\"swd-rec-hint\").textContent = t(\"msgRecordEmpty\"); toast(t(\"msgRecordEmpty\")); return; }\r\n      blob.arrayBuffer().then(decodeBytes).then(function (buf) {\r\n        var arr = toMono(buf, SR);\r\n        st.partial = false; st.forceBrowser = false; st.dubPcm = arr; st.dubBlob = encodeWAV(arr, SR); rec.prev = null;\r\n        st.dubUrl = URL.createObjectURL(st.dubBlob); dubAudio.src = st.dubUrl;\r\n        show($(\"swd-dubplayer-wrap\"), true);\r\n        $(\"swd-rec-hint\").textContent = \"Recorded \" + (arr.length \/ SR).toFixed(1) + \" s. Listen or download now.\";\r\n        toast(t(\"msgRecordSaved\"));\r\n        applyVolume();\r\n      }).catch(function (err) {\r\n        restorePrev();\r\n        if (window.console) { console.error(\"Record decode error\", err); }\r\n        $(\"swd-rec-hint\").textContent = \"Could not read the recording. Try again.\";\r\n        toast(\"Could not read the recording.\");\r\n      });\r\n    }\r\n    on($(\"swd-rec\"), \"click\", function () {\r\n      var vid = $(\"swd-video2\"), hasTr = false, i;\r\n      for (i = 0; i < st.segs.length; i++) { if (st.segs[i].tr && st.segs[i].tr.trim()) { hasTr = true; break; } }\r\n      if (!st.file) { toast(t(\"msgNoFile\")); return; }\r\n      if (!hasTr) { toast(t(\"msgNoTr\")); return; }\r\n      if (!navigator.mediaDevices || !navigator.mediaDevices.getDisplayMedia || !window.MediaRecorder) {\r\n        toast(\"This browser cannot record tab audio. Use desktop Chrome or Edge.\"); return;\r\n      }\r\n      navigator.mediaDevices.getDisplayMedia({ video: true, audio: true, preferCurrentTab: true }).then(function (stream) {\r\n        var tracks = stream.getAudioTracks();\r\n        if (!tracks.length) {\r\n          stream.getTracks().forEach(function (tk) { tk.stop(); });\r\n          toast(\"No audio was shared. Tick Share tab audio and try again.\"); return;\r\n        }\r\n        rec.stream = stream; rec.chunks = [];\r\n        rec.prev = { blob: st.dubBlob, pcm: st.dubPcm, url: st.dubUrl }; st.dubBlob = null;\r\n        var opts = {};\r\n        if (MediaRecorder.isTypeSupported && MediaRecorder.isTypeSupported(\"audio\/webm;codecs=opus\")) { opts.mimeType = \"audio\/webm;codecs=opus\"; }\r\n        rec.mr = new MediaRecorder(new MediaStream(tracks), opts);\r\n        rec.mr.ondataavailable = function (ev) { if (ev.data && ev.data.size) { rec.chunks.push(ev.data); } };\r\n        rec.mr.onstop = onRecStop;\r\n        tracks[0].onended = stopRec;\r\n        $(\"swd-live\").checked = true; st.spoken = {}; applyVolume();\r\n        vid.pause(); vid.currentTime = 0;\r\n        rec.mr.start(500); rec.active = true; recUi(true); progStart(\"swd-rec-prog\");\r\n        $(\"swd-rec-hint\").textContent = t(\"msgRecHint\");\r\n        vid.play();\r\n      }).catch(function (err) {\r\n        if (window.console) { console.error(\"Capture error\", err); }\r\n        toast(\"Recording was cancelled or blocked.\");\r\n      });\r\n    });\r\n    on($(\"swd-recstop\"), \"click\", stopRec);\r\n    on($(\"swd-video2\"), \"ended\", function () { if (rec.active) { setTimeout(stopRec, 2000); } });\r\n    on($(\"swd-video2\"), \"timeupdate\", function () {\r\n      var rv = $(\"swd-video2\");\r\n      if (rec.active && isFinite(rv.duration) && rv.duration > 0) {\r\n        progSet(\"swd-rec-prog\", \"swd-rec-hint\", Math.round(rv.currentTime), Math.round(rv.duration), \"Recorded seconds\");\r\n      }\r\n    });\r\n\r\n    \/\/ ---------- captions ----------\r\n    function updateCap() {\r\n      var cap = $(\"swd-cap\"), mode = $(\"swd-capmode\").value, t2 = $(\"swd-video2\").currentTime, i3, sg = null, txt = \"\";\r\n      for (i3 = 0; i3 < st.segs.length; i3++) { if (t2 >= st.segs[i3].start && t2 < st.segs[i3].end) { sg = st.segs[i3]; break; } }\r\n      if (sg && mode !== \"off\") {\r\n        if (mode === \"src\") { txt = sg.src; }\r\n        else if (mode === \"both\") { txt = (sg.tr ? sg.tr + \"\\n\" : \"\") + sg.src; }\r\n        else { txt = sg.tr || sg.src; }\r\n      }\r\n      cap.textContent = txt; cap.style.whiteSpace = \"pre-line\"; cap.style.display = txt ? \"block\" : \"none\";\r\n    }\r\n    on($(\"swd-video2\"), \"timeupdate\", updateCap);\r\n    on($(\"swd-video2\"), \"seeked\", updateCap);\r\n    on($(\"swd-capmode\"), \"change\", updateCap);\r\n\r\n    \/\/ ---------- Tab 3 & Tab 4: exports ----------\r\n    function cueText(sg) { return (sg.tr && sg.tr.trim()) ? sg.tr.trim() : sg.src; }\r\n    function buildSubs(format) {\r\n      var i, out = \"\";\r\n      if (format === \"sbv\") {\r\n        for (i = 0; i < st.segs.length; i++) {\r\n          out += fmtSbvTime(st.segs[i].start) + \",\" + fmtSbvTime(st.segs[i].end) + \"\\n\" + cueText(st.segs[i]) + \"\\n\\n\";\r\n        }\r\n        return out;\r\n      }\r\n      var isVtt = format === \"vtt\";\r\n      var sep = isVtt ? \".\" : \",\";\r\n      out = isVtt ? \"WEBVTT\\n\\n\" : \"\";\r\n      for (i = 0; i < st.segs.length; i++) {\r\n        out += (isVtt ? \"\" : (i + 1) + \"\\n\") + fmtTime(st.segs[i].start, sep) + \" --\" + String.fromCharCode(62) + \" \" + fmtTime(st.segs[i].end, sep) + \"\\n\" + cueText(st.segs[i]) + \"\\n\\n\";\r\n      }\r\n      return out;\r\n    }\r\n    on($(\"swd-dl-srt\"), \"click\", function () {\r\n      if (!st.segs.length) { toast(t(\"msgNothingExport\")); return; }\r\n      download(new Blob([buildSubs(\"srt\")], { type: \"text\/plain;charset=utf-8\" }), \"dubbed-subtitles.srt\");\r\n    });\r\n    on($(\"swd-dl-vtt\"), \"click\", function () {\r\n      if (!st.segs.length) { toast(t(\"msgNothingExport\")); return; }\r\n      download(new Blob([buildSubs(\"vtt\")], { type: \"text\/vtt;charset=utf-8\" }), \"dubbed-subtitles.vtt\");\r\n    });\r\n    on($(\"swd-dl-sbv\"), \"click\", function () {\r\n      if (!st.segs.length) { toast(t(\"msgNothingExport\")); return; }\r\n      download(new Blob([buildSubs(\"sbv\")], { type: \"text\/plain;charset=utf-8\" }), \"dubbed-subtitles.sbv\");\r\n    });\r\n\r\n    function triggerWavDownload() {\r\n      if (!st.dubBlob) { toast(t(\"msgBuildFirst\")); return; }\r\n      if (st.partial) { toast(t(\"msgPartial\")); return; }\r\n      download(st.dubBlob, \"dubbed-audio.wav\");\r\n    }\r\n    on($(\"swd-dl-wav\"), \"click\", triggerWavDownload);\r\n    on($(\"swd-dl-wav-fast\"), \"click\", triggerWavDownload);\r\n\r\n    function triggerMp3Download() {\r\n      if (!st.dubBlob) { toast(t(\"msgBuildFirst\")); return; }\r\n      if (st.partial) { toast(t(\"msgPartial\")); return; }\r\n\r\n      show($(\"swd-mux-prog\"), true);\r\n      progIndet(\"swd-mux-prog\", \"swd-mux-hint\", \"Converting to MP3...\");\r\n      toast(\"Converting to MP3...\");\r\n\r\n      getFFmpeg().then(function (ff) {\r\n        if (ff.setProgress) {\r\n          ff.setProgress(function (pr) { progSet(\"swd-mux-prog\", \"swd-mux-hint\", Math.min(100, Math.round((pr.ratio || 0) * 100)), 100, \"Converting MP3\"); });\r\n        }\r\n        return st.dubBlob.arrayBuffer().then(function (arr) {\r\n          ff.FS(\"writeFile\", \"in_audio.wav\", new Uint8Array(arr));\r\n          return ff.run(\"-y\", \"-i\", \"in_audio.wav\", \"-b:a\", \"192k\", \"out_audio.mp3\");\r\n        }).then(function () {\r\n          return ff.FS(\"readFile\", \"out_audio.mp3\");\r\n        });\r\n      }).then(function (data) {\r\n        download(new Blob([data.buffer], { type: \"audio\/mpeg\" }), \"dubbed-audio.mp3\");\r\n        progEnd(\"swd-mux-prog\");\r\n        $(\"swd-mux-hint\").textContent = \"MP3 conversion done.\";\r\n        toast(\"MP3 ready.\");\r\n      }).catch(function (err) {\r\n        progEnd(\"swd-mux-prog\");\r\n        $(\"swd-mux-hint\").textContent = \"MP3 conversion failed.\";\r\n        if (window.console) { console.error(\"mp3 error\", err); }\r\n        toast(\"MP3 export failed.\");\r\n      });\r\n    }\r\n    on($(\"swd-dl-mp3\"), \"click\", triggerMp3Download);\r\n    on($(\"swd-dl-mp3-fast\"), \"click\", triggerMp3Download);\r\n\r\n    var VID = String.fromCharCode(118);\r\n    function muxVideo(mode) {\r\n      return getFFmpeg().then(function (ff) {\r\n        if (ff.setProgress) {\r\n          progStart(\"swd-mux-prog\");\r\n          ff.setProgress(function (pr) { progSet(\"swd-mux-prog\", \"swd-mux-hint\", Math.min(100, Math.round((pr.ratio || 0) * 100)), 100, \"Muxing\"); });\r\n        }\r\n        return Promise.all([fileToU8(st.file), st.dubBlob.arrayBuffer()]).then(function (arr) {\r\n          ff.FS(\"writeFile\", \"in_video\", arr[0]);\r\n          ff.FS(\"writeFile\", \"in_audio\", new Uint8Array(arr[1]));\r\n          if (mode !== \"none\") { ff.FS(\"writeFile\", \"subs.srt\", strToU8(buildSubs(\"srt\"))); }\r\n          if (mode === \"burn\") {\r\n            return ff.run(\"-y\", \"-i\", \"in_video\", \"-i\", \"in_audio\", \"-map\", \"0:\" + VID + \":0\", \"-map\", \"1:a:0\", \"-vf\", \"subtitles=subs.srt\",\r\n              \"-c:\" + VID, \"libx264\", \"-preset\", \"ultrafast\", \"-c:a\", \"aac\", \"-shortest\", \"out_final.mp4\");\r\n          }\r\n          if (mode === \"soft\") {\r\n            return ff.run(\"-y\", \"-i\", \"in_video\", \"-i\", \"in_audio\", \"-i\", \"subs.srt\", \"-map\", \"0:\" + VID + \":0\", \"-map\", \"1:a:0\", \"-map\", \"2:0\",\r\n              \"-c:\" + VID, \"copy\", \"-c:a\", \"aac\", \"-c:s\", \"mov_text\", \"-shortest\", \"out_final.mp4\");\r\n          }\r\n          return ff.run(\"-y\", \"-i\", \"in_video\", \"-i\", \"in_audio\", \"-map\", \"0:\" + VID + \":0\", \"-map\", \"1:a:0\", \"-c:\" + VID, \"copy\", \"-c:a\", \"aac\", \"-shortest\", \"out_final.mp4\");\r\n        }).then(function () { return ff.FS(\"readFile\", \"out_final.mp4\"); });\r\n      });\r\n    }\r\n    on($(\"swd-dl-mp4\"), \"click\", function () {\r\n      if (!st.file || !st.isVideo) { toast(t(\"msgVideoOnly\")); return; }\r\n      if (!st.dubBlob) { toast(t(\"msgBuildFirst\")); return; }\r\n      if (st.partial) { toast(t(\"msgPartial\")); return; }\r\n      var mode = $(\"swd-sub-mode\").value, fellBack = false;\r\n      progIndet(\"swd-mux-prog\", \"swd-mux-hint\", \"Loading the video engine (first use downloads it)...\");\r\n      muxVideo(mode).catch(function (err) {\r\n        if (mode !== \"burn\") { throw err; }\r\n        ffLoaded = false; ffmpeg = null; fellBack = true;\r\n        $(\"swd-mux-hint\").textContent = \"Burn-in is not supported by this video engine. Switching to soft subtitles...\";\r\n        return muxVideo(\"soft\");\r\n      }).then(function (data) {\r\n        download(new Blob([data.buffer], { type: \"video\/mp4\" }), \"dubbed-video.mp4\");\r\n        progEnd(\"swd-mux-prog\");\r\n        $(\"swd-mux-hint\").textContent = fellBack ? \"Done with soft subtitles (burn-in was not available).\" : \"Done. Files never left your browser.\";\r\n        toast(t(\"msgMuxDone\"));\r\n      }).catch(function (err) {\r\n        progEnd(\"swd-mux-prog\");\r\n        $(\"swd-mux-hint\").textContent = \"Mux failed. Try Chrome or Edge, a smaller file, or Subtitles = None.\";\r\n        if (window.console) { console.error(\"mux error\", err); }\r\n        toast(t(\"msgMuxFail\"));\r\n      });\r\n    });\r\n\r\n    \/\/ ---------- keyboard shortcuts ----------\r\n    on(document, \"keydown\", function (ev) {\r\n      if (!wrap.contains(document.activeElement) && ev.target !== document.body) { return; }\r\n      var tag = (ev.target && ev.target.tagName) || \"\";\r\n      if (tag === \"INPUT\" || tag === \"TEXTAREA\" || tag === \"SELECT\") { return; }\r\n      if ((ev.ctrlKey || ev.metaKey) && ev.key.toLowerCase() === \"z\") {\r\n        ev.preventDefault();\r\n        if (ev.shiftKey) { $(\"swd-redo\").click(); } else { $(\"swd-undo\").click(); }\r\n        return;\r\n      }\r\n      if (ev.key === \" \" && st.file) {\r\n        var pv = $(\"swd-video2\");\r\n        ev.preventDefault();\r\n        if (pv.paused) { pv.play(); } else { pv.pause(); }\r\n      }\r\n    });\r\n\r\n    restoreSession();\r\n    renderSegs();\r\n  }\r\n\r\n  \/\/ ---------- bootstrap ----------\r\n  boot();\r\n  if (document.readyState === \"loading\") { document.addEventListener(\"DOMContentLoaded\", boot); }\r\n  window.addEventListener(\"load\", function () { setTimeout(boot, 50); });\r\n  var mo = new MutationObserver(function () { boot(); });\r\n  try { mo.observe(document.documentElement, { childList: true, subtree: true }); } catch (e2) {}\r\n  setTimeout(function () { try { mo.disconnect(); } catch (e3) {} }, 10000);\r\n})();\r\n<\/script>\n\n\n\n<style>\n.sict-videodub-bottom{max-width:100%;width:100%;box-sizing:border-box;font-family:system-ui,-apple-system,\"Segoe UI\",Roboto,sans-serif;color:#1e293b;line-height:1.7;font-size:16px}\n.sict-videodub-bottom *{box-sizing:border-box}\n.sict-videodub-bottom h2{font-size:22px;font-weight:800;color:#0f172a;margin:28px 0 10px}\n.sict-videodub-bottom p{margin:0 0 12px}\n.sict-videodub-bottom a{color:#0369A1;text-decoration:underline}\n.sict-videodub-bottom ul{list-style:none;margin:0 0 14px;padding:0}\n.sict-videodub-bottom li{position:relative;padding-left:26px;margin:0 0 8px}\n.sict-videodub-bottom li:before{content:\"\\1F535\";position:absolute;left:0;top:0;font-size:12px;line-height:1.9}\n.sict-vd-boxes{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:14px;margin:0 0 20px}\n.sict-vd-box{background:#f0f9ff;border:1px solid #bae6fd;border-radius:12px;padding:14px 16px}\n.sict-vd-box h3{margin:0 0 6px;font-size:17px;color:#0c4a6e}\n.sict-vd-box p{margin:0;font-size:15px}\n.sict-vd-steps{display:grid;gap:12px;margin:0 0 10px}\n.sict-vd-step{display:flex;gap:12px;align-items:flex-start;background:#fff;border:1px solid #e2e8f0;border-radius:12px;padding:12px 14px}\n.sict-vd-num{flex:0 0 32px;height:32px;border-radius:50%;background:#0369A1;color:#fff;font-weight:800;display:flex;align-items:center;justify-content:center}\n.sict-vd-step p{margin:0;font-size:15px}\n.sict-videodub-bottom .sict-faq-card{background:#fff;border:1px solid #e2e8f0;border-radius:12px;padding:12px 16px;margin:0 0 10px}\n.sict-videodub-bottom .sict-faq-q{font-weight:800;color:#0f172a;margin:0 0 4px}\n.sict-videodub-bottom .sict-faq-a{margin:0;font-size:15px}\n.sict-videodub-bottom .sict-updated{font-size:13px;color:#64748b}\n<\/style>\n<div class=\"sict-videodub-bottom\">\n  <div class=\"sict-vd-boxes\">\n    <div class=\"sict-vd-box\"><h3>\ud83d\udfe2 Dub inside your browser<\/h3><p>Your video is never uploaded. Speech is transcribed on your own device, so only short pieces of text go to the translation service you choose.<\/p><\/div>\n    <div class=\"sict-vd-box\"><h3>\ud83d\udd35 Fix it before it speaks<\/h3><p>Every sentence appears in an editable table. Correct a name or an awkward phrase first, then generate the voice.<\/p><\/div>\n    <div class=\"sict-vd-box\"><h3>\ud83d\udfe3 Take away what you need<\/h3><p>Download subtitles (.srt or .vtt), a dubbed audio track (.wav) or a finished video (.mp4).<\/p><\/div>\n  <\/div>\n  <div class=\"sict-vd-steps\">\n    <div class=\"sict-vd-step\"><span class=\"sict-vd-num\">1<\/span><p><strong>Load &amp; Transcribe:<\/strong> drop your video, choose the spoken language and press <strong>Transcribe<\/strong>.<\/p><\/div>\n    <div class=\"sict-vd-step\"><span class=\"sict-vd-num\">2<\/span><p><strong>Translate &amp; Edit:<\/strong> pick the target language, paste your Gemini key and press <strong>Translate all segments<\/strong>. Fix any line you do not like.<\/p><\/div>\n    <div class=\"sict-vd-step\"><span class=\"sict-vd-num\">3<\/span><p><strong>Voice &amp; Sync:<\/strong> choose a voice engine and press <strong>Build dubbed audio<\/strong>, or use <strong>Start recording<\/strong> with the browser voice.<\/p><\/div>\n    <div class=\"sict-vd-step\"><span class=\"sict-vd-num\">4<\/span><p><strong>Export:<\/strong> download the <strong>.srt<\/strong>, the dubbed <strong>.wav<\/strong> or the dubbed <strong>.mp4<\/strong>.<\/p><\/div>\n  <\/div>\n<\/div>\n\n\n\n<h2 id=\"\ud83d\udd34-who-gets-real-value-from-a-video-dubbing-tool-in-school\" class=\"wp-block-heading\">\ud83d\udd34 Who gets real value from a video dubbing tool in school?<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Teachers with recorded lessons.<\/strong> A 6-minute English walkthrough of a spreadsheet formula can be offered to Sinhala or Tamil medium students without re-recording it.<\/li>\n\n\n\n<li><strong>Students making projects.<\/strong> A group video about a school science fair reaches relatives who do not speak the language of the original narration.<\/li>\n\n\n\n<li><strong>Accessibility.<\/strong> Subtitles help students who are deaf or hard of hearing, and anyone watching without sound on a bus.<\/li>\n<\/ul>\n\n\n\n<h2 id=\"\ud83d\udfe1-one-sentence-from-input-to-output\" class=\"wp-block-heading\">\ud83d\udfe1 One sentence, from input to output<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Take a single line from a lesson video. The narrator says <em>&#8220;Save your work before you close the program.&#8221;<\/em> starting at 2.0 seconds. The next sentence begins at 5.2 seconds, so this sentence has a 3.2 second slot.<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Transcribe:<\/strong> the tool stores the text with its start and end time.<\/li>\n\n\n\n<li><strong>Translate (Spanish as an example):<\/strong> <em>&#8220;Guarda tu trabajo antes de cerrar el programa.&#8221;<\/em><\/li>\n\n\n\n<li><strong>Voice:<\/strong> the spoken Spanish clip comes out 4.0 seconds long, which is longer than the slot.<\/li>\n\n\n\n<li><strong>Sync:<\/strong> the tool speeds the clip up by 4.0 \/ 3.2 = 1.25x so it ends before the next sentence starts. Your <strong>Max speed-up<\/strong> slider (default 1.35x) is the ceiling.<\/li>\n<\/ul>\n\n\n\n<h2 id=\"\ud83d\udfe2-mistakes-that-waste-your-time\" class=\"wp-block-heading\">\ud83d\udfe2 Mistakes that waste your time<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Skipping the edit step.<\/strong> Machine translation can mistranslate names, subject terms and jokes. Read the table in Tab 2 before you build any audio.<\/li>\n\n\n\n<li><strong>Expecting a file from Browser voice.<\/strong> That engine only speaks live through your speakers. For a downloadable dub use a cloud engine, or use the Start recording button.<\/li>\n\n\n\n<li><strong>Pausing while recording.<\/strong> The recording is real time. A pause shifts the voice against the picture.<\/li>\n\n\n\n<li><strong>Starting with a long video.<\/strong> Try a 1 to 2 minute clip first, check the result, then process the full lesson.<\/li>\n<\/ul>\n\n\n\n<h2 id=\"\ud83d\udd35-honest-limits-you-should-know\" class=\"wp-block-heading\">\ud83d\udd35 Honest limits you should know<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Accuracy varies by language.<\/strong> The in-browser Whisper models are small. Clear speech with little background music works best, and Sinhala transcripts need more manual correction than English.<\/li>\n\n\n\n<li><strong>Voice availability depends on the device.<\/strong> The tool shows whether your browser has a voice for the chosen language. If not, export subtitles or use a cloud voice.<\/li>\n\n\n\n<li><strong>Speed-up changes pitch.<\/strong> Fitting long sentences by playing them faster can sound slightly high. Shorten the translated line in Tab 2 for a more natural result.<\/li>\n\n\n\n<li><strong>Recording depends on the browser.<\/strong> Tab audio capture works in desktop Chrome and Edge, and some systems do not pass the browser voice into the recording.<\/li>\n\n\n\n<li><strong>Large MP4 exports need memory.<\/strong> Dubbed video is made inside the browser, so short clips work best.<\/li>\n\n\n\n<li><strong>Cloud voices and translation use your own API key<\/strong>, and your provider may charge for heavy use.<\/li>\n<\/ul>\n\n\n\n<h2 id=\"\ud83d\udfe3-use-only-what-you-have-the-right-to-dub\" class=\"wp-block-heading\">\ud83d\udfe3 Use only what you have the right to dub<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Dub your own videos or ones you have permission to adapt. Read more about the craft in the <a href=\"https:\/\/en.wikipedia.org\/wiki\/Dubbing\" target=\"_blank\" rel=\"noopener\">Wikipedia article on dubbing<\/a>. To change a single spoken word instead of a whole video, try the <a href=\"https:\/\/schoolict.net\/word-level-audio-editor\/\">Word-Level Audio Editor<\/a>. To learn how the technology works, read <a href=\"https:\/\/schoolict.net\/how-speech-recognition-and-text-to-speech-work\/\">How Speech Recognition and Text-to-Speech Work<\/a> .<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Last updated: October 2026<\/p>\n\n\n\n<div class=\"sict-videodub-bottom\">\n  <h2>\ud83e\udd14 Frequently asked questions<\/h2>\n  <div class=\"sict-faq-card\"><p class=\"sict-faq-q\">Can I dub a video into Sinhala for free?<\/p><p class=\"sict-faq-a\">Transcribing, editing, subtitles and the live browser voice cost nothing. Translation needs a Gemini key, which has a free tier. A cloud voice for a downloadable dub uses your own key and may have limits or charges.<\/p><\/div>\n  <div class=\"sict-faq-card\"><p class=\"sict-faq-q\">Is my video uploaded to your server?<\/p><p class=\"sict-faq-a\">No. The file stays in your browser. Only the text of each sentence is sent to the translation or voice provider you choose.<\/p><\/div>\n  <div class=\"sict-faq-card\"><p class=\"sict-faq-q\">Why can&#8217;t I export audio with the Browser voice?<\/p><p class=\"sict-faq-a\">A browser speaks that voice straight to your speakers and does not hand over an audio file. Use a cloud engine, or press Start recording to capture the tab audio.<\/p><\/div>\n  <div class=\"sict-faq-card\"><p class=\"sict-faq-q\">The dubbed voice sounds too fast. What can I do?<\/p><p class=\"sict-faq-a\">Lower the Max speed-up slider, or shorten the translated sentence in Tab 2 so it fits the time slot naturally.<\/p><\/div>\n  <div class=\"sict-faq-card\"><p class=\"sict-faq-q\">How long can the video be?<\/p><p class=\"sict-faq-a\">Short and medium clips of up to about 10 minutes work best. For a long lesson, split it into parts and dub each one.<\/p><\/div>\n  <div class=\"sict-faq-card\"><p class=\"sict-faq-q\">Can I use only the subtitles?<\/p><p class=\"sict-faq-a\">Yes. After Tab 2 you can download an .srt or .vtt file and skip the voice steps entirely.<\/p><\/div>\n  <div class=\"sict-faq-card\"><p class=\"sict-faq-q\">Does it work on a phone?<\/p><p class=\"sict-faq-a\">Transcription, translation and subtitles work on most phones. Start recording needs desktop Chrome or Edge, and large video exports are better on a computer.<\/p><\/div>\n  <div class=\"sict-faq-card\"><p class=\"sict-faq-q\">Why does the first transcription take so long?<\/p><p class=\"sict-faq-a\">The speech model downloads once and is then cached by your browser. Later runs start much faster.<\/p><\/div>\n<\/div>\n","protected":false},"excerpt":{"rendered":"<p>Translate and Dub Videos Free Free video dubbing in your browser: transcribe, translate, add a voice and export SRT, WAV or MP4. Your video never leaves your device. \ud83d\udfe2 Dub inside your browser Your video is never uploaded. Speech is transcribed on your own device, so only short pieces of text go to the translation &#8230; <a title=\"Video Dub Studio | Translate and Dub Videos Free\" class=\"read-more\" href=\"https:\/\/schoolict.net\/tools\/video-dub-studio\/\" aria-label=\"Read more about Video Dub Studio | Translate and Dub Videos Free\">Read more<\/a><\/p>\n","protected":false},"author":1,"featured_media":7179,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"footnotes":""},"class_list":["post-7167","page","type-page","status-publish","has-post-thumbnail"],"_links":{"self":[{"href":"https:\/\/schoolict.net\/tools\/wp-json\/wp\/v2\/pages\/7167","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/schoolict.net\/tools\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/schoolict.net\/tools\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/schoolict.net\/tools\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/schoolict.net\/tools\/wp-json\/wp\/v2\/comments?post=7167"}],"version-history":[{"count":4,"href":"https:\/\/schoolict.net\/tools\/wp-json\/wp\/v2\/pages\/7167\/revisions"}],"predecessor-version":[{"id":7180,"href":"https:\/\/schoolict.net\/tools\/wp-json\/wp\/v2\/pages\/7167\/revisions\/7180"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/schoolict.net\/tools\/wp-json\/wp\/v2\/media\/7179"}],"wp:attachment":[{"href":"https:\/\/schoolict.net\/tools\/wp-json\/wp\/v2\/media?parent=7167"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}