Chrome语音合成如何处理较长的文本?

59

我在使用Chrome 33中的语音合成API时遇到了问题。它对于较短的文本可以完美地工作,但如果我尝试更长的文本,则会在中途停止。一旦出现这样的情况,Speech Synthesis就不会在Chrome的任何位置工作,直到重新启动浏览器。

示例代码(http://jsfiddle.net/Mdm47/1/):

function speak(text) {
    var msg = new SpeechSynthesisUtterance();
    var voices = speechSynthesis.getVoices();
    msg.voice = voices[10];
    msg.voiceURI = 'native';
    msg.volume = 1;
    msg.rate = 1;
    msg.pitch = 2;
    msg.text = text;
    msg.lang = 'en-US';

    speechSynthesis.speak(msg);
}

speak('Short text');
speak('Collaboratively administrate empowered markets via plug-and-play networks. Dynamically procrastinate B2C users after installed base benefits. Dramatically visualize customer directed convergence without revolutionary ROI. Efficiently unleash cross-media information without cross-media value. Quickly maximize timely deliverables for real-time schemas. Dramatically maintain clicks-and-mortar solutions without functional solutions.');
speak('Another short text');

第二段文字中间停止朗读,之后我无法让任何其他页面继续朗读。

这是浏览器的bug还是某种安全限制?


4
不确定问题出在哪里,但在我的电脑上一定能够再现。 - Qantas 94 Heavy
1
这可能是一个bug,请参见https://code.google.com/p/chromium/issues/detail?id=335907。当有更多信息时,我会进行更新/回答。 - Andrey Shchekin
FYI:现在似乎已经修复了 - 现在它会在文本过长时切换到系统语音,然后即使在朗读较短的文本时也会继续使用该语音。 - Louis St-Amour
Chromium/Google Chrome中的错误仍然存在,尽管#335907已合并到#369472 - Joel Purra
我的解决方案已经在Google Chrome扩展Talkie源代码)中实现,它将文本分成段落,然后将每个段落拆分为子句/句子/字符串,每个最大长度为100。尝试在自然语音停顿的位置进行文本拆分,例如逗号、冒号等之后。然后将每个部分添加到数组中,并依次朗读。如果用户取消,则跳过剩余部分。快来试试吧! - Joel Purra
我知道已经过去六年了,对于被接受的答案有什么想法吗?我可以确认jjsa的答案对我完美地起作用了。 - HoldOffHunger
12个回答

67

使用Google Chrome语音合成遇到问题,经过调查发现以下问题:

  • 断句问题只会在非本地语音时出现,
  • 通常在200-300个字符之间会出现中断,
  • 当它中断时,您可以通过执行speechSynthesis.cancel();来取消它,
  • onend”事件有时不会触发。一个奇怪的解决方法是在讲话前将utterance对象打印到控制台中。我还发现用setTimeout回调包装说话调用有助于平滑这些问题。

针对这些问题,我编写了一个函数,通过将文本分成更小的utterances并依次播放来克服字符限制。显然,有时你会听到一些奇怪的声音,因为句子可能会被分成两个独立的utterances,并在每个之间留下一点时间延迟,但是代码会尝试按标点符号划分这些点,以使声音中断的时间更短。

更新

我已经将此解决方案公开发布,网址为:https://gist.github.com/woollsta/2d146f13878a301b36d7#file-chunkify-js,感谢Brett Zamir的贡献。

该函数:

var speechUtteranceChunker = function (utt, settings, callback) {
    settings = settings || {};
    var newUtt;
    var txt = (settings && settings.offset !== undefined ? utt.text.substring(settings.offset) : utt.text);
    if (utt.voice && utt.voice.voiceURI === 'native') { // Not part of the spec
        newUtt = utt;
        newUtt.text = txt;
        newUtt.addEventListener('end', function () {
            if (speechUtteranceChunker.cancel) {
                speechUtteranceChunker.cancel = false;
            }
            if (callback !== undefined) {
                callback();
            }
        });
    }
    else {
        var chunkLength = (settings && settings.chunkLength) || 160;
        var pattRegex = new RegExp('^[\\s\\S]{' + Math.floor(chunkLength / 2) + ',' + chunkLength + '}[.!?,]{1}|^[\\s\\S]{1,' + chunkLength + '}$|^[\\s\\S]{1,' + chunkLength + '} ');
        var chunkArr = txt.match(pattRegex);

        if (chunkArr[0] === undefined || chunkArr[0].length <= 2) {
            //call once all text has been spoken...
            if (callback !== undefined) {
                callback();
            }
            return;
        }
        var chunk = chunkArr[0];
        newUtt = new SpeechSynthesisUtterance(chunk);
        var x;
        for (x in utt) {
            if (utt.hasOwnProperty(x) && x !== 'text') {
                newUtt[x] = utt[x];
            }
        }
        newUtt.addEventListener('end', function () {
            if (speechUtteranceChunker.cancel) {
                speechUtteranceChunker.cancel = false;
                return;
            }
            settings.offset = settings.offset || 0;
            settings.offset += chunk.length - 1;
            speechUtteranceChunker(utt, settings, callback);
        });
    }

    if (settings.modifier) {
        settings.modifier(newUtt);
    }
    console.log(newUtt); //IMPORTANT!! Do not remove: Logging the object out fixes some onend firing issues.
    //placing the speak invocation inside a callback fixes ordering and onend issues.
    setTimeout(function () {
        speechSynthesis.speak(newUtt);
    }, 0);
};

如何使用它...

//create an utterance as you normally would...
var myLongText = "This is some long text, oh my goodness look how long I'm getting, wooooohooo!";

var utterance = new SpeechSynthesisUtterance(myLongText);

//modify it as you normally would
var voiceArr = speechSynthesis.getVoices();
utterance.voice = voiceArr[2];

//pass it into the chunking function to have it played out.
//you can set the max number of characters by changing the chunkLength property below.
//a callback function can also be added that will fire once the entire text has been spoken.
speechUtteranceChunker(utterance, {
    chunkLength: 120
}, function () {
    //some code to execute when done
    console.log('done');
});

希望人们会觉得这个有用。


3
@BrettZamir 是它在演讲过程中失败了还是根本没有说话?尝试在调用分块模式之前执行 speechSynthesis.cancel(); 以清除任何已排队的语音发声。如果在演讲过程中出现问题,请考虑将 chunkLength 属性调整为较小的值。如果这有帮助,请告诉我。 - Peter Woolley
1
你好!干得好。我们正在使用cordova在IOS上进行语音合成,其中记录一些话语会导致日志记录器崩溃。我们发现,只需在js中的某个地方存储话语的引用(例如在数组中),也可以正常工作,而不会产生日志记录。我们在speechSynthesis取消时清理数组-因此没有内存泄漏。 - Paul Weber
1
顺便提一下,这就是为什么 console.log 可以解决它的原因:https://dev59.com/lV4b5IYBdhLWcg3wpzPv - Chris Cinelli
1
你可以将文本分成块并加载到webSpeech中。我在https://textfromtospeech.com/uk/text-to-voice/上实现了它。 - Paul R
2
正如其他回答中提到的,有一个更简单的解决方案:setInterval(() => { speechSynthesis.pause(); speechSynthesis.resume(); }, 5000); - joe
显示剩余9条评论

24

我在解决问题时使用了一个计时器函数,该函数调用了暂停(pause())和继续(resume())函数,并重新设置了计时器。在onend事件中,我清除了计时器。

    var myTimeout;
    function myTimer() {
        window.speechSynthesis.pause();
        window.speechSynthesis.resume();
        myTimeout = setTimeout(myTimer, 10000);
    }
    ...
        window.speechSynthesis.cancel();
        myTimeout = setTimeout(myTimer, 10000);
        var toSpeak = "some text";
        var utt = new SpeechSynthesisUtterance(toSpeak);
        ...
        utt.onend =  function() { clearTimeout(myTimeout); }
        window.speechSynthesis.speak(utt);
    ...

这似乎工作得很好。


1
你让我开心极了! - Andrea Riva
1
哇,我简直不敢相信这个回答只有一个赞。对我来说,这个答案非常有效,可以读取相当长的字符串。 - saricden
1
这篇文章值得获得更多的赞。根据我的经验,这是唯一稳定的解决方案。 - Alex Oleksiiuk
1
这是最佳解决方案。如果你不改变声音,你不需要做任何事情,但如果你改变声音,这比尝试分块大文本更简单和更稳定。 - wookie924
1
哇,哇,哇,这个答案太棒了。与Peter Woolley的答案不同,每120个字符没有任何问题。而且,上面的代码对window.speechSynthesis.stop()做出了响应,以实现暂停按钮,而Peter的答案则有点劫持了stop()功能,只是破坏了一些东西。这个答案非常好,干得好,谢谢! - HoldOffHunger
显示剩余2条评论

11

一个简单而有效的解决方案是定期恢复。

function resumeInfinity() {
    window.speechSynthesis.resume();
    timeoutResumeInfinity = setTimeout(resumeInfinity, 1000);
}
你可以将这个与 onend 和 onstart 事件关联起来,这样只有在必要时才会调用 resume。像这样:
你可以将此与“onend”和“onstart”事件相关联,以便仅在必要时调用“resume”。例如:
var utterance = new SpeechSynthesisUtterance();

utterance.onstart = function(event) {
    resumeInfinity();
};

utterance.onend = function(event) {
    clearTimeout(timeoutResumeInfinity);
};

我是偶然发现这个的!

希望这能帮到你!


1428个字符以下的工作正常,超过1428个字符就不行了。很奇怪。 - Patrioticcow
1
我认为我们面临的不是字符限制,而是时间限制(请参见此问题和其他地方的其他答案)。我刚刚在Chrome 63.0.3239.132(官方版本)(64位)和Windows 7/64 Pro下测试了https://dev59.com/gVgQ5IYBdhLWcg3wXizH上发布的fiddle,其中包含超过1800个非本地语言字符,并且它可以正常工作。 - CODE-REaD
我尝试了你的解决方案,对我来说很有效。谢谢分享。 - Andrei Bacescu
有人知道吗,如果在“onend”触发之前删除了话语,这是否会导致内存泄漏? - Jankapunkt
这个解决方案似乎已经运行了几年,但现在对于更长的文本不再起作用了。是否有新的解决方案? - Jeff Baker
显示剩余2条评论

8
Peter的回答存在问题,当你有一系列语音合成排队等待时,它无法正常工作。脚本会将新的块放在队列的末尾,从而导致顺序混乱。示例:https://jsfiddle.net/1gzkja90/
<script type='text/javascript' src='http://code.jquery.com/jquery-2.1.0.js'></script>
<script type='text/javascript'>    
    u = new SpeechSynthesisUtterance();
    $(document).ready(function () {
        $('.t').each(function () {
            u = new SpeechSynthesisUtterance($(this).text());

            speechUtteranceChunker(u, {
                chunkLength: 120
            }, function () {
                console.log('end');
            });
        });
    });
     /**
     * Chunkify
     * Google Chrome Speech Synthesis Chunking Pattern
     * Fixes inconsistencies with speaking long texts in speechUtterance objects 
     * Licensed under the MIT License
     *
     * Peter Woolley and Brett Zamir
     */
    var speechUtteranceChunker = function (utt, settings, callback) {
        settings = settings || {};
        var newUtt;
        var txt = (settings && settings.offset !== undefined ? utt.text.substring(settings.offset) : utt.text);
        if (utt.voice && utt.voice.voiceURI === 'native') { // Not part of the spec
            newUtt = utt;
            newUtt.text = txt;
            newUtt.addEventListener('end', function () {
                if (speechUtteranceChunker.cancel) {
                    speechUtteranceChunker.cancel = false;
                }
                if (callback !== undefined) {
                    callback();
                }
            });
        }
        else {
            var chunkLength = (settings && settings.chunkLength) || 160;
            var pattRegex = new RegExp('^[\\s\\S]{' + Math.floor(chunkLength / 2) + ',' + chunkLength + '}[.!?,]{1}|^[\\s\\S]{1,' + chunkLength + '}$|^[\\s\\S]{1,' + chunkLength + '} ');
            var chunkArr = txt.match(pattRegex);

            if (chunkArr[0] === undefined || chunkArr[0].length <= 2) {
                //call once all text has been spoken...
                if (callback !== undefined) {
                    callback();
                }
                return;
            }
            var chunk = chunkArr[0];
            newUtt = new SpeechSynthesisUtterance(chunk);
            var x;
            for (x in utt) {
                if (utt.hasOwnProperty(x) && x !== 'text') {
                    newUtt[x] = utt[x];
                }
            }
            newUtt.addEventListener('end', function () {
                if (speechUtteranceChunker.cancel) {
                    speechUtteranceChunker.cancel = false;
                    return;
                }
                settings.offset = settings.offset || 0;
                settings.offset += chunk.length - 1;
                speechUtteranceChunker(utt, settings, callback);
            });
        }

        if (settings.modifier) {
            settings.modifier(newUtt);
        }
        console.log(newUtt); //IMPORTANT!! Do not remove: Logging the object out fixes some onend firing issues.
        //placing the speak invocation inside a callback fixes ordering and onend issues.
        setTimeout(function () {
            speechSynthesis.speak(newUtt);
        }, 0);
    };
</script>
<p class="t">MLA format follows the author-page method of in-text citation. This means that the author's last name and the page number(s) from which the quotation or paraphrase is taken must appear in the text, and a complete reference should appear on your Works Cited page. The author's name may appear either in the sentence itself or in parentheses following the quotation or paraphrase, but the page number(s) should always appear in the parentheses, not in the text of your sentence.</p>
<p class="t">Joe waited for the train.</p>
<p class="t">The train was late.</p>
<p class="t">Mary and Samantha took the bus.</p>

在我的情况下,答案是将字符串“分块”后再添加到队列中。请参见此处:http://jsfiddle.net/vqvyjzq4/ 非常感谢Peter提供的想法和正则表达式(我仍然没有征服它)。我相信JavaScript可以被整理得更干净,这只是一个概念证明。
<script type='text/javascript' src='http://code.jquery.com/jquery-2.1.0.js'></script>
<script type='text/javascript'>    
    var chunkLength = 120;
    var pattRegex = new RegExp('^[\\s\\S]{' + Math.floor(chunkLength / 2) + ',' + chunkLength + '}[.!?,]{1}|^[\\s\\S]{1,' + chunkLength + '}$|^[\\s\\S]{1,' + chunkLength + '} ');

    $(document).ready(function () {
        var element = this;
        var arr = [];
        var txt = replaceBlank($(element).text());
        while (txt.length > 0) {
            arr.push(txt.match(pattRegex)[0]);
            txt = txt.substring(arr[arr.length - 1].length);
        }
        $.each(arr, function () {
            var u = new SpeechSynthesisUtterance(this.trim());
            window.speechSynthesis.speak(u);
        });
    });
</script>
<p class="t">MLA format follows the author-page method of in-text citation. This means that the author's last name and the page number(s) from which the quotation or paraphrase is taken must appear in the text, and a complete reference should appear on your Works Cited page. The author's name may appear either in the sentence itself or in parentheses following the quotation or paraphrase, but the page number(s) should always appear in the parentheses, not in the text of your sentence.</p>
<p class="t">Joe waited for the train.</p>
<p class="t">The train was late.</p>
<p class="t">Mary and Samantha took the bus.</p>

6

到了2017年,这个bug仍然存在。我恰好对这个问题非常了解,因为我是屡获殊荣的Chrome扩展程序Read Aloud的开发者。好吧,只是开玩笑,并没有获得奖项。

  1. 如果你的演讲时间超过15秒,它会卡住。
  2. 我发现Chrome使用了一个15秒的空闲定时器来决定何时停用扩展程序的事件/后台页面。我认为这就是罪魁祸首。

我使用的解决方法是一个相当复杂的分块算法,它遵循标点符号。对于拉丁语系的语言,我的最大分块大小为36个单词。如果您有兴趣,代码是开源的:https://github.com/ken107/read-aloud/blob/315f1e1d5be6b28ba47fe0c309961025521de516/js/speech.js#L212

36个单词的限制在大多数情况下都能很好地工作,在15秒内完成。但是有些情况下还是会卡住。为了从中恢复过来,我使用了一个16秒的定时器。


1
太好了!我来这里是因为我正在做一个类似的项目,遇到了这个问题。很高兴看到你已经有了可以工作的Chrome和Firefox插件! - Jason O'Neil

6
这是我最终得到的结果,它只是在句号"."处分割我的句子。
var voices = window.speechSynthesis.getVoices();

var sayit = function ()
{
    var msg = new SpeechSynthesisUtterance();

    msg.voice = voices[10]; // Note: some voices don't support altering params
    msg.voiceURI = 'native';
    msg.volume = 1; // 0 to 1
    msg.rate = 1; // 0.1 to 10
    msg.pitch = 2; //0 to 2
    msg.lang = 'en-GB';
    msg.onstart = function (event) {

        console.log("started");
    };
    msg.onend = function(event) {
        console.log('Finished in ' + event.elapsedTime + ' seconds.');
    };
    msg.onerror = function(event)
    {

        console.log('Errored ' + event);
    }
    msg.onpause = function (event)
    {
        console.log('paused ' + event);

    }
    msg.onboundary = function (event)
    {
        console.log('onboundary ' + event);
    }

    return msg;
}


var speekResponse = function (text)
{
    speechSynthesis.cancel(); // if it errors, this clears out the error.

    var sentences = text.split(".");
    for (var i=0;i< sentences.length;i++)
    {
        var toSay = sayit();
        toSay.text = sentences[i];
        speechSynthesis.speak(toSay);
    }
}

5
我最终将文本进行分块,并对处理各种标点符号(如句号、逗号等)的方式进行了一些智能调整。例如,如果逗号是数字的一部分(即$10,000),则不希望在逗号处将文本分开。
我已经对其进行了测试,似乎可以处理任意大的输入,并且它似乎不仅适用于桌面端,还适用于安卓手机和iPhone。
为语音合成器设置了一个 GitHub 页面:https://github.com/unk1911/speech 你可以在以下链接中实时查看:http://edeliverables.com/tts/

最新版本的Chrome似乎支持更长的块,因此不再需要将文本分成较小的部分。 - user3892260
5
实际上,我收回之前的说法,在最新的Chrome版本中,只有原住民说英语的人才支持更长的文本块。其他人(例如英国人)仍然会遇到这个问题,所以我不得不将分块器重新放入我的代码中。 - user3892260
1
我的Android平板电脑安装了多种语言,我发现Chrome语音合成在我尝试过的外语(法语)中说话时间较长时没有问题。因此,我怀疑问题不在于语言是否为本地语言,而在于是否在本地安装了语音。 - CODE-REaD

1

new Vue({
  el: "#app",
  data: {
    text: `Collaboratively administrate empowered markets via plug-and-play networks. Dynamically procrastinate B2C users after installed base benefits. Dramatically visualize customer directed convergence without revolutionary ROI. Efficiently unleash cross-media information without cross-media value. Quickly maximize timely deliverables for real-time schemas. Dramatically maintain clicks-and-mortar solutions without functional solutions.`
  },

  methods:{
    stop_reading() {
      const synth = window.speechSynthesis;
      synth.cancel();
    },

    talk() {
      const synth = window.speechSynthesis;
      const textInput = this.text;

      const utterThis = new SpeechSynthesisUtterance(textInput);
      utterThis.pitch = 0;
      utterThis.rate = 1;
      synth.speak(utterThis);

      const resumeInfinity = () => {
        window.speechSynthesis.resume();
        const timeoutResumeInfinity = setTimeout(resumeInfinity, 1000);
      }
      
      utterThis.onstart = () => {
        resumeInfinity();
      };
    }
  }
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
  <button @click="talk">Speak</button>
  <button @click="stop_reading">Stop</button>
</div>


0
我想说,通过使用Chrome扩展和应用程序,我使用chrome.tts解决了这个相当恼人的问题,因为chrome.tts允许您通过浏览器而不是窗口说话,从而停止在关闭窗口时讲话。
使用下面的代码,您可以修复上面的大量发言问题:
chrome.tts.speak("Abnormally large string, over 250 characters, etc...");
setInterval(() => { chrome.tts.resume(); }, 100);

我相信那会起作用,但我这样做只是为了安全起见:

var largeData = "";
var smallChunks = largeData.match(/.{1,250}/g);
for (var chunk of smallChunks) {
  chrome.tts.speak(chunk, {'enqueue': true});
}

希望这能帮助到某些人!它有助于使我的应用程序更加功能强大和史诗般。

0

正如Michael所提出的那样,Peter的解决方案非常棒,除非你的文本在不同的行上。Michael创建了演示来更好地说明它的问题。- https://jsfiddle.net/1gzkja90/ 并提出了另一种解决方案。

为了添加一种可能更简单的解决方法,可以从Peter的解决方案中删除文本区域中的换行符,这样就可以完美解决问题了。

//javascript
var noLineBreaks = document.getElementById('mytextarea').replace(/\n/g,'');

//jquery
var noLineBreaks = $('#mytextarea').val().replace(/\n/g,'');

因此,在Peter的解决方案中,它可能如下所示:

utterance.text = $('#mytextarea').val().replace(/\n/g,'');

但是取消语音仍然存在问题。它只是转到另一个序列,无法停止。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接