Clean up opentts_abc
This commit is contained in:
parent
ef85fc4c5c
commit
bfdc26621e
4 changed files with 318 additions and 179 deletions
|
|
@ -0,0 +1,4 @@
|
||||||
|
# Open Text to Speech (TTS) Abstract Base Classes (ABC)
|
||||||
|
|
||||||
|
Base classes for open text to speech systems.
|
||||||
|
|
||||||
|
|
@ -1,68 +1,107 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Base classes for Open Text to Speech systems"""
|
"""Base classes for Open Text to Speech systems"""
|
||||||
import dataclasses
|
|
||||||
import io
|
import io
|
||||||
import typing
|
import typing
|
||||||
import wave
|
import wave
|
||||||
from abc import ABCMeta, abstractmethod
|
from abc import ABCMeta, abstractmethod
|
||||||
from contextlib import AbstractContextManager
|
from contextlib import AbstractContextManager
|
||||||
from copy import deepcopy
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Settings:
|
class Settings:
|
||||||
|
"""Current settings for TTS system"""
|
||||||
|
|
||||||
voice: typing.Optional[str] = None
|
voice: typing.Optional[str] = None
|
||||||
|
"""Current voice key"""
|
||||||
|
|
||||||
language: typing.Optional[str] = None
|
language: typing.Optional[str] = None
|
||||||
|
"""Current language (e.g., en_US)"""
|
||||||
|
|
||||||
volume: typing.Optional[float] = None
|
volume: typing.Optional[float] = None
|
||||||
|
"""Current speaking volume"""
|
||||||
|
|
||||||
rate: typing.Optional[float] = None
|
rate: typing.Optional[float] = None
|
||||||
|
"""Current speaking rate"""
|
||||||
|
|
||||||
pitch: typing.Optional[float] = None
|
pitch: typing.Optional[float] = None
|
||||||
active_lexicons: typing.Optional[typing.Sequence[str]] = None
|
"""Current speaking pitch"""
|
||||||
|
|
||||||
other_settings: typing.Optional[typing.Mapping[str, typing.Any]] = None
|
other_settings: typing.Optional[typing.Mapping[str, typing.Any]] = None
|
||||||
|
"""Custom settings"""
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class BaseToken(metaclass=ABCMeta):
|
class BaseToken(metaclass=ABCMeta):
|
||||||
|
"""Base class for spoken tokens"""
|
||||||
|
|
||||||
text: str
|
text: str
|
||||||
|
"""Text of the token"""
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Word(BaseToken):
|
class Word(BaseToken):
|
||||||
|
"""Token representing a single word"""
|
||||||
|
|
||||||
role: typing.Optional[str] = None
|
role: typing.Optional[str] = None
|
||||||
|
"""Role of the word (typically part of speech)"""
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Phonemes(BaseToken):
|
class Phonemes(BaseToken):
|
||||||
|
"""Token representing a phonemized word"""
|
||||||
|
|
||||||
alphabet: typing.Optional[str] = None
|
alphabet: typing.Optional[str] = None
|
||||||
|
"""Phoneme alphabet (e.g., ipa)"""
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class SayAs(BaseToken):
|
class SayAs(BaseToken):
|
||||||
|
"""Token representing a word or phrase that must be spoken a particular way"""
|
||||||
|
|
||||||
interpret_as: str
|
interpret_as: str
|
||||||
|
"""Implementation-dependent token interpretation (e.g., characters or digits)"""
|
||||||
|
|
||||||
format: typing.Optional[str] = None
|
format: typing.Optional[str] = None
|
||||||
|
"""Implementation-dependent token format (depends on interpret_as)"""
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class _BaseResultDefaults:
|
class _BaseResultDefaults:
|
||||||
|
"""Base class of results from TTS end_utterance"""
|
||||||
|
|
||||||
tag: typing.Optional[typing.Any] = None
|
tag: typing.Optional[typing.Any] = None
|
||||||
|
"""Optional tag to associate with results"""
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class BaseResult(metaclass=ABCMeta):
|
class BaseResult(metaclass=ABCMeta):
|
||||||
pass
|
"""Base class of results from TTS end_utterance"""
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class _AudioResultBase:
|
class _AudioResultBase:
|
||||||
|
"""Synthesized audio result"""
|
||||||
|
|
||||||
sample_rate_hz: int
|
sample_rate_hz: int
|
||||||
|
"""Sample rate in Hertz (e.g., 22050)"""
|
||||||
|
|
||||||
sample_width_bytes: int
|
sample_width_bytes: int
|
||||||
|
"""Sample width in bytes (e.g., 2)"""
|
||||||
|
|
||||||
num_channels: int
|
num_channels: int
|
||||||
|
"""Number of audio channels (e.g., 1)"""
|
||||||
|
|
||||||
audio_bytes: bytes
|
audio_bytes: bytes
|
||||||
|
"""Raw audio bytes (no header)"""
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class AudioResult(BaseResult, _BaseResultDefaults, _AudioResultBase):
|
class AudioResult(BaseResult, _BaseResultDefaults, _AudioResultBase):
|
||||||
|
"""Synthesized audio result"""
|
||||||
|
|
||||||
def to_wav_bytes(self) -> bytes:
|
def to_wav_bytes(self) -> bytes:
|
||||||
|
"""Convert audio bytes to WAV"""
|
||||||
with io.BytesIO() as wav_io:
|
with io.BytesIO() as wav_io:
|
||||||
wav_file: wave.Wave_write = wave.open(wav_io, "wb")
|
wav_file: wave.Wave_write = wave.open(wav_io, "wb")
|
||||||
with wav_file:
|
with wav_file:
|
||||||
|
|
@ -76,92 +115,126 @@ class AudioResult(BaseResult, _BaseResultDefaults, _AudioResultBase):
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class _MarkResultBase:
|
class _MarkResultBase:
|
||||||
|
"""Result indicating a <mark> has been reached in SSML"""
|
||||||
|
|
||||||
name: str
|
name: str
|
||||||
|
"""Name of the <mark>"""
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class MarkResult(BaseResult, _BaseResultDefaults, _MarkResultBase):
|
class MarkResult(BaseResult, _BaseResultDefaults, _MarkResultBase):
|
||||||
pass
|
"""Result indicating a <mark> has been reached in SSML"""
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Voice:
|
class Voice:
|
||||||
|
"""Details of a voice in a text to speech system"""
|
||||||
|
|
||||||
key: str
|
key: str
|
||||||
|
"""Unique key that can be used to reference the voice"""
|
||||||
|
|
||||||
name: str
|
name: str
|
||||||
|
"""Human-readable name of the voice"""
|
||||||
|
|
||||||
language: str
|
language: str
|
||||||
|
"""Language of the voice (e.g., en_US)"""
|
||||||
|
|
||||||
description: str
|
description: str
|
||||||
|
"""Human-readable description of the voice"""
|
||||||
|
|
||||||
speakers: typing.Optional[typing.Sequence[str]] = None
|
speakers: typing.Optional[typing.Sequence[str]] = None
|
||||||
|
"""List of speakers within the voice model if multi-speaker"""
|
||||||
|
|
||||||
properties: typing.Optional[typing.Mapping[str, typing.Any]] = None
|
properties: typing.Optional[typing.Mapping[str, typing.Any]] = None
|
||||||
|
"""Additional properties associated with the voice"""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_multispeaker(self) -> bool:
|
def is_multispeaker(self) -> bool:
|
||||||
|
"""True if voice has multiple speakers"""
|
||||||
return (self.speakers is not None) and (len(self.speakers) > 1)
|
return (self.speakers is not None) and (len(self.speakers) > 1)
|
||||||
|
|
||||||
|
|
||||||
# @dataclass
|
|
||||||
# class LexiconEntry:
|
|
||||||
# word: str
|
|
||||||
# pronunciation: str
|
|
||||||
# role: typing.Optional[str] = None
|
|
||||||
|
|
||||||
|
|
||||||
# @dataclass
|
|
||||||
# class Lexicon:
|
|
||||||
# name: str
|
|
||||||
# entries: typing.Mapping[str, typing.Sequence[LexiconEntry]]
|
|
||||||
|
|
||||||
|
|
||||||
class TextToSpeechSystem(AbstractContextManager, metaclass=ABCMeta):
|
class TextToSpeechSystem(AbstractContextManager, metaclass=ABCMeta):
|
||||||
"""Abstract base class for open text to speech systems"""
|
"""Abstract base class for open text to speech systems.
|
||||||
|
|
||||||
|
Expected usage:
|
||||||
|
|
||||||
|
begin_utterance()
|
||||||
|
speak_text(...)
|
||||||
|
add_break(...)
|
||||||
|
set_mark(...)
|
||||||
|
speak_tokens(...)
|
||||||
|
speak_text(...)
|
||||||
|
results = end_utterance()
|
||||||
|
|
||||||
|
In between begin_utterance() and end_utterance(), the voice/language may
|
||||||
|
also be changed.
|
||||||
|
"""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def voice(self) -> str:
|
def voice(self) -> str:
|
||||||
pass
|
"""Get the current voice key"""
|
||||||
|
|
||||||
@voice.setter
|
@voice.setter
|
||||||
def voice(self, new_voice: str):
|
def voice(self, new_voice: str):
|
||||||
pass
|
"""Set the current voice key"""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def language(self) -> str:
|
def language(self) -> str:
|
||||||
pass
|
"""Get the current voice language"""
|
||||||
|
|
||||||
@language.setter
|
@language.setter
|
||||||
def language(self, new_language: str):
|
def language(self, new_language: str):
|
||||||
pass
|
"""Set the current voice language"""
|
||||||
|
|
||||||
def shutdown(self):
|
def shutdown(self):
|
||||||
pass
|
"""Called by the host program when the text to speech system should be stopped"""
|
||||||
|
|
||||||
def __exit__(self, exc_type, exc_value, traceback):
|
def __exit__(self, exc_type, exc_value, traceback):
|
||||||
|
"""Automatically call shutdown when context manager has exited"""
|
||||||
self.shutdown()
|
self.shutdown()
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_voices(self) -> typing.Iterable[Voice]:
|
def get_voices(self) -> typing.Iterable[Voice]:
|
||||||
pass
|
"""Returns an iterable of available voices"""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def begin_utterance(self):
|
def begin_utterance(self):
|
||||||
pass
|
"""Begins a new utterance"""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def speak_text(self, text: str):
|
def speak_text(self, text: str):
|
||||||
pass
|
"""Speaks text using the underlying system's tokenization mechanism.
|
||||||
|
|
||||||
|
Becomes an AudioResult in end_utterance()
|
||||||
|
"""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def speak_tokens(self, tokens: typing.Iterable[BaseToken]):
|
def speak_tokens(self, tokens: typing.Iterable[BaseToken]):
|
||||||
pass
|
"""Speak user-defined tokens.
|
||||||
|
|
||||||
|
Becomes an AudioResult in end_utterance()
|
||||||
|
"""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def add_break(self, time_ms: int):
|
def add_break(self, time_ms: int):
|
||||||
pass
|
"""Add milliseconds of silence to the current utterance.
|
||||||
|
|
||||||
|
Becomes an AudioResult in end_utterance()
|
||||||
|
"""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def set_mark(self, name: str):
|
def set_mark(self, name: str):
|
||||||
pass
|
"""Set a named mark at this point in the utterance.
|
||||||
|
|
||||||
|
Becomes a MarkResult in end_utterance()
|
||||||
|
"""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def end_utterance(self) -> typing.Iterable[BaseResult]:
|
def end_utterance(self) -> typing.Iterable[BaseResult]:
|
||||||
pass
|
"""Complete an utterance after begin_utterance().
|
||||||
|
|
||||||
|
Returns an iterable of results (audio, marks, etc.)
|
||||||
|
"""
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
"""Support for Speech Synthesis Markup Language (SSML)"""
|
||||||
import enum
|
import enum
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
|
@ -6,14 +7,7 @@ import typing
|
||||||
import xml.etree.ElementTree as etree
|
import xml.etree.ElementTree as etree
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
from opentts_abc import (
|
from opentts_abc import BaseResult, Phonemes, SayAs, TextToSpeechSystem, Word
|
||||||
BaseResult,
|
|
||||||
Phonemes,
|
|
||||||
SayAs,
|
|
||||||
Settings,
|
|
||||||
TextToSpeechSystem,
|
|
||||||
Word,
|
|
||||||
)
|
|
||||||
|
|
||||||
LOG = logging.getLogger("opentts_abc.ssml")
|
LOG = logging.getLogger("opentts_abc.ssml")
|
||||||
NO_NAMESPACE_PATTERN = re.compile(r"^{[^}]+}")
|
NO_NAMESPACE_PATTERN = re.compile(r"^{[^}]+}")
|
||||||
|
|
@ -43,15 +37,6 @@ class ParsingState(int, enum.Enum):
|
||||||
IN_PHONEME = enum.auto()
|
IN_PHONEME = enum.auto()
|
||||||
"""Inside <phoneme>"""
|
"""Inside <phoneme>"""
|
||||||
|
|
||||||
IN_LEXICON = enum.auto()
|
|
||||||
"""Inside <lexicon>"""
|
|
||||||
|
|
||||||
IN_LEXICON_GRAPHEME = enum.auto()
|
|
||||||
"""Inside <lexicon><grapheme>..."""
|
|
||||||
|
|
||||||
IN_LEXICON_PHONEME = enum.auto()
|
|
||||||
"""Inside <lexicon><phoneme>..."""
|
|
||||||
|
|
||||||
IN_METADATA = enum.auto()
|
IN_METADATA = enum.auto()
|
||||||
"""Inside <metadata>"""
|
"""Inside <metadata>"""
|
||||||
|
|
||||||
|
|
@ -63,18 +48,24 @@ class ParsingState(int, enum.Enum):
|
||||||
|
|
||||||
|
|
||||||
class SSMLSpeaker:
|
class SSMLSpeaker:
|
||||||
|
"""Wrapper for TextToSpeechSystem that parses/implements SSML.
|
||||||
|
|
||||||
|
See: https://www.w3.org/TR/speech-synthesis11/
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, tts: TextToSpeechSystem):
|
def __init__(self, tts: TextToSpeechSystem):
|
||||||
self.state_stack: typing.List[ParsingState] = [ParsingState.DEFAULT]
|
self._state_stack: typing.List[ParsingState] = [ParsingState.DEFAULT]
|
||||||
self.element_stack: typing.List[etree.Element] = []
|
self._element_stack: typing.List[etree.Element] = []
|
||||||
self.voice_stack: typing.List[str] = []
|
self._voice_stack: typing.List[str] = []
|
||||||
self.lang_stack: typing.List[str] = []
|
self._lang_stack: typing.List[str] = []
|
||||||
self.interpret_as: typing.Optional[str] = None
|
self._interpret_as: typing.Optional[str] = None
|
||||||
self.say_as_format: typing.Optional[str] = None
|
self._say_as_format: typing.Optional[str] = None
|
||||||
self.tts = tts
|
self.tts = tts
|
||||||
|
|
||||||
def speak(
|
def speak(
|
||||||
self, ssml: typing.Union[str, etree.Element]
|
self, ssml: typing.Union[str, etree.Element]
|
||||||
) -> typing.Iterable[BaseResult]:
|
) -> typing.Iterable[BaseResult]:
|
||||||
|
"""Parses and realizes a set of SSML utterances using the underlying TextToSpeechSystem"""
|
||||||
|
|
||||||
if isinstance(ssml, etree.Element):
|
if isinstance(ssml, etree.Element):
|
||||||
root_element = ssml
|
root_element = ssml
|
||||||
|
|
@ -84,39 +75,39 @@ class SSMLSpeaker:
|
||||||
# Process sub-elements and text chunks
|
# Process sub-elements and text chunks
|
||||||
for elem_or_text in text_and_elements(root_element):
|
for elem_or_text in text_and_elements(root_element):
|
||||||
if isinstance(elem_or_text, str):
|
if isinstance(elem_or_text, str):
|
||||||
if self.state in {ParsingState.IN_METADATA}:
|
if self._state in {ParsingState.IN_METADATA}:
|
||||||
# Skip metadata text
|
# Skip metadata text
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Text chunk
|
# Text chunk
|
||||||
text = typing.cast(str, elem_or_text)
|
text = typing.cast(str, elem_or_text)
|
||||||
self.handle_text(text)
|
self._handle_text(text)
|
||||||
elif isinstance(elem_or_text, EndElement):
|
elif isinstance(elem_or_text, EndElement):
|
||||||
# End of an element (e.g., </w>)
|
# End of an element (e.g., </w>)
|
||||||
end_elem = typing.cast(EndElement, elem_or_text)
|
end_elem = typing.cast(EndElement, elem_or_text)
|
||||||
end_tag = tag_no_namespace(end_elem.element.tag)
|
end_tag = tag_no_namespace(end_elem.element.tag)
|
||||||
|
|
||||||
if end_tag == "s":
|
if end_tag == "s":
|
||||||
yield from self.handle_end_sentence()
|
yield from self._handle_end_sentence()
|
||||||
elif end_tag in {"w", "token"}:
|
elif end_tag in {"w", "token"}:
|
||||||
self.handle_end_word()
|
self._handle_end_word()
|
||||||
elif end_tag in {"phoneme"}:
|
elif end_tag in {"phoneme"}:
|
||||||
self.handle_end_phoneme()
|
self._handle_end_phoneme()
|
||||||
elif end_tag == "voice":
|
elif end_tag == "voice":
|
||||||
self.handle_end_voice()
|
self._handle_end_voice()
|
||||||
elif end_tag == "say-as":
|
elif end_tag == "say-as":
|
||||||
self.handle_end_say_as()
|
self._handle_end_say_as()
|
||||||
elif end_tag in {"sub"}:
|
elif end_tag in {"sub"}:
|
||||||
# Handled in handle_text
|
# Handled in handle_text
|
||||||
pass
|
pass
|
||||||
elif end_tag in {"metadata", "meta"}:
|
elif end_tag in {"metadata", "meta"}:
|
||||||
self.handle_end_metadata()
|
self._handle_end_metadata()
|
||||||
elif end_tag == "speak":
|
elif end_tag == "speak":
|
||||||
yield from self.handle_end_speak()
|
yield from self._handle_end_speak()
|
||||||
else:
|
else:
|
||||||
LOG.debug("Ignoring end tag: %s", end_tag)
|
LOG.debug("Ignoring end tag: %s", end_tag)
|
||||||
else:
|
else:
|
||||||
if self.state in {ParsingState.IN_METADATA}:
|
if self._state in {ParsingState.IN_METADATA}:
|
||||||
# Skip metadata text
|
# Skip metadata text
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|
@ -132,86 +123,90 @@ class SSMLSpeaker:
|
||||||
elem_tag = tag_no_namespace(elem.tag)
|
elem_tag = tag_no_namespace(elem.tag)
|
||||||
|
|
||||||
if elem_tag == "s":
|
if elem_tag == "s":
|
||||||
self.handle_begin_sentence()
|
self._handle_begin_sentence()
|
||||||
elif elem_tag in {"w", "token"}:
|
elif elem_tag in {"w", "token"}:
|
||||||
self.handle_begin_word(elem)
|
self._handle_begin_word(elem)
|
||||||
elif elem_tag == "sub":
|
elif elem_tag == "sub":
|
||||||
self.handle_begin_sub(elem)
|
self._handle_begin_sub(elem)
|
||||||
elif elem_tag == "phoneme":
|
elif elem_tag == "phoneme":
|
||||||
self.handle_begin_phoneme(elem)
|
self._handle_begin_phoneme(elem)
|
||||||
elif elem_tag == "break":
|
elif elem_tag == "break":
|
||||||
self.handle_break(elem)
|
self._handle_break(elem)
|
||||||
elif elem_tag == "mark":
|
elif elem_tag == "mark":
|
||||||
self.handle_mark(elem)
|
self._handle_mark(elem)
|
||||||
elif elem_tag == "voice":
|
elif elem_tag == "voice":
|
||||||
self.handle_begin_voice(elem)
|
self._handle_begin_voice(elem)
|
||||||
elif elem_tag == "say-as":
|
elif elem_tag == "say-as":
|
||||||
self.handle_begin_say_as(elem)
|
self._handle_begin_say_as(elem)
|
||||||
elif elem_tag in {"metadata", "meta"}:
|
elif elem_tag in {"metadata", "meta"}:
|
||||||
self.handle_begin_metadata()
|
self._handle_begin_metadata()
|
||||||
else:
|
else:
|
||||||
LOG.debug("Ignoring start tag: %s", elem_tag)
|
LOG.debug("Ignoring start tag: %s", elem_tag)
|
||||||
|
|
||||||
assert self.state in {
|
assert self._state in {
|
||||||
ParsingState.IN_SENTENCE,
|
ParsingState.IN_SENTENCE,
|
||||||
ParsingState.DEFAULT,
|
ParsingState.DEFAULT,
|
||||||
}, self.state
|
}, self._state
|
||||||
if self.state in {ParsingState.IN_SENTENCE}:
|
|
||||||
yield from self.handle_end_sentence()
|
if self._state in {ParsingState.IN_SENTENCE}:
|
||||||
|
yield from self._handle_end_sentence()
|
||||||
|
|
||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
|
|
||||||
def handle_text(self, text: str):
|
def _handle_text(self, text: str):
|
||||||
assert self.state in {
|
"""Handle sentence/word text"""
|
||||||
|
assert self._state in {
|
||||||
ParsingState.DEFAULT,
|
ParsingState.DEFAULT,
|
||||||
ParsingState.IN_SENTENCE,
|
ParsingState.IN_SENTENCE,
|
||||||
ParsingState.IN_WORD,
|
ParsingState.IN_WORD,
|
||||||
ParsingState.IN_SUB,
|
ParsingState.IN_SUB,
|
||||||
ParsingState.IN_PHONEME,
|
ParsingState.IN_PHONEME,
|
||||||
ParsingState.IN_SAY_AS,
|
ParsingState.IN_SAY_AS,
|
||||||
}, self.state
|
}, self._state
|
||||||
|
|
||||||
if self.state == ParsingState.IN_PHONEME:
|
if self._state == ParsingState.IN_PHONEME:
|
||||||
# Phonemes were emitted in handle_begin_phoneme
|
# Phonemes were emitted in handle_begin_phoneme
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.state == ParsingState.IN_SUB:
|
if self._state == ParsingState.IN_SUB:
|
||||||
# Substitute text
|
# Substitute text
|
||||||
assert self.element is not None
|
assert self._element is not None
|
||||||
text = attrib_no_namespace(self.element, "alias", "")
|
text = attrib_no_namespace(self._element, "alias", "")
|
||||||
LOG.debug("alias text: %s", text)
|
LOG.debug("alias text: %s", text)
|
||||||
|
|
||||||
# Terminate <sub> early
|
# Terminate <sub> early
|
||||||
self.handle_end_sub()
|
self._handle_end_sub()
|
||||||
|
|
||||||
if self.state == ParsingState.DEFAULT:
|
if self._state == ParsingState.DEFAULT:
|
||||||
self.handle_begin_sentence()
|
self._handle_begin_sentence()
|
||||||
|
|
||||||
LOG.debug("text: %s", text)
|
LOG.debug("text: %s", text)
|
||||||
|
|
||||||
if self.state == ParsingState.IN_WORD:
|
if self._state == ParsingState.IN_WORD:
|
||||||
self.handle_word(text, self.element)
|
self._handle_word(text, self._element)
|
||||||
elif self.state == ParsingState.IN_SAY_AS:
|
elif self._state == ParsingState.IN_SAY_AS:
|
||||||
assert self.interpret_as is not None
|
assert self._interpret_as is not None
|
||||||
self.tts.speak_tokens(
|
self.tts.speak_tokens(
|
||||||
[
|
[
|
||||||
SayAs(
|
SayAs(
|
||||||
text=text,
|
text=text,
|
||||||
interpret_as=self.interpret_as,
|
interpret_as=self._interpret_as,
|
||||||
format=self.say_as_format,
|
format=self._say_as_format,
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self.tts.speak_text(text)
|
self.tts.speak_text(text)
|
||||||
|
|
||||||
def handle_begin_word(self, elem: etree.Element):
|
def _handle_begin_word(self, elem: etree.Element):
|
||||||
|
"""Handle <w> or <t>"""
|
||||||
LOG.debug("begin word")
|
LOG.debug("begin word")
|
||||||
self.push_element(elem)
|
self._push_element(elem)
|
||||||
self.push_state(ParsingState.IN_WORD)
|
self._push_state(ParsingState.IN_WORD)
|
||||||
|
|
||||||
def handle_word(self, text: str, elem: typing.Optional[etree.Element] = None):
|
def _handle_word(self, text: str, elem: typing.Optional[etree.Element] = None):
|
||||||
assert self.state in {ParsingState.IN_WORD}, self.state
|
"""Handle text from word"""
|
||||||
|
assert self._state in {ParsingState.IN_WORD}, self._state
|
||||||
|
|
||||||
role: typing.Optional[str] = None
|
role: typing.Optional[str] = None
|
||||||
if elem is not None:
|
if elem is not None:
|
||||||
|
|
@ -219,28 +214,32 @@ class SSMLSpeaker:
|
||||||
|
|
||||||
self.tts.speak_tokens([Word(text, role=role)])
|
self.tts.speak_tokens([Word(text, role=role)])
|
||||||
|
|
||||||
def handle_end_word(self):
|
def _handle_end_word(self):
|
||||||
|
"""Handle </w> or </t>"""
|
||||||
LOG.debug("end word")
|
LOG.debug("end word")
|
||||||
assert self.state in {ParsingState.IN_WORD}, self.state
|
assert self._state in {ParsingState.IN_WORD}, self._state
|
||||||
self.pop_state()
|
self._pop_state()
|
||||||
self.pop_element()
|
self._pop_element()
|
||||||
|
|
||||||
def handle_begin_sub(self, elem: etree.Element):
|
def _handle_begin_sub(self, elem: etree.Element):
|
||||||
|
"""Handle <sub>"""
|
||||||
LOG.debug("begin sub")
|
LOG.debug("begin sub")
|
||||||
self.push_element(elem)
|
self._push_element(elem)
|
||||||
self.push_state(ParsingState.IN_SUB)
|
self._push_state(ParsingState.IN_SUB)
|
||||||
|
|
||||||
def handle_end_sub(self):
|
def _handle_end_sub(self):
|
||||||
|
"""Handle </sub>"""
|
||||||
LOG.debug("end sub")
|
LOG.debug("end sub")
|
||||||
assert self.state in {ParsingState.IN_SUB}, self.state
|
assert self._state in {ParsingState.IN_SUB}, self._state
|
||||||
self.pop_state()
|
self._pop_state()
|
||||||
self.pop_element()
|
self._pop_element()
|
||||||
|
|
||||||
def handle_begin_phoneme(self, elem: etree.Element):
|
def _handle_begin_phoneme(self, elem: etree.Element):
|
||||||
|
"""Handle <phoneme>"""
|
||||||
LOG.debug("begin phoneme")
|
LOG.debug("begin phoneme")
|
||||||
|
|
||||||
if self.state == ParsingState.DEFAULT:
|
if self._state == ParsingState.DEFAULT:
|
||||||
self.handle_begin_sentence()
|
self._handle_begin_sentence()
|
||||||
|
|
||||||
phonemes = attrib_no_namespace(elem, "ph", "")
|
phonemes = attrib_no_namespace(elem, "ph", "")
|
||||||
alphabet = attrib_no_namespace(elem, "alphabet", "")
|
alphabet = attrib_no_namespace(elem, "alphabet", "")
|
||||||
|
|
@ -249,61 +248,70 @@ class SSMLSpeaker:
|
||||||
|
|
||||||
self.tts.speak_tokens([Phonemes(text=phonemes, alphabet=alphabet)])
|
self.tts.speak_tokens([Phonemes(text=phonemes, alphabet=alphabet)])
|
||||||
|
|
||||||
self.push_element(elem)
|
self._push_element(elem)
|
||||||
self.push_state(ParsingState.IN_PHONEME)
|
self._push_state(ParsingState.IN_PHONEME)
|
||||||
|
|
||||||
def handle_end_phoneme(self):
|
def _handle_end_phoneme(self):
|
||||||
|
"""Handle </phoneme>"""
|
||||||
LOG.debug("end phoneme")
|
LOG.debug("end phoneme")
|
||||||
assert self.state in {ParsingState.IN_PHONEME}, self.state
|
assert self._state in {ParsingState.IN_PHONEME}, self._state
|
||||||
self.pop_state()
|
self._pop_state()
|
||||||
self.pop_element()
|
self._pop_element()
|
||||||
|
|
||||||
def handle_begin_metadata(self):
|
def _handle_begin_metadata(self):
|
||||||
|
"""Handle <metadata>"""
|
||||||
LOG.debug("begin metadata")
|
LOG.debug("begin metadata")
|
||||||
self.push_state(ParsingState.IN_METADATA)
|
self._push_state(ParsingState.IN_METADATA)
|
||||||
|
|
||||||
def handle_end_metadata(self):
|
def _handle_end_metadata(self):
|
||||||
|
"""Handle </metadata>"""
|
||||||
LOG.debug("end metadata")
|
LOG.debug("end metadata")
|
||||||
assert self.state in {ParsingState.IN_METADATA}, self.state
|
assert self._state in {ParsingState.IN_METADATA}, self._state
|
||||||
self.pop_state()
|
self._pop_state()
|
||||||
|
|
||||||
def handle_begin_sentence(self):
|
def _handle_begin_sentence(self):
|
||||||
|
"""Handle <s>"""
|
||||||
LOG.debug("begin sentence")
|
LOG.debug("begin sentence")
|
||||||
assert self.state in {ParsingState.DEFAULT}, self.state
|
assert self._state in {ParsingState.DEFAULT}, self._state
|
||||||
self.push_state(ParsingState.IN_SENTENCE)
|
self._push_state(ParsingState.IN_SENTENCE)
|
||||||
self.tts.begin_utterance()
|
self.tts.begin_utterance()
|
||||||
|
|
||||||
def handle_end_sentence(self) -> typing.Iterable[BaseResult]:
|
def _handle_end_sentence(self) -> typing.Iterable[BaseResult]:
|
||||||
|
"""Handle </s>"""
|
||||||
LOG.debug("end sentence")
|
LOG.debug("end sentence")
|
||||||
assert self.state in {ParsingState.IN_SENTENCE}, self.state
|
assert self._state in {ParsingState.IN_SENTENCE}, self._state
|
||||||
self.pop_state()
|
self._pop_state()
|
||||||
|
|
||||||
yield from self.tts.end_utterance()
|
yield from self.tts.end_utterance()
|
||||||
|
|
||||||
def handle_end_speak(self) -> typing.Iterable[BaseResult]:
|
def _handle_end_speak(self) -> typing.Iterable[BaseResult]:
|
||||||
|
"""Handle </speak>"""
|
||||||
LOG.debug("end speak")
|
LOG.debug("end speak")
|
||||||
assert self.state in {ParsingState.DEFAULT}, self.state
|
assert self._state in {ParsingState.DEFAULT}, self._state
|
||||||
|
|
||||||
yield from self.tts.end_utterance()
|
yield from self.tts.end_utterance()
|
||||||
|
|
||||||
def handle_begin_voice(self, elem: etree.Element):
|
def _handle_begin_voice(self, elem: etree.Element):
|
||||||
|
"""Handle <voice>"""
|
||||||
LOG.debug("begin voice")
|
LOG.debug("begin voice")
|
||||||
voice_name = attrib_no_namespace(elem, "name")
|
voice_name = attrib_no_namespace(elem, "name")
|
||||||
|
|
||||||
LOG.debug("voice: %s", voice_name)
|
LOG.debug("voice: %s", voice_name)
|
||||||
self.push_voice(voice_name)
|
self._push_voice(voice_name)
|
||||||
|
|
||||||
# Set new voice
|
# Set new voice
|
||||||
self.tts.voice = voice_name
|
self.tts.voice = voice_name
|
||||||
|
|
||||||
def handle_end_voice(self):
|
def _handle_end_voice(self):
|
||||||
|
"""Handle </voice>"""
|
||||||
LOG.debug("end voice")
|
LOG.debug("end voice")
|
||||||
voice_name = self.pop_voice()
|
voice_name = self._pop_voice()
|
||||||
|
|
||||||
# Restore voice
|
# Restore voice
|
||||||
self.tts.voice = voice_name
|
self.tts.voice = voice_name
|
||||||
|
|
||||||
def handle_break(self, elem: etree.Element):
|
def _handle_break(self, elem: etree.Element):
|
||||||
|
"""Handle <break>"""
|
||||||
time_str = attrib_no_namespace(elem, "time", "").strip()
|
time_str = attrib_no_namespace(elem, "time", "").strip()
|
||||||
time_ms: int = 0
|
time_ms: int = 0
|
||||||
|
|
||||||
|
|
@ -316,90 +324,105 @@ class SSMLSpeaker:
|
||||||
LOG.debug("Break: %s ms", time_ms)
|
LOG.debug("Break: %s ms", time_ms)
|
||||||
self.tts.add_break(time_ms)
|
self.tts.add_break(time_ms)
|
||||||
|
|
||||||
def handle_mark(self, elem: etree.Element):
|
def _handle_mark(self, elem: etree.Element):
|
||||||
|
"""Handle <mark>"""
|
||||||
name = attrib_no_namespace(elem, "name", "")
|
name = attrib_no_namespace(elem, "name", "")
|
||||||
|
|
||||||
LOG.debug("Mark: %s", name)
|
LOG.debug("Mark: %s", name)
|
||||||
self.tts.set_mark(name)
|
self.tts.set_mark(name)
|
||||||
|
|
||||||
def handle_begin_say_as(self, elem: etree.Element):
|
def _handle_begin_say_as(self, elem: etree.Element):
|
||||||
|
"""Handle <say-as>"""
|
||||||
LOG.debug("begin say-as")
|
LOG.debug("begin say-as")
|
||||||
self.interpret_as = attrib_no_namespace(elem, "interpret-as", "")
|
self._interpret_as = attrib_no_namespace(elem, "interpret-as", "")
|
||||||
self.say_as_format = attrib_no_namespace(elem, "format", "")
|
self._say_as_format = attrib_no_namespace(elem, "format", "")
|
||||||
|
|
||||||
LOG.debug("Say as %s, format=%s", self.interpret_as, self.say_as_format)
|
LOG.debug("Say as %s, format=%s", self._interpret_as, self._say_as_format)
|
||||||
self.push_state(ParsingState.IN_SAY_AS)
|
self._push_state(ParsingState.IN_SAY_AS)
|
||||||
|
|
||||||
def handle_end_say_as(self):
|
def _handle_end_say_as(self):
|
||||||
|
"""Handle </say-as>"""
|
||||||
LOG.debug("end say-as")
|
LOG.debug("end say-as")
|
||||||
assert self.state in {ParsingState.IN_SAY_AS}
|
assert self._state in {ParsingState.IN_SAY_AS}
|
||||||
self.interpret_as = None
|
self._interpret_as = None
|
||||||
self.say_as_format = None
|
self._say_as_format = None
|
||||||
self.pop_state()
|
self._pop_state()
|
||||||
|
|
||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self) -> ParsingState:
|
def _state(self) -> ParsingState:
|
||||||
if self.state_stack:
|
"""Get state at the top of the stack"""
|
||||||
return self.state_stack[-1]
|
if self._state_stack:
|
||||||
|
return self._state_stack[-1]
|
||||||
|
|
||||||
return ParsingState.DEFAULT
|
return ParsingState.DEFAULT
|
||||||
|
|
||||||
def push_state(self, new_state: ParsingState):
|
def _push_state(self, new_state: ParsingState):
|
||||||
self.state_stack.append(new_state)
|
"""Push new state on to the stack"""
|
||||||
|
self._state_stack.append(new_state)
|
||||||
|
|
||||||
def pop_state(self) -> ParsingState:
|
def _pop_state(self) -> ParsingState:
|
||||||
if self.state_stack:
|
"""Pop state off the stack"""
|
||||||
return self.state_stack.pop()
|
if self._state_stack:
|
||||||
|
return self._state_stack.pop()
|
||||||
|
|
||||||
return ParsingState.DEFAULT
|
return ParsingState.DEFAULT
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def element(self) -> typing.Optional[etree.Element]:
|
def _element(self) -> typing.Optional[etree.Element]:
|
||||||
if self.element_stack:
|
"""Get XML element at the top of the stack"""
|
||||||
return self.element_stack[-1]
|
if self._element_stack:
|
||||||
|
return self._element_stack[-1]
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def push_element(self, new_element: etree.Element):
|
def _push_element(self, new_element: etree.Element):
|
||||||
self.element_stack.append(new_element)
|
"""Push new XML element on to the stack"""
|
||||||
|
self._element_stack.append(new_element)
|
||||||
|
|
||||||
def pop_element(self) -> typing.Optional[etree.Element]:
|
def _pop_element(self) -> typing.Optional[etree.Element]:
|
||||||
if self.element_stack:
|
"""Pop XML element off the stack"""
|
||||||
return self.element_stack.pop()
|
if self._element_stack:
|
||||||
|
return self._element_stack.pop()
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def lang(self) -> typing.Optional[str]:
|
def _lang(self) -> typing.Optional[str]:
|
||||||
if self.lang_stack:
|
"""Get language at the top of the stack"""
|
||||||
return self.lang_stack[-1]
|
if self._lang_stack:
|
||||||
|
return self._lang_stack[-1]
|
||||||
|
|
||||||
return self.tts.language
|
return self.tts.language
|
||||||
|
|
||||||
def push_lang(self, new_lang: str):
|
def _push_lang(self, new_lang: str):
|
||||||
self.lang_stack.append(new_lang)
|
"""Push new language on to the stack"""
|
||||||
|
self._lang_stack.append(new_lang)
|
||||||
|
|
||||||
def pop_lang(self) -> typing.Optional[str]:
|
def _pop_lang(self) -> typing.Optional[str]:
|
||||||
if self.lang_stack:
|
"""Pop language off the stop of the stack"""
|
||||||
return self.lang_stack.pop()
|
if self._lang_stack:
|
||||||
|
return self._lang_stack.pop()
|
||||||
|
|
||||||
return self.tts.language
|
return self.tts.language
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def voice(self) -> typing.Optional[str]:
|
def _voice(self) -> typing.Optional[str]:
|
||||||
if self.voice_stack:
|
"""Get voice at the top of the stack"""
|
||||||
return self.voice_stack[-1]
|
if self._voice_stack:
|
||||||
|
return self._voice_stack[-1]
|
||||||
|
|
||||||
return self.tts.voice
|
return self.tts.voice
|
||||||
|
|
||||||
def push_voice(self, new_voice: str):
|
def _push_voice(self, new_voice: str):
|
||||||
self.voice_stack.append(new_voice)
|
"""Push new voice on to the stack"""
|
||||||
|
self._voice_stack.append(new_voice)
|
||||||
|
|
||||||
def pop_voice(self) -> typing.Optional[str]:
|
def _pop_voice(self) -> typing.Optional[str]:
|
||||||
if self.voice_stack:
|
"""Pop voice off the top of the stack"""
|
||||||
return self.voice_stack.pop()
|
if self._voice_stack:
|
||||||
|
return self._voice_stack.pop()
|
||||||
|
|
||||||
return self.tts.voice
|
return self.tts.voice
|
||||||
|
|
||||||
|
|
|
||||||
39
opentts-abc/pylintrc
Normal file
39
opentts-abc/pylintrc
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
[MESSAGES CONTROL]
|
||||||
|
disable=
|
||||||
|
format,
|
||||||
|
abstract-class-little-used,
|
||||||
|
abstract-method,
|
||||||
|
cyclic-import,
|
||||||
|
duplicate-code,
|
||||||
|
global-statement,
|
||||||
|
import-outside-toplevel,
|
||||||
|
inconsistent-return-statements,
|
||||||
|
locally-disabled,
|
||||||
|
not-context-manager,
|
||||||
|
redefined-variable-type,
|
||||||
|
too-few-public-methods,
|
||||||
|
too-many-arguments,
|
||||||
|
too-many-branches,
|
||||||
|
too-many-instance-attributes,
|
||||||
|
too-many-lines,
|
||||||
|
too-many-locals,
|
||||||
|
too-many-public-methods,
|
||||||
|
too-many-return-statements,
|
||||||
|
too-many-statements,
|
||||||
|
too-many-boolean-expressions,
|
||||||
|
unnecessary-pass,
|
||||||
|
unused-argument,
|
||||||
|
broad-except,
|
||||||
|
too-many-nested-blocks,
|
||||||
|
invalid-name,
|
||||||
|
unused-import,
|
||||||
|
no-self-use,
|
||||||
|
fixme,
|
||||||
|
useless-super-delegation,
|
||||||
|
missing-module-docstring,
|
||||||
|
missing-class-docstring,
|
||||||
|
missing-function-docstring,
|
||||||
|
import-error
|
||||||
|
|
||||||
|
[FORMAT]
|
||||||
|
expected-line-ending-format=LF
|
||||||
Loading…
Add table
Add a link
Reference in a new issue