main: implement mixxx sqlite reading
This commit is contained in:
Generated
+29
@@ -887,6 +887,7 @@ dependencies = [
|
|||||||
"scraper",
|
"scraper",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"sqlite",
|
||||||
"static-iref",
|
"static-iref",
|
||||||
"throbber-widgets-tui",
|
"throbber-widgets-tui",
|
||||||
"tokio",
|
"tokio",
|
||||||
@@ -3589,6 +3590,34 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sqlite"
|
||||||
|
version = "0.37.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f66e9c01a11936154f3910dbba732c01f8b591543bc4d6672bddee79fd9c4783"
|
||||||
|
dependencies = [
|
||||||
|
"sqlite3-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sqlite3-src"
|
||||||
|
version = "0.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e5b6d3c860886b0a33e69e421796a5f4a27f23597a182c2450f6d7ace5103120"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"pkg-config",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sqlite3-sys"
|
||||||
|
version = "0.18.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a7781d97adc13a1d5081127a9ee29afad8427f3757bd984daf814d8265267039"
|
||||||
|
dependencies = [
|
||||||
|
"sqlite3-src",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "stable_deref_trait"
|
name = "stable_deref_trait"
|
||||||
version = "1.2.1"
|
version = "1.2.1"
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ schemars = "1.2.1"
|
|||||||
scraper = "0.27.0"
|
scraper = "0.27.0"
|
||||||
serde = { version = "1.0.228", features = ["derive"] }
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
serde_json = "1.0.150"
|
serde_json = "1.0.150"
|
||||||
|
sqlite = "0.37.0"
|
||||||
static-iref = "3.0.0"
|
static-iref = "3.0.0"
|
||||||
throbber-widgets-tui = "0.11.0"
|
throbber-widgets-tui = "0.11.0"
|
||||||
tokio = { version = "1.52.3", features = ["full"] }
|
tokio = { version = "1.52.3", features = ["full"] }
|
||||||
|
|||||||
+41
-33
@@ -6,6 +6,7 @@ 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, List, ListDirection, ListItem, ListState, Paragraph, Wrap}};
|
||||||
|
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;
|
||||||
@@ -33,7 +34,7 @@ use futures::{StreamExt, future::FutureExt};
|
|||||||
|
|
||||||
use ratatui::prelude::*;
|
use ratatui::prelude::*;
|
||||||
|
|
||||||
use crate::scene::{ConversationEntry, Scene};
|
use crate::scene::{ConversationEntry, PlaylistEntry, Scene};
|
||||||
|
|
||||||
mod scene;
|
mod scene;
|
||||||
|
|
||||||
@@ -62,12 +63,6 @@ struct GeneratedResponses {
|
|||||||
responses: Vec<PossibleResponse>,
|
responses: Vec<PossibleResponse>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Serialize, Deserialize, Clone, JsonSchema)]
|
|
||||||
struct StageDirectionUpdate {
|
|
||||||
episode_number: Option<u32>,
|
|
||||||
narrative: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Default, Serialize, Deserialize, Clone, JsonSchema)]
|
#[derive(Debug, Default, Serialize, Deserialize, Clone, JsonSchema)]
|
||||||
struct StageEventArgs {
|
struct StageEventArgs {
|
||||||
text: String,
|
text: String,
|
||||||
@@ -260,6 +255,7 @@ impl App {
|
|||||||
if let Ok(episode_number) = arg.trim().parse::<u32>() {
|
if let Ok(episode_number) = arg.trim().parse::<u32>() {
|
||||||
self.scene.direction.episode_number = episode_number;
|
self.scene.direction.episode_number = episode_number;
|
||||||
self.scene.insert_conversation(ConversationEntry::SystemMessage(format!("Updated episode number: {}", self.scene.direction.episode_number)));
|
self.scene.insert_conversation(ConversationEntry::SystemMessage(format!("Updated episode number: {}", self.scene.direction.episode_number)));
|
||||||
|
self.reload_mixxx_playlist();
|
||||||
} else {
|
} else {
|
||||||
self.scene.insert_conversation(ConversationEntry::SystemMessage("Invalid episode number format. Use /episode [number]".into()));
|
self.scene.insert_conversation(ConversationEntry::SystemMessage("Invalid episode number format. Use /episode [number]".into()));
|
||||||
}
|
}
|
||||||
@@ -323,23 +319,38 @@ impl App {
|
|||||||
self.is_requesting = true;
|
self.is_requesting = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn reload_mixxx_playlist(&mut self) {
|
||||||
|
self.scene.direction.current_playlist.clear();
|
||||||
|
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 mut statement = connection.prepare(query).unwrap();
|
||||||
|
statement.bind((1, format!("BFF.fm - Episode {}", self.scene.direction.episode_number).as_str())).unwrap();
|
||||||
|
statement.next().unwrap();
|
||||||
|
let latest_id = statement.read::<i64, _>("id").unwrap();
|
||||||
|
|
||||||
|
let query = "SELECT title, artist, album, comment, url, bpm FROM library LEFT JOIN PlaylistTracks ON PlaylistTracks.track_id = library.id WHERE PlaylistTracks.playlist_id = ? ORDER BY position";
|
||||||
|
for track in connection.prepare(query).unwrap().into_iter().bind((1, latest_id)).unwrap().map(|row| row.unwrap()) {
|
||||||
|
let title = track.try_read::<&str, _>("title").unwrap_or("Untitled Track");
|
||||||
|
let artist = track.try_read::<&str, _>("artist").unwrap_or("Unknown Artist");
|
||||||
|
let album = track.try_read::<&str, _>("album").unwrap_or("Unknown Album");
|
||||||
|
let bpm = track.try_read::<f64, _>("bpm").unwrap_or(0.);
|
||||||
|
self.scene.direction.current_playlist.push(PlaylistEntry {
|
||||||
|
artist: artist.into(),
|
||||||
|
album: album.into(),
|
||||||
|
title: title.into(),
|
||||||
|
bpm
|
||||||
|
});
|
||||||
|
}
|
||||||
|
self.scene.insert_conversation(ConversationEntry::SystemMessage("Mixxx playlist reloaded.".into()));
|
||||||
|
}
|
||||||
|
|
||||||
fn on_response(&mut self, response: CreateChatCompletionResponse) {
|
fn on_response(&mut self, response: CreateChatCompletionResponse) {
|
||||||
self.is_requesting = false;
|
self.is_requesting = false;
|
||||||
if let Some(calls) = &response.choices[0].message.tool_calls {
|
if let Some(calls) = &response.choices[0].message.tool_calls {
|
||||||
for call in calls {
|
for call in calls {
|
||||||
match call {
|
match call {
|
||||||
ChatCompletionMessageToolCalls::Function(call) => {
|
ChatCompletionMessageToolCalls::Function(call) => {
|
||||||
if call.function.name == "set_stage_direction" {
|
if call.function.name == "log_stage_event" {
|
||||||
let args: StageDirectionUpdate = serde_json::from_str(call.function.arguments.as_str()).unwrap();
|
|
||||||
if let Some(episode_number) = args.episode_number {
|
|
||||||
self.scene.direction.episode_number = episode_number;
|
|
||||||
self.scene.insert_conversation(ConversationEntry::ShipComputer(format!("Updated episode number: {}", self.scene.direction.episode_number)));
|
|
||||||
}
|
|
||||||
if let Some(narrative) = args.narrative {
|
|
||||||
self.scene.direction.narrative = narrative;
|
|
||||||
self.scene.insert_conversation(ConversationEntry::ShipComputer(format!("Updated stage direction: {}", self.scene.direction.narrative)));
|
|
||||||
}
|
|
||||||
} else if call.function.name == "log_stage_event" {
|
|
||||||
let args: StageEventArgs = serde_json::from_str(call.function.arguments.as_str()).unwrap();
|
let args: StageEventArgs = serde_json::from_str(call.function.arguments.as_str()).unwrap();
|
||||||
self.scene.insert_conversation(ConversationEntry::StageDirection(args.text));
|
self.scene.insert_conversation(ConversationEntry::StageDirection(args.text));
|
||||||
}
|
}
|
||||||
@@ -348,19 +359,23 @@ impl App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.regenerate_responses();
|
self.regenerate_responses();
|
||||||
|
} else {
|
||||||
|
if response.choices.is_empty() {
|
||||||
|
self.scene.insert_conversation(ConversationEntry::SystemMessage("OpenAI returned no responses".into()));
|
||||||
|
} else if response.choices[0].message.content.is_none() {
|
||||||
|
self.scene.insert_conversation(ConversationEntry::SystemMessage("OpenAI response did not contain content!".into()));
|
||||||
} else {
|
} else {
|
||||||
let json_resp: GeneratedResponses = serde_json::from_str(response.choices[0].message.content.as_ref().unwrap().as_str()).unwrap();
|
let json_resp: GeneratedResponses = serde_json::from_str(response.choices[0].message.content.as_ref().unwrap().as_str()).unwrap();
|
||||||
|
|
||||||
self.next_reply_options = json_resp.responses;
|
self.next_reply_options = json_resp.responses;
|
||||||
self.reply_state.select_first();
|
self.reply_state.select_first();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
color_eyre::install().unwrap();
|
color_eyre::install().unwrap();
|
||||||
|
|
||||||
let mut terminal: Terminal<CrosstermBackend<std::io::Stdout>> = ratatui::init();
|
let mut terminal: Terminal<CrosstermBackend<std::io::Stdout>> = ratatui::init();
|
||||||
|
|
||||||
let (prediction_in, mut prediction_out) = tokio::sync::watch::channel(None);
|
let (prediction_in, mut prediction_out) = tokio::sync::watch::channel(None);
|
||||||
@@ -369,20 +384,10 @@ async fn main() {
|
|||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let client: Client<OpenAIConfig> = Client::default();
|
let client: Client<OpenAIConfig> = Client::default();
|
||||||
loop {
|
loop {
|
||||||
prediction_request_out.changed().await.unwrap();
|
if let Ok(_) = prediction_request_out.changed().await {
|
||||||
let request = prediction_request_out.borrow().clone();
|
let request = prediction_request_out.borrow().clone();
|
||||||
let chat_request = CreateChatCompletionRequest {
|
let chat_request = CreateChatCompletionRequest {
|
||||||
tools: Some(vec![
|
/*tools: Some(vec![
|
||||||
ChatCompletionTools::Function(
|
|
||||||
ChatCompletionTool {
|
|
||||||
function: FunctionObject {
|
|
||||||
name: "set_stage_direction".into(),
|
|
||||||
description: Some("Set or update the current stage direction instructions.".into()),
|
|
||||||
parameters: Some(schema_for!(StageDirectionUpdate).into()),
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
),
|
|
||||||
ChatCompletionTools::Function(
|
ChatCompletionTools::Function(
|
||||||
ChatCompletionTool {
|
ChatCompletionTool {
|
||||||
function: FunctionObject {
|
function: FunctionObject {
|
||||||
@@ -393,11 +398,14 @@ async fn main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
]),
|
]),*/
|
||||||
..request.into()
|
..request.into()
|
||||||
};
|
};
|
||||||
let response = client.chat().create(chat_request).await.unwrap();
|
let response = client.chat().create(chat_request).await.unwrap();
|
||||||
prediction_in.send(Some(response)).unwrap();
|
prediction_in.send(Some(response)).unwrap();
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+13
-2
@@ -1,8 +1,10 @@
|
|||||||
use async_openai::types::chat::*;
|
use async_openai::types::chat::*;
|
||||||
use chrono::Duration;
|
use chrono::Duration;
|
||||||
|
use crossterm::event::MediaKeyCode::Play;
|
||||||
use schemars::schema_for;
|
use schemars::schema_for;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
use sqlite::OpenFlags;
|
||||||
|
|
||||||
use crate::GeneratedResponses;
|
use crate::GeneratedResponses;
|
||||||
|
|
||||||
@@ -22,7 +24,16 @@ pub struct StageDirection {
|
|||||||
pub episode_number: u32,
|
pub episode_number: u32,
|
||||||
pub time_remaining: Duration,
|
pub time_remaining: Duration,
|
||||||
pub narrative: String,
|
pub narrative: String,
|
||||||
pub artifacts: Vec<String>
|
pub artifacts: Vec<String>,
|
||||||
|
pub current_playlist: Vec<PlaylistEntry>
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
|
||||||
|
pub struct PlaylistEntry {
|
||||||
|
pub artist: String,
|
||||||
|
pub album: String,
|
||||||
|
pub title: String,
|
||||||
|
pub bpm: f64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -55,7 +66,7 @@ impl Into<CreateChatCompletionRequest> for Scene {
|
|||||||
}));
|
}));
|
||||||
let response_schema: Value = schema_for!(GeneratedResponses).into();
|
let response_schema: Value = schema_for!(GeneratedResponses).into();
|
||||||
CreateChatCompletionRequest {
|
CreateChatCompletionRequest {
|
||||||
model: "gpt-5.4-mini".into(),
|
model: "gpt-5.4".into(),
|
||||||
messages: messages,
|
messages: messages,
|
||||||
max_completion_tokens: Some(350),
|
max_completion_tokens: Some(350),
|
||||||
response_format: Some(ResponseFormat::JsonSchema { json_schema: ResponseFormatJsonSchema { description: None, name: "responses".into(), schema: response_schema, strict: None } }),
|
response_format: Some(ResponseFormat::JsonSchema { json_schema: ResponseFormatJsonSchema { description: None, name: "responses".into(), schema: response_schema, strict: None } }),
|
||||||
|
|||||||
Reference in New Issue
Block a user