Skip to content

Instantly share code, notes, and snippets.

@boisei0 boisei0/__init__.py
Last active May 7, 2019

Embed
What would you like to do?
av_speech, high level python interface around the AVSpeechSynthesizer in ObjectiveC. Code requires Pythonista 3.2, with Python 3.6. WIP, can't guarantee yet it won't crash.
from .av_speech_synthesis_voice import AVSpeechSynthesisVoice
from .av_speech_synthesis_voice_quality import AVSpeechSynthesisVoiceQuality
from .av_speech_synthesis_voice_gender import AVSpeechSynthesisVoiceGender
from .av_speech_utterance import AVSpeechUtterance
from .av_speech_synthesizer import AVSpeechSynthesizer
__all__ = [
"AVSpeechSynthesisVoice",
"AVSpeechSynthesisVoiceQuality",
"AVSpeechSynthesisVoiceGender",
"AVSpeechUtterance",
"AVSpeech",
]
class AVSpeech:
voice: AVSpeechSynthesisVoice = None
def __init__(self):
self._synthesizer = AVSpeechSynthesizer()
def set_voice(self, voice: AVSpeechSynthesisVoice):
self.voice = voice
def say(self, text):
utterance = AVSpeechUtterance(text, self.voice)
self._synthesizer.speak_utterance(utterance)
# Collection of low level converters to/from objective C
from objc_util import ObjCInstance
__all__ = ["ns_string_to_py_string"]
def ns_string_to_py_string(ns_string: ObjCInstance) -> str:
"""Low level function to convert NSString (or subclass thereof) input
to a python `str`.
Confirmed to work work on:
- NSString
- NSTaggedPointerString
- NSMutableString
:param ns_string: A NSString to convert
:return: The converted string as Python `str`.
"""
return ns_string.UTF8String().decode("utf-8")
from typing import List, Optional
from objc_util import ObjCClass, ObjCInstance
from .av_speech_synthesis_voice_quality import AVSpeechSynthesisVoiceQuality
from .av_speech_synthesis_voice_gender import AVSpeechSynthesisVoiceGender
from ._utils import ns_string_to_py_string
__all__ = ["AVSpeechSynthesisVoice"]
_objc_class = ObjCClass("AVSpeechSynthesisVoice")
class AVSpeechSynthesisVoice:
def __init__(
self, language: str, identifier: str, name: str, quality: int, gender: int
) -> None:
"""A distinct voice for use in speech synthesis.
(Implements AVFoundation's AVSpeechSynthesisVoice)
:param language: A BCP 47 code identifying the voice’s language and locale.
:type language: str
:param identifier: The unique identifier for a voice object.
:type identifier: str
:param name: The name for a voice object.
:type: name: str
:param quality: The speech quality for a voice object.
:type quality: int
:param gender: The gender of the voice
:type gender: int
"""
self.language: str = language
self.identifier: str = identifier
self.name: str = name
self.quality: AVSpeechSynthesisVoiceQuality = AVSpeechSynthesisVoiceQuality(
quality
)
self.gender: AVSpeechSynthesisVoiceGender = AVSpeechSynthesisVoiceGender(gender)
@property
def current_language_code(self) -> str:
"""Returns the code for the user’s current locale.
:return: A string containing the BCP 47 language and locale code for
the user’s current locale.
"""
return ns_string_to_py_string(_objc_class.currentLanguageCode())
@classmethod
def get_speech_voices(cls) -> List["AVSpeechSynthesisVoice"]:
"""Returns all available voices.
:return: A list of all available voices as AVSpeechSynthesisVoice
"""
voices = _objc_class.speechVoices()
data = []
for voice in voices:
data.append(
cls(
ns_string_to_py_string(voice.language()),
ns_string_to_py_string(voice.identifier()),
ns_string_to_py_string(voice.name()),
voice.quality(),
voice.gender(),
)
)
return data
@classmethod
def voice_with_identifier(
cls, identifier: str
) -> Optional["AVSpeechSynthesisVoice"]:
"""Returns a voice object for the specified identifier.
:param identifier: The unique identifier for a voice.
:type identifier: str
:return: The voice object for the requested identifier,
or `None` if the identifier is invalid or isn't available on the device.
"""
voice = _objc_class.voiceWithIdentifier_(identifier)
if not voice:
# Voice not found or not available
return None
return cls(
ns_string_to_py_string(voice.language()),
ns_string_to_py_string(voice.identifier()),
ns_string_to_py_string(voice.name()),
voice.quality(),
voice.gender(),
)
@classmethod
def voice_with_language(
cls, language: str, gender: Optional[AVSpeechSynthesisVoiceGender] = None
) -> Optional["AVSpeechSynthesisVoice"]:
"""Returns a voice object for the specified language and locale.
:param language: A BCP 47 code specifying language and locale for a voice.
:type language: str
:param gender: Optional gender to match with the voice
:type: gender: AVSpeechSynthesisVoiceGender or `None`
:return: The voice object for the requested language as AVSpeechSynthesisVoice,
or `None` if the language references a locale/language for which no voice exists.
"""
if not gender:
voice = _objc_class.voiceWithLanguage_(language)
if not voice:
# No voice available for input
return None
return cls(
ns_string_to_py_string(voice.language()),
ns_string_to_py_string(voice.identifier()),
ns_string_to_py_string(voice.name()),
voice.quality(),
voice.gender(),
)
else:
voices = cls.get_speech_voices()
for voice in voices:
if voice.language == language and voice.gender == gender:
return voice
else:
return None
def to_ns(self) -> ObjCInstance:
"""Make the original class available for usage in objc_utils code.
:return: This voice as AVSpeechSynthesisVoice ObjCInstance
"""
return _objc_class.voiceWithIdentifier_(self.identifier)
def __repr__(self):
return (
f"<{self.__class__.__name__!r}: Language: {self.language}, Name: {self.name}, "
f"Gender: {self.gender} Quality: {self.quality} [{self.identifier}]>"
)
from enum import IntEnum, unique
__all__ = ["AVSpeechSynthesisVoiceGender"]
@unique
class AVSpeechSynthesisVoiceGender(IntEnum):
FEMALE = 1
MALE = 2
UNKNOWN = 3
from enum import IntEnum, unique
__all__ = ["AVSpeechSynthesisVoiceQuality"]
@unique
class AVSpeechSynthesisVoiceQuality(IntEnum):
DEFAULT = 1
ENHANCED = 2
from objc_util import ObjCClass, ObjCInstance
from .av_speech_utterance import AVSpeechUtterance
__all__ = ["AVSpeechSynthesizer"]
_objc_class = ObjCClass("AVSpeechSynthesizer")
class AVSpeechSynthesizer:
def __init__(self):
self._synthesizer = _objc_class.new()
def to_ns(self) -> ObjCInstance:
return self._synthesizer
def speak_utterance(self, utterance: AVSpeechUtterance):
self._synthesizer.speakUtterance_(utterance.to_ns())
from typing import Optional
from objc_util import ObjCClass, ObjCInstance
from .av_speech_synthesis_voice import AVSpeechSynthesisVoice
__all__ = ["AVSpeechUtterance"]
_objc_class = ObjCClass("AVSpeechUtterance")
class AVSpeechUtterance:
def __init__(
self,
speech_string: str,
voice: Optional[AVSpeechSynthesisVoice] = None,
rate: Optional[float] = None,
volume: Optional[float] = None,
pitch_multiplier: Optional[float] = None,
pre_utterance_delay: Optional[float] = None,
post_utterance_delay: Optional[float] = None,
):
""""A chunk of text to be spoken, along with parameters that affect its speech.
:param speech_string:
:param voice:
:param rate:
:param volume:
:param pitch_multiplier:
:param pre_utterance_delay:
:param post_utterance_delay:
"""
self._speech_string: str = speech_string
self.voice: AVSpeechSynthesisVoice = voice
self.rate: float = rate
self.volume: float = volume
self.pitch_multiplier: float = pitch_multiplier
self.pre_utterance_delay: float = pre_utterance_delay
self.post_utterance_delay: float = post_utterance_delay
@property
def speech_string(self) -> str:
"""The text to be spoken in the utterance. (read only)
"""
return self._speech_string
@classmethod
def from_speech_string(cls, speech_string: str) -> "AVSpeechUtterance":
data = _objc_class.speechUtteranceWithString_(speech_string)
return cls(
speech_string,
voice=None,
rate=data.rate(),
volume=data.volume(),
pitch_multiplier=data.pitchMultiplier(),
pre_utterance_delay=data.preUtteranceDelay(),
post_utterance_delay=data.postUtteranceDelay(),
)
def to_ns(self) -> ObjCInstance:
data = _objc_class.speechUtteranceWithString_(self.speech_string)
if self.voice is not None:
data.voice = self.voice.to_ns()
if self.rate is not None:
data.rate = self.rate
if self.volume is not None:
data.volume = self.volume
if self.pitch_multiplier is not None:
data.pitchMultiplier = self.pitch_multiplier
if self.pre_utterance_delay is not None:
data.preUtteranceDelay = self.pre_utterance_delay
if self.post_utterance_delay is not None:
data.postUtteranceDelay = self.post_utterance_delay
return data
def __repr__(self):
# TODO: DRY
if self.voice:
return (
f"<{self.__class__.__name__!r}: String: {self.speech_string}\n"
f"Voice: {self.voice.name} ({self.voice.language})\n"
f"Rate: {self.rate}\n"
f"Volume: {self.volume}\n"
f"Pitch Multiplier: {self.pitch_multiplier}"
f"Delays: Pre: {self.pre_utterance_delay}(s) Post: {self.post_utterance_delay}(s)>"
)
else:
return (
f"<{self.__class__.__name__!r}: String: {self.speech_string}\n"
f"Voice: (none)\n"
f"Rate: {self.rate}\n"
f"Volume: {self.volume}\n"
f"Pitch Multiplier: {self.pitch_multiplier}"
f"Delays: Pre: {self.pre_utterance_delay}(s) Post: {self.post_utterance_delay}(s)>"
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.