File size: 2,109 Bytes
2ea5479
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
import { sleep } from './utils'

const synth = window.speechSynthesis

export class TTS {
  currentText = ''
  speakText = ''
  private controller = new AbortController()
  speaking = false
  get isSpeaking() {
    return this.speaking
  }
  finished = false
  constructor() {}
  abort = () => {
    this.controller.abort()
  }

  reset = () => {
    this.speaking = false
    this.finished = true
    this.currentText = ''
    this.speakText = ''
    this.abort()
  }

  speak = (text: string) => {
    if (!synth || text?.trim()?.length < 2) {
      return
    }
    this.currentText = text.replace(/[^\u4e00-\u9fa5_a-zA-Z0-9,。?,:;\.,:]+/g, '')
    this.finished = false
    this.loop()
  }

  private async doSpeek() {
    return new Promise((resolve) => {
      const endIndex = this.finished ? this.currentText.length :
        Math.max(
          this.currentText.lastIndexOf('。'),
          this.currentText.lastIndexOf(';'),
          this.currentText.lastIndexOf('、'),
          this.currentText.lastIndexOf('?'),
          this.currentText.lastIndexOf('\n')
        )
      const startIndex = this.speakText.length ? Math.max(0, this.currentText.lastIndexOf(this.speakText) + this.speakText.length) : 0

      if (startIndex >= endIndex) {
        return resolve(true)
      }
      const text = this.currentText.slice(startIndex, endIndex)
      this.speakText = text
      const utterThis = new SpeechSynthesisUtterance(text)
      this.controller.signal.onabort = () => {
        synth.cancel()
        this.finished = true
        resolve(false)
      }

      utterThis.onend = function (event) {
        resolve(true)
      }

      utterThis.onerror = function (event) {
        resolve(false)
      }

      const voice = synth.getVoices().find(v => v.name.includes('Microsoft Yunxi Online')) ?? null
      utterThis.voice = voice
      synth.speak(utterThis)
    })
  }

  private async loop() {
    if (this.speaking) return
    this.speaking = true
    while(!this.finished) {
      await Promise.all([sleep(1000), this.doSpeek()])
    }
    this.speaking = false
  }
}