为微软的文本转语音服务的 demo 页面添加下载按钮
< Feedback on Azure Speech Download
这个代码很管用,界面非常简洁了。我想问一下,你那儿的插件现在还能用吗?我这边出现分割错误,而且无法显示播放进度条,也无法下载音频
我这边使用一切正常,你卸载脚本再重装试试
我这边使用一切正常,你卸载脚本再重装试试
明白了,看来是我这里的问题,我重装一下插件试试,非常感谢你的回复
你好,我尝试把浏览器更新到最新的版本,然后重新安装了插件,也重装了脚本,但是依然无法使用。
点击自动拆分按钮的同时就开始播放了,文本框中也没有出现拆分的动作,进度条还是灰色的,语音在背景播放。点击下载按钮的话,背景中会出现第二条音轨,但是也没有弹出下载链接。
以上是目前的情况,不知道你方不方便给我发一份你的代码,非常感谢。
我测试了所有功能好像都没有问题,你可以尝试更换浏览器测试一下。下面是我的完整代码:
// ==UserScript==// @name Azure Speech Download// @namespace// @version 1.0.1// @description 为微软的文本转语音服务的 demo 页面添加下载按钮// @author Puteulanus// @homepage https://greasyfork.org/zh-CN/scripts/444347-azure-speech-download// @match https://azure.microsoft.com/*/products/cognitive-services/text-to-speech/*// @icon https://www.microsoft.com/favicon.ico// @require https://cdn.bootcdn.net/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js// @grant none// @run-at document-end// @namespace https://greasyfork.org/users/909438// ==/UserScript==/* globals saveAs *//* jshint esversion: 6 */(function() {'use strict';// Your code here...if(!window.saveAs) {window.saveAs = (blob, name) => {const a = document.createElement("a");document.body.appendChild(a);a.style = "display: none";const url = window.URL.createObjectURL(blob);a.href = url;a.download = name;a.click();window.URL.revokeObjectURL(url);}}const SpeechSDK = window.SpeechSDKlet fileSize = 0let streamSize = 0let wavFragments = []let enableDownload = falselet enableCollect = falselet autoProcessing = falselet tasks = []let fileExt = '.mp3'let enableSaveOptions = falseconst i18n = {zh: {document1: "\n\n\n收集模式:\n\n打开之后,点击“下载”按钮转换的音频会被收集,在收集模式关闭时合成一个音频下载",document2: "\n\n自动拆分:\n\n将长文本拆分为多个接近“段落长度”的片段,并只在“分隔符”处截断,避免句子被截断,影响阅读效果",document3: "\n\n\n\n拖拽 txt 文件至此框可加载文本文件",download: '下载',downloading: '下载中',downloaded: '下载完成',split: '自动拆分',spliting: '拆分中',codec: '音频编码',saveSetting: '保存设置',lengthWarning: '下载长度超过免费限额,请分割文本后使用收集模式',splitedMsg: "自动拆分完成\n\n使用下方播放器播放,或关闭收集模式下载音频文件",length: '段落长度',delimiter: '分隔符',collectionOn: '收集模式开',collectionOff: '收集模式关',received: '已接收',taskQueue: '剩余分段',profileName: '配置名',createProfile: '创建配置',},eng: {document1: "\n\n\nCollection:\n\nCollect audio files converted by clicking \"Download\" button, do the really download when it is turned off",document2: "\n\nSplit:\n\nSplit long text into segments close to the \"paragraph length\", which only truncate at \"delimiter\"",document3: "\n\n\n\nYou can drag .txt file to this text box to load a text file",download: 'Download',downloading: 'Downloading',downloaded: 'Download complete',split: 'Split',spliting: 'Spliting',codec: 'Codec',saveSetting: 'Save settings',lengthWarning: 'Text length exceeds the free limit, please split the text and use collection mode',splitedMsg: "Split finished\n\nUse the player below to play, or turn off collection mode to download the audio file",length: 'Paragraph length',delimiter: 'Delimiter',collectionOn: 'Collection On',collectionOff: 'Collection Off',received: 'Received:',taskQueue: 'Task queue:',profileName: 'Profile name',createProfile: 'Create profile',}}const lang = window.Acom.currentCultureif (lang === 'zh-cn' || lang === 'zh-tw') {i18n.lang = i18n.zh} else {i18n.lang = i18n.eng}function createButton(id, color, content) {const button = document.getElementById('playli').cloneNode(true)button.id = idbutton.querySelector('span:last-of-type').textContent = contentbutton.querySelector('button').style.backgroundColor = colorbutton.querySelector('button').style.borderColor = colorreturn button}function setButton(button, color, content) {button.querySelector('span:last-of-type').textContent = contentbutton.querySelector('button').style.backgroundColor = colorbutton.querySelector('button').style.borderColor = color}function downloadAndClean() {const sentAudio = new window.Uint8Array(fileSize)fileSize = 0streamSize = 0wavFragments.reduce((size, fragment) => {sentAudio.set(new window.Uint8Array(fragment), size)return size + fragment.byteLength}, 0)wavFragments.length = 0saveAs(new Blob([sentAudio]), (new Date()).toISOString().replace('T', ' ').replace(':', '_').split('.')[0] + fileExt)}function switchOptionDisplay() {if (enableCollect) {autoSplitButton.style.display = 'block'optionArea.style.display = 'block'previewPlayer.style.display = 'inline-block'} else {autoSplitButton.style.display = 'none'optionArea.style.display = 'none'previewPlayer.style.display = 'none'}}function syncAudioToPlayer() {const sentAudio = new window.Uint8Array(fileSize)wavFragments.reduce((size, fragment) => {sentAudio.set(new window.Uint8Array(fragment), size)return size + fragment.byteLength}, 0)const audioBlob = new Blob([sentAudio], {type : 'audio/ogg'})previewPlayer.src = URL.createObjectURL(audioBlob)}function dispatchTextChange() {const evt = document.createEvent('HTMLEvents')evt.initEvent('input', true, true)ttstext.dispatchEvent(evt)}function saveOptions() {if (!enableSaveOptions) returnlocalStorage.setItem('savedOptions', JSON.stringify(getCurrentSettings()))}function restoreOptions() {const optionsJSON = localStorage.getItem('savedOptions')if (!optionsJSON) returnconst options = JSON.parse(optionsJSON)setSettings(options)saveCheckBox.checked = trueenableSaveOptions = true}function bindSaveOption() {languageInput.addEventListener('change', saveOptions)voiceInput.addEventListener('change', saveOptions)styleInput.addEventListener('change', saveOptions)codecInput.addEventListener('change', saveOptions)speedInput.addEventListener('change', saveOptions)pitchInput.addEventListener('change', saveOptions)maxSizeInput.addEventListener('change', saveOptions)delimiterInput.addEventListener('change', saveOptions)}function initSpeedAndPitch() {const evt = document.createEvent('HTMLEvents')evt.initEvent('input', true, true)speedInput.value = '0'speedInput.dispatchEvent(evt)pitchInput.value = '0'pitchInput.dispatchEvent(evt)}function createProfile(name, profile) {const profiles = JSON.parse(localStorage.getItem('savedProfiles'))localStorage.setItem('savedProfiles', JSON.stringify([...profiles.filter(profile => profile.name !== name),{name,setting: profile}]))refreshProfile()}function removeProfile(name) {let profiles = JSON.parse(localStorage.getItem('savedProfiles'))localStorage.setItem('savedProfiles', JSON.stringify(profiles.filter(profile => profile.name !== name)))refreshProfile()}function refreshProfile() {let profilesJSON = localStorage.getItem('savedProfiles')let profilesif (!profilesJSON) {profiles = []localStorage.setItem('savedProfiles', JSON.stringify(profiles))} else {profiles = JSON.parse(profilesJSON)}profileContainer.innerHTML = ''profiles.forEach(profile => {const profileDiv = document.createElement("div")const profileName = document.createElement("span")const profileDelete = document.createElement("span")profileDiv.style.display = 'inline-block'profileDiv.style.border = '1px solid'profileDiv.style.marginLeft = '5px'profileDiv.style.cursor = 'pointer'profileName.innerText = profile.nameprofileName.style.padding = '5px'profileDelete.innerText = 'X'profileDelete.style.backgroundColor = 'black'profileDelete.style.color = 'white'profileDelete.style.padding = '2px'profileDiv.appendChild(profileName)profileDiv.append(profileDelete)profileContainer.append(profileDiv)profileName.addEventListener('click', () => {const textBackup = ttstext.valuesetSettings(profile.setting)ttstext.value = textBackupdispatchTextChange()})profileDelete.addEventListener('click', () => {removeProfile(profile.name)})})}function getCurrentSettings() {return {language: languageInput.value,voice: voiceInput.value,style: styleInput.value,codec: codecInput.value,speed: speedInput.value,pitch: pitchInput.value,splitLength: maxSizeInput.value,delimiter: delimiterInput.value}}function setSettings(setting) {let evt = document.createEvent('HTMLEvents')evt.initEvent('change', true, true)languageInput.value = setting.languagelanguageInput.dispatchEvent(evt)voiceInput.value = setting.voicevoiceInput.dispatchEvent(evt)styleInput.value = setting.stylestyleInput.dispatchEvent(evt)codecInput.value = setting.codeccodecInput.dispatchEvent(evt)speedInput.value = setting.speedspeedInput.dispatchEvent(evt)pitchInput.value = setting.pitchpitchInput.dispatchEvent(evt)evt = document.createEvent('HTMLEvents')evt.initEvent('input', true, true)speedInput.dispatchEvent(evt)pitchInput.dispatchEvent(evt)maxSizeInput.value = setting.splitLengthdelimiterInput.value = setting.delimiter}const downloadStatus = document.createElement('div')const downloadSize = document.createElement('div')const buttonArea = document.getElementById('playli').parentElementconst ttstext = document.getElementById('ttstext')const styleSelecter = document.getElementById('voicestyleselect').parentElementconst languageInput = document.getElementById('languageselect')const voiceInput = document.getElementById('voiceselect')const styleInput = document.getElementById('voicestyleselect')const speedInput = document.getElementById('speed')const pitchInput = document.getElementById('pitch')ttstext.ondrop = async (e) => {const files = e.dataTransfer.filesif (files.length === 1 && files[0].type === 'text/plain') {e.preventDefault()const file = files[0]ttstext.value = await file.text()dispatchTextChange()}}// reuqired by Firefoxttstext.ondra###er = function(e){e.preventDefault();}// set documentsetTimeout(() => {setTimeout(() => {const onchange = languageInput.onchangelanguageInput.onchange = (...args) => {onchange(...args)languageInput.onchange = onchangeinitSpeedAndPitch()restoreOptions()bindSaveOption()ttstext.value += i18n.lang.document1ttstext.value += i18n.lang.document2ttstext.value += i18n.lang.document3}}, 0)}, 0)// clear the documentfunction replaceBody(element) {let oldBody = document.body;let newBody = element;let scripts = oldBody.getElementsByTagName("script");// Remove all child nodes from the body, except for scriptswhile (oldBody.firstChild) {if (oldBody.firstChild.tagName !== "SCRIPT") {oldBody.removeChild(oldBody.firstChild);} else {oldBody.firstChild.remove();}}// Append the new body elementoldBody.appendChild(newBody);// Re-add the scripts to the bodyfor (let i = 0; i < scripts.length; i++) {oldBody.appendChild(scripts[i]);}}let element = document.querySelector('.section-size4')element.style.paddingTop = '0'replaceBody(element);document.querySelector('.row').style.display='none'document.querySelectorAll('.column')[1].style.display='none'document.querySelectorAll('.column')[2].style.margin='0'document.querySelector(".text-body4").parentElement.style.display='none'// set download buttonconst downloadButton = createButton('donwloadli', 'green', i18n.lang.download)downloadButton.addEventListener('click', () => {downloadStatus.textContent = i18n.lang.downloadingenableDownload = truestreamSize = 0document.getElementById('playbtn').click()enableDownload = false})downloadStatus.style.marginTop = '10px'buttonArea.appendChild(downloadButton)// set collect buttonconst collectButton = createButton('collectli', 'red', i18n.lang.collectionOff)collectButton.addEventListener('click', () => {if(!enableCollect) {enableCollect = trueswitchOptionDisplay()setButton(collectButton, 'green', i18n.lang.collectionOn)} else {enableCollect = falseswitchOptionDisplay()setButton(collectButton, 'red', i18n.lang.collectionOff)if (!fileSize) returndownloadAndClean()}})collectButton.style.marginRight = '10px'buttonArea.appendChild(collectButton)// set optionsconst optionArea = document.createElement('div')const maxSizeInput = document.createElement('input')const delimiterInput = document.createElement('input')const maxSizeLabel = document.createElement('span')const delimiterLabel = document.createElement('span')optionArea.id = 'optiondiv'optionArea.style.display = 'none'maxSizeLabel.textContent = i18n.lang.lengthmaxSizeInput.style.width = '50px'maxSizeInput.style.margin = '10px'maxSizeInput.value = '300'delimiterLabel.textContent = i18n.lang.delimiterdelimiterInput.style.width = '100px'delimiterInput.style.margin = '10px'delimiterInput.value = ',。?,.?'optionArea.appendChild(maxSizeLabel)optionArea.appendChild(maxSizeInput)optionArea.appendChild(delimiterLabel)optionArea.appendChild(delimiterInput)buttonArea.parentElement.appendChild(optionArea)// set download statusbuttonArea.parentElement.appendChild(downloadStatus)buttonArea.parentElement.appendChild(downloadSize)// set auto split buttonconst autoSplitButton = createButton('autosplit', 'red', i18n.lang.split)autoSplitButton.addEventListener('click', () => {setButton(autoSplitButton, 'green', i18n.lang.spliting)autoProcessing = trueconst maxSize = +maxSizeInput.valueconst delimiters = delimiterInput.value.split('')const text = ttstext.valueconst textHandler = text.split('').reduce((obj, char, index, arr) => {obj.buffer.push(char)if (delimiters.indexOf(char) >= 0) obj.end = indexif (obj.buffer.length === maxSize) {obj.res.push(obj.buffer.splice(0, obj.end + 1 - obj.offset).join(''))obj.offset += obj.res[obj.res.length - 1].length}return obj}, {buffer: [],end: 0,offset:0,res: []})textHandler.res.push(textHandler.buffer.join(''))ttstext.value = textHandler.res.shift()tasks = textHandler.resdispatchTextChange()downloadButton.click()})autoSplitButton.style.display = 'none'buttonArea.appendChild(autoSplitButton)// set preview playerconst previewPlayer = document.createElement('audio')previewPlayer.controls = truepreviewPlayer.style.display = 'none'previewPlayer.style.width = '100%'previewPlayer.style.marginTop = '10px'ttstext.after(previewPlayer)// set formatting optionslet codecInputtry {const optionSelector = styleSelecter.cloneNode(true)const label = optionSelector.querySelector('label')label.textContent = i18n.lang.codeclabel.htmlFor = 'voiceformatselect'codecInput = optionSelector.querySelector('select')codecInput.id = 'voiceformatselect'codecInput.innerHTML = ''Object.entries(SpeechSDK.SpeechSynthesisOutputFormat).filter(item => !isNaN(item[0])).filter(item => /(^Audio.+Mp3$)|(^Ogg)|(^Webm)/.test(item[1])).forEach(item => {const format = item[1]const option = document.createElement("option")option.value = formatoption.text = formatif (format === 'Audio24Khz96KBitRateMonoMp3') option.selected = truecodecInput.appendChild(option)})styleSelecter.after(optionSelector)const audio24Khz96KBitRateMonoMp3 = SpeechSDK.SpeechSynthesisOutputFormat.Audio24Khz96KBitRateMonoMp3codecInput.addEventListener('change', () => {SpeechSDK.SpeechSynthesisOutputFormat.Audio24Khz96KBitRateMonoMp3 = SpeechSDK.SpeechSynthesisOutputFormat[codecInput.value]if (codecInput.value === 'Audio24Khz96KBitRateMonoMp3') {SpeechSDK.SpeechSynthesisOutputFormat.Audio24Khz96KBitRateMonoMp3 = audio24Khz96KBitRateMonoMp3}if (codecInput.value.startsWith('Ogg')) {fileExt = '.ogg'} else if (codecInput.value.startsWith('Webm')) {fileExt = '.webm'} else {fileExt = '.mp3'}})} catch (e) {console.log(e)}// set save optionsconst saveLabel = document.createElement("span")saveLabel.innerText = i18n.lang.saveSettingsaveLabel.style.marginLeft = '5px'const saveCheckBox = document.createElement("input")saveCheckBox.type = 'checkbox'const pitchArea = document.getElementById('pitchlabel').parentElementpitchArea.appendChild(saveCheckBox)pitchArea.appendChild(saveLabel)saveCheckBox.addEventListener('change', () => {if (saveCheckBox.checked) {enableSaveOptions = truesaveOptions()} else {enableSaveOptions = falselocalStorage.removeItem('savedOptions')}})// set profile manageconst profileArea = document.createElement("div")const createProfileInput = document.createElement("input")const createProfileButton = document.createElement("button")const profileContainer = document.createElement("div")createProfileInput.placeholder = i18n.lang.profileNamecreateProfileInput.style.width = '120px'createProfileButton.innerText = i18n.lang.createProfilecreateProfileButton.style.border = '1px solid'createProfileButton.style.marginLeft = '5px'createProfileButton.style.padding = '2px'profileContainer.style.display = 'inline-block'profileArea.appendChild(createProfileInput)profileArea.appendChild(createProfileButton)profileArea.appendChild(profileContainer)profileArea.style.marginTop = '10px'previewPlayer.after(profileArea)refreshProfile()createProfileButton.addEventListener('click', () => {if (!createProfileInput.value) returnconst profile = getCurrentSettings()createProfile(createProfileInput.value, profile)createProfileInput.value = ''})const streamHandler = {write: function (dataBuffer) {streamSize += dataBuffer.byteLengthif (streamSize <= 1900800) {fileSize += dataBuffer.byteLengthdownloadSize.textContent = `${i18n.lang.received} ${fileSize / 1000} kb`if (autoProcessing) downloadSize.textContent = `${i18n.lang.taskQueue} ${tasks.length} ` + downloadSize.textContentwavFragments.push(dataBuffer)}if (streamSize === 1900800) {downloadStatus.textContent = i18n.lang.lengthWarningif (!enableCollect) {fileSize = 0wavFragments.length = 0} else {fileSize -= 1900800wavFragments.length -= 1320}}},close: function () {downloadStatus.textContent = i18n.lang.downloadedif (!enableCollect) {downloadAndClean()return}if (!autoProcessing) {syncAudioToPlayer()return}if (tasks.length) {ttstext.value = tasks.shift()dispatchTextChange()downloadButton.click()} else {autoProcessing = falsesetButton(autoSplitButton, 'red', i18n.lang.split)ttstext.value = i18n.lang.splitedMsgsyncAudioToPlayer()}}}const outputStream = SpeechSDK.PushAudioOutputStream.create(streamHandler)SpeechSDK.AudioConfig.fromSpeakerOutput = (() => {const fromSpeakerOutput = SpeechSDK.AudioConfig.fromSpeakerOutputreturn function (audioDestination) {return enableDownload ? audioDestination.onAudioEnd() || SpeechSDK.AudioConfig.fromStreamOutput(outputStream) : fromSpeakerOutput(audioDestination)}})()})();
非常感谢你的帮助
非常感谢你的帮助
我目前发现使用晓晨的时候会发生这种情况,分段过多的话可能会遇到
非常感谢你的帮助
我目前发现使用晓晨的时候会发生这种情况,分段过多的话可能会遇到
我后来改用Edgel浏览器后就可以了,谷歌和QQ浏览器的支持有问题
我也是用晓晨的
写了几行代码,可以去除原网页多余的部分,只留下转文字的部分