app
Adom TTS — Text to Speech
Public Made by Adomby adom
Adom's shared voice service. Reads your AI's answers back to you while you drive, and renders perfectly-timed voiceover tracks for your demo videos — so you never record your own voice.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
//! Lazy config loader for adom-tts.
//!
//! Reads `~/.adom/tts/config.toml` if present; otherwise returns defaults.
//! Schema is flat enough that a tiny hand-rolled parser beats pulling in
//! the `toml` crate. Currently only the `[play]` section exists.
//!
//! ```toml
//! [play]
//! surface = "hydrogen" # "hydrogen" | "pup"
//! pup_session = "adom-tts"
//! pup_profile = "adom-tts"
//! ```
use anyhow::Result;
use std::path::PathBuf;
use std::str::FromStr;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Surface {
Hydrogen,
Pup,
}
impl FromStr for Surface {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self> {
match s.trim().to_ascii_lowercase().as_str() {
"hydrogen" | "h" | "webview" => Ok(Surface::Hydrogen),
"pup" | "p" | "browser" => Ok(Surface::Pup),
other => anyhow::bail!("unknown surface '{other}' (want 'hydrogen' or 'pup')"),
}
}
}
impl Surface {
pub fn as_str(self) -> &'static str {
match self {
Surface::Hydrogen => "hydrogen",
Surface::Pup => "pup",
}
}
}
#[derive(Debug, Clone)]
pub struct PlayConfig {
pub surface: Surface,
pub pup_session: String,
pub pup_profile: String,
}
impl Default for PlayConfig {
fn default() -> Self {
Self {
surface: Surface::Hydrogen,
pup_session: "adom-tts".to_string(),
pup_profile: "adom-tts".to_string(),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct Config {
pub play: PlayConfig,
}
pub fn config_path() -> Option<PathBuf> {
let home = std::env::var("HOME").ok()?;
Some(PathBuf::from(home).join(".adom/tts/config.toml"))
}
/// Infallible — any error returns defaults.
pub fn load() -> Config {
let path = match config_path() {
Some(p) if p.exists() => p,
_ => return Config::default(),
};
let src = match std::fs::read_to_string(&path) {
Ok(s) => s,
Err(_) => return Config::default(),
};
parse(&src)
}
/// Resolve effective surface following the documented precedence:
/// CLI flag → env var (`$ADOM_TTS_SURFACE`) → config file → default.
pub fn resolve_surface(cli: Option<Surface>, cfg: &PlayConfig) -> Surface {
if let Some(s) = cli {
return s;
}
if let Ok(s) = std::env::var("ADOM_TTS_SURFACE") {
if let Ok(parsed) = Surface::from_str(&s) {
return parsed;
}
eprintln!("adom-tts: ignoring invalid $ADOM_TTS_SURFACE='{s}'");
}
cfg.surface
}
/// Tiny TOML-ish reader. Recognises `[section]` headers and `key = "value"` /
/// `key = value` lines. Strips `#` line comments. Quoted strings only.
fn parse(src: &str) -> Config {
let mut cfg = Config::default();
let mut section = String::new();
for raw in src.lines() {
let line = match raw.split_once('#') {
Some((before, _)) => before,
None => raw,
}.trim();
if line.is_empty() {
continue;
}
if let Some(name) = line.strip_prefix('[').and_then(|s| s.strip_suffix(']')) {
section = name.trim().to_string();
continue;
}
let Some((key, value)) = line.split_once('=') else { continue };
let key = key.trim();
let value = unquote(value.trim());
match (section.as_str(), key) {
("play", "surface") => {
if let Ok(s) = Surface::from_str(&value) {
cfg.play.surface = s;
} else {
eprintln!("adom-tts: ignoring invalid surface '{value}' in config");
}
}
("play", "pup_session") => cfg.play.pup_session = value,
("play", "pup_profile") => cfg.play.pup_profile = value,
_ => {}
}
}
cfg
}
fn unquote(s: &str) -> String {
let s = s.trim();
if (s.starts_with('"') && s.ends_with('"') && s.len() >= 2)
|| (s.starts_with('\'') && s.ends_with('\'') && s.len() >= 2)
{
s[1..s.len() - 1].to_string()
} else {
s.to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_full() {
let src = r#"
# header
[play]
surface = "pup"
pup_session = "my-session"
pup_profile = 'my-profile'
"#;
let cfg = parse(src);
assert_eq!(cfg.play.surface, Surface::Pup);
assert_eq!(cfg.play.pup_session, "my-session");
assert_eq!(cfg.play.pup_profile, "my-profile");
}
#[test]
fn parse_empty_returns_defaults() {
let cfg = parse("");
assert_eq!(cfg.play.surface, Surface::Hydrogen);
assert_eq!(cfg.play.pup_session, "adom-tts");
}
#[test]
fn surface_from_str() {
assert_eq!(Surface::from_str("hydrogen").unwrap(), Surface::Hydrogen);
assert_eq!(Surface::from_str("Pup").unwrap(), Surface::Pup);
assert!(Surface::from_str("nope").is_err());
}
}