Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "speechmarkdown-rust"
version = "0.4.13"
version = "0.5.0"
edition = "2021"
authors = ["SpeechMarkdown Contributors"]
description = "High-performance SpeechMarkdown parser with multi-language bindings"
Expand Down
2 changes: 1 addition & 1 deletion build.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
fn main() {
// Tell cargo to rerun this build script if important files change
println!("cargo:rerun-if-changed=src/");
}
}
56 changes: 56 additions & 0 deletions examples/gen_gemini_fixtures.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
//! Generates `.gemini.ssml` corpus expectations for the shared fixture
//! inputs, in the speechmarkdown-test-files layout.
//!
//! Run from the repo root:
//! cargo run --example gen_gemini_fixtures -- <output-dir>
//!
//! Inputs mirror the elevenlabs-v3-* set where one exists (so dialects
//! are directly comparable); the rest cover gemini-specific policies
//! (backchannels as plain text, sound effects dropped, pause thresholds).
use speechmarkdown_rust::{Platform, SpeechMarkdownParser};
use std::fs;
use std::path::Path;

fn main() {
let out_dir = std::env::args()
.nth(1)
.expect("usage: gen_gemini_fixtures <output-dir>");
let cases: Vec<(&str, &str)> = vec![
("gemini-break-short", "Sample [3s] speech [250ms] markdown"),
(
"gemini-break-strength",
"None [break:\"none\"] x-weak [break:\"x-weak\"]\nweak [break:\"weak\"] medium [break:\"medium\"]\nstrong [break:\"strong\"] x-strong [break:\"x-strong\"]",
),
(
"gemini-emphasis-short",
"This is ++important++ and +small+ and -muted- and ~flat~.",
),
("gemini-expressive", "He [laugh] and then [applause] left."),
("gemini-ipa-short", "Say (speech)/spitʃ/ clearly."),
(
"gemini-modifiers",
"A (secret)[whisper], a (fast thing)[rate:\"fast\"], and a (loud thing)[volume:\"loud\"].",
),
("gemini-section-style", "#[whisper] Hello world"),
("gemini-sub-short", "The {AL}aluminum is recycled."),
(
"gemini-vocal-aliases",
"[cheering] Go team! [whew] ... [throat-clear] anyway.",
),
("gemini-backchannels", "I [mhm] agree. [oh] really? Well [wow]."),
(
"gemini-pauses",
"Hold on [750ms] alright... got it [1s] moving on [120ms] now.",
),
];

for (name, input) in cases {
let dir = Path::new(&out_dir).join(name);
fs::create_dir_all(&dir).expect("create dir");
fs::write(dir.join(format!("{name}.smd")), input).expect("write smd");
let out = SpeechMarkdownParser::to_ssml(input, Platform::Gemini)
.unwrap_or_else(|e| panic!("{name}: {e}"));
fs::write(dir.join(format!("{name}.gemini.ssml")), out).expect("write expected");
println!("wrote {name}");
}
}
88 changes: 88 additions & 0 deletions src/capabilities.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ pub fn get_supported_ssml(platform: Platform) -> PlatformCapabilities {
Platform::SamsungBixby => samsung_bixby_capabilities(),
Platform::ElevenLabs => elevenlabs_capabilities(),
Platform::ElevenLabsV3 => elevenlabs_v3_capabilities(),
Platform::Gemini => gemini_capabilities(),
Platform::IbmWatson => ibm_watson_capabilities(),
}
}
Expand Down Expand Up @@ -529,6 +530,93 @@ fn elevenlabs_v3_capabilities() -> PlatformCapabilities {
}
}

fn gemini_capabilities() -> PlatformCapabilities {
// Gemini 3.8 TTS dialect: no SSML. The transcript is a verbatim script
// directed with angle-bracket vocal bursts/pauses, CAPS emphasis and
// plain-text disfluencies; sustained delivery lives in the turn-level
// speech_metadata.style field (engine channel), not inline.
PlatformCapabilities {
platform: "gemini".into(),
ssml_elements: vec![
SsmlCapability {
element: "<vocal burst>".into(),
description: "Angle-bracket vocal bursts (<laugh>, <sigh>, \
<gasp>, <cough>, <chuckle>, <giggle>, <groan>, \
<scream>, <yawn>, <throat-clearing>, ...); \
English tags recommended even for non-English \
transcripts"
.into(),
attributes: vec![],
speech_markdown_syntax: vec!["[laugh]".into(), "[sigh]".into(), "[gasp]".into()],
example: "Wait... did you hear that? <sigh>".into(),
},
SsmlCapability {
element: "<short pause>/<long pause>".into(),
description: "Two pause steps only; sub-beat pauses become \
ellipses (...). No exact durations"
.into(),
attributes: vec![],
speech_markdown_syntax: vec![
"[500ms]".into(),
"[2s]".into(),
"[break:strong]".into(),
],
example: "Hold on, let me think... <short pause> Alright".into(),
},
SsmlCapability {
element: "CAPS emphasis".into(),
description: "Emphasis via capitalization (moderate and \
strong collapse to one level)"
.into(),
attributes: vec![],
speech_markdown_syntax: vec!["++word++".into(), "+word+".into()],
example: "This is a VERY important point!".into(),
},
SsmlCapability {
element: "<whispers>".into(),
description: "Whisper as an inline vocal burst".into(),
attributes: vec![],
speech_markdown_syntax: vec!["(text)[whisper]".into(), "#[whisper] text".into()],
example: "<whispers> it's a secret".into(),
},
SsmlCapability {
element: "plain-text disfluencies".into(),
description: "Backchannels ([mhm], [oh], [wow], ...) are \
spoken as plain text — Gemini wants transcripts \
written like real speech"
.into(),
attributes: vec![],
speech_markdown_syntax: vec!["[mhm]".into(), "[oh]".into(), "[yeah]".into()],
example: "Oh uh yeah I think... hm, so that's interesting".into(),
},
],
unsupported: vec![
"break (no SSML; two-tag pause approximation)".into(),
"prosody (rate/pitch/volume are turn-level speech_metadata.style, \
an engine channel — dropped inline)"
.into(),
"phoneme (no equivalent; Gemini handles pronunciation natively, \
the word itself is spoken)"
.into(),
"say-as (text normalization is native)".into(),
"sub (alias is spoken instead of the text)".into(),
"audio (no equivalent)".into(),
"mark (no word timestamps from the API)".into(),
"voice (switch via generationConfig.speech_config)".into(),
"lang (no equivalent)".into(),
"[applause]/[boo] and other sound effects (prompting guide says \
avoid non-vocal SFX)"
.into(),
"emphasis levels (moderate/strong both become CAPS)".into(),
"amazon:effect".into(),
"amazon:emotion".into(),
"amazon:domain".into(),
"mstts:express-as".into(),
"google:style".into(),
],
}
}

fn ibm_watson_capabilities() -> PlatformCapabilities {
PlatformCapabilities {
platform: "ibm-watson".into(),
Expand Down
12 changes: 12 additions & 0 deletions src/formatters/base.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ pub enum Platform {
/// Eleven v3 audio-tag dialect (eleven_v3 / eleven_v3_conversational):
/// bracketed natural-language tags instead of SSML.
ElevenLabsV3,
/// Gemini 3.8 TTS dialect (gemini-3.8-flash-tts / -flash-lite-tts):
/// angle-bracket vocal bursts and pauses, CAPS emphasis, and plain-text
/// disfluencies instead of SSML.
Gemini,
IbmWatson,
}

Expand All @@ -30,6 +34,7 @@ impl Platform {
"samsung-bixby" | "bixby" => Some(Platform::SamsungBixby),
"elevenlabs" => Some(Platform::ElevenLabs),
"elevenlabs-v3" | "elevenlabs_v3" | "eleven-v3" => Some(Platform::ElevenLabsV3),
"gemini" | "gemini-3.8" | "gemini-tts" | "google-gemini" => Some(Platform::Gemini),
"ibm-watson" | "watson" => Some(Platform::IbmWatson),
_ => None,
}
Expand All @@ -46,6 +51,7 @@ impl Platform {
Platform::SamsungBixby => "samsung-bixby",
Platform::ElevenLabs => "elevenlabs",
Platform::ElevenLabsV3 => "elevenlabs-v3",
Platform::Gemini => "gemini",
Platform::IbmWatson => "ibm-watson",
}
}
Expand Down Expand Up @@ -120,6 +126,12 @@ pub fn create_formatter(platform: Platform, options: FormatterOptions) -> Box<dy
// natural-language tags, punctuation pauses, native slash IPA.
Box::new(super::elevenlabs_v3::ElevenLabsV3Formatter::new(options))
}
Platform::Gemini => {
// Gemini 3.8 TTS dialect: no SSML — the transcript is directed
// with angle-bracket vocal bursts/pauses, CAPS emphasis, and
// plain-text disfluencies.
Box::new(super::gemini::GeminiFormatter::new(options))
}
_ => Box::new(super::TextFormatter::new()),
}
}
Loading
Loading