226 lines
7.1 KiB
Rust
226 lines
7.1 KiB
Rust
use std::{env, net::IpAddr, path::PathBuf};
|
|
|
|
#[derive(Clone)]
|
|
pub enum SpeechProviderConfig {
|
|
Local { api_url: String },
|
|
Tencent(TencentSpeechConfig),
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct TencentSpeechConfig {
|
|
pub app_id: u64,
|
|
pub secret_id: String,
|
|
pub secret_key: String,
|
|
pub engine_type: String,
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct SpeechConfig {
|
|
pub provider: SpeechProviderConfig,
|
|
pub request_timeout_seconds: u64,
|
|
pub worker_concurrency: usize,
|
|
pub job_lease_seconds: i64,
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct SummaryConfig {
|
|
pub api_url: String,
|
|
pub api_key: Option<String>,
|
|
pub model: String,
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct WechatAuthConfig {
|
|
pub app_id: String,
|
|
pub app_secret: String,
|
|
pub code2session_url: String,
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct AuthConfig {
|
|
pub wechat: Option<WechatAuthConfig>,
|
|
pub session_ttl_days: i64,
|
|
pub allow_development_user_header: bool,
|
|
}
|
|
|
|
#[derive(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>,
|
|
pub auth: AuthConfig,
|
|
}
|
|
|
|
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()?;
|
|
let auth = auth_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,
|
|
auth,
|
|
})
|
|
}
|
|
}
|
|
|
|
fn auth_config() -> Result<AuthConfig, String> {
|
|
let app_id = optional_env("WECHAT_APP_ID");
|
|
let app_secret = optional_env("WECHAT_APP_SECRET");
|
|
let wechat = match (app_id, app_secret) {
|
|
(Some(app_id), Some(app_secret)) => Some(WechatAuthConfig {
|
|
app_id,
|
|
app_secret,
|
|
code2session_url: optional_env("WECHAT_CODE2SESSION_URL")
|
|
.unwrap_or_else(|| "https://api.weixin.qq.com/sns/jscode2session".to_owned()),
|
|
}),
|
|
(None, None) => None,
|
|
_ => {
|
|
return Err(
|
|
"WECHAT_APP_ID and WECHAT_APP_SECRET must be configured together".to_owned(),
|
|
);
|
|
}
|
|
};
|
|
let session_ttl_days = parse_env("AUTH_SESSION_TTL_DAYS", 30_i64)?;
|
|
if !(1..=365).contains(&session_ttl_days) {
|
|
return Err("AUTH_SESSION_TTL_DAYS must be between 1 and 365".to_owned());
|
|
}
|
|
let allow_development_user_header = parse_bool_env("ALLOW_DEVELOPMENT_USER_HEADER", false)?;
|
|
|
|
Ok(AuthConfig {
|
|
wechat,
|
|
session_ttl_days,
|
|
allow_development_user_header,
|
|
})
|
|
}
|
|
|
|
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")),
|
|
)
|
|
}
|
|
|
|
fn parse_bool_env(name: &str, default: bool) -> Result<bool, String> {
|
|
let Some(value) = optional_env(name) else {
|
|
return Ok(default);
|
|
};
|
|
match value.to_ascii_lowercase().as_str() {
|
|
"1" | "true" | "yes" | "on" => Ok(true),
|
|
"0" | "false" | "no" | "off" => Ok(false),
|
|
_ => Err(format!("{name} must be true or false")),
|
|
}
|
|
}
|