add file-based TTS say implementation as well

This commit is contained in:
Damien Elmes 2020-01-21 13:21:43 +10:00
parent c713683f63
commit 2ac86ac400
2 changed files with 49 additions and 1 deletions

View file

@ -112,6 +112,10 @@ class AVPlayer:
def play_file(self, filename: str) -> None: def play_file(self, filename: str) -> None:
self.play_tags([SoundOrVideoTag(filename=filename)]) self.play_tags([SoundOrVideoTag(filename=filename)])
def insert_file(self, filename: str) -> None:
self._enqueued.insert(0, SoundOrVideoTag(filename=filename))
self._play_next_if_idle()
def toggle_pause(self): def toggle_pause(self):
if self.current_player: if self.current_player:
self.current_player.toggle_pause() self.current_player.toggle_pause()

View file

@ -25,13 +25,16 @@ expose the name of the engine, which would mean the user could write
from __future__ import annotations from __future__ import annotations
import os
import re import re
import subprocess import subprocess
from concurrent.futures import Future
from dataclasses import dataclass from dataclasses import dataclass
from typing import List, Optional from typing import List, Optional
from anki.sound import AVTag, TTSTag from anki.sound import AVTag, TTSTag
from aqt.sound import SimpleProcessPlayer from anki.utils import tmpdir
from aqt.sound import OnDoneCallback, PlayerInterrupted, SimpleProcessPlayer
@dataclass @dataclass
@ -81,6 +84,7 @@ class TTSPlayer:
class TTSProcessPlayer(SimpleProcessPlayer, TTSPlayer): class TTSProcessPlayer(SimpleProcessPlayer, TTSPlayer):
# mypy gets confused if rank_for_tag is defined in TTSPlayer
def rank_for_tag(self, tag: AVTag) -> Optional[int]: def rank_for_tag(self, tag: AVTag) -> Optional[int]:
if not isinstance(tag, TTSTag): if not isinstance(tag, TTSTag):
return None return None
@ -97,6 +101,8 @@ class TTSProcessPlayer(SimpleProcessPlayer, TTSPlayer):
class MacTTSPlayer(TTSProcessPlayer): class MacTTSPlayer(TTSProcessPlayer):
"Invokes a process to play the audio in the background."
VOICE_HELP_LINE_RE = re.compile(r"^(\S+)\s+(\S+)\s+.*$") VOICE_HELP_LINE_RE = re.compile(r"^(\S+)\s+(\S+)\s+.*$")
def _play(self, tag: AVTag) -> None: def _play(self, tag: AVTag) -> None:
@ -134,3 +140,41 @@ class MacTTSPlayer(TTSProcessPlayer):
if not m: if not m:
return None return None
return TTSVoice(name=m.group(1), lang=m.group(2)) return TTSVoice(name=m.group(1), lang=m.group(2))
class MacTTSFilePlayer(MacTTSPlayer):
"Generates an .aiff file, which is played using av_player."
tmppath = os.path.join(tmpdir(), "tts.aiff")
def _play(self, tag: AVTag) -> None:
assert isinstance(tag, TTSTag)
match = self.voice_for_tag(tag)
assert match
voice = match.voice
self._process = subprocess.Popen(
["say", "-v", voice.name, "-f", "-", "-o", self.tmppath],
stdin=subprocess.PIPE,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
# write the input text to stdin
self._process.stdin.write(tag.field_text.encode("utf8"))
self._process.stdin.close()
self._wait_for_termination()
def _on_done(self, ret: Future, cb: OnDoneCallback) -> None:
try:
ret.result()
except PlayerInterrupted:
# don't fire done callback when interrupted
return
# inject file into the top of the audio queue
from aqt.sound import av_player
av_player.insert_file(self.tmppath)
# then tell player to advance, which will cause the file to be played
cb()