diff --git a/changelog.md b/changelog.md
index a11bb3f8..6beefab8 100644
--- a/changelog.md
+++ b/changelog.md
@@ -13,7 +13,7 @@
- Добавлена поддержка 9animetv (#748)
- Добавлена поддержка EpicGames Developers (#255, #505)
- Добавлена поддержка Odysee (#755)
-
+- Автосгенериванные субтитры с YouTube, теперь, используют уже существующие токены, а не генерируют новые
- Изменен загрузчик стилей при сборке расширения, благодаря этому скорость сборки стала немного быстрее, а итоговый размер кода, отвечающего за стили, уменьшен в ~1.65 раза
- Исправлена работа расширения без наличия WebAudio (#749)
- Исправлена ошибка из-за которой кнопка перевода могла не появляться до первичного получения субтитров или завершения автоперевода
diff --git a/dist/vot-min.user.js b/dist/vot-min.user.js
index 3c997646..38e30b97 100644
--- a/dist/vot-min.user.js
+++ b/dist/vot-min.user.js
@@ -227,7 +227,7 @@
- `,c);const h=fe(a,r,s,{onSelectCb:l});return d.append(u.container,c,h.container),{container:d,fromSelect:u,icon:c,toSelect:h}},updateSlider:ve,createDetails:function(t){const e=document.createElement("vot-block");e.classList.add("vot-details");const o=document.createElement("vot-block");o.append(t);const n=document.createElement("vot-block");return n.classList.add("vot-details-arrow-icon"),ne(pe,n),e.append(o,n),{container:e,header:o,arrowIcon:n}}};const we={async translate(t,e){try{const o=await q(`${ce.yandex}?${new URLSearchParams({text:t,lang:e})}`,{timeout:3e3});if(o instanceof Error)throw o;const n=await o.json();if(200!==n.code)throw n.message;return n.text[0]}catch(e){return console.error("Error translating text:",e),t}},async detect(t){try{const e=await q(`${ue.yandex}?${new URLSearchParams({text:t})}`,{timeout:3e3});if(e instanceof Error)throw e;const o=await e.json();if(200!==o.code)throw o.message;return o.lang??"en"}catch(t){return console.error("Error getting lang from text:",t),"en"}}},xe={async detect(t){try{const e=await q(ue.rustServer,{method:"POST",body:t});if(e instanceof Error)throw e;return await e.text()}catch(t){return console.error("Error getting lang from text:",t),"en"}}},Se={async translate(t,e="auto",o="ru"){try{const n=await q(ce.deepl,{method:"POST",headers:{"content-type":"application/x-www-form-urlencoded"},body:new URLSearchParams({text:t,source_lang:e,target_lang:o})});if(n instanceof Error)throw n;const i=await n.json();if(200!==i.code)throw i.message;return i.data}catch(e){return console.error("Error translating text:",e),t}}};const ke=Object.keys(ce),Te=Object.keys(ue).map((t=>"rustServer"===t?"rust-server":t));async function Ve(t,e,o,n){if(!window.location.hostname.includes("m.youtube.com")&&t?.getAudioTrack){const e=t.getAudioTrack(),o=e?.getLanguageInfo();if("und"!==o?.id)return R(o.id.split(".")[0])}const i=e?.captions?.playerCaptionsTracklistRenderer?.captionTracks;if(i?.length){const t=i.find((t=>"asr"===t.kind));if(t&&t.languageCode)return R(t.languageCode)}const a=function(t,e){return`${t} ${e?e.split("\n").filter((t=>!F.test(t))).join(" "):""}`.slice(0,450).replace(/[^\p{L}\s]+|\s+/gu," ").trim()}(o,n);return C.log(`Detecting language text: ${a}`),async function(t){switch(await P.get("detectService",de)){case"yandex":return await we.detect(t);case"rust-server":return await xe.detect(t);default:return"en"}}(a)}function Le(){return/^m\.youtube\.com$/.test(window.location.hostname)}function Me(){return window.location.pathname.startsWith("/shorts/")&&!Le()?document.querySelector("#shorts-player"):document.querySelector("#movie_player")}function Ae(){const t=Me();return t?.getPlayerResponse?t?.getPlayerResponse?.call()??null:t?.data?.playerResponse??null}function Oe(){const t=Me();return t?.getVideoData?t?.getVideoData?.call()??null:t?.data?.playerResponse?.videoDetails??null}const Ie={isMobile:Le,getPlayer:Me,getPlayerResponse:Ae,getPlayerData:Oe,getVideoVolume:function(){const t=Me();return t?.getVolume?t.getVolume.call()/100:1},getSubtitles:function(){const t=Ae();let e=t?.captions?.playerCaptionsTracklistRenderer?.captionTracks??[];return e=e.reduce(((t,e)=>{if("languageCode"in e){const o=e?.languageCode?R(e?.languageCode):void 0,n=e?.url||e?.baseUrl;o&&n&&t.push({source:"youtube",language:o,isAutoGenerated:"asr"===e?.kind,url:`${n.startsWith("http")?n:`${window.location.origin}/${n}`}&fmt=json3`})}return t}),[]),C.log("youtube subtitles:",e),e},getVideoData:async function(){const t=Me(),e=Ae(),o=Oe(),{title:n}=o??{},{shortDescription:i,isLive:a}=e?.videoDetails??{};let r=n?await Ve(t,e,n,i):"en";r=nt.includes(r)?r:"en";const s={isLive:!!a,title:n,description:i,detectedLanguage:r};return C.log("youtube video data:",s),console.log("[VOT] Detected language: ",s.detectedLanguage),s},setVideoVolume:function(t){const e=Me();if(e?.setVolume)return e.setVolume(Math.round(100*t)),!0},videoSeek:function(t,e){C.log("videoSeek",e);const o=(Me()?.getProgressState()?.seekableEnd||t.currentTime)-e;t.currentTime=o},isMuted:function(){const t=Me();return!!t?.isMuted&&t.isMuted.call()},isMusic:function(){const t=Oe().author,e=Oe().title.toUpperCase(),o=e.match(/\w+/g),n=document.body.querySelector("ytd-watch-flexy")?.playerData;return[e,document.URL,t,n?.microformat?.playerMicroformatRenderer.category,n?.title].some((t=>t?.toUpperCase().includes("MUSIC")))||document.body.querySelector("#upload-info #channel-name .badge-style-type-verified-artist")||t&&/(VEVO|Topic|Records|RECORDS|Recordings|AMV)$/.test(t)||t&&/(MUSIC|ROCK|SOUNDS|SONGS)/.test(t.toUpperCase())||o?.length&&["🎵","♫","SONG","SONGS","SOUNDTRACK","LYRIC","LYRICS","AMBIENT","MIX","VEVO","CLIP","KARAOKE","OPENING","COVER","COVERED","VOCAL","INSTRUMENTAL","ORCHESTRAL","DUBSTEP","DJ","DNB","BASS","BEAT","ALBUM","PLAYLIST","DUBSTEP","CHILL","RELAX","CLASSIC","CINEMATIC"].some((t=>o.includes(t)))||["OFFICIAL VIDEO","OFFICIAL AUDIO","FEAT.","FT.","LIVE RADIO","DANCE VER","HIP HOP","ROCK N ROLL","HOUR VER","HOURS VER","INTRO THEME"].some((t=>e.includes(t)))||o?.length&&["OP","ED","MV","OST","NCS","BGM","EDM","GMV","AMV","MMD","MAD"].some((t=>o.includes(t)))}};function Ce(t){const e=t.startMs+t.durationMs;return t.tokens.reduce(((o,n,i)=>{const a=t.tokens[i+1];let r;o.length>0&&(r=o[o.length-1]);const s=r?.alignRange?.end??0,l=s+n.text.length;if(n.alignRange={start:s,end:l},o.push(n),a){const t=n.startMs+n.durationMs,i=a.startMs?a.startMs-t:e-t;o.push({text:" ",startMs:t,durationMs:i,alignRange:{start:l,end:l+1}})}return o}),[])}function Pe(t,e){const o=t.text.split(/([\n \t])/).reduce(((t,o)=>{if(o.length){const n=t[t.length-1]??e,i=n?.alignRange?.end??0,a=i+o.length;t.push({text:o,alignRange:{start:i,end:a}})}return t}),[]),n=Math.floor(t.durationMs/o.length),i=t.startMs+t.durationMs;return o.map(((e,a)=>{const r=a===o.length-1,s=t.startMs+n*a;return{...e,startMs:s,durationMs:r?i-s:n}}))}async function Ee(t){const e=(async()=>{try{const e=await q(t.url,{timeout:5e3});return await e.json()}catch(t){return console.error("[VOT] Failed to fetch subtitles.",t),{containsTokens:!1,subtitles:[]}}})();let o=await e;return"youtube"===t.source&&(o=function(t){const e={containsTokens:!1,subtitles:[]};if("object"!=typeof t||!("events"in t)||!Array.isArray(t.events))return console.error("[VOT] Failed to format youtube subtitles",t),e;for(let o=0;ot.utf8.replace(/^( +| +)$/g,""))).join("");let i=t.events[o].dDurationMs;t.events[o+1]&&t.events[o].tStartMs+t.events[o].dDurationMs>t.events[o+1].tStartMs&&(i=t.events[o+1].tStartMs-t.events[o].tStartMs),"\n"!==n&&e.subtitles.push({text:n,startMs:t.events[o].tStartMs,durationMs:i})}return e}(o)),o.subtitles=function(t,e){const o=[];let n;for(let i=0;isetTimeout((()=>e(new Error("Timeout"))),5e3)));try{const e=await Promise.race([t.getSubtitles({videoData:{host:o,url:n,videoId:a,duration:r},requestLang:i}),l]);console.log("[VOT] Subtitles response: ",e),e.waiting&&console.error("[VOT] Failed to get yandex subtitles");let d=e.subtitles??[];return d=d.reduce(((t,e)=>(e.language&&!t.find((t=>"yandex"===t.source&&t.language===e.language&&!t.translatedFromLanguage))&&t.push({source:"yandex",language:e.language,url:e.url}),e.translatedLanguage&&t.push({source:"yandex",language:e.translatedLanguage,translatedFromLanguage:e.language,url:e.translatedUrl}),t)),[]),[...d,...s].sort(((t,e)=>{if(t.source!==e.source)return"yandex"===t.source?-1:1;if(t.language!==e.language&&(t.language===$||e.language===$))return t.language===$?-1:1;if("yandex"===t.source){if(t.translatedFromLanguage!==e.translatedFromLanguage)return t.translatedFromLanguage&&e.translatedFromLanguage?t.translatedFromLanguage===i?-1:1:t.language===e.language?t.translatedFromLanguage?1:-1:t.translatedFromLanguage?-1:1;if(!t.translatedFromLanguage)return t.language===i?-1:1}return"youtube"===t.source&&t.isAutoGenerated!==e.isAutoGenerated?t.isAutoGenerated?1:-1:0}))}catch(t){throw"Timeout"===t.message?console.error("[VOT] Failed to get yandex subtitles. Reason: timeout"):console.error("[VOT] Error in getSubtitles function",t),t}}class Ne{constructor(t,e,o){this.video=t,this.container="youtube"===o.host&&"mobile"!==o.additionalData?e.parentElement:e,this.site=o,this.subtitlesContainer=this.createSubtitlesContainer(),this.position={left:25,top:75},this.dragging={active:!1,offset:{x:0,y:0}},this.subtitles=null,this.lastContent=null,this.highlightWords=!1,this.fontSize=20,this.opacity=.2,this.maxLength=300,this.maxLengthRegexp=/.{1,300}(?:\s|$)/g,this.bindEvents(),this.updateContainerRect()}createSubtitlesContainer(){const t=document.createElement("vot-block");return t.classList.add("vot-subtitles-widget"),this.container.appendChild(t),t}bindEvents(){this.onMouseDownBound=t=>this.onMouseDown(t),this.onMouseUpBound=()=>this.onMouseUp(),this.onMouseMoveBound=t=>this.onMouseMove(t),this.onTimeUpdateBound=this.debounce((()=>this.update()),100),document.addEventListener("mousedown",this.onMouseDownBound),document.addEventListener("mouseup",this.onMouseUpBound),document.addEventListener("mousemove",this.onMouseMoveBound),this.video?.addEventListener("timeupdate",this.onTimeUpdateBound),this.resizeObserver=new ResizeObserver((()=>this.onResize())),this.resizeObserver.observe(this.container)}onMouseDown(t){if(this.subtitlesContainer.contains(t.target)){const e=this.subtitlesContainer.getBoundingClientRect(),o=this.container.getBoundingClientRect();this.dragging={active:!0,offset:{x:t.clientX-e.left,y:t.clientY-e.top},containerOffset:{x:o.left,y:o.top}}}}onMouseUp(){this.dragging.active=!1}onMouseMove(t){if(this.dragging.active){t.preventDefault();const{width:e,height:o}=this.container.getBoundingClientRect(),n=this.dragging.containerOffset;this.position={left:(t.clientX-this.dragging.offset.x-n.x)/e*100,top:(t.clientY-this.dragging.offset.y-n.y)/o*100},this.applySubtitlePosition()}}onResize(){this.updateContainerRect()}updateContainerRect(){this.containerRect=this.container.getBoundingClientRect(),this.applySubtitlePosition()}applySubtitlePosition(){const{width:t,height:e}=this.containerRect,{offsetWidth:o,offsetHeight:n}=this.subtitlesContainer,i=(t-o)/t*100,a=(e-n)/e*100;this.position.left=Math.max(0,Math.min(this.position.left,i)),this.position.top=Math.max(0,Math.min(this.position.top,a)),this.subtitlesContainer.style.left=`${this.position.left}%`,this.subtitlesContainer.style.top=`${this.position.top}%`}setContent(t){t&&this.video?(this.subtitles=t,this.update()):(this.subtitles=null,ne(null,this.subtitlesContainer))}setMaxLength(t){"number"==typeof t&&t&&(this.maxLength=t,this.maxLengthRegexp=new RegExp(`.{1,${t}}(?:\\s|$)`,"g"),this.update())}setHighlightWords(t){this.highlightWords=Boolean(t),this.update()}setFontSize(t){this.fontSize=t;const e=this.subtitlesContainer?.querySelector(".vot-subtitles");e&&(e.style.fontSize=`${this.fontSize}px`)}setOpacity(t){this.opacity=((100-+t)/100).toFixed(2);const e=this.subtitlesContainer?.querySelector(".vot-subtitles");e&&e.style.setProperty("--vot-subtitles-opacity",this.opacity)}update(){if(!this.video||!this.subtitles)return;const t=1e3*this.video.currentTime,e=this.subtitles.subtitles?.findLast((e=>e.startMs`,c);const h=fe(a,r,s,{onSelectCb:l});return d.append(u.container,c,h.container),{container:d,fromSelect:u,icon:c,toSelect:h}},updateSlider:ve,createDetails:function(t){const e=document.createElement("vot-block");e.classList.add("vot-details");const o=document.createElement("vot-block");o.append(t);const n=document.createElement("vot-block");return n.classList.add("vot-details-arrow-icon"),ne(pe,n),e.append(o,n),{container:e,header:o,arrowIcon:n}}};const we={async translate(t,e){try{const o=await q(`${ce.yandex}?${new URLSearchParams({text:t,lang:e})}`,{timeout:3e3});if(o instanceof Error)throw o;const n=await o.json();if(200!==n.code)throw n.message;return n.text[0]}catch(e){return console.error("Error translating text:",e),t}},async detect(t){try{const e=await q(`${ue.yandex}?${new URLSearchParams({text:t})}`,{timeout:3e3});if(e instanceof Error)throw e;const o=await e.json();if(200!==o.code)throw o.message;return o.lang??"en"}catch(t){return console.error("Error getting lang from text:",t),"en"}}},xe={async detect(t){try{const e=await q(ue.rustServer,{method:"POST",body:t});if(e instanceof Error)throw e;return await e.text()}catch(t){return console.error("Error getting lang from text:",t),"en"}}},Se={async translate(t,e="auto",o="ru"){try{const n=await q(ce.deepl,{method:"POST",headers:{"content-type":"application/x-www-form-urlencoded"},body:new URLSearchParams({text:t,source_lang:e,target_lang:o})});if(n instanceof Error)throw n;const i=await n.json();if(200!==i.code)throw i.message;return i.data}catch(e){return console.error("Error translating text:",e),t}}};const ke=Object.keys(ce),Te=Object.keys(ue).map((t=>"rustServer"===t?"rust-server":t));async function Ve(t,e,o,n){if(!window.location.hostname.includes("m.youtube.com")&&t?.getAudioTrack){const e=t.getAudioTrack(),o=e?.getLanguageInfo();if("und"!==o?.id)return R(o.id.split(".")[0])}const i=e?.captions?.playerCaptionsTracklistRenderer?.captionTracks;if(i?.length){const t=i.find((t=>"asr"===t.kind));if(t&&t.languageCode)return R(t.languageCode)}const a=function(t,e){return`${t} ${e?e.split("\n").filter((t=>!F.test(t))).join(" "):""}`.slice(0,450).replace(/[^\p{L}\s]+|\s+/gu," ").trim()}(o,n);return C.log(`Detecting language text: ${a}`),async function(t){switch(await P.get("detectService",de)){case"yandex":return await we.detect(t);case"rust-server":return await xe.detect(t);default:return"en"}}(a)}function Le(){return/^m\.youtube\.com$/.test(window.location.hostname)}function Me(){return window.location.pathname.startsWith("/shorts/")&&!Le()?document.querySelector("#shorts-player"):document.querySelector("#movie_player")}function Ae(){const t=Me();return t?.getPlayerResponse?t?.getPlayerResponse?.call()??null:t?.data?.playerResponse??null}function Oe(){const t=Me();return t?.getVideoData?t?.getVideoData?.call()??null:t?.data?.playerResponse?.videoDetails??null}const Ie={isMobile:Le,getPlayer:Me,getPlayerResponse:Ae,getPlayerData:Oe,getVideoVolume:function(){const t=Me();return t?.getVolume?t.getVolume.call()/100:1},getSubtitles:function(){const t=Ae();let e=t?.captions?.playerCaptionsTracklistRenderer?.captionTracks??[];return e=e.reduce(((t,e)=>{if("languageCode"in e){const o=e?.languageCode?R(e?.languageCode):void 0,n=e?.url||e?.baseUrl;o&&n&&t.push({source:"youtube",language:o,isAutoGenerated:"asr"===e?.kind,url:`${n.startsWith("http")?n:`${window.location.origin}/${n}`}&fmt=json3`})}return t}),[]),C.log("youtube subtitles:",e),e},getVideoData:async function(){const t=Me(),e=Ae(),o=Oe(),{title:n}=o??{},{shortDescription:i,isLive:a}=e?.videoDetails??{};let r=n?await Ve(t,e,n,i):"en";r=nt.includes(r)?r:"en";const s={isLive:!!a,title:n,description:i,detectedLanguage:r};return C.log("youtube video data:",s),console.log("[VOT] Detected language: ",s.detectedLanguage),s},setVideoVolume:function(t){const e=Me();if(e?.setVolume)return e.setVolume(Math.round(100*t)),!0},videoSeek:function(t,e){C.log("videoSeek",e);const o=(Me()?.getProgressState()?.seekableEnd||t.currentTime)-e;t.currentTime=o},isMuted:function(){const t=Me();return!!t?.isMuted&&t.isMuted.call()},isMusic:function(){const t=Oe().author,e=Oe().title.toUpperCase(),o=e.match(/\w+/g),n=document.body.querySelector("ytd-watch-flexy")?.playerData;return[e,document.URL,t,n?.microformat?.playerMicroformatRenderer.category,n?.title].some((t=>t?.toUpperCase().includes("MUSIC")))||document.body.querySelector("#upload-info #channel-name .badge-style-type-verified-artist")||t&&/(VEVO|Topic|Records|RECORDS|Recordings|AMV)$/.test(t)||t&&/(MUSIC|ROCK|SOUNDS|SONGS)/.test(t.toUpperCase())||o?.length&&["🎵","♫","SONG","SONGS","SOUNDTRACK","LYRIC","LYRICS","AMBIENT","MIX","VEVO","CLIP","KARAOKE","OPENING","COVER","COVERED","VOCAL","INSTRUMENTAL","ORCHESTRAL","DUBSTEP","DJ","DNB","BASS","BEAT","ALBUM","PLAYLIST","DUBSTEP","CHILL","RELAX","CLASSIC","CINEMATIC"].some((t=>o.includes(t)))||["OFFICIAL VIDEO","OFFICIAL AUDIO","FEAT.","FT.","LIVE RADIO","DANCE VER","HIP HOP","ROCK N ROLL","HOUR VER","HOURS VER","INTRO THEME"].some((t=>e.includes(t)))||o?.length&&["OP","ED","MV","OST","NCS","BGM","EDM","GMV","AMV","MMD","MAD"].some((t=>o.includes(t)))}};function Ce(t){const e=t.startMs+t.durationMs;return t.tokens.reduce(((o,n,i)=>{const a=t.tokens[i+1];let r;o.length>0&&(r=o[o.length-1]);const s=r?.alignRange?.end??0,l=s+n.text.length;if(n.alignRange={start:s,end:l},o.push(n),a){const t=n.startMs+n.durationMs,i=a.startMs?a.startMs-t:e-t;o.push({text:" ",startMs:t,durationMs:i,alignRange:{start:l,end:l+1}})}return o}),[])}function Pe(t,e){const o=t.text.split(/([\n \t])/).reduce(((t,o)=>{if(o.length){const n=t[t.length-1]??e,i=n?.alignRange?.end??0,a=i+o.length;t.push({text:o,alignRange:{start:i,end:a}})}return t}),[]),n=Math.floor(t.durationMs/o.length),i=t.startMs+t.durationMs;return o.map(((e,a)=>{const r=a===o.length-1,s=t.startMs+n*a;return{...e,startMs:s,durationMs:r?i-s:n}}))}async function Ee(t){const e=(async()=>{try{const e=await q(t.url,{timeout:5e3});return await e.json()}catch(t){return console.error("[VOT] Failed to fetch subtitles.",t),{containsTokens:!1,subtitles:[]}}})();let o=await e;const{source:n,isAutoGenerated:i}=t;return"youtube"===n&&(o=function(t,e=!1){const o={containsTokens:e,subtitles:[]};if("object"!=typeof t||!Array.isArray(t.events))return console.error("[VOT] Failed to format youtube subtitles",t),o;for(let n=0;nt.events[n+1].tStartMs&&(a=t.events[n+1].tStartMs-i.tStartMs);const r=[];let s=a;for(let t=0;tt.text)).join(" ");l&&o.subtitles.push({text:l,startMs:i.tStartMs,durationMs:a,...e?{tokens:r}:{},speakerId:"0"})}return o}(o,i)),o.subtitles=function(t,e){const o=[];let n;const{source:i,isAutoGenerated:a}=e;for(let e=0;esetTimeout((()=>e(new Error("Timeout"))),5e3)));try{const e=await Promise.race([t.getSubtitles({videoData:{host:o,url:n,videoId:a,duration:r},requestLang:i}),l]);console.log("[VOT] Subtitles response: ",e),e.waiting&&console.error("[VOT] Failed to get yandex subtitles");let d=e.subtitles??[];return d=d.reduce(((t,e)=>(e.language&&!t.find((t=>"yandex"===t.source&&t.language===e.language&&!t.translatedFromLanguage))&&t.push({source:"yandex",language:e.language,url:e.url}),e.translatedLanguage&&t.push({source:"yandex",language:e.translatedLanguage,translatedFromLanguage:e.language,url:e.translatedUrl}),t)),[]),[...d,...s].sort(((t,e)=>{if(t.source!==e.source)return"yandex"===t.source?-1:1;if(t.language!==e.language&&(t.language===$||e.language===$))return t.language===$?-1:1;if("yandex"===t.source){if(t.translatedFromLanguage!==e.translatedFromLanguage)return t.translatedFromLanguage&&e.translatedFromLanguage?t.translatedFromLanguage===i?-1:1:t.language===e.language?t.translatedFromLanguage?1:-1:t.translatedFromLanguage?-1:1;if(!t.translatedFromLanguage)return t.language===i?-1:1}return"youtube"===t.source&&t.isAutoGenerated!==e.isAutoGenerated?t.isAutoGenerated?1:-1:0}))}catch(t){throw"Timeout"===t.message?console.error("[VOT] Failed to get yandex subtitles. Reason: timeout"):console.error("[VOT] Error in getSubtitles function",t),t}}class Ne{constructor(t,e,o){this.video=t,this.container="youtube"===o.host&&"mobile"!==o.additionalData?e.parentElement:e,this.site=o,this.subtitlesContainer=this.createSubtitlesContainer(),this.position={left:25,top:75},this.dragging={active:!1,offset:{x:0,y:0}},this.subtitles=null,this.lastContent=null,this.highlightWords=!1,this.fontSize=20,this.opacity=.2,this.maxLength=300,this.maxLengthRegexp=/.{1,300}(?:\s|$)/g,this.bindEvents(),this.updateContainerRect()}createSubtitlesContainer(){const t=document.createElement("vot-block");return t.classList.add("vot-subtitles-widget"),this.container.appendChild(t),t}bindEvents(){this.onMouseDownBound=t=>this.onMouseDown(t),this.onMouseUpBound=()=>this.onMouseUp(),this.onMouseMoveBound=t=>this.onMouseMove(t),this.onTimeUpdateBound=this.debounce((()=>this.update()),100),document.addEventListener("mousedown",this.onMouseDownBound),document.addEventListener("mouseup",this.onMouseUpBound),document.addEventListener("mousemove",this.onMouseMoveBound),this.video?.addEventListener("timeupdate",this.onTimeUpdateBound),this.resizeObserver=new ResizeObserver((()=>this.onResize())),this.resizeObserver.observe(this.container)}onMouseDown(t){if(this.subtitlesContainer.contains(t.target)){const e=this.subtitlesContainer.getBoundingClientRect(),o=this.container.getBoundingClientRect();this.dragging={active:!0,offset:{x:t.clientX-e.left,y:t.clientY-e.top},containerOffset:{x:o.left,y:o.top}}}}onMouseUp(){this.dragging.active=!1}onMouseMove(t){if(this.dragging.active){t.preventDefault();const{width:e,height:o}=this.container.getBoundingClientRect(),n=this.dragging.containerOffset;this.position={left:(t.clientX-this.dragging.offset.x-n.x)/e*100,top:(t.clientY-this.dragging.offset.y-n.y)/o*100},this.applySubtitlePosition()}}onResize(){this.updateContainerRect()}updateContainerRect(){this.containerRect=this.container.getBoundingClientRect(),this.applySubtitlePosition()}applySubtitlePosition(){const{width:t,height:e}=this.containerRect,{offsetWidth:o,offsetHeight:n}=this.subtitlesContainer,i=(t-o)/t*100,a=(e-n)/e*100;this.position.left=Math.max(0,Math.min(this.position.left,i)),this.position.top=Math.max(0,Math.min(this.position.top,a)),this.subtitlesContainer.style.left=`${this.position.left}%`,this.subtitlesContainer.style.top=`${this.position.top}%`}setContent(t){t&&this.video?(this.subtitles=t,this.update()):(this.subtitles=null,ne(null,this.subtitlesContainer))}setMaxLength(t){"number"==typeof t&&t&&(this.maxLength=t,this.maxLengthRegexp=new RegExp(`.{1,${t}}(?:\\s|$)`,"g"),this.update())}setHighlightWords(t){this.highlightWords=Boolean(t),this.update()}setFontSize(t){this.fontSize=t;const e=this.subtitlesContainer?.querySelector(".vot-subtitles");e&&(e.style.fontSize=`${this.fontSize}px`)}setOpacity(t){this.opacity=((100-+t)/100).toFixed(2);const e=this.subtitlesContainer?.querySelector(".vot-subtitles");e&&e.style.setProperty("--vot-subtitles-opacity",this.opacity)}update(){if(!this.video||!this.subtitles)return;const t=1e3*this.video.currentTime,e=this.subtitles.subtitles?.findLast((e=>e.startMs${n} e.utf8.replace(/^( +| +)$/g, ""))
- .join("");
- let durationMs = subtitles.events[i].dDurationMs;
+ const subtitle = subtitles.events[i];
+ if (!subtitle.segs) continue;
+
+ let durationMs = subtitle.dDurationMs;
if (
subtitles.events[i + 1] &&
- subtitles.events[i].tStartMs + subtitles.events[i].dDurationMs >
+ subtitle.tStartMs + subtitle.dDurationMs >
subtitles.events[i + 1].tStartMs
) {
- durationMs =
- subtitles.events[i + 1].tStartMs - subtitles.events[i].tStartMs;
+ durationMs = subtitles.events[i + 1].tStartMs - subtitle.tStartMs;
}
- if (text !== "\n") {
+
+ const tokens = [];
+ let lastSegDuration = durationMs;
+ for (let i = 0; i < subtitle.segs.length; i++) {
+ const seg = subtitle.segs[i];
+ const text = seg.utf8.trim();
+ if (text === "\n") {
+ continue;
+ }
+
+ const offset = seg.tOffsetMs ?? 0;
+ let segDuration = durationMs;
+ const nextSeg = subtitle.segs[i + 1];
+ if (nextSeg?.tOffsetMs) {
+ segDuration = nextSeg.tOffsetMs - offset;
+ lastSegDuration -= segDuration;
+ }
+
+ tokens.push({
+ text,
+ startMs: subtitle.tStartMs + offset,
+ durationMs: nextSeg ? segDuration : lastSegDuration,
+ });
+ }
+
+ const text = tokens.map((e) => e.text).join(" ");
+ if (text) {
result.subtitles.push({
text,
- startMs: subtitles.events[i].tStartMs,
+ startMs: subtitle.tStartMs,
durationMs,
+ ...(isAsr ? { tokens } : {}),
+ speakerId: "0",
});
}
}
@@ -5515,12 +5537,12 @@ async function fetchSubtitles(subtitlesObject) {
})();
let subtitles = await fetchPromise;
-
- if (subtitlesObject.source === "youtube") {
- subtitles = formatYoutubeSubtitles(subtitles);
+ const { source, isAutoGenerated } = subtitlesObject;
+ if (source === "youtube") {
+ subtitles = formatYoutubeSubtitles(subtitles, isAutoGenerated);
}
- subtitles.subtitles = getSubtitlesTokens(subtitles, subtitlesObject.source);
+ subtitles.subtitles = getSubtitlesTokens(subtitles, subtitlesObject);
console.log("[VOT] subtitles:", subtitles);
return subtitles;
}
@@ -6259,8 +6281,8 @@ class VideoHandler {
await this.updateTranslationErrorMsg(
res.remainingTime > 0
? secsToStrTime(res.remainingTime)
- : (res.message ??
- localizationProvider.get("translationTakeFewMinutes")),
+ : res.message ??
+ localizationProvider.get("translationTakeFewMinutes"),
);
} catch (err) {
console.error("[VOT] Failed to translate video", err);
diff --git a/src/subtitles.js b/src/subtitles.js
index 72218253..6af33319 100644
--- a/src/subtitles.js
+++ b/src/subtitles.js
@@ -67,23 +67,22 @@ function createSubtitlesTokens(line, previousLineLastToken) {
});
}
-function getSubtitlesTokens(subtitles, source) {
+function getSubtitlesTokens(subtitles, subtitlesObject) {
const result = [];
let lastToken;
+ const { source, isAutoGenerated } = subtitlesObject;
for (let i = 0; i < subtitles.subtitles.length; i++) {
const line = subtitles.subtitles[i];
- let tokens;
- if (line?.tokens?.length) {
- if (source === "yandex") {
- tokens = formatYandexSubtitlesTokens(line);
- } else {
- console.warn("[VOT] Unsupported subtitles tokens type: ", source);
- subtitles.containsTokens = false;
- return null;
- }
- } else {
- tokens = createSubtitlesTokens(line, lastToken);
+ if (line?.tokens?.length && !["yandex", "youtube"].includes(source)) {
+ console.warn("[VOT] Unsupported subtitles tokens type: ", source);
+ subtitles.containsTokens = false;
+ return null;
}
+
+ let tokens =
+ source === "yandex" || (source === "youtube" && isAutoGenerated)
+ ? formatYandexSubtitlesTokens(line)
+ : createSubtitlesTokens(line, lastToken);
lastToken = tokens[tokens.length - 1];
result.push({
...line,
@@ -94,38 +93,61 @@ function getSubtitlesTokens(subtitles, source) {
return result;
}
-function formatYoutubeSubtitles(subtitles) {
+function formatYoutubeSubtitles(subtitles, isAsr = false) {
const result = {
- containsTokens: false,
+ containsTokens: isAsr,
subtitles: [],
};
- if (
- typeof subtitles !== "object" ||
- !("events" in subtitles) ||
- !Array.isArray(subtitles.events)
- ) {
+ if (typeof subtitles !== "object" || !Array.isArray(subtitles.events)) {
console.error("[VOT] Failed to format youtube subtitles", subtitles);
return result;
}
+
for (let i = 0; i < subtitles.events.length; i++) {
- if (!subtitles.events[i].segs) continue;
- const text = subtitles.events[i].segs
- .map((e) => e.utf8.replace(/^( +| +)$/g, ""))
- .join("");
- let durationMs = subtitles.events[i].dDurationMs;
+ const subtitle = subtitles.events[i];
+ if (!subtitle.segs) continue;
+
+ let durationMs = subtitle.dDurationMs;
if (
subtitles.events[i + 1] &&
- subtitles.events[i].tStartMs + subtitles.events[i].dDurationMs >
+ subtitle.tStartMs + subtitle.dDurationMs >
subtitles.events[i + 1].tStartMs
) {
- durationMs =
- subtitles.events[i + 1].tStartMs - subtitles.events[i].tStartMs;
+ durationMs = subtitles.events[i + 1].tStartMs - subtitle.tStartMs;
}
- if (text !== "\n") {
+
+ const tokens = [];
+ let lastSegDuration = durationMs;
+ for (let i = 0; i < subtitle.segs.length; i++) {
+ const seg = subtitle.segs[i];
+ const text = seg.utf8.trim();
+ if (text === "\n") {
+ continue;
+ }
+
+ const offset = seg.tOffsetMs ?? 0;
+ let segDuration = durationMs;
+ const nextSeg = subtitle.segs[i + 1];
+ if (nextSeg?.tOffsetMs) {
+ segDuration = nextSeg.tOffsetMs - offset;
+ lastSegDuration -= segDuration;
+ }
+
+ tokens.push({
+ text,
+ startMs: subtitle.tStartMs + offset,
+ durationMs: nextSeg ? segDuration : lastSegDuration,
+ });
+ }
+
+ const text = tokens.map((e) => e.text).join(" ");
+ if (text) {
result.subtitles.push({
text,
- startMs: subtitles.events[i].tStartMs,
+ startMs: subtitle.tStartMs,
durationMs,
+ ...(isAsr ? { tokens } : {}),
+ speakerId: "0",
});
}
}
@@ -147,12 +169,12 @@ export async function fetchSubtitles(subtitlesObject) {
})();
let subtitles = await fetchPromise;
-
- if (subtitlesObject.source === "youtube") {
- subtitles = formatYoutubeSubtitles(subtitles);
+ const { source, isAutoGenerated } = subtitlesObject;
+ if (source === "youtube") {
+ subtitles = formatYoutubeSubtitles(subtitles, isAutoGenerated);
}
- subtitles.subtitles = getSubtitlesTokens(subtitles, subtitlesObject.source);
+ subtitles.subtitles = getSubtitlesTokens(subtitles, subtitlesObject);
console.log("[VOT] subtitles:", subtitles);
return subtitles;
}