use std::collections::HashMap;
use std::env;
use std::error::Error;
use std::fmt::Write;
use std::sync::{Arc, Mutex, MutexGuard};
use futures::{SinkExt, StreamExt};
use sapi_lite::stt::{Grammar, Recognizer, RuleArena};
use sapi_lite::tokio::{AsyncSynthesizer, UnicastContext};
use sapi_lite::tts::SpeechBuilder;
use tokio::net::{TcpListener, TcpStream};
use tokio::sync::mpsc;
use tokio_util::codec::{Framed, LinesCodec};
fn main() -> Result<(), Box<dyn Error>> {
use sapi_lite::tokio::BuilderExt;
sapi_lite::initialize()?;
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.enable_sapi()
.build()?
.block_on(tokio_main())?;
sapi_lite::finalize();
Ok(())
}
async fn tokio_main() -> Result<(), Box<dyn Error>> {
let recognizer = Recognizer::new()?;
let (reco_ctx, mut reco_sub) = UnicastContext::new(&recognizer, 5)?;
let restaurant = Arc::new(Restaurant::new(reco_ctx)?);
let listen_addr = env::args()
.nth(1)
.unwrap_or_else(|| "127.0.0.1:6677".to_string());
let listener = TcpListener::bind(listen_addr).await?;
loop {
tokio::select! {
Ok((stream, _)) = listener.accept() => {
let restaurant = restaurant.clone();
tokio::spawn(async move {
let _ = process_guest(restaurant, stream).await;
});
},
phrase = reco_sub.recognize() => {
let item = *phrase.semantics[0].value.as_int().unwrap() as usize;
let guest_name = phrase.semantics[1]
.value
.as_string()
.unwrap()
.to_string_lossy()
.to_string();
restaurant.serve_item(MENU[item], guest_name);
},
_ = tokio::signal::ctrl_c() => {
break;
}
}
}
Ok(())
}
const MENU: [&'static str; 7] = [
"Pan Galactic Gargle Blaster",
"jinond-o-nicks",
"ol' Janx Spirit",
"Dish of the Day",
"Vegan Rhino cutlet",
"Algolian Zylatburger",
"salad",
];
type GuestTx = mpsc::UnboundedSender<&'static str>;
struct Restaurant {
guests: Mutex<Guests>,
reco_ctx: UnicastContext,
synthesizer: AsyncSynthesizer,
}
struct Guests {
map: HashMap<String, GuestTx>,
grammar: Option<Grammar>,
}
impl Restaurant {
fn new(reco_ctx: UnicastContext) -> Result<Self, Box<dyn Error>> {
Ok(Self {
guests: Mutex::new(Guests {
map: HashMap::new(),
grammar: None,
}),
reco_ctx,
synthesizer: AsyncSynthesizer::new()?,
})
}
fn add_guest(&self, name: &str, guest: GuestTx) -> Result<(), GuestTx> {
let mut guests = self.guests.lock().unwrap();
if guests.map.contains_key(name) {
return Err(guest);
}
guests.map.insert(name.to_string(), guest);
self.update_grammar(guests);
Ok(())
}
fn remove_guest(&self, name: &str) {
let mut guests = self.guests.lock().unwrap();
guests.map.remove(name);
self.update_grammar(guests);
}
fn serve_item(&self, item: &'static str, name: String) {
let mut speech = SpeechBuilder::new();
{
let guests = self.guests.lock().unwrap();
match guests.map.get(&name) {
Some(guest) => {
let _ = guest.send(item);
write!(speech, "{} has been served to {}.", &item, name).unwrap();
}
None => {
write!(speech, "There is no one called {} here.", name).unwrap();
}
}
}
self.synthesizer.speak_and_forget(speech).unwrap();
}
fn update_grammar(&self, mut guests: MutexGuard<Guests>) {
if let Some(old_grammar) = guests.grammar.take() {
old_grammar.set_enabled(false).unwrap();
}
if guests.map.is_empty() {
return;
}
let arena = RuleArena::new();
let mut name_choices = Vec::new();
for (name, _) in guests.map.iter() {
name_choices.push(arena.semantic(name.as_str(), arena.text(name)));
}
let mut item_choices = Vec::new();
for item in 0..MENU.len() {
item_choices.push(arena.semantic(item as i32, arena.text(MENU[item])));
}
let grammar = self
.reco_ctx
.grammar_builder()
.add_rule(arena.sequence(vec![
arena.text("serve"),
arena.choice(item_choices),
arena.text("to"),
arena.choice(name_choices),
]))
.build()
.unwrap();
grammar.set_enabled(true).unwrap();
guests.grammar = Some(grammar);
}
}
async fn process_guest<'a>(
restaurant: Arc<Restaurant>,
stream: TcpStream,
) -> Result<(), Box<dyn Error>> {
let synthesizer = AsyncSynthesizer::new().unwrap();
let (mut tx, mut rx) = mpsc::unbounded_channel();
let mut lines = Framed::new(stream, LinesCodec::new());
lines
.send(
"Welcome to Welcome to Milliways, the Restaurant at the End of the Universe!\r\n\
May I have your name, please?",
)
.await?;
let name = loop {
let name = match lines.next().await {
Some(Ok(line)) => line.trim().to_string(),
_ => {
return Ok(());
}
};
if let Err(err_tx) = restaurant.add_guest(&name, tx) {
lines
.send(
"Hmm, it would seem you are already here tonight.\r\n\
Meeting yourself is impossible at Milliways.\r\n\
Most likely you are misremembering who you are.\r\n\
It is not unusual for our customers to be disoriented by the time journey.\r\n\
Take your time.\r\n\
When you are feeling up to it, please let me know your name.",
)
.await?;
tx = err_tx;
} else {
break name;
}
};
synthesizer
.speak(format!("{} has arrived", &name))
.await
.unwrap();
lines
.send("Excellent! Your reservation seems to be in order.")
.await?;
send_menu(&mut lines).await?;
lines
.send("To go back to your own timeline, just type in 'leave'.")
.await?;
loop {
tokio::select! {
line = lines.next() => match line {
Some(Ok(command)) => {
if command == "leave" {
break;
} else if command == "menu" {
send_menu(&mut lines).await?;
} else if let Some(arg) = command.strip_prefix("order ") {
if let Some(item) = find_item_in_menu(arg) {
synthesizer.speak(format!("{} ordered {}", name, item)).await?;
}
}
},
_ => break
},
event = rx.recv() => match event {
Some(item) => {
lines.send(format!("Here's your {}. Share and enjoy!", item)).await?;
},
_ => {
return Ok(());
}
}
}
}
restaurant.remove_guest(&name);
synthesizer
.speak(format!("{} has left", &name))
.await
.unwrap();
Ok(())
}
async fn send_menu(lines: &mut Framed<TcpStream, LinesCodec>) -> Result<(), Box<dyn Error>> {
lines.feed("Our menu for today is:").await?;
for item in MENU.iter() {
lines.feed(item).await?;
}
lines
.send(
"To order anything from the menu, type in 'order <item>'.\r\n\
For example: order Pan Galactic Gargle Blaster\r\n\
To see the menu again, type in 'menu'.",
)
.await?;
Ok(())
}
fn find_item_in_menu(text: &str) -> Option<&'static str> {
for item in MENU.iter() {
if text.eq_ignore_ascii_case(item) {
return Some(item);
}
}
None
}