main: add lots of TODOs, clean up system messages when loading the session, and use espeak-ng directly which seems to have fewer audio glitches

This commit is contained in:
2026-06-02 11:32:32 +02:00
parent 9efa1f14b5
commit a4f29a4d0d
+26 -5
View File
@@ -1,19 +1,32 @@
use async_openai::{Client, config::OpenAIConfig, types::chat::{ChatCompletionMessageToolCalls, ChatCompletionTool, ChatCompletionTools, CreateChatCompletionRequest, CreateChatCompletionResponse, FunctionObject}}; use async_openai::{Client, config::OpenAIConfig, types::{InputSource, audio::{AudioInput, CreateTranscriptionRequest}, chat::{ChatCompletionMessageToolCalls, ChatCompletionTool, ChatCompletionTools, CreateChatCompletionRequest, CreateChatCompletionResponse, FunctionObject}}};
use chrono::{DateTime, Duration, Utc}; use chrono::{DateTime, Duration, Utc};
use futures_timer::Delay; use futures_timer::Delay;
use jack::{AudioIn, ClientOptions};
use oximedia_metering::vu_meter::VuMeter;
use schemars::{JsonSchema, schema_for}; use schemars::{JsonSchema, schema_for};
use scraper::{Html, Selector}; use scraper::{Html, Selector};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use ratatui::{Frame, layout::{Constraint, Direction, Layout}, widgets::{Block, BorderType, Clear, List, ListDirection, ListItem, ListState, Paragraph, Wrap}}; use ratatui::{Frame, layout::{Constraint, Direction, Layout}, widgets::{Block, BorderType, Clear, Gauge, List, ListDirection, ListItem, ListState, Paragraph, Wrap}};
use serde_json::Value;
use sqlite::OpenFlags; use sqlite::OpenFlags;
use throbber_widgets_tui::{Throbber, ThrobberState}; use throbber_widgets_tui::{Throbber, ThrobberState};
use crossterm::{event::{self, EventStream, KeyCode, KeyModifiers}}; use crossterm::{event::{self, EventStream, KeyCode, KeyModifiers}};
use tokio::sync::watch; use tokio::{sync::watch, time::Instant};
use tui_input::{Input, backend::crossterm::EventHandler}; use tui_input::{Input, backend::crossterm::EventHandler};
use std::process::Command; use std::{io::Read, process::Command};
use futures::{StreamExt, future::FutureExt}; use futures::{StreamExt, future::FutureExt};
// TODO: We should have a separate 'state.json' file, which remembers jack connections, and the world time for the show to end. Then we only update the 'time remaining' field in the scene and only deal with relative durations inside the scene data
// 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
/* Usage loop: /* Usage loop:
- Prompt user to select one of: - Prompt user to select one of:
- Select response 1 (1) - Select response 1 (1)
@@ -73,6 +86,7 @@ struct App {
scene: Scene, scene: Scene,
next_reply_options: Vec<PossibleResponse>, next_reply_options: Vec<PossibleResponse>,
reply_state: ListState, reply_state: ListState,
conversation_state: ListState,
user_input: Input, user_input: Input,
end_time: DateTime<Utc>, end_time: DateTime<Utc>,
throbber_state: ThrobberState, throbber_state: ThrobberState,
@@ -181,6 +195,7 @@ impl App {
let status_line = Line::from_iter([ let status_line = Line::from_iter([
Span::from(format!("Episode {}", self.scene.direction.episode_number)).style(ratatui::style::Color::LightBlue), Span::from(format!("Episode {}", self.scene.direction.episode_number)).style(ratatui::style::Color::LightBlue),
Span::from(" | ").style(ratatui::style::Color::DarkGray), Span::from(" | ").style(ratatui::style::Color::DarkGray),
// FIXME: Looks weird with negative numbers, and it doesn't actually blink in the vscode terminal.
Span::from(format!("Time Remaining: {:0>2}:{:0>2}:{:0>2}", self.scene.direction.time_remaining.num_hours(), self.scene.direction.time_remaining.num_minutes() % 60, self.scene.direction.time_remaining.num_seconds() % 60)).style(time_style) Span::from(format!("Time Remaining: {:0>2}:{:0>2}:{:0>2}", self.scene.direction.time_remaining.num_hours(), self.scene.direction.time_remaining.num_minutes() % 60, self.scene.direction.time_remaining.num_seconds() % 60)).style(time_style)
]); ]);
frame.render_widget(status_line, area); frame.render_widget(status_line, area);
@@ -415,6 +430,7 @@ impl App {
} }
async fn add_bandcamp_artifact(&mut self, url: &str) { async fn add_bandcamp_artifact(&mut self, url: &str) {
// FIXME: This can crash if the page doesn't load properly, or if the structure of the Bandcamp page changes. We should add some error handling here.
let body = reqwest::get(url).await.unwrap().text().await.unwrap(); let body = reqwest::get(url).await.unwrap().text().await.unwrap();
let fragment = Html::parse_document(&body); let fragment = Html::parse_document(&body);
let selector = Selector::parse("script[type=\"application/ld+json\"]").unwrap(); let selector = Selector::parse("script[type=\"application/ld+json\"]").unwrap();
@@ -431,6 +447,8 @@ impl App {
if let Ok(save_data) = std::fs::read_to_string("save.json") { if let Ok(save_data) = std::fs::read_to_string("save.json") {
if let Ok(scene) = serde_json::from_str(&save_data) { if let Ok(scene) = serde_json::from_str(&save_data) {
self.scene = scene; self.scene = scene;
// FIXME: These should get wiped out when we save as well, or even better, be completely excluded via a custom serde implementation.
self.scene.conversation.retain(|line| { if let ConversationEntry::SystemMessage(_) = line { false } else { true }});
self.scene.insert_conversation(ConversationEntry::SystemMessage("Loaded stored session.".into())); self.scene.insert_conversation(ConversationEntry::SystemMessage("Loaded stored session.".into()));
} else { } else {
self.scene.insert_conversation(ConversationEntry::SystemMessage("Failed to load saved session!".into())); self.scene.insert_conversation(ConversationEntry::SystemMessage("Failed to load saved session!".into()));
@@ -439,7 +457,9 @@ impl App {
} }
fn speak(&mut self, text: &str) { fn speak(&mut self, text: &str) {
Command::new("spd-say").arg("-y").arg("English (America)+Linda").arg(text).spawn().unwrap().wait().unwrap(); // FIXME: Utterances should be handled in another task, so the UI doesn't lock up while waiting for speech to finish
// TODO: We should also have espeak pipe out to stdout, then we can apply some audio effects and write to our own jack port.
Command::new("espeak-ng").arg("-v").arg("en-us+f3").arg(text).spawn().unwrap().wait().unwrap();
} }
fn regenerate_responses(&mut self) { fn regenerate_responses(&mut self) {
@@ -449,6 +469,7 @@ impl App {
} }
fn reload_mixxx_playlist(&mut self) { fn reload_mixxx_playlist(&mut self) {
// TODO: Should have some status message which states how many tracks are in the playlist
self.scene.direction.current_playlist.clear(); self.scene.direction.current_playlist.clear();
let connection = sqlite::Connection::open_thread_safe_with_flags("mixxxdb.sqlite", OpenFlags::new().with_read_only()).unwrap(); let connection = sqlite::Connection::open_thread_safe_with_flags("mixxxdb.sqlite", OpenFlags::new().with_read_only()).unwrap();
let query = "SELECT id FROM Playlists WHERE name = ? ORDER BY id DESC LIMIT 1"; let query = "SELECT id FROM Playlists WHERE name = ? ORDER BY id DESC LIMIT 1";