-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmain.js
More file actions
17 lines (13 loc) · 32.1 KB
/
Copy pathmain.js
File metadata and controls
17 lines (13 loc) · 32.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/*
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
If you want to view the source, please visit the github repository.
*/
var G=Object.create;var P=Object.defineProperty;var Q=Object.getOwnPropertyDescriptor;var Z=Object.getOwnPropertyNames;var tt=Object.getPrototypeOf,et=Object.prototype.hasOwnProperty;var st=(n,s)=>{for(var t in s)P(n,t,{get:s[t],enumerable:!0})},M=(n,s,t,e)=>{if(s&&typeof s=="object"||typeof s=="function")for(let i of Z(s))!et.call(n,i)&&i!==t&&P(n,i,{get:()=>s[i],enumerable:!(e=Q(s,i))||e.enumerable});return n};var S=(n,s,t)=>(t=n!=null?G(tt(n)):{},M(s||!n||!n.__esModule?P(t,"default",{value:n,enumerable:!0}):t,n)),it=n=>M(P({},"__esModule",{value:!0}),n);var gt={};st(gt,{default:()=>L});module.exports=it(gt);var a=require("obsidian"),f=S(require("child_process")),d=S(require("fs")),j=S(require("https")),z=S(require("http")),b=S(require("os")),g=S(require("path")),$="d84d6edb62e3e5aba94fc24930d70266a772d45a",nt="http://127.0.0.1:51273";function rt(){if(a.Platform.isMacOS)return[g.join(b.homedir(),"Library","Application Support","LocalTTS","local-tts-service","ttsctl.sh")];if(a.Platform.isWin){let n=process.env.LOCALAPPDATA||g.join(b.homedir(),"AppData","Local");return[g.join(n,"LocalTTS","local-tts-service","ttsctl.ps1")]}return[g.join(b.homedir(),".local","share","local-tts-service","ttsctl.sh")]}function at(n){try{return d.existsSync(n)}catch(s){return!1}}var E={outputFolder:".open-reader/audio",speed:1,playbackSpeed:1,maxChunkCharacters:450,stripFrontmatter:!0,skipCodeBlocks:!0,keepAudioFiles:!1,openFolderAfterSynthesis:!1,showProgressBar:!0,enableDragToMove:!0,customCharsToFilter:"",filterHtmlTags:!0,filterExtraWhitespace:!0},O=class O extends a.Plugin{constructor(){super(...arguments);this.currentAudio=null;this.objectUrls=[];this.generatedFiles=[];this.shouldStop=!1;this.controllerEl=null;this.controllerStatusEl=null;this.controllerTitleEl=null;this.pauseButtonEl=null;this.resumeButtonEl=null;this.finishCurrentPlayback=null;this.activeFilePath=null;this.progressBarEl=null;this.progressTextEl=null;this.fileNameEl=null;this.speedButtonEls=[];this.layoutChangeCallback=null}async onload(){await this.loadSettings(),this.statusBarEl=this.addStatusBarItem(),this.updateStatus("idle"),this.createController(),this.addRibbonIcon("volume-2","Read note aloud or show controller",()=>{(!this.controllerEl||!this.controllerEl.isShown())&&this.showController(),this.readActiveDocument()}),this.addCommand({id:"show-tts-controller",name:"Show TTS controller",callback:()=>{this.showController()}}),this.addCommand({id:"read-selection-or-note",name:"Read selected text or active note aloud",callback:()=>{this.readActiveDocument()}}),this.addCommand({id:"pause-reading",name:"Pause reading",callback:()=>this.pauseReading()}),this.addCommand({id:"resume-reading",name:"Resume reading",callback:()=>{this.resumeReading()}}),this.addCommand({id:"stop-reading",name:"Stop reading",callback:()=>this.stopReading()}),this.addCommand({id:"test-local-tts-cli",name:"Test Local TTS CLI",callback:()=>{this.testLocalTtsCli()}}),this.addCommand({id:"open-tts-output-folder",name:"Open TTS output folder",callback:()=>{this.openOutputFolder()}}),this.addSettingTab(new R(this.app,this))}onunload(){var t;this.stopReading(!1),this.unregisterFileCloseListener(),(t=this.controllerEl)==null||t.remove()}isSourceFileStillOpen(){return this.activeFilePath?this.app.workspace.getLeavesOfType("markdown").some(t=>{var e;return t.view instanceof a.MarkdownView&&((e=t.view.file)==null?void 0:e.path)===this.activeFilePath}):!1}registerFileCloseListener(){this.unregisterFileCloseListener(),this.layoutChangeCallback=()=>{this.activeFilePath&&(this.isSourceFileStillOpen()||this.stopReading(!0))},this.app.workspace.on("layout-change",this.layoutChangeCallback)}unregisterFileCloseListener(){this.layoutChangeCallback&&(this.app.workspace.off("layout-change",this.layoutChangeCallback),this.layoutChangeCallback=null)}async loadSettings(){let t=await this.loadData();this.settings=Object.assign({},E,q(t)?t:{}),this.removeLegacySharedFields()}getTtsctlPath(){let t=this.detectTtsctlPath();if(!t)throw new Error("Local TTS is not installed. Open settings to install it.");return t}detectTtsctlPath(){return rt().find(at)||null}removeLegacySharedFields(){let t=this.settings;delete t.systemName,delete t.ttsctlPath,delete t.windowsTtsctlPath,delete t.macTtsctlPath,delete t.linuxTtsctlPath,delete t.ttsctlPathsBySystem}async saveSettings(){await this.saveData(this.settings)}async installLocalTts(){if(!a.Platform.isMacOS&&!a.Platform.isWin){new a.Notice("Open Reader: one-click Local TTS installation currently supports macOS and Windows.");return}if(!await pt(this.app,"Install Local TTS?","Open Reader will download Local TTS from github.com/lornezhang66/local-tts-service, install Python dependencies, and download about 130 MB of speech models. Continue?"))return;let e=a.Platform.isWin?"ps1":"sh",i=a.Platform.isWin?"install_windows_user.ps1":"install_macos_user.sh",o=g.join(b.tmpdir(),`open-reader-local-tts-installer.${e}`),r=`https://raw.githubusercontent.com/lornezhang66/local-tts-service/${$}/scripts/${i}`,l={...process.env,LOCAL_TTS_INSTALL_REF:$},c=new a.Notice("Open Reader: downloading and installing Local TTS. This may take several minutes.",0);try{if(await K(r,o),a.Platform.isWin?await U("powershell.exe",["-NoProfile","-ExecutionPolicy","Bypass","-File",o],l):await U("/bin/bash",[o],l),!this.detectTtsctlPath())throw new Error("installer finished but ttsctl was not found");new a.Notice("Open Reader: Local TTS installed successfully.")}catch(u){new a.Notice(`Open Reader: Local TTS installation failed: ${C(u)}`,1e4),console.error("Local TTS installation failed",u)}finally{c.hide(),await d.promises.rm(o,{force:!0})}}async readActiveDocument(){if(!a.Platform.isDesktopApp){new a.Notice("Open Reader: Local TTS CLI is only supported on desktop.");return}let t=this.getFileSystemAdapter();if(!t)return;let e=this.getSelectedFile()||this.app.workspace.getActiveFile();if(!e){new a.Notice("Open Reader: no file selected or active.");return}if(!this.detectTtsctlPath()&&(await this.installLocalTts(),!this.detectTtsctlPath()))return;this.stopReading(!1),this.activeFilePath=e.path,this.registerFileCloseListener();let i=this.prepareText(await this.getTextToRead());if(!i.trim()){new a.Notice("Open Reader: no readable text found.");return}this.shouldStop=!1,this.generatedFiles=[],this.showController();let o=ot(i,Y(this.settings.maxChunkCharacters,80,4e3));new a.Notice(`Open Reader: synthesizing ${o.length} chunk(s) with local TTS.`);try{await this.ensureOutputFolder();let r=this.getTtsctlPath(),l=await I(r);for(let c=0;c<o.length&&!this.shouldStop;c+=1){this.updateStatus("loading",c+1,o.length);let u=this.getChunkOutputPath(t,c+1),h={text:o[c],outputPath:u,speed:B(this.settings.speed,.5,2)};if(l)try{await _(h)}catch(y){console.warn("Local TTS HTTP failed; falling back to ttsctl",y),l=!1,await F({ttsctlPath:r,...h})}else await F({ttsctlPath:r,...h});if(this.generatedFiles.push(u),this.shouldStop)break;await this.playAudioFile(u,()=>{this.updateStatus("playing",c+1,o.length)}),this.currentAudio=null}this.shouldStop||(this.updateStatus("idle"),this.settings.openFolderAfterSynthesis&&await this.openOutputFolder())}catch(r){this.updateStatus("idle"),new a.Notice(`Open Reader failed: ${C(r)}`),console.error("Open Reader failed",r)}finally{this.currentAudio=null,this.activeFilePath=null,this.unregisterFileCloseListener(),this.settings.keepAudioFiles||await V(this.generatedFiles),this.releaseObjectUrls()}}pauseReading(){!this.currentAudio||this.currentAudio.paused||(this.currentAudio.pause(),this.updateStatus("paused"),new a.Notice("\u5DF2\u6682\u505C"))}async resumeReading(){if(this.currentAudio){if(this.currentAudio.paused){try{await this.currentAudio.play(),this.updateStatus("playing"),new a.Notice("\u7EE7\u7EED\u64AD\u653E")}catch(t){new a.Notice(`\u65E0\u6CD5\u7EE7\u7EED\u64AD\u653E: ${C(t)}`)}return}if(this.currentAudio.readyState>=2)try{await this.currentAudio.play(),this.updateStatus("playing"),new a.Notice("\u7EE7\u7EED\u64AD\u653E")}catch(t){new a.Notice(`\u65E0\u6CD5\u7EE7\u7EED\u64AD\u653E: ${C(t)}`)}else new a.Notice("\u97F3\u9891\u8FD8\u672A\u51C6\u5907\u597D\uFF0C\u8BF7\u7A0D\u540E")}}stopReading(t=!0){var e;this.shouldStop=!0,this.currentAudio&&(this.currentAudio.pause(),this.currentAudio.removeAttribute("src"),this.currentAudio.load(),this.currentAudio=null),(e=this.finishCurrentPlayback)==null||e.call(this),this.finishCurrentPlayback=null,this.activeFilePath=null,this.unregisterFileCloseListener(),this.updateStatus("stopped"),t&&new a.Notice("Open Reader: stopped.")}async testLocalTtsCli(){if(!a.Platform.isDesktopApp){new a.Notice("Open Reader: Local TTS CLI is only supported on desktop.");return}let t=this.getFileSystemAdapter();if(t)try{await this.ensureOutputFolder();let e=this.getNamedOutputPath(t,"ttsctl-test.wav"),i=this.getTtsctlPath(),o={text:"Obsidian \u672C\u5730\u6717\u8BFB\u63D2\u4EF6\u6D4B\u8BD5\u6210\u529F\u3002",outputPath:e,speed:B(this.settings.speed,.5,2)};if(await I(i))try{await _(o)}catch(r){await F({ttsctlPath:i,...o})}else await F({ttsctlPath:i,...o});new a.Notice("Open Reader: Local TTS test passed."),this.showController();try{await this.playAudioFile(e,()=>{this.updateStatus("playing")})}finally{this.currentAudio=null,this.finishCurrentPlayback=null,this.updateStatus("idle"),this.releaseObjectUrls(),this.settings.keepAudioFiles||await V([e])}}catch(e){new a.Notice(`Open Reader TTS test failed: ${C(e)}`),console.error("Open Reader TTS test failed",e)}}async openOutputFolder(){let t=this.getFileSystemAdapter();if(!t)return;await this.ensureOutputFolder();let e=N(t.getBasePath(),k(this.settings.outputFolder));await ht(e)}getFileSystemAdapter(){let t=this.app.vault.adapter;return t instanceof a.FileSystemAdapter?t:(new a.Notice("Open Reader: this vault adapter cannot expose local file paths."),null)}async getTextToRead(){let t=this.getSelectedFile();if(t){let o=await this.app.vault.read(t);if(o.trim())return o}let e=this.app.workspace.getActiveViewOfType(a.MarkdownView);if(e!=null&&e.editor){let o=e.editor.getSelection();if(o.trim())return o;let r=e.editor.getValue();if(r.trim())return r}let i=this.app.workspace.getActiveFile();return i?await this.app.vault.read(i):""}getSelectedFile(){let t=this.app.workspace.getLeavesOfType("markdown");if(t&&t.length>0)for(let i of t){let o=i.view instanceof a.MarkdownView?i.view.selectedFiles:void 0;if(o&&o.length>0)return o[0]}let e=this.app.workspace.getActiveFile();return e||null}prepareText(t){let e=t.replace(/\r\n/g,`
`);if(this.settings.stripFrontmatter&&(e=e.replace(/^---\n[\s\S]*?\n---\n?/,"")),e=dt(e,this.settings.skipCodeBlocks),this.settings.filterHtmlTags&&(e=e.replace(/<[^>]+>/g,"")),this.settings.filterExtraWhitespace&&(e=e.replace(/[ \t]+/g," "),e=e.replace(/\n{3,}/g,`
`)),this.settings.customCharsToFilter)for(let o of this.settings.customCharsToFilter)e=e.split(o).join("");let i=e.replace(/!\[[^\]]*\]\([^)]+\)/g,"").replace(/\[[^\]]+\]\([^)]+\)/g,"").replace(/https?:\/\/\S+/g,"").replace(/\[\[([^\]|]+)\|([^\]]+)\]\]/g,"$2").replace(/\[\[([^\]]+)\]\]/g,"$1").replace(/^[ \t]*#{1,6}[ \t]+/gm,"").replace(/^[ \t]*>[ \t]?/gm,"").replace(/^[ \t]*[-*+][ \t]+/gm,"").replace(/^[ \t]*\d+\.[ \t]+/gm,"").replace(/\[[ xX]\][ \t]*/g,"").replace(/^[ \t]*[-:| ]{3,}$/gm,"").replace(/\|/g," ").replace(/\*\*([^*]+)\*\*/g,"$1").replace(/\*([^*]+)\*/g,"$1").replace(/~~([^~]+)~~/g,"$1").replace(/`([^`]+)`/g,"$1").replace(/[\w./~ -]+:\d+(?::\d+)?/g,"").trim();return i=i.replace(/\n\n+/g,`\u3002
`),i}async playAudioFile(t,e){let i=await d.promises.readFile(t),o=i.buffer.slice(i.byteOffset,i.byteOffset+i.byteLength),r=URL.createObjectURL(new Blob([o],{type:"audio/wav"}));this.objectUrls.push(r),await this.playAudioUrl(r,e)}playAudioUrl(t,e){return new Promise((i,o)=>{let r=new Audio(t);r.preload="auto",r.playbackRate=this.settings.playbackSpeed,this.currentAudio=r,r.onended=()=>{this.currentAudio=null,i()},r.onerror=l=>{this.currentAudio=null,o(new Error(lt(r)))},r.play().then(()=>{e==null||e(),this.finishCurrentPlayback=i}).catch(l=>{this.finishCurrentPlayback=i,new a.Notice("\u64AD\u653E\u88AB\u963B\u6B62\uFF0C\u8BF7\u70B9\u51FB\u300C\u7EE7\u7EED\u300D\u6309\u94AE\u64AD\u653E"),console.warn("Playback blocked, waiting for user interaction",l),this.refreshControllerButtons()})})}updateStatus(t,e,i){let r=this.activeFilePath&&this.activeFilePath.split("/").pop()||"";if(t==="loading"){this.statusBarEl.setText(`TTS: synthesizing ${e}/${i}`),this.updateController("\u6B63\u5728\u5408\u6210",e,i,r);return}if(t==="playing"&&e&&i){this.statusBarEl.setText(`TTS: playing ${e}/${i}`),this.updateController("\u6B63\u5728\u64AD\u653E",e,i,r);return}if(t==="paused"){this.statusBarEl.setText("TTS: paused"),this.updateController("\u5DF2\u6682\u505C",e,i,r);return}if(t==="stopped"){this.statusBarEl.setText("TTS: stopped"),this.updateController("\u5DF2\u505C\u6B62",e,i,r);return}this.statusBarEl.setText("TTS: idle"),this.updateController("\u7A7A\u95F2",e,i,r)}createController(){let e=this.app.workspace.containerEl.ownerDocument.body.createDiv({cls:"open-reader-controller"});this.controllerEl=e,e.hide();let i=e.createDiv({cls:"open-reader-controller-header"}),o=i.createDiv({cls:"open-reader-controller-drag"});o.setText("\u2630"),o.setAttribute("aria-label","\u62D6\u62FD\u79FB\u52A8");let r=i.createDiv({cls:"open-reader-controller-title-area"});this.controllerTitleEl=r.createDiv({cls:"open-reader-controller-title",text:"Open Reader"}),this.fileNameEl=r.createDiv({cls:"open-reader-controller-filename",text:""});let l=i.createEl("button",{cls:"open-reader-controller-close",attr:{"aria-label":"\u5173\u95ED"}});l.setText("\u2715"),l.addEventListener("click",()=>this.hideController());let c=e.createDiv({cls:"open-reader-controller-progress"}),u=c.createDiv({cls:"open-reader-controller-progress-track"});this.progressBarEl=u.createDiv({cls:"open-reader-controller-progress-fill"}),this.progressTextEl=c.createDiv({cls:"open-reader-controller-progress-text"});let h=e.createDiv({cls:"open-reader-controller-status-area"});this.controllerStatusEl=h.createDiv({cls:"open-reader-controller-status",text:"\u7A7A\u95F2"});let y=e.createDiv({cls:"open-reader-controller-speed"});y.createDiv({cls:"open-reader-controller-speed-label",text:"\u64AD\u653E\u901F\u5EA6"});let D=y.createDiv({cls:"open-reader-controller-speed-buttons"});this.speedButtonEls=[];for(let p of O.PLAYBACK_SPEED_OPTIONS){let v=D.createEl("button",{cls:"open-reader-controller-speed-btn",text:`${p}x`,attr:{"data-rate":String(p)}});v.addEventListener("click",()=>this.setPlaybackSpeed(p)),this.speedButtonEls.push(v)}let m=e.createDiv({cls:"open-reader-controller-actions"});this.pauseButtonEl=m.createEl("button",{text:"\u6682\u505C"}),this.pauseButtonEl.addEventListener("click",()=>this.pauseReading()),this.resumeButtonEl=m.createEl("button",{text:"\u7EE7\u7EED"}),this.resumeButtonEl.addEventListener("click",()=>{this.resumeReading()}),m.createEl("button",{text:"\u505C\u6B62"}).addEventListener("click",()=>this.stopReading());let T=m.createEl("button",{text:"\u2699"});T.setAttribute("aria-label","\u8BBE\u7F6E"),T.addEventListener("click",()=>{var p,v;(v=(p=this.app.setting)==null?void 0:p.openTabById)==null||v.call(p,"open-reader")});let x=m.createEl("button",{text:"\u{1F4C1}"});x.setAttribute("aria-label","\u6253\u5F00\u76EE\u5F55"),x.addEventListener("click",()=>{this.openOutputFolder()}),this.settings.enableDragToMove&&this.setupControllerDrag(),this.refreshControllerButtons()}setupControllerDrag(){let t=this.controllerEl;if(!t)return;let e=t.querySelector(".open-reader-controller-drag");if(!e)return;let i=t.ownerDocument,o=i.defaultView||window,r=!1,l=0,c=0,u=0,h=0,y=w=>{w.preventDefault(),r=!0,l=w.clientX,c=w.clientY;let T=o.getComputedStyle(t);u=parseInt(T.right)||24,h=parseInt(T.bottom)||28,t.setCssStyles({right:"auto",left:`${o.innerWidth-u-t.offsetWidth}px`,top:`${o.innerHeight-h-t.offsetHeight}px`,bottom:"auto"}),t.classList.add("is-dragging")},D=w=>{if(!r)return;let T=w.clientX-l,x=w.clientY-c,p=Math.max(0,o.innerWidth-u-t.offsetWidth+T),v=Math.max(0,o.innerHeight-h-t.offsetHeight+x);t.setCssStyles({left:`${p}px`,top:`${v}px`})},m=()=>{r&&(r=!1,t.classList.remove("is-dragging"))};e.addEventListener("mousedown",y),i.addEventListener("mousemove",D),i.addEventListener("mouseup",m)}hideController(){var t;this.stopReading(!1),(t=this.controllerEl)==null||t.hide()}showController(){var t;(t=this.controllerEl)==null||t.show()}updateController(t,e,i,o){if(!(!this.controllerEl||!this.controllerStatusEl)){if(this.controllerStatusEl.setText(t),this.fileNameEl&&this.fileNameEl.setText(o||""),this.progressBarEl&&this.progressTextEl&&e!==void 0&&i!==void 0){let r=i>0?e/i*100:0;this.progressBarEl.setCssStyles({width:`${r}%`}),this.progressTextEl.textContent=`${e}/${i}`}else this.progressBarEl&&this.progressTextEl&&(this.progressBarEl.setCssStyles({width:"0%"}),this.progressTextEl.textContent="");this.refreshControllerButtons()}}refreshControllerButtons(){this.pauseButtonEl&&(this.pauseButtonEl.disabled=!this.currentAudio||this.currentAudio.paused),this.resumeButtonEl&&(this.resumeButtonEl.disabled=!this.currentAudio||!this.currentAudio.paused),this.refreshSpeedButtons()}setPlaybackSpeed(t){this.settings.playbackSpeed=t,this.saveSettings(),this.currentAudio&&(this.currentAudio.playbackRate=t),this.refreshSpeedButtons()}refreshSpeedButtons(){let t=this.settings.playbackSpeed;for(let e of this.speedButtonEls){let i=Number(e.getAttribute("data-rate"));e.toggleClass("is-active",i===t)}}releaseObjectUrls(){for(let t of this.objectUrls)URL.revokeObjectURL(t);this.objectUrls=[]}async ensureOutputFolder(){let t=k(this.settings.outputFolder||E.outputFolder);await ct(this.app,t)}getChunkOutputPath(t,e){let i=ut(new Date),o=String(e).padStart(3,"0");return this.getNamedOutputPath(t,`${i}-chunk-${o}.wav`)}getNamedOutputPath(t,e){let i=k(this.settings.outputFolder||E.outputFolder);return N(t.getBasePath(),`${i}/${e}`)}};O.PLAYBACK_SPEED_OPTIONS=[.75,1,1.25,1.5,2];var L=O,R=class extends a.PluginSettingTab{constructor(s,t){super(s,t),this.plugin=t}getSettingDefinitions(){return[{type:"group",heading:"TTS \u670D\u52A1 \xB7 TTS service",items:[{name:"\u672C\u5730\u8BED\u97F3\u5F15\u64CE \xB7 local speech engine",desc:"\u68C0\u6D4B\u6216\u5B89\u88C5\u672C\u673A Local TTS\u3002 \xB7 Detect or install Local TTS on this computer.",render:s=>{s.addButton(t=>t.setButtonText(this.plugin.detectTtsctlPath()?"\u91CD\u65B0\u68C0\u6D4B \xB7 detect":"\u4E00\u952E\u5B89\u88C5 \xB7 install").onClick(async()=>{this.plugin.detectTtsctlPath()||await this.plugin.installLocalTts()})).addButton(t=>t.setButtonText("\u9879\u76EE\u8BF4\u660E \xB7 docs").onClick(()=>{window.open("https://github.com/lornezhang66/local-tts-service#cli")}))}},{name:"\u8BED\u901F \xB7 speech speed",desc:"\u8303\u56F4 0.5 \u5230 2\uFF0C\u63A8\u8350 0.8\u20131.2\u3002 \xB7 Range: 0.5\u20132; recommended: 0.8\u20131.2.",control:{type:"slider",key:"speed",min:.5,max:2,step:.05}}]},{type:"group",heading:"\u6587\u672C\u5904\u7406 \xB7 text processing",items:[{name:"\u8F93\u51FA\u6587\u4EF6\u5939 \xB7 output folder",desc:"\u76F8\u5BF9\u4E8E vault \u6839\u76EE\u5F55\u7684\u97F3\u9891\u76EE\u5F55\u3002 \xB7 Vault-relative folder for generated audio.",control:{type:"text",key:"outputFolder",placeholder:E.outputFolder}},{name:"\u5206\u6BB5\u5B57\u7B26\u6570 \xB7 max chunk characters",desc:"\u957F\u6587\u6863\u5206\u6BB5\u4E0A\u9650\uFF0C\u63A8\u8350 300\u2013600\u3002 \xB7 Maximum characters per chunk; recommended: 300\u2013600.",control:{type:"number",key:"maxChunkCharacters",min:80,max:4e3,step:1}},{name:"\u79FB\u9664 YAML \u524D\u7F6E\u5143\u6570\u636E \xB7 strip YAML frontmatter",control:{type:"toggle",key:"stripFrontmatter"}},{name:"\u8DF3\u8FC7\u975E\u6587\u672C\u4EE3\u7801\u5757 \xB7 skip non-text code blocks",control:{type:"toggle",key:"skipCodeBlocks"}}]},{type:"group",heading:"\u6587\u672C\u8FC7\u6EE4 \xB7 text filtering",items:[{name:"\u8FC7\u6EE4 HTML \u6807\u7B7E \xB7 remove HTML tags",control:{type:"toggle",key:"filterHtmlTags"}},{name:"\u8FC7\u6EE4\u591A\u4F59\u7A7A\u767D \xB7 remove extra whitespace",control:{type:"toggle",key:"filterExtraWhitespace"}},{name:"\u81EA\u5B9A\u4E49\u8FC7\u6EE4\u5B57\u7B26 \xB7 custom characters",control:{type:"text",key:"customCharsToFilter",placeholder:"\u4F8B\u5982 \xB7 e.g. #$%^&"}}]},{type:"group",heading:"\u5176\u4ED6 \xB7 other",items:[{name:"\u4FDD\u7559\u97F3\u9891\u6587\u4EF6 \xB7 keep generated audio",control:{type:"toggle",key:"keepAudioFiles"}},{name:"\u6717\u8BFB\u5B8C\u6210\u540E\u6253\u5F00\u6587\u4EF6\u5939 \xB7 open folder when finished",control:{type:"toggle",key:"openFolderAfterSynthesis"}},{name:"\u6D4B\u8BD5 TTS \xB7 test TTS",render:s=>{s.addButton(t=>t.setButtonText("\u6D4B\u8BD5 \xB7 test").onClick(()=>{this.plugin.testLocalTtsCli()}))}},{name:"\u6253\u5F00\u8F93\u51FA\u6587\u4EF6\u5939 \xB7 open output folder",render:s=>{s.addButton(t=>t.setButtonText("\u6253\u5F00 \xB7 open").onClick(()=>{this.plugin.openOutputFolder()}))}}]}]}display(){let{containerEl:s}=this;s.empty(),s.addClass("open-reader-settings"),new a.Setting(s).setName("\u8BED\u97F3\u6717\u8BFB \xB7 text to speech").setHeading(),new a.Setting(s).setName("TTS \u670D\u52A1 \xB7 TTS service").setHeading(),new a.Setting(s).setName("\u672C\u5730\u8BED\u97F3\u5F15\u64CE \xB7 local speech engine").setDesc(this.plugin.detectTtsctlPath()?"\u5DF2\u5B89\u88C5\u3002\u6BCF\u53F0\u7535\u8111\u72EC\u7ACB\u5B89\u88C5\uFF0C\u4E0D\u540C\u6B65\u7EDD\u5BF9\u8DEF\u5F84\u3002 \xB7 Installed separately on each computer; absolute paths are not synced.":"\u672A\u5B89\u88C5\u3002\u6A21\u578B\u4E0B\u8F7D\u7EA6 130 MB\uFF0C\u6587\u5B57\u548C\u8BED\u97F3\u5747\u4FDD\u7559\u5728\u672C\u673A\u3002 \xB7 Not installed. The model is about 130 MB; text and audio stay on this device.").addButton(t=>t.setButtonText(this.plugin.detectTtsctlPath()?"\u91CD\u65B0\u68C0\u6D4B \xB7 Detect":"\u4E00\u952E\u5B89\u88C5 \xB7 Install").onClick(async()=>{this.plugin.detectTtsctlPath()||await this.plugin.installLocalTts()})).addButton(t=>t.setButtonText("\u9879\u76EE\u8BF4\u660E \xB7 docs").onClick(()=>{window.open("https://github.com/lornezhang66/local-tts-service#cli")})),new a.Setting(s).setName("\u8BED\u901F \xB7 speech speed").setDesc("\u8303\u56F4 0.5 \u5230 2\uFF0C\u63A8\u8350 0.8\u20131.2\u3002 \xB7 Range: 0.5\u20132; recommended: 0.8\u20131.2.").addSlider(t=>t.setLimits(.5,2,.05).setValue(this.plugin.settings.speed).onChange(async e=>{this.plugin.settings.speed=B(e,.5,2),await this.plugin.saveSettings()})),new a.Setting(s).setName("\u6587\u672C\u5904\u7406 \xB7 text processing").setHeading(),new a.Setting(s).setName("\u8F93\u51FA\u6587\u4EF6\u5939 \xB7 output folder").setDesc("\u76F8\u5BF9\u4E8E vault \u6839\u76EE\u5F55\u7684\u97F3\u9891\u76EE\u5F55\u3002 \xB7 vault-relative folder for generated audio.").addText(t=>t.setPlaceholder(E.outputFolder).setValue(this.plugin.settings.outputFolder).onChange(async e=>{this.plugin.settings.outputFolder=k(e||E.outputFolder),await this.plugin.saveSettings()})),new a.Setting(s).setName("\u5206\u6BB5\u5B57\u7B26\u6570 \xB7 max chunk characters").setDesc("\u957F\u6587\u6863\u5206\u6BB5\u4E0A\u9650\uFF0C\u63A8\u8350 300\u2013600\u3002 \xB7 maximum characters per chunk; recommended: 300\u2013600.").addText(t=>t.setPlaceholder("450").setValue(String(this.plugin.settings.maxChunkCharacters)).onChange(async e=>{this.plugin.settings.maxChunkCharacters=Y(Number(e),80,4e3),await this.plugin.saveSettings()})),new a.Setting(s).setName("\u79FB\u9664 YAML \u524D\u7F6E\u5143\u6570\u636E \xB7 strip YAML frontmatter").setDesc("\u6717\u8BFB\u65F6\u8DF3\u8FC7\u6587\u6863\u5F00\u5934\u7684 YAML \u533A\u57DF\u3002 \xB7 skip YAML metadata at the beginning of a note.").addToggle(t=>t.setValue(this.plugin.settings.stripFrontmatter).onChange(async e=>{this.plugin.settings.stripFrontmatter=e,await this.plugin.saveSettings()})),new a.Setting(s).setName("\u8DF3\u8FC7\u975E\u6587\u672C\u4EE3\u7801\u5757 \xB7 skip non-text code blocks").setDesc("\u6717\u8BFB text/txt/plain \u4EE3\u7801\u5757\uFF0C\u8DF3\u8FC7\u5176\u4ED6\u4EE3\u7801\u5757\u3002 \xB7 read text/txt/plain blocks and skip other code blocks.").addToggle(t=>t.setValue(this.plugin.settings.skipCodeBlocks).onChange(async e=>{this.plugin.settings.skipCodeBlocks=e,await this.plugin.saveSettings()})),new a.Setting(s).setName("\u6587\u672C\u8FC7\u6EE4 \xB7 text filtering").setHeading(),new a.Setting(s).setName("\u8FC7\u6EE4 HTML \u6807\u7B7E \xB7 remove HTML tags").setDesc("\u79FB\u9664 <div>\u3001<span> \u7B49\u6B8B\u7559\u6807\u7B7E\u3002 \xB7 Remove leftover tags such as <div> and <span>.").addToggle(t=>t.setValue(this.plugin.settings.filterHtmlTags).onChange(async e=>{this.plugin.settings.filterHtmlTags=e,await this.plugin.saveSettings()})),new a.Setting(s).setName("\u8FC7\u6EE4\u591A\u4F59\u7A7A\u767D \xB7 remove extra whitespace").setDesc("\u79FB\u9664\u591A\u4F59\u7A7A\u683C\u3001\u5236\u8868\u7B26\u548C\u8FDE\u7EED\u6362\u884C\u3002 \xB7 remove extra spaces, tabs, and repeated line breaks.").addToggle(t=>t.setValue(this.plugin.settings.filterExtraWhitespace).onChange(async e=>{this.plugin.settings.filterExtraWhitespace=e,await this.plugin.saveSettings()})),new a.Setting(s).setName("\u81EA\u5B9A\u4E49\u8FC7\u6EE4\u5B57\u7B26 \xB7 custom characters").setDesc("\u8F93\u5165\u8981\u7CBE\u786E\u79FB\u9664\u7684\u5B57\u7B26\uFF0C\u5982 \u201C\u201D\u2014\uFF1B\u5DE6\u53F3\u5F15\u53F7\u9700\u5206\u522B\u8F93\u5165\u3002 \xB7 enter exact characters to remove; opening and closing quotes are separate characters.").addText(t=>t.setPlaceholder("\u4F8B\u5982 \xB7 e.g. #$%^&").setValue(this.plugin.settings.customCharsToFilter).onChange(async e=>{this.plugin.settings.customCharsToFilter=e,await this.plugin.saveSettings()})),new a.Setting(s).setName("\u5176\u4ED6 \xB7 other").setHeading(),new a.Setting(s).setName("\u4FDD\u7559\u97F3\u9891\u6587\u4EF6 \xB7 keep generated audio").setDesc("\u6717\u8BFB\u540E\u4FDD\u7559 WAV\uFF0C\u5426\u5219\u81EA\u52A8\u5220\u9664\u3002 \xB7 keep WAV files after playback; otherwise delete them.").addToggle(t=>t.setValue(this.plugin.settings.keepAudioFiles).onChange(async e=>{this.plugin.settings.keepAudioFiles=e,await this.plugin.saveSettings()})),new a.Setting(s).setName("\u5B8C\u6210\u540E\u6253\u5F00\u6587\u4EF6\u5939 \xB7 open folder when finished").setDesc("\u6717\u8BFB\u7ED3\u675F\u540E\u81EA\u52A8\u6253\u5F00\u8F93\u51FA\u76EE\u5F55\u3002 \xB7 open the output folder after reading.").addToggle(t=>t.setValue(this.plugin.settings.openFolderAfterSynthesis).onChange(async e=>{this.plugin.settings.openFolderAfterSynthesis=e,await this.plugin.saveSettings()})),new a.Setting(s).setName("\u6D4B\u8BD5 TTS \xB7 test TTS").setDesc("\u751F\u6210\u5E76\u64AD\u653E\u6D4B\u8BD5\u8BED\u97F3\u3002 \xB7 generate and play a test sentence.").addButton(t=>t.setButtonText("\u6D4B\u8BD5 \xB7 test").onClick(()=>{this.plugin.testLocalTtsCli()})),new a.Setting(s).setName("\u6253\u5F00\u8F93\u51FA\u6587\u4EF6\u5939 \xB7 open output folder").setDesc("\u5728\u6587\u4EF6\u7BA1\u7406\u5668\u4E2D\u6253\u5F00\u97F3\u9891\u76EE\u5F55\u3002 \xB7 open the audio folder in the file manager.").addButton(t=>t.setButtonText("\u6253\u5F00 \xB7 open").onClick(()=>{this.plugin.openOutputFolder()}))}};function ot(n,s){let t=[],e=n.split(/\n{2,}/).map(i=>i.trim()).filter(Boolean);for(let i of e){if(i.length<=s){A(t,i,s);continue}let o=i.split(/(?<=[。!?;;.!?])\s*/).map(r=>r.trim()).filter(Boolean);if(o.length<=1){H(i,s).forEach(r=>A(t,r,s));continue}for(let r of o)r.length<=s?A(t,r,s):H(r,s).forEach(l=>A(t,l,s))}return t}function A(n,s,t){let e=n[n.length-1],i=`
`;if(e&&e.length+i.length+s.length<=t){n[n.length-1]=`${e}${i}${s}`;return}n.push(s)}function H(n,s){let t=[],e=n.trim();for(;e.length>s;){let i=Math.max(e.lastIndexOf(`
`,s),e.lastIndexOf("\u3002",s)+1,e.lastIndexOf("\uFF0C",s)+1,e.lastIndexOf(" ",s));i<s*.5&&(i=s),t.push(e.slice(0,i).trim()),e=e.slice(i).trim()}return e&&t.push(e),t}function B(n,s,t){return Number.isFinite(n)?Math.min(Math.max(n,s),t):s}function Y(n,s,t){return Math.round(B(n,s,t))}function C(n){return n instanceof Error?n.message:String(n)}function lt(n){var t;let s=(t=n.error)==null?void 0:t.code;return s===MediaError.MEDIA_ERR_ABORTED?"Audio playback was aborted.":s===MediaError.MEDIA_ERR_NETWORK?"Audio file could not be loaded.":s===MediaError.MEDIA_ERR_DECODE?"Audio file could not be decoded.":s===MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED?"Audio format is not supported.":"Audio playback failed."}function k(n){return n.replace(/\\/g,"/").replace(/^\/+/,"").trim()}async function ct(n,s){let t=k(s).split("/").filter(Boolean),e="";for(let i of t)e=e?`${e}/${i}`:i,await n.vault.adapter.exists(e)||await n.vault.createFolder(e)}function N(n,s){return g.join(n,s)}function ut(n){let s=t=>String(t).padStart(2,"0");return[n.getFullYear(),s(n.getMonth()+1),s(n.getDate()),"-",s(n.getHours()),s(n.getMinutes()),s(n.getSeconds())].join("")}function dt(n,s){return n.replace(/(^|\n)(```|~~~)[ \t]*([^\n`]*)\n([\s\S]*?)\n\2[ \t]*(?=\n|$)/g,(t,e,i,o,r)=>{let l=String(o||"").trim().toLowerCase().split(/\s+/)[0];return["text","txt","plain","plaintext"].includes(l)?`${e}${String(r).trim()}
`:s?`${e}
`:`${e}${String(r).trim()}
`})}async function F(n){if(await J(n.ttsctlPath,["say",n.text,"--output",n.outputPath,"--speed",String(n.speed)]),(await d.promises.stat(n.outputPath)).size===0)throw new Error("Local TTS CLI generated an empty audio file.")}async function I(n){if(await W())return!0;try{await J(n,["ensure"])}catch(s){return console.warn("Unable to start Local TTS daemon; using CLI compatibility mode",s),!1}return W()}async function W(){try{let n=await X("GET","/api/health",void 0,3e3),s=JSON.parse(n.toString("utf8"));return q(s)&&s.protocol===1}catch(n){return!1}}function q(n){return typeof n=="object"&&n!==null}function pt(n,s,t){return new Promise(e=>{let i=new a.Modal(n);i.titleEl.setText(s),i.contentEl.createEl("p",{text:t}),new a.Setting(i.contentEl).addButton(o=>o.setButtonText("Cancel").onClick(()=>i.close())).addButton(o=>o.setButtonText("Install").setCta().onClick(()=>{e(!0),i.close()})),i.onClose=()=>e(!1),i.open()})}async function _(n){let s=await X("POST","/api/synthesize",Buffer.from(JSON.stringify({text:n.text,speed:n.speed})),12e4);if(!s.length)throw new Error("Local TTS HTTP returned empty audio.");await d.promises.writeFile(n.outputPath,s)}function X(n,s,t,e=3e3){return new Promise((i,o)=>{let r=z.request(`${nt}${s}`,{method:n,headers:t?{"Content-Type":"application/json","Content-Length":String(t.length)}:void 0},l=>{let c=[];l.on("data",u=>c.push(u)),l.on("end",()=>{let u=Buffer.concat(c);l.statusCode&&l.statusCode>=200&&l.statusCode<300?i(u):o(new Error(`Local TTS HTTP ${l.statusCode}: ${u.toString("utf8").slice(0,500)}`))})});r.setTimeout(e,()=>r.destroy(new Error("Local TTS HTTP request timed out"))),r.on("error",o),t&&r.write(t),r.end()})}function J(n,s){let t=n.trim();return t?new Promise((e,i)=>{var c;let o=g.extname(t).toLowerCase(),r=a.Platform.isWin&&o===".ps1"?f.spawn("powershell.exe",["-NoProfile","-ExecutionPolicy","Bypass","-File",t,...s],{windowsHide:!0}):f.spawn(t,s,{windowsHide:!0}),l="";(c=r.stderr)==null||c.on("data",u=>{l+=u.toString()}),r.on("error",i),r.on("close",u=>{if(u===0){e();return}i(new Error(l.trim()||`Local TTS CLI exited with code ${u}.`))})}):Promise.reject(new Error("Local TTS CLI path is empty."))}async function V(n){await Promise.all(n.map(s=>d.promises.rm(s,{force:!0})))}function U(n,s,t){return new Promise((e,i)=>{var l;let o=f.spawn(n,s,{env:t,windowsHide:!0,stdio:["ignore","ignore","pipe"]}),r="";(l=o.stderr)==null||l.on("data",c=>{r=`${r}${c.toString()}`.slice(-4e3)}),o.on("error",i),o.on("close",c=>{c===0?e():i(new Error(r.trim()||`${n} exited with code ${c}`))})})}function K(n,s,t=0){return t>5?Promise.reject(new Error("too many download redirects")):new Promise((e,i)=>{j.get(n,{headers:{"User-Agent":"Open-Reader"}},r=>{let l=r.headers.location;if(l&&r.statusCode&&r.statusCode>=300&&r.statusCode<400){r.resume(),K(new URL(l,n).toString(),s,t+1).then(e,i);return}if(r.statusCode!==200){r.resume(),i(new Error(`download failed with HTTP ${r.statusCode}`));return}let c=d.createWriteStream(s);r.pipe(c),c.on("finish",()=>c.close(()=>e())),c.on("error",i)}).on("error",i)})}async function ht(n){let t=(await d.promises.stat(n)).isDirectory();if(a.Platform.isWin){t?f.spawn("explorer.exe",[n],{detached:!0,stdio:"ignore",windowsHide:!0}).unref():f.spawn("explorer.exe",["/select,",n],{detached:!0,stdio:"ignore",windowsHide:!0}).unref();return}if(a.Platform.isMacOS){f.spawn("open",[n],{detached:!0,stdio:"ignore"}).unref();return}f.spawn("xdg-open",[n],{detached:!0,stdio:"ignore"}).unref()}