Files
eva-pwm-cohost/src/main.rs
T

162 lines
5.6 KiB
Rust

use std::sync::Arc;
use async_openai::types::chat::ChatCompletionRequestMessage;
use serde::{Deserialize, Serialize};
use static_cell::StaticCell;
use crossterm::{event::{self, EventStream, KeyCode, KeyModifiers}};
use futures::StreamExt;
use ratatui::prelude::*;
use crate::{audio::start_audio_input, scene::{Scenery, StageDirection}, tts::start_tts, ui::Ui};
mod scene;
mod events;
mod transcription;
mod tts;
mod prediction;
mod audio;
mod artifacts;
mod archive;
mod ui;
mod widgets;
// TODO: We should be able to delete entries from the conversation, or at least go back and edit something I said.
// TODO: I want a "mark" command or keyboard shortcut, that inserts a marker into the log, so I know where to come back for the next speaking segment.
// TODO: If we insert text without speaking, this should be indicated visually somehow
// FIXME: The playlist ordering is always reversed when Eva talks about it? We also need some way to estimate where in the playlist we are currently, based on timekeeping and 'last played' state in mixxx
// TODO: We should be able to 'close' an episode, by having openai summarize the script into some description, which can be used automatically in the next episode.
// FIXME: It is unclear what would happen if we are live editing the save.json, have a typo, then reload. The file might get wiped without recovery.
// TODO: Would be nice to have some SFX integrated, with bleeps and calculation nosies or something periodically
// TODO: Should rewrite the entire prompt prediction loop, so the UI pushes modification events to the session and receives a new Scene from time to time whenever the prediction engine thinks it should regenerate one.
/* Usage loop:
- Prompt user to select one of:
- Select response 1 (1)
- Select response 2 (2)
- Select response 3 (3)
- Trigger scenario (a, b, c)
- Add additional user input (t)
- Regenerate responses (r)
- Speak selected response
- Regenerate next responses while speaking
UI layout:
- Top panel: Conversation history
- Bottom panel: User input / response options (depending on mode)
- Status bar: Shows current show time, episode, and any other relevant information. Has a throbber for network activity.
- Right panel: Shortcuts for triggering scenarios, soundboard events, etc
*/
#[derive(Serialize, Deserialize, Debug, Default)]
pub struct SaveData {
pub direction: StageDirection,
pub messages: Vec<ChatCompletionRequestMessage>,
pub scenery: Scenery
}
impl SaveData {
fn save(&self) {
let save_data = serde_json::to_string_pretty(self).unwrap();
std::fs::write("save.json", save_data).unwrap();
}
}
struct SysMessageLogger(Arc<tokio::sync::mpsc::UnboundedSender<String>>);
impl log::Log for SysMessageLogger {
fn enabled(&self, _metadata: &log::Metadata) -> bool {
true
}
fn flush(&self) {}
fn log(&self, record: &log::Record) {
self.0.send(format!("{}", record.args())).unwrap();
}
}
#[tokio::main]
async fn main() {
let (panic_hook, eyre_hook) = color_eyre::config::HookBuilder::default()
.display_env_section(true)
.display_location_section(true)
.into_hooks();
eyre_hook.install().unwrap();
std::panic::set_hook(Box::new(move |panic_info| {
let msg = format!("{}", panic_hook.panic_report(panic_info));
println!("Panic: {}", msg);
}));
let (sys_message_sink, sys_message_src) = tokio::sync::mpsc::unbounded_channel();
static LOGGER: StaticCell<SysMessageLogger> = StaticCell::new();
let logger = LOGGER.init(SysMessageLogger(Arc::new(sys_message_sink)));
log::set_logger(logger).unwrap();
log::set_max_level(log::LevelFilter::Info);
dotenv::dotenv().ok();
if std::env::var("OPENAI_API_KEY").is_err() {
eprintln!("Error: OPENAI_API_KEY environment variable not set. The application will not function without it.");
return;
}
let mut terminal: Terminal<CrosstermBackend<std::io::Stdout>> = ratatui::init();
let saved_session = if let Ok(save_data) = std::fs::read_to_string("save.json") {
match serde_json::from_str(&save_data) {
Ok(ret) => {
log::info!("Loaded session from save.json");
ret
},
Err(err) => {
log::error!("Could not load saved session! {:?}", err);
SaveData::default()
}
}
} else {
log::info!("Creating new session in save.json");
SaveData::default()
};
let prediction_ctrl = prediction::start_prediction(saved_session, sys_message_src).await;
let (audio_ctrl, mic_stream, tts_output) = start_audio_input().await;
let tts_ctrl = start_tts(tts_output).await;
let transcription_ctrl = transcription::start_transcription(mic_stream).await;
let mut app = Ui::new(prediction_ctrl, audio_ctrl, transcription_ctrl, tts_ctrl);
let mut events = EventStream::new();
loop {
terminal.draw(|frame| { app.draw(frame)}).unwrap();
tokio::select! {
_ = app.update() => {},
maybe_event = events.next() => {
match maybe_event {
Some(Ok(event)) => {
if let event::Event::Key(key) = event {
if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('c') {
break;
}
}
app.on_event(event).await;
},
_ => break
}
}
};
}
ratatui::restore();
}