Skip to content

Commit

Permalink
feat: serde support, closes #2
Browse files Browse the repository at this point in the history
  • Loading branch information
decahedron1 committed Mar 18, 2024
1 parent f8279fc commit 8adcdf5
Show file tree
Hide file tree
Showing 15 changed files with 246 additions and 23 deletions.
6 changes: 6 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,9 @@ repository = "https://github.com/pykeio/ssml"

[dependencies]
dyn-clone = "1.0"
serde = { version = "1.0", optional = true, features = [ "derive" ] }
erased-serde = { version = "0.4", optional = true }

[features]
default = []
serde = [ "dep:serde", "dep:erased-serde" ]
2 changes: 2 additions & 0 deletions src/audio.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use crate::{

/// Specify repeating an [`Audio`] element's playback for a certain number of times, or for a determined duration.
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum AudioRepeat {
/// Repeat the audio a certain number of times. A fractional value is allowed and describes a portion of the
/// rendered media.
Expand All @@ -18,6 +19,7 @@ pub enum AudioRepeat {
/// [`Audio`] supports the insertion of recorded audio files and the insertion of other audio formats in conjunction
/// with synthesized speech output.
#[derive(Clone, Debug, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Audio {
src: String,
desc: Option<String>,
Expand Down
2 changes: 2 additions & 0 deletions src/break.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::{Serialize, SerializeOptions, TimeDesignation, XmlWriter};

#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum BreakStrength {
None,
ExtraWeak,
Expand All @@ -12,6 +13,7 @@ pub enum BreakStrength {
}

#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Break {
Strength(BreakStrength),
Time(TimeDesignation)
Expand Down
153 changes: 152 additions & 1 deletion src/element.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,12 @@ macro_rules! el {
}
};
}
pub(crate) use el;

el! {
/// Represents all SSML elements.
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub enum Element {
Text(Text),
Expand All @@ -50,6 +52,7 @@ el! {
Break(Break),
Emphasis(Emphasis),
Mark(Mark),
FlavorMSTTS(crate::mstts::Element),
/// A dyn element can be used to implement your own custom elements outside of the `ssml` crate. See
/// [`DynElement`] for more information and examples.
Dyn(Box<dyn DynElement>)
Expand All @@ -64,6 +67,139 @@ el! {
}
}

#[cfg(feature = "serde")]
impl<'a> serde::Deserialize<'a> for Element {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'a>
{
#[allow(non_camel_case_types)]
enum ElementField {
Text,
Audio,
Voice,
Meta,
Break,
Emphasis,
Mark
}

struct ElementFieldVisitor;

impl<'de> serde::de::Visitor<'de> for ElementFieldVisitor {
type Value = ElementField;

fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.write_str("variant identifier")
}

fn visit_u64<__E>(self, val: u64) -> serde::__private::Result<Self::Value, __E>
where
__E: serde::de::Error
{
match val {
0u64 => Ok(ElementField::Text),
1u64 => Ok(ElementField::Audio),
2u64 => Ok(ElementField::Voice),
3u64 => Ok(ElementField::Meta),
4u64 => Ok(ElementField::Break),
5u64 => Ok(ElementField::Emphasis),
6u64 => Ok(ElementField::Mark),
7u64 => Err(serde::de::Error::custom("DynElements cannot be deserialized")),
_ => Err(serde::de::Error::invalid_value(serde::de::Unexpected::Unsigned(val), &"variant index 0 <= i < 8"))
}
}

fn visit_str<E>(self, val: &str) -> Result<Self::Value, E>
where
E: serde::de::Error
{
match val {
"Text" => Ok(ElementField::Text),
"Audio" => Ok(ElementField::Audio),
"Voice" => Ok(ElementField::Voice),
"Meta" => Ok(ElementField::Meta),
"Break" => Ok(ElementField::Break),
"Emphasis" => Ok(ElementField::Emphasis),
"Mark" => Ok(ElementField::Mark),
"Dyn" => Err(serde::de::Error::custom("DynElements cannot be deserialized")),
_ => Err(serde::de::Error::unknown_variant(val, VARIANTS))
}
}

fn visit_bytes<E>(self, val: &[u8]) -> serde::__private::Result<Self::Value, E>
where
E: serde::de::Error
{
match val {
b"Text" => Ok(ElementField::Text),
b"Audio" => Ok(ElementField::Audio),
b"Voice" => Ok(ElementField::Voice),
b"Meta" => Ok(ElementField::Meta),
b"Break" => Ok(ElementField::Break),
b"Emphasis" => Ok(ElementField::Emphasis),
b"Mark" => Ok(ElementField::Mark),
b"Dyn" => Err(serde::de::Error::custom("DynElements cannot be deserialized")),
_ => {
let __value = &String::from_utf8_lossy(val);
Err(serde::de::Error::unknown_variant(__value, VARIANTS))
}
}
}
}

impl<'de> serde::Deserialize<'de> for ElementField {
#[inline]
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>
{
serde::Deserializer::deserialize_identifier(deserializer, ElementFieldVisitor)
}
}

#[doc(hidden)]
struct Visitor<'de> {
marker: std::marker::PhantomData<Element>,
lifetime: std::marker::PhantomData<&'de ()>
}
impl<'de> serde::de::Visitor<'de> for Visitor<'de> {
type Value = Element;

fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.write_str("enum Element")
}

fn visit_enum<A>(self, data: A) -> Result<Self::Value, A::Error>
where
A: serde::de::EnumAccess<'de>
{
match serde::de::EnumAccess::variant(data)? {
(ElementField::Text, variant) => serde::de::VariantAccess::newtype_variant::<Text>(variant).map(Element::Text),
(ElementField::Audio, variant) => serde::de::VariantAccess::newtype_variant::<Audio>(variant).map(Element::Audio),
(ElementField::Voice, variant) => serde::de::VariantAccess::newtype_variant::<Voice>(variant).map(Element::Voice),
(ElementField::Meta, variant) => serde::de::VariantAccess::newtype_variant::<Meta>(variant).map(Element::Meta),
(ElementField::Break, variant) => serde::de::VariantAccess::newtype_variant::<Break>(variant).map(Element::Break),
(ElementField::Emphasis, variant) => serde::de::VariantAccess::newtype_variant::<Emphasis>(variant).map(Element::Emphasis),
(ElementField::Mark, variant) => serde::de::VariantAccess::newtype_variant::<Mark>(variant).map(Element::Mark)
}
}
}

#[doc(hidden)]
const VARIANTS: &[&str] = &["Text", "Audio", "Voice", "Meta", "Break", "Emphasis", "Mark"];
serde::Deserializer::deserialize_enum(
deserializer,
"Element",
VARIANTS,
Visitor {
marker: serde::__private::PhantomData::<Element>,
lifetime: serde::__private::PhantomData
}
)
}
}

impl<T: ToString> From<T> for Element {
fn from(value: T) -> Self {
Element::Text(Text(value.to_string()))
Expand All @@ -76,6 +212,7 @@ impl<T: ToString> From<T> for Element {
/// use ssml::{DynElement, Element, Serialize, SerializeOptions, XmlWriter};
///
/// #[derive(Debug, Clone)]
/// #[cfg_attr(feature = "serde", derive(serde::Serialize))]
/// pub struct TomfooleryElement {
/// value: f32,
/// children: Vec<Element>
Expand Down Expand Up @@ -123,7 +260,8 @@ impl<T: ToString> From<T> for Element {
/// # Ok(())
/// # }
/// ```
pub trait DynElement: Debug + DynClone + Send {
#[allow(private_bounds)]
pub trait DynElement: Debug + DynClone + Send + OptionalErasedSerialize {
/// Serialize this dynamic element into an [`XmlWriter`].
///
/// See [`Serialize::serialize_xml`] for more information.
Expand All @@ -145,6 +283,19 @@ pub trait DynElement: Debug + DynClone + Send {
}
}

#[cfg(feature = "serde")]
erased_serde::serialize_trait_object!(DynElement);

#[cfg(feature = "serde")]
trait OptionalErasedSerialize: erased_serde::Serialize {}
#[cfg(feature = "serde")]
impl<T: serde::Serialize> OptionalErasedSerialize for T {}

#[cfg(not(feature = "serde"))]
trait OptionalErasedSerialize {}
#[cfg(not(feature = "serde"))]
impl<T> OptionalErasedSerialize for T {}

dyn_clone::clone_trait_object!(DynElement);

impl Serialize for Box<dyn DynElement> {
Expand Down
2 changes: 2 additions & 0 deletions src/emphasis.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::{Element, Serialize, SerializeOptions, XmlWriter};

#[derive(Default, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum EmphasisLevel {
Reduced,
None,
Expand All @@ -10,6 +11,7 @@ pub enum EmphasisLevel {
}

#[derive(Clone, Default, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Emphasis {
level: EmphasisLevel,
pub(crate) children: Vec<Element>
Expand Down
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ pub use self::{
/// metadata required by certain services.
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Flavor {
/// Generic SSML.
///
Expand Down Expand Up @@ -163,6 +164,7 @@ pub trait Serialize {
/// It differs from [`Text`] in that the contents of `Meta` are not escaped, meaning `Meta` can be used to write raw
/// XML into the document.
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Meta {
raw: String,
name: Option<String>,
Expand Down
1 change: 1 addition & 0 deletions src/mark.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::{Serialize, SerializeOptions, XmlWriter};

#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Mark {
name: String
}
Expand Down
40 changes: 20 additions & 20 deletions src/mstts/express.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
use crate::{util, DynElement, Element, Flavor, SerializeOptions, XmlWriter};
use crate::{util, Element, Flavor, Serialize, SerializeOptions, XmlWriter};

/// A generic expression for use in [`Express`]. Contains the name of the expression and the intensity/degree (default
/// `1.0`).
#[derive(Debug, Clone, Copy)]
pub struct Expression(&'static str, f32);
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Expression(String, f32);

macro_rules! define_expressions {
($($(#[$outer:meta])* $x:ident => $e:expr),*) => {
Expand All @@ -17,7 +18,7 @@ macro_rules! define_expressions {

impl From<$x> for Expression {
fn from(_: $x) -> Expression {
Expression($e, 1.0)
Expression(String::from($e), 1.0)
}
}

Expand All @@ -28,7 +29,7 @@ macro_rules! define_expressions {
/// results in a slight tendency for the target style. A value of `2` results in a doubling of the
/// default style intensity.
pub fn with_degree(&self, degree: f32) -> Expression {
Expression($e, degree.clamp(0.01, 2.0))
Expression(String::from($e), degree.clamp(0.01, 2.0))
}
}
)*
Expand Down Expand Up @@ -120,6 +121,7 @@ define_expressions! {
///
/// [ms]: https://learn.microsoft.com/en-us/azure/ai-services/speech-service/language-support?tabs=tts#voice-styles-and-roles
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Express {
expression: Expression,
children: Vec<Element>
Expand All @@ -137,7 +139,7 @@ impl Express {
/// Some("en-US"),
/// [ssml::voice(
/// "en-US-JaneNeural",
/// [mstts::express(mstts::express::Cheerful.with_degree(0.5), ["Good morning!"]).into_dyn()]
/// [mstts::express(mstts::express::Cheerful.with_degree(0.5), ["Good morning!"])]
/// )]
/// );
///
Expand Down Expand Up @@ -181,32 +183,30 @@ impl Express {
&mut self.children
}

/// Converts this element into an [`Element::Dyn`].
pub fn into_dyn(self) -> Element {
Element::Dyn(Box::new(self))
/// Converts this element into an [`Element`].
pub fn into_el(self) -> Element {
Element::FlavorMSTTS(super::Element::Express(self))
}
}

impl From<Express> for crate::Element {
fn from(value: Express) -> Self {
value.into_el()
}
}

impl DynElement for Express {
impl Serialize for Express {
fn serialize_xml(&self, writer: &mut XmlWriter<'_>, options: &SerializeOptions) -> crate::Result<()> {
if options.perform_checks && options.flavor != Flavor::MicrosoftAzureCognitiveSpeechServices {
return Err(crate::error!("`mstts::Express` is only supported in ACSS/MSTTS"));
}

writer.element("mstts:express-as", |writer| {
writer.attr("style", self.expression.0)?;
writer.attr("style", &self.expression.0)?;
writer.attr("styledegree", self.expression.1.to_string())?;
util::serialize_elements(writer, &self.children, options)
})
}

fn children(&self) -> Option<&Vec<Element>> {
Some(&self.children)
}

fn tag_name(&self) -> Option<&str> {
Some("mstts:express-as")
}
}

/// Creates a new [`Express`] section to modify the speaking style of a section of elements.
Expand All @@ -220,7 +220,7 @@ impl DynElement for Express {
/// Some("en-US"),
/// [ssml::voice(
/// "en-US-JaneNeural",
/// [mstts::express(mstts::express::Cheerful.with_degree(0.5), ["Good morning!"]).into_dyn()]
/// [mstts::express(mstts::express::Cheerful.with_degree(0.5), ["Good morning!"])]
/// )]
/// );
///
Expand Down
8 changes: 8 additions & 0 deletions src/mstts/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ use crate::{voice::Voice, Flavor, Meta};
pub mod express;
pub use self::express::{express, Express};

crate::element::el! {
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Element {
Express(Express)
}
}

/// Viseme configuration for MSTTS.
///
/// See [`MicrosoftVoiceExt::with_mstts_viseme`].
Expand Down
Loading

0 comments on commit 8adcdf5

Please sign in to comment.