Compare commits
7
Commits
bcf37182fe
...
919e878d5f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
919e878d5f | ||
|
|
7157d4bb39 | ||
|
|
0080b1239c | ||
|
|
0d270e80b3 | ||
|
|
03d2cdf500 | ||
|
|
42f764c914 | ||
|
|
646ea5bca3 |
Generated
+13
-3
@@ -1212,6 +1212,7 @@ dependencies = [
|
||||
"futures-timer",
|
||||
"hound",
|
||||
"iref 4.0.0",
|
||||
"itertools 0.15.0",
|
||||
"jack",
|
||||
"json-ld",
|
||||
"log",
|
||||
@@ -2069,6 +2070,15 @@ dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b4baf93f58d4425749ca49a51c50ebab072c5df6994d08fed93541c331481dc"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
@@ -3528,7 +3538,7 @@ dependencies = [
|
||||
"compact_str",
|
||||
"hashbrown 0.16.1",
|
||||
"indoc",
|
||||
"itertools",
|
||||
"itertools 0.14.0",
|
||||
"kasuari",
|
||||
"lru",
|
||||
"strum",
|
||||
@@ -3580,7 +3590,7 @@ dependencies = [
|
||||
"hashbrown 0.16.1",
|
||||
"indoc",
|
||||
"instability",
|
||||
"itertools",
|
||||
"itertools 0.14.0",
|
||||
"line-clipping",
|
||||
"ratatui-core",
|
||||
"strum",
|
||||
@@ -5303,7 +5313,7 @@ version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "16b380a1238663e5f8a691f9039c73e1cdae598a30e9855f541d29b08b53e9a5"
|
||||
dependencies = [
|
||||
"itertools",
|
||||
"itertools 0.14.0",
|
||||
"unicode-segmentation",
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
@@ -16,6 +16,7 @@ futures = "0.3.32"
|
||||
futures-timer = "3.0.4"
|
||||
hound = "3.5.1"
|
||||
iref = { version = "4.0.0", features = ["url", "serde"] }
|
||||
itertools = "0.15.0"
|
||||
jack = "0.13.5"
|
||||
json-ld = { version = "0.21.4", features = ["reqwest", "serde"] }
|
||||
log = "0.4.32"
|
||||
|
||||
+1324
File diff suppressed because it is too large
Load Diff
+22
-11
@@ -1,5 +1,4 @@
|
||||
# UI
|
||||
[ ] Conversation options that exceed a certain predicted amount of TTS time (1-2 seconds) should get highlighted differently
|
||||
[ ] log line colors
|
||||
|
||||
# Features
|
||||
@@ -7,27 +6,39 @@
|
||||
[ ] patch mixxx to expose a rest api for data
|
||||
[ ] MIDI input / output
|
||||
[ ] Different prompts between "recording an episode", "preparing for an episode", or "exploring"
|
||||
[ ] Mixxx playlists other than the regular episode
|
||||
[ ] Realtime updates to the mixxx playlist
|
||||
[x] Mixxx playlists other than the regular episode
|
||||
[ ] Include mixxx ratings into metadata
|
||||
[ ] Realtime updates to the mixxx playlist
|
||||
[ ] somehow optimize the massive json blob of data we send?
|
||||
|
||||
# Refactoring
|
||||
[ ] Move tools into a sub-module
|
||||
[ ] Debug log should show up into the UI in a separate list
|
||||
[x] Move tools into a sub-module
|
||||
[x] Debug log should show up into the UI in a separate list
|
||||
[ ] Use pipewire over jack
|
||||
[ ] split out UI into several widgets
|
||||
[x] split out UI into several widgets
|
||||
|
||||
# Realtime behaviors
|
||||
[ ] "Hey eva, take a note, this one is pretty crunchy." -> note list in sidebar?
|
||||
[ ] "Hey eva, lets wrap up, can you prep me a report?" -> generates a summary of the episode, stashes it in the sidebar, saves it for the next episode, maybe also stores in beets/mixxx?
|
||||
[ ] "Hit it, eva" "Spin up the buffers, eva" - ship computer starts playing sfx to transition out
|
||||
[ ] ship should give off some warning sfx or something when the episode time hits 1h, 20m, 5m, 0, etc.
|
||||
[ ] asking eva to query the archive or a relay or fetch something should be happening in the background, with the computer feeding updates into the conversation. Eva can then react as it goes along.
|
||||
[X] asking eva to query the archive or a relay or fetch something should be happening in the background, with the computer feeding updates into the conversation. Eva can then react as it goes along.
|
||||
[ ] Twitch chat?
|
||||
|
||||
# Next Episode
|
||||
[ ] Ambient SFX
|
||||
[ ] Musicbrainz artifacts
|
||||
# Episode 205
|
||||
[x] Ambient SFX
|
||||
[x] Musicbrainz artifacts
|
||||
[x] Preload musicbrainz data from playlist with a command
|
||||
|
||||
# Episode 206
|
||||
[ ] Automatic quiet detection instead of push-to-talk
|
||||
[ ] Preload musicbrainz data from playlist with a command
|
||||
[ ] Fact database, to remember things about tracks during the show
|
||||
[ ] Conversation options that exceed a certain predicted amount of TTS time (1-2 seconds) should get highlighted differently
|
||||
[ ] Archive metadata and context should go at the very end of a prompt, to utilize token caching
|
||||
[ ] Report cache status to the UI
|
||||
[ ] "Environmental Status" panel that shows token usage as a sparkgraph, archive size, context window size
|
||||
[ ] Ship computer is kinda bad when confronted with non-research tasks
|
||||
[ ] Eva doesn't talk to the computer as much anymore for research tasks
|
||||
[ ] Save the computer task list to save.json
|
||||
[ ] Eva keeps saying "if you want a lore angle:" stuff
|
||||
[ ] Should be a kind of rapid-fire eva dialog mode, where eva immediately replies to voice prompts
|
||||
@@ -35,7 +35,7 @@ impl DataSource for MixxxDB {
|
||||
let mut ret = vec![];
|
||||
let playlist_name = args.playlist_name.as_str();
|
||||
log::info!("Loading Mixxx playlist {}", playlist_name);
|
||||
let connection = sqlite::Connection::open_thread_safe_with_flags("mixxxdb.sqlite", OpenFlags::new().with_read_only())?;
|
||||
let connection = sqlite::Connection::open_thread_safe_with_flags("/home/tdfischer/.mixxx/mixxxdb.sqlite", OpenFlags::new().with_read_only())?;
|
||||
let query = "SELECT id FROM Playlists WHERE name = ? ORDER BY id DESC LIMIT 1";
|
||||
let mut statement = connection.prepare(query)?;
|
||||
statement.bind((1, playlist_name))?;
|
||||
|
||||
@@ -22,6 +22,7 @@ Most of the time, the requests will be referring to tracks, artists, or albums i
|
||||
A successful session will result in the local collection of artifacts having the most complete available data.
|
||||
|
||||
You may call these functions as much as you need, whenever you feel it is nessicary to complete the task you are given.
|
||||
Calling the same tool with the same parameters more than once is wasteful, as repeated calls will always return the same data.
|
||||
|
||||
When deciding which tools to call in which order, consider the following:
|
||||
- Beets will provide the fastest and cheapest responses, as it is local to the ship.
|
||||
@@ -29,6 +30,9 @@ When deciding which tools to call in which order, consider the following:
|
||||
- Musicbrainz queries are free, but not instant. This information comes from Earth via a pirate signal bouncing off of satelite relays.
|
||||
- Mixxx will return you a very minimal list of tracks which will always require synchronizing against Beets, along with changing the current playlist to the given name. You must not call this function unless you are directly asked to change the playlist.
|
||||
|
||||
For each of these tools, it is more efficient to pull metadata using an album query where supported, instead of pulling metadata on individual tracks.
|
||||
Pulling track metadata individually can be very slow and expensive, whereas asking Beets for all information on an artist's album is nearly instantaneous.
|
||||
|
||||
For each task you plan to perform, you must add it to the todo list with the "task_list" tool.
|
||||
After each task is completed, you must mark it as complete it using the same tool.
|
||||
|
||||
|
||||
+3
-4
@@ -134,13 +134,12 @@ async fn main() {
|
||||
|
||||
let (audio_ctrl, mic_stream, tts_output, sfx_output) = start_audio_input().await;
|
||||
let tts_ctrl = start_tts(tts_output).await;
|
||||
let mut sfx_ctrl = start_sfx(sfx_output).await;
|
||||
sfx_ctrl.play_ambient().await.unwrap();
|
||||
let sfx_ctrl = start_sfx(sfx_output).await;
|
||||
let transcription_ctrl = transcription::start_transcription(mic_stream).await;
|
||||
|
||||
let prediction_ctrl = prediction::conversation_task(saved_session, conversation_src, sfx_ctrl).await;
|
||||
let prediction_ctrl = prediction::conversation_task(saved_session, conversation_src, sfx_ctrl.clone()).await;
|
||||
|
||||
let mut app = Ui::new(prediction_ctrl, audio_ctrl, transcription_ctrl, tts_ctrl);
|
||||
let mut app = Ui::new(prediction_ctrl, audio_ctrl, transcription_ctrl, tts_ctrl, sfx_ctrl);
|
||||
|
||||
let mut events = EventStream::new();
|
||||
|
||||
|
||||
@@ -99,7 +99,11 @@ impl Conversation {
|
||||
self.backlog.push(entry.clone());
|
||||
self.event_sink.send(SessionUpdate::Conversation(self.backlog.clone())).unwrap();
|
||||
match entry {
|
||||
ConversationEntry::Spoken(_, _) => {
|
||||
ConversationEntry::Spoken(speaker, _) => {
|
||||
match speaker {
|
||||
Speaker::User => (),
|
||||
_ => self.sfx.play_ambient().await.unwrap(),
|
||||
}
|
||||
if let Ok(next_msg) = TryInto::<ChatCompletionRequestMessage>::try_into(entry) {
|
||||
self.send_to(Speaker::Eva, next_msg.clone()).await;
|
||||
let cxt = self.context_for_speaker(Speaker::Eva).await;
|
||||
@@ -158,11 +162,11 @@ impl Conversation {
|
||||
async fn process_dialog(&mut self, speaker: Speaker, value: Value) {
|
||||
match speaker {
|
||||
Speaker::Eva => {
|
||||
let next_options = serde_json::from_value(value).unwrap();
|
||||
// TODO: Handle crash from bad json
|
||||
let next_options = serde_json::from_value(value).unwrap_or_default();
|
||||
self.event_sink.send(SessionUpdate::Responses(next_options)).unwrap();
|
||||
},
|
||||
Speaker::ShipComputer => {
|
||||
self.sfx.play_ambient().await.unwrap();
|
||||
let response: ComputerResponse = serde_json::from_value(value).unwrap();
|
||||
self.insert(ConversationEntry::Spoken(Speaker::ShipComputer, response.message)).await;
|
||||
if response.finished.unwrap_or_default() {
|
||||
@@ -326,8 +330,8 @@ pub async fn conversation_task(save_data: SaveData, sys_log_messages: tokio::syn
|
||||
]),
|
||||
event_sink,
|
||||
input_src,
|
||||
eva_backlog: save_data.messages,
|
||||
backlog,
|
||||
eva_backlog: Default::default(),
|
||||
tokens_consumed: save_data.tokens_consumed,
|
||||
direction: save_data.direction,
|
||||
archive,
|
||||
|
||||
+169
-76
@@ -1,5 +1,8 @@
|
||||
use std::path::Path;
|
||||
|
||||
use rand::seq::IteratorRandom;
|
||||
use symphonia::core::{formats::{TrackType, probe::Hint}, io::MediaSourceStream};
|
||||
use resampler::ResamplerFir;
|
||||
use symphonia::core::{codecs::audio::AudioDecoder, formats::{FormatReader, TrackType, probe::Hint}, io::MediaSourceStream};
|
||||
|
||||
use crate::audio::AudioOutStream;
|
||||
|
||||
@@ -8,7 +11,7 @@ pub enum SfxRequest {
|
||||
RandomAmbient
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SfxControl {
|
||||
sink: tokio::sync::mpsc::Sender<SfxRequest>
|
||||
}
|
||||
@@ -19,87 +22,177 @@ impl SfxControl {
|
||||
}
|
||||
}
|
||||
|
||||
struct Sample {
|
||||
format: Box<dyn FormatReader>,
|
||||
decoder: Box<dyn AudioDecoder>,
|
||||
channel_bufs: Vec<f32>,
|
||||
sample_rate: u32,
|
||||
channel_num: usize,
|
||||
bitrate_resample: ResamplerFir
|
||||
}
|
||||
|
||||
impl Sample {
|
||||
fn new(format: Box<dyn FormatReader>, decoder: Box<dyn AudioDecoder>, output_sample_rate: u32) -> Self {
|
||||
let sample_rate = decoder.codec_params().sample_rate.unwrap();
|
||||
let channel_num = decoder.codec_params().channels.as_ref().unwrap().count();
|
||||
let bitrate_resample = resampler::ResamplerFir::new_from_hz(channel_num, sample_rate, output_sample_rate, Default::default(), Default::default());
|
||||
Self {
|
||||
format,
|
||||
decoder,
|
||||
channel_bufs: vec![],
|
||||
sample_rate,
|
||||
channel_num,
|
||||
bitrate_resample
|
||||
}
|
||||
}
|
||||
|
||||
fn next(&mut self) -> Result<Option<Vec<f32>>, symphonia::core::errors::Error> {
|
||||
let packet = match self.format.next_packet()? {
|
||||
Some(packet) => packet,
|
||||
None => return Ok(None)
|
||||
};
|
||||
|
||||
match self.decoder.decode_ref(&packet.as_packet_ref()) {
|
||||
Ok(samples) => {
|
||||
self.channel_bufs.resize(samples.samples_interleaved(), 0.);
|
||||
samples.copy_to_slice_interleaved(&mut self.channel_bufs);
|
||||
|
||||
let mut resampled = [0.; 4096];
|
||||
let (read_count, write_count) = self.bitrate_resample.resample(&self.channel_bufs, &mut resampled).unwrap();
|
||||
if read_count < self.channel_bufs.len() {
|
||||
log::error!("Resampling buffer is too small for a {}Hz file! We need an additional {}", self.sample_rate, read_count);
|
||||
}
|
||||
|
||||
// First we convert the audio feed from stereo down to mono by simple average
|
||||
// TODO: This should be something smarter, like a saturating add..?
|
||||
let mono_stream = resampled[..write_count].chunks(self.channel_num).map(|channels| {
|
||||
let total_volume = channels.iter().cloned().reduce(|a, b| a + b).unwrap_or_default();
|
||||
total_volume / (self.channel_num as f32)
|
||||
});
|
||||
|
||||
Ok(Some(mono_stream.collect()))
|
||||
},
|
||||
Err(err) => {
|
||||
// Dump the audio buffer on failure
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Player {
|
||||
audio_sink: AudioOutStream,
|
||||
audio_out_buf: Vec<f32>,
|
||||
playing_samples: Vec<Sample>
|
||||
}
|
||||
|
||||
impl Player {
|
||||
async fn process(&mut self) {
|
||||
if self.playing_samples.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
'out: loop {
|
||||
let mut next_batch = vec![];
|
||||
|
||||
self.playing_samples.retain_mut(|sample| {
|
||||
match sample.next() {
|
||||
Ok(Some(buf)) => {
|
||||
next_batch.push(buf);
|
||||
true
|
||||
},
|
||||
Ok(None) => {
|
||||
false
|
||||
},
|
||||
Err(err) => {
|
||||
log::error!("Audio error: {:?}", err);
|
||||
false
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
loop {
|
||||
let mut this_sample = 0.;
|
||||
let mut any_valid = false;
|
||||
for stream in &mut next_batch {
|
||||
if let Some(next_sample) = stream.pop() {
|
||||
this_sample += next_sample;
|
||||
any_valid = true;
|
||||
}
|
||||
}
|
||||
|
||||
if !any_valid {
|
||||
if self.audio_out_buf.is_empty() {
|
||||
log::debug!("End of audio files");
|
||||
break 'out;
|
||||
}
|
||||
//log::debug!("No more valid files, but the buffer is still running!");
|
||||
break;
|
||||
}
|
||||
let mixed_sample = this_sample / next_batch.len() as f32;
|
||||
self.audio_out_buf.push(mixed_sample);
|
||||
if self.audio_out_buf.len() >= 1024 {
|
||||
self.flush().await;
|
||||
}
|
||||
}
|
||||
|
||||
if self.playing_samples.is_empty() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn submit_buffer(&mut self) {
|
||||
self.audio_sink.sink.send(std::mem::take(&mut self.audio_out_buf)).await.unwrap();
|
||||
}
|
||||
|
||||
async fn flush(&mut self) {
|
||||
if !self.audio_out_buf.is_empty() {
|
||||
self.submit_buffer().await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn play_sample(&mut self, path: &Path) {
|
||||
log::debug!("Queuing sound playback for {:?}", path);
|
||||
let sfx_fd = std::fs::File::open(path).unwrap();
|
||||
let mss = MediaSourceStream::new(Box::new(sfx_fd), Default::default());
|
||||
let meta_opts = Default::default();
|
||||
let fmt_opts = Default::default();
|
||||
let mut hint = Hint::new();
|
||||
// FIXME: use actual file extension
|
||||
hint.with_extension(".mp3");
|
||||
let format = symphonia::default::get_probe()
|
||||
.probe(&hint, mss, fmt_opts, meta_opts)
|
||||
.expect("Unsupported audio format");
|
||||
let track = format.default_track(TrackType::Audio).expect("No audio track");
|
||||
let dec_opts = Default::default();
|
||||
let decoder = symphonia::default::get_codecs()
|
||||
.make_audio_decoder(
|
||||
track.codec_params.as_ref().expect("codec params missing").audio().unwrap(),
|
||||
&dec_opts
|
||||
).expect("Unsupported audio codec");
|
||||
|
||||
self.playing_samples.push(Sample::new(format, decoder, self.audio_sink.sample_rate));
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn start_sfx(audio_sink: AudioOutStream) -> SfxControl {
|
||||
let (event_sink, mut event_src) = tokio::sync::mpsc::channel(32);
|
||||
tokio::spawn(async move {
|
||||
let mut player = Player { audio_sink, audio_out_buf: vec![] , playing_samples: vec![]};
|
||||
let sfx_dir = std::path::Path::new("./sfx");
|
||||
|
||||
loop {
|
||||
while let Some(event) = event_src.recv().await {
|
||||
match event {
|
||||
SfxRequest::RandomAmbient => {
|
||||
let avail_files = std::fs::read_dir(sfx_dir).unwrap();
|
||||
let chosen_file = avail_files.choose(&mut rand::rng()).unwrap().unwrap();
|
||||
log::debug!("Queuing ambient sound playback with {:?}", chosen_file);
|
||||
let sfx_fd = std::fs::File::open(chosen_file.path()).unwrap();
|
||||
let mss = MediaSourceStream::new(Box::new(sfx_fd), Default::default());
|
||||
let meta_opts = Default::default();
|
||||
let fmt_opts = Default::default();
|
||||
let mut hint = Hint::new();
|
||||
hint.with_extension(".mp3");
|
||||
let mut format = symphonia::default::get_probe()
|
||||
.probe(&hint, mss, fmt_opts, meta_opts)
|
||||
.expect("Unsupported audio format");
|
||||
let track = format.default_track(TrackType::Audio).expect("No audio track");
|
||||
let track_id = track.id;
|
||||
let dec_opts = Default::default();
|
||||
let mut decoder = symphonia::default::get_codecs()
|
||||
.make_audio_decoder(
|
||||
track.codec_params.as_ref().expect("codec params missing").audio().unwrap(),
|
||||
&dec_opts
|
||||
).expect("Unsupported audio codec");
|
||||
|
||||
let sample_rate = decoder.codec_params().sample_rate.unwrap();
|
||||
let channel_num = decoder.codec_params().channels.as_ref().unwrap().count();
|
||||
log::debug!("Resampling {} -> {}", sample_rate, audio_sink.sample_rate);
|
||||
// Our resampler works on a mono input
|
||||
let mut bitrate_resample = resampler::ResamplerFir::new_from_hz(channel_num, sample_rate, audio_sink.sample_rate, Default::default(), Default::default());
|
||||
|
||||
log::debug!("Starting stream");
|
||||
let mut audio_out_buf = vec![];
|
||||
let mut channel_bufs: Vec<f32> = vec![];
|
||||
loop {
|
||||
let packet = match format.next_packet() {
|
||||
Ok(Some(packet)) => packet,
|
||||
Ok(None) => break,
|
||||
Err(err) => panic!()
|
||||
};
|
||||
|
||||
if packet.track_id != track_id {
|
||||
continue
|
||||
}
|
||||
|
||||
match decoder.decode_ref(&packet.as_packet_ref()) {
|
||||
Ok(samples) => {
|
||||
channel_bufs.resize(samples.samples_interleaved(), 0.);
|
||||
samples.copy_to_slice_interleaved(&mut channel_bufs);
|
||||
|
||||
let mut resampled = [0.; 2048];
|
||||
let (_, write_count) = bitrate_resample.resample(&channel_bufs, &mut resampled).unwrap();
|
||||
|
||||
// First we convert the audio feed from stereo down to mono by simple average
|
||||
// TODO: This should be something smarter, like a saturating add..?
|
||||
let mono_stream = resampled[..write_count].chunks(channel_num).map(|channels| {
|
||||
let total_volume = channels.iter().cloned().reduce(|a, b| a + b).unwrap_or_default();
|
||||
total_volume / (channel_num as f32)
|
||||
});
|
||||
|
||||
// Then we write out the resampled audio to our staging buffer
|
||||
audio_out_buf.extend(mono_stream);
|
||||
|
||||
// Once we have 1024 samples (jack default, I guess), we send it to the audio output
|
||||
if audio_out_buf.len() >= 1024 {
|
||||
audio_sink.sink.send(audio_out_buf).await.unwrap();
|
||||
audio_out_buf = vec![];
|
||||
}
|
||||
},
|
||||
Err(err) => panic!()
|
||||
}
|
||||
tokio::select! {
|
||||
_ = player.process(), if !player.playing_samples.is_empty() => {},
|
||||
Some(event) = event_src.recv() => {
|
||||
match event {
|
||||
SfxRequest::RandomAmbient => {
|
||||
log::debug!("Playing random audio sample");
|
||||
let avail_files = std::fs::read_dir(sfx_dir.join("ambient")).unwrap();
|
||||
let chosen_file = avail_files.choose(&mut rand::rng()).unwrap().unwrap();
|
||||
player.play_sample(&chosen_file.path()).await;
|
||||
}
|
||||
if !audio_out_buf.is_empty() {
|
||||
audio_sink.sink.send(audio_out_buf).await.unwrap();
|
||||
}
|
||||
log::debug!("Playback complete");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,12 +91,15 @@ pub async fn start_transcription(mut mic_src: AudioInStream) -> TranscriptionCon
|
||||
let c = client.clone();
|
||||
let t = transcription_in.clone();
|
||||
tokio::spawn(async move {
|
||||
let response = c.audio().transcription().create(CreateTranscriptionRequest {
|
||||
if let Ok(response) = c.audio().transcription().create(CreateTranscriptionRequest {
|
||||
file: AudioInput { source: InputSource::Bytes { filename: "transcription.wav".into(), bytes } },
|
||||
model: "gpt-4o-mini-transcribe".into(),
|
||||
..Default::default()
|
||||
}).await.unwrap();
|
||||
t.send(response.text).await.unwrap();
|
||||
}).await {
|
||||
t.send(response.text).await.unwrap();
|
||||
} else {
|
||||
log::error!("Error with transcription");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ use tokio::time::Instant;
|
||||
use tui_input::{Input, backend::crossterm::EventHandler};
|
||||
use tui_skeleton::{AnimationMode, Block, Constraint, SkeletonText};
|
||||
|
||||
use crate::{audio::AudioInputControl, prediction::{GeneratedResponses, PredictionAction, SessionControl, SessionUpdate}, scene::{Scene, conversation::{ConversationEntry, Speaker}}, transcription::TranscriptionControl, tts::TtsControl};
|
||||
use crate::{audio::AudioInputControl, prediction::{GeneratedResponses, PredictionAction, SessionControl, SessionUpdate}, scene::{Scene, conversation::{ConversationEntry, Speaker}}, sfx::SfxControl, transcription::TranscriptionControl, tts::TtsControl};
|
||||
use crate::widgets::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -30,7 +30,8 @@ pub struct Ui {
|
||||
audio: AudioInputControl,
|
||||
tts: TtsControl,
|
||||
predictions: SessionControl,
|
||||
conversation: Vec<ConversationEntry>
|
||||
conversation: Vec<ConversationEntry>,
|
||||
sfx: SfxControl
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -40,7 +41,7 @@ enum FocusState {
|
||||
}
|
||||
|
||||
impl Ui {
|
||||
pub fn new(predictions: SessionControl, audio: AudioInputControl, transcription: TranscriptionControl, tts: TtsControl) -> Self {
|
||||
pub fn new(predictions: SessionControl, audio: AudioInputControl, transcription: TranscriptionControl, tts: TtsControl, sfx: SfxControl) -> Self {
|
||||
Self {
|
||||
scene: Default::default(),
|
||||
reply_state: Default::default(),
|
||||
@@ -58,7 +59,8 @@ impl Ui {
|
||||
predictions,
|
||||
last_tick: Instant::now(),
|
||||
conversation: vec![],
|
||||
reply_options: Default::default()
|
||||
reply_options: Default::default(),
|
||||
sfx
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,6 +195,9 @@ impl Ui {
|
||||
"/computer" => {
|
||||
self.predictions.insert(PredictionAction::ComputerCommand(arg.to_string())).await;
|
||||
},
|
||||
"/ambient" => {
|
||||
self.sfx.play_ambient().await.unwrap()
|
||||
},
|
||||
_ => {
|
||||
log::error!("Unknown command. Available commands: /episode [number], /narrative [text], /event [text], /computer [text], /timer [minutes]");
|
||||
}
|
||||
@@ -220,6 +225,7 @@ impl Ui {
|
||||
let row_num = self.conversation_state.selected().unwrap();
|
||||
if let ConversationEntry::Spoken(Speaker::Eva, text) = &self.conversation[self.conversation.len() - 1 - row_num] {
|
||||
self.tts.speak(text.clone()).await.unwrap();
|
||||
self.sfx.play_ambient().await.unwrap();
|
||||
self.focus_state = FocusState::UserInput;
|
||||
self.conversation_state.select(None);
|
||||
self.reply_state.select_first();
|
||||
|
||||
+2
-1
@@ -1,6 +1,7 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use chrono::Duration;
|
||||
use itertools::Itertools;
|
||||
use ratatui::{layout::Rect, style::Style, text::{Line, Text}, widgets::*};
|
||||
use tui_skeleton::Block;
|
||||
use ratatui::prelude::*;
|
||||
@@ -16,7 +17,7 @@ impl Widget for TaskList<'_> {
|
||||
let borders = Block::bordered().border_style(style::Color::Red).title("Computer Tasks");
|
||||
let wrap_options_unfinished = textwrap::Options::new(area.width as usize).initial_indent("[ ] ").subsequent_indent(" ");
|
||||
let wrap_options_finished = textwrap::Options::new(area.width as usize).initial_indent("[X] ").subsequent_indent(" ");
|
||||
let options: Vec<Text> = self.0.iter().map(|(text, is_finished)| {
|
||||
let options: Vec<Text> = self.0.iter().sorted_by(|a, b| Ord::cmp(a.1, b.1)).map(|(text, is_finished)| {
|
||||
let (options, color) = if *is_finished {
|
||||
(wrap_options_finished.clone(), style::Color::DarkGray)
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user