chore: merge Rust backend into monorepo

git-subtree-dir: server
git-subtree-mainline: fb6a84d75f
git-subtree-split: 8a88706ff9
This commit is contained in:
2026-07-20 10:50:48 +08:00
18 changed files with 6191 additions and 0 deletions

167
server/src/config.rs Normal file
View File

@@ -0,0 +1,167 @@
use std::{env, net::IpAddr, path::PathBuf};
#[derive(Debug, Clone)]
pub enum SpeechProviderConfig {
Local { api_url: String },
Tencent(TencentSpeechConfig),
}
#[derive(Debug, Clone)]
pub struct TencentSpeechConfig {
pub app_id: u64,
pub secret_id: String,
pub secret_key: String,
pub engine_type: String,
}
#[derive(Debug, Clone)]
pub struct SpeechConfig {
pub provider: SpeechProviderConfig,
pub request_timeout_seconds: u64,
pub worker_concurrency: usize,
pub job_lease_seconds: i64,
}
#[derive(Debug, Clone)]
pub struct SummaryConfig {
pub api_url: String,
pub api_key: Option<String>,
pub model: String,
}
#[derive(Debug, Clone)]
pub struct Config {
pub host: IpAddr,
pub port: u16,
pub database_url: Option<String>,
pub database_max_connections: u32,
pub cors_allowed_origin: Option<String>,
pub audio_storage_dir: PathBuf,
pub speech: Option<SpeechConfig>,
pub summary: Option<SummaryConfig>,
}
impl Config {
pub fn from_env() -> Result<Self, String> {
let host = env::var("HOST")
.unwrap_or_else(|_| "127.0.0.1".to_owned())
.parse()
.map_err(|_| "HOST must be a valid IP address".to_owned())?;
let port = env::var("PORT")
.unwrap_or_else(|_| "8080".to_owned())
.parse()
.map_err(|_| "PORT must be a valid u16 value".to_owned())?;
let database_max_connections = env::var("DATABASE_MAX_CONNECTIONS")
.unwrap_or_else(|_| "5".to_owned())
.parse()
.map_err(|_| "DATABASE_MAX_CONNECTIONS must be a valid u32 value".to_owned())?;
let speech = speech_config()?;
let summary = summary_config()?;
Ok(Self {
host,
port,
database_url: env::var("DATABASE_URL")
.ok()
.filter(|value| !value.trim().is_empty()),
database_max_connections,
cors_allowed_origin: env::var("CORS_ALLOWED_ORIGIN")
.ok()
.filter(|value| !value.trim().is_empty()),
audio_storage_dir: env::var("AUDIO_STORAGE_DIR")
.map(PathBuf::from)
.unwrap_or_else(|_| PathBuf::from("./data/audio")),
speech,
summary,
})
}
}
fn speech_config() -> Result<Option<SpeechConfig>, String> {
let provider = optional_env("ASR_PROVIDER").unwrap_or_else(|| "local".to_owned());
if matches!(provider.as_str(), "disabled" | "none") {
return Ok(None);
}
let provider = match provider.as_str() {
"local" | "funasr" => SpeechProviderConfig::Local {
api_url: optional_env("LOCAL_ASR_URL")
.unwrap_or_else(|| "http://127.0.0.1:10095".to_owned())
.trim_end_matches('/')
.to_owned(),
},
"tencent" => {
let app_id = required_env("TENCENT_CLOUD_ASR_APP_ID")?
.parse()
.map_err(|_| "TENCENT_CLOUD_ASR_APP_ID must be an integer".to_owned())?;
SpeechProviderConfig::Tencent(TencentSpeechConfig {
app_id,
secret_id: required_env("TENCENT_CLOUD_ASR_SECRET_ID")?,
secret_key: required_env("TENCENT_CLOUD_ASR_SECRET_KEY")?,
engine_type: optional_env("TENCENT_CLOUD_ASR_ENGINE_TYPE")
.unwrap_or_else(|| "16k_zh".to_owned()),
})
}
_ => {
return Err(
"ASR_PROVIDER must be local, funasr, tencent, disabled, or none".to_owned(),
);
}
};
let request_timeout_seconds = parse_env("ASR_REQUEST_TIMEOUT_SECONDS", 1_800_u64)?;
let worker_concurrency = parse_env("ASR_WORKER_CONCURRENCY", 1_usize)?;
let job_lease_seconds = parse_env("ASR_JOB_LEASE_SECONDS", 3_600_i64)?;
if request_timeout_seconds == 0 {
return Err("ASR_REQUEST_TIMEOUT_SECONDS must be greater than 0".to_owned());
}
if worker_concurrency == 0 {
return Err("ASR_WORKER_CONCURRENCY must be greater than 0".to_owned());
}
if job_lease_seconds <= 0 {
return Err("ASR_JOB_LEASE_SECONDS must be greater than 0".to_owned());
}
Ok(Some(SpeechConfig {
provider,
request_timeout_seconds,
worker_concurrency,
job_lease_seconds,
}))
}
fn summary_config() -> Result<Option<SummaryConfig>, String> {
let Some(api_url) = optional_env("FEEDBACK_SUMMARY_API_URL") else {
return Ok(None);
};
let model = optional_env("FEEDBACK_SUMMARY_MODEL").ok_or_else(|| {
"FEEDBACK_SUMMARY_MODEL is required when summary API is configured".to_owned()
})?;
Ok(Some(SummaryConfig {
api_url: api_url.trim_end_matches('/').to_owned(),
api_key: optional_env("FEEDBACK_SUMMARY_API_KEY"),
model,
}))
}
fn optional_env(name: &str) -> Option<String> {
env::var(name)
.ok()
.map(|value| value.trim().to_owned())
.filter(|value| !value.is_empty())
}
fn required_env(name: &str) -> Result<String, String> {
optional_env(name).ok_or_else(|| format!("{name} is required for the selected ASR provider"))
}
fn parse_env<T>(name: &str, default: T) -> Result<T, String>
where
T: std::str::FromStr,
{
optional_env(name).map_or_else(
|| Ok(default),
|value| value.parse().map_err(|_| format!("{name} is invalid")),
)
}