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; }