#![warn(missing_docs)]
#![warn(clippy::all)]
#![warn(clippy::pedantic)]
#![allow(clippy::module_name_repetitions)]
#![allow(clippy::missing_errors_doc)]
#![allow(clippy::missing_panics_doc)]
pub mod error;
pub mod graphql;
pub mod openapi;
pub mod rest;
pub mod services;
pub mod websocket;
use std::sync::Arc;
use axum::Router;
use tokio::sync::broadcast;
use tower_http::{
compression::CompressionLayer,
trace::TraceLayer,
};
pub use services::{
AudioPipeline, ClusterEngine, EmbeddingModel, InterpretationEngine, VectorIndex,
};
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
#[derive(Debug, Clone)]
pub struct Config {
pub host: String,
pub port: u16,
pub cors_origins: Vec<String>,
pub rate_limit_rps: u32,
pub max_upload_size: usize,
pub enable_playground: bool,
pub api_key: Option<String>,
}
impl Default for Config {
fn default() -> Self {
Self {
host: "0.0.0.0".to_string(),
port: 8080,
cors_origins: vec!["*".to_string()],
rate_limit_rps: 100,
max_upload_size: 100 * 1024 * 1024, enable_playground: true,
api_key: None,
}
}
}
impl Config {
pub fn from_env() -> anyhow::Result<Self> {
dotenvy::dotenv().ok();
Ok(Self {
host: std::env::var("SEVENSENSE_HOST").unwrap_or_else(|_| "0.0.0.0".to_string()),
port: std::env::var("SEVENSENSE_PORT")
.ok()
.and_then(|p| p.parse().ok())
.unwrap_or(8080),
cors_origins: std::env::var("SEVENSENSE_CORS_ORIGINS")
.map(|s| s.split(',').map(String::from).collect())
.unwrap_or_else(|_| vec!["*".to_string()]),
rate_limit_rps: std::env::var("SEVENSENSE_RATE_LIMIT")
.ok()
.and_then(|r| r.parse().ok())
.unwrap_or(100),
max_upload_size: std::env::var("SEVENSENSE_MAX_UPLOAD")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(100 * 1024 * 1024),
enable_playground: std::env::var("SEVENSENSE_ENABLE_PLAYGROUND")
.map(|s| s == "true" || s == "1")
.unwrap_or(true),
api_key: std::env::var("SEVENSENSE_API_KEY").ok(),
})
}
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct ProcessingEvent {
pub recording_id: uuid::Uuid,
pub status: ProcessingStatus,
pub progress: f32,
pub message: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, utoipa::ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum ProcessingStatus {
Queued,
Loading,
Segmenting,
Embedding,
Indexing,
Analyzing,
Complete,
Failed,
}
#[derive(Clone)]
pub struct AppContext {
pub audio_pipeline: Arc<AudioPipeline>,
pub embedding_model: Arc<EmbeddingModel>,
pub vector_index: Arc<VectorIndex>,
pub cluster_engine: Arc<ClusterEngine>,
pub interpretation_engine: Arc<InterpretationEngine>,
pub event_tx: broadcast::Sender<ProcessingEvent>,
pub config: Arc<Config>,
}
impl AppContext {
pub async fn new(config: Config) -> anyhow::Result<Self> {
let audio_pipeline = Arc::new(AudioPipeline::new(Default::default())?);
let embedding_model = Arc::new(EmbeddingModel::new(Default::default()).await?);
let vector_index = Arc::new(VectorIndex::new(Default::default())?);
let cluster_engine = Arc::new(ClusterEngine::new(Default::default())?);
let interpretation_engine = Arc::new(InterpretationEngine::new(Default::default())?);
let (event_tx, _) = broadcast::channel(1024);
Ok(Self {
audio_pipeline,
embedding_model,
vector_index,
cluster_engine,
interpretation_engine,
event_tx,
config: Arc::new(config),
})
}
#[must_use]
pub fn subscribe_events(&self) -> broadcast::Receiver<ProcessingEvent> {
self.event_tx.subscribe()
}
pub fn publish_event(&self, event: ProcessingEvent) {
let _ = self.event_tx.send(event);
}
}
pub struct AppBuilder {
config: Config,
context: Option<AppContext>,
}
impl AppBuilder {
#[must_use]
pub fn new(config: Config) -> Self {
Self {
config,
context: None,
}
}
#[must_use]
pub fn with_context(mut self, context: AppContext) -> Self {
self.context = Some(context);
self
}
pub async fn build(self) -> anyhow::Result<Router> {
let context = match self.context {
Some(ctx) => ctx,
None => AppContext::new(self.config.clone()).await?,
};
let rest_router = rest::routes::create_router(context.clone());
let graphql_router = graphql::create_router(context.clone());
let ws_router = websocket::create_router(context.clone());
let openapi_router = openapi::create_router();
let app = Router::new()
.nest("/api/v1", rest_router)
.nest("/graphql", graphql_router)
.nest("/ws", ws_router)
.nest("/docs", openapi_router)
.layer(
tower::ServiceBuilder::new()
.layer(TraceLayer::new_for_http())
.layer(CompressionLayer::new())
.layer(rest::middleware::cors_layer(&self.config)),
)
.with_state(context);
Ok(app)
}
}
#[derive(Debug, serde::Serialize, utoipa::ToSchema)]
pub struct HealthResponse {
pub status: String,
pub version: String,
pub uptime_secs: u64,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_config_default() {
let config = Config::default();
assert_eq!(config.host, "0.0.0.0");
assert_eq!(config.port, 8080);
assert!(config.enable_playground);
}
#[test]
fn test_processing_status_serialize() {
let status = ProcessingStatus::Embedding;
let json = serde_json::to_string(&status).unwrap();
assert_eq!(json, "\"embedding\"");
}
}