Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 25 additions & 11 deletions apps/cli/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
use anyhow::Result;
use tokio::io::AsyncBufReadExt;

use typex_config::AppConfig;
use typex_config::{AppConfig, LogLevel};
use typex_core::{TypeXBuildOptions, build_typex_from_config};

#[tokio::main]
async fn main() -> Result<()> {
tracing_subscriber::fmt()
.with_env_filter("typex=debug")
.init();
let config_path = std::path::Path::new("config.toml");
let config_exists = config_path.exists();
let config = load_config(config_path)?;
init_tracing(config.logging.level);
if config_exists {
tracing::info!("loaded config from {}", config_path.display());
} else {
tracing::info!("using default config");
}

let config = load_config()?;
let input_file = parse_input_arg()?;
let options = if input_file.is_some() {
TypeXBuildOptions::file()
Expand Down Expand Up @@ -79,6 +84,19 @@ async fn main() -> Result<()> {
Ok(())
}

fn init_tracing(level: LogLevel) {
let env_filter = tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new(typex_log_filter(level)));
tracing_subscriber::fmt().with_env_filter(env_filter).init();
}

fn typex_log_filter(level: LogLevel) -> String {
let level = level.as_str();
format!(
"typex={level},typex_cli={level},typex_core={level},typex_pipeline={level},typex_asr={level},typex_llm={level},typex_plugin={level},typex_audio={level},typex_injector={level}"
)
}

fn parse_input_arg() -> Result<Option<String>> {
let mut args = std::env::args().skip(1);
while let Some(arg) = args.next() {
Expand All @@ -92,14 +110,10 @@ fn parse_input_arg() -> Result<Option<String>> {
Ok(None)
}

fn load_config() -> Result<AppConfig> {
let config_path = std::path::Path::new("config.toml");
fn load_config(config_path: &std::path::Path) -> Result<AppConfig> {
if config_path.exists() {
let config = AppConfig::load(config_path)?;
tracing::info!("loaded config from {}", config_path.display());
Ok(config)
AppConfig::load(config_path)
} else {
tracing::info!("using default config");
Ok(AppConfig::default())
}
}
18 changes: 18 additions & 0 deletions apps/desktop/frontend/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -1405,6 +1405,21 @@ <h1 class="page-title" data-i18n="settings.page_title"></h1>
<div class="field-help" data-i18n="settings.performance_note"></div>
</div>
</div>

<div class="settings-subsection">
<div class="subsection-title" data-i18n="settings.logging"></div>
<div class="field">
<label class="field-label" for="logging-level" data-i18n="settings.logging_level"></label>
<select id="logging-level">
<option value="error" data-i18n="settings.log_level_error"></option>
<option value="warn" data-i18n="settings.log_level_warn"></option>
<option value="info" data-i18n="settings.log_level_info"></option>
<option value="debug" data-i18n="settings.log_level_debug"></option>
<option value="trace" data-i18n="settings.log_level_trace"></option>
</select>
<div class="field-help" data-i18n="settings.logging_help"></div>
</div>
</div>
</div>
</details>

Expand Down Expand Up @@ -1923,6 +1938,7 @@ <h1 class="page-title" data-i18n="settings.page_title"></h1>

document.getElementById('injector-method').value = currentConfig.injector.method || 'clipboard';
document.getElementById('pipeline-performance').value = currentConfig.pipeline.performance || 'balanced';
document.getElementById('logging-level').value = currentConfig.logging?.level || 'info';

document.getElementById('history-log-limit').value = currentConfig.history.log_limit;

Expand Down Expand Up @@ -1982,6 +1998,8 @@ <h1 class="page-title" data-i18n="settings.page_title"></h1>
if (document.getElementById('plugin-cleaner').checked) plugins.push('text_cleaner');
currentConfig.pipeline.plugins = plugins;
currentConfig.pipeline.performance = document.getElementById('pipeline-performance').value || 'balanced';
currentConfig.logging = currentConfig.logging || {};
currentConfig.logging.level = document.getElementById('logging-level').value || 'info';

currentConfig.injector.method = document.getElementById('injector-method').value || 'clipboard';
currentConfig.audio.device = document.getElementById('audio-device').value || null;
Expand Down
8 changes: 8 additions & 0 deletions apps/desktop/frontend/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,14 @@
"settings.new_llm_connection": "New LLM Connection",
"settings.advanced": "Advanced",
"settings.advanced_desc": "System prompt, plugin pipeline, and experimental performance options.",
"settings.logging": "Logging",
"settings.logging_level": "Log level",
"settings.logging_help": "Controls TypeX diagnostic logs. Use Debug to inspect ASR, plugin, and LLM text transformations. Debug logs may include transcribed text.",
"settings.log_level_error": "Error",
"settings.log_level_warn": "Warn",
"settings.log_level_info": "Info",
"settings.log_level_debug": "Debug",
"settings.log_level_trace": "Trace",
"settings.llm_provider_settings": "AI Provider Settings",
"settings.llm_disabled_advanced": "Enable AI Polish above to configure provider, endpoint, model, and prompt.",
"settings.test_connection": "Test",
Expand Down
8 changes: 8 additions & 0 deletions apps/desktop/frontend/locales/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,14 @@
"settings.new_llm_connection": "新的 LLM 连接",
"settings.advanced": "高级设置",
"settings.advanced_desc": "系统 Prompt、插件流水线和实验性性能选项。",
"settings.logging": "日志",
"settings.logging_level": "日志等级",
"settings.logging_help": "控制 TypeX 诊断日志。排查 ASR、插件和 LLM 文本处理问题时可切换到 Debug;Debug 日志可能包含你的语音转写内容。",
"settings.log_level_error": "Error",
"settings.log_level_warn": "Warn",
"settings.log_level_info": "Info",
"settings.log_level_debug": "Debug",
"settings.log_level_trace": "Trace",
"settings.llm_provider_settings": "AI Provider 设置",
"settings.llm_disabled_advanced": "先在上方启用 AI 润色,再配置 provider、endpoint、model 和 prompt。",
"settings.test_connection": "测试",
Expand Down
49 changes: 43 additions & 6 deletions apps/desktop/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ use tauri::{
tray::TrayIconBuilder,
};
use tauri_plugin_global_shortcut::{GlobalShortcutExt, ShortcutState};
use typex_config::{AppConfig, AsrConnection, LlmConnection};
use tracing_subscriber::{EnvFilter, Registry, prelude::*};
use typex_config::{AppConfig, AsrConnection, LlmConnection, LogLevel};
use typex_core::typex_asr::AsrProvider;
use typex_core::typex_llm::LlmProvider;
use typex_core::{TypeX, TypeXBuildOptions, build_typex_from_config};
Expand All @@ -27,6 +28,7 @@ use typex_core::{TypeX, TypeXBuildOptions, build_typex_from_config};
/// Awaiting it yields the accumulator JoinHandle, which in turn yields the PCM data.
type RecordingAccFuture =
tokio::task::JoinHandle<anyhow::Result<tokio::task::JoinHandle<anyhow::Result<Vec<u8>>>>>;
type LogReloadHandle = tracing_subscriber::reload::Handle<EnvFilter, Registry>;

struct AppState {
config: Mutex<AppConfig>,
Expand All @@ -46,6 +48,7 @@ struct AppState {
overlay_save_token: AtomicU64,
/// Prevents concurrent recording start attempts.
recording_starting: AtomicBool,
log_filter: Option<LogReloadHandle>,
/// Tokio runtime handle — needed because global shortcut callbacks run outside Tokio context.
rt: tokio::runtime::Handle,
}
Expand All @@ -71,6 +74,26 @@ fn db_path(app: &tauri::AppHandle) -> std::path::PathBuf {
.join("typex.db")
}

fn typex_log_filter(level: LogLevel) -> String {
let level = level.as_str();
format!(
"typex_desktop_lib={level},typex_desktop={level},typex_core={level},typex_pipeline={level},typex_asr={level},typex_llm={level},typex_plugin={level},typex_audio={level},typex_injector={level}"
)
}

fn env_filter_for_level(level: LogLevel) -> EnvFilter {
EnvFilter::new(typex_log_filter(level))
}

fn reload_log_filter(handle: &Option<LogReloadHandle>, level: LogLevel) {
let Some(handle) = handle else {
return;
};
if let Err(e) = handle.reload(env_filter_for_level(level)) {
tracing::warn!("failed to reload log filter: {}", e);
}
}

fn init_db(conn: &Connection) -> rusqlite::Result<()> {
conn.execute_batch(
"CREATE TABLE IF NOT EXISTS history (
Expand Down Expand Up @@ -612,6 +635,7 @@ fn save_config(state: tauri::State<AppState>, mut config: AppConfig) -> Result<(
}

// 3. Update in-memory config immediately
reload_log_filter(&state.log_filter, config.logging.level);
*state.config.lock().unwrap() = config.clone();

// 4. Rebuild pipeline (non-fatal: keep old pipeline on failure)
Expand Down Expand Up @@ -1100,11 +1124,22 @@ fn set_language(state: tauri::State<AppState>, language: String) -> Result<(), S
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
// Initialize tracing subscriber so log messages are visible in console.
// Uses RUST_LOG env var for filtering (e.g. RUST_LOG=typex_audio=trace,typex_desktop=info).
// Falls back to "info" level if RUST_LOG is not set.
let env_filter = tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info"));
tracing_subscriber::fmt().with_env_filter(env_filter).init();
// RUST_LOG takes precedence over the UI-configured log level.
let log_filter = match EnvFilter::try_from_default_env() {
Ok(env_filter) => {
tracing_subscriber::fmt().with_env_filter(env_filter).init();
None
}
Err(_) => {
let (filter_layer, handle) =
tracing_subscriber::reload::Layer::new(env_filter_for_level(LogLevel::Info));
tracing_subscriber::registry()
.with(filter_layer)
.with(tracing_subscriber::fmt::layer())
.init();
Some(handle)
}
};

// Create a dedicated Tokio runtime that lives for the entire application
// lifetime (run() blocks until exit). Its handle is shared with global
Expand Down Expand Up @@ -1149,6 +1184,7 @@ pub fn run() {
}
config
};
reload_log_filter(&log_filter, config.logging.level);

let current_shortcut = config.shortcut.record.clone();

Expand Down Expand Up @@ -1189,6 +1225,7 @@ pub fn run() {
overlay_error_token: AtomicU64::new(0),
overlay_save_token: AtomicU64::new(0),
recording_starting: AtomicBool::new(false),
log_filter,
rt: rt.clone(),
});

Expand Down
3 changes: 3 additions & 0 deletions config.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ provider = "mock" # mock | openai-compatible
performance = "balanced" # low | balanced | high
plugins = ["filler_remover", "text_cleaner"]

[logging]
level = "info" # error | warn | info | debug | trace

[injector]
method = "clipboard" # clipboard | platform

Expand Down
32 changes: 32 additions & 0 deletions crates/config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ pub struct AppConfig {

#[serde(default)]
pub ui: UiConfig,

#[serde(default)]
pub logging: LoggingConfig,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
Expand Down Expand Up @@ -371,6 +374,35 @@ pub struct UiConfig {
pub theme: String,
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct LoggingConfig {
#[serde(default)]
pub level: LogLevel,
}

#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum LogLevel {
Error,
Warn,
#[default]
Info,
Debug,
Trace,
}

impl LogLevel {
pub fn as_str(self) -> &'static str {
match self {
Self::Error => "error",
Self::Warn => "warn",
Self::Info => "info",
Self::Debug => "debug",
Self::Trace => "trace",
}
}
}

fn default_language() -> String {
"auto".into()
}
Expand Down
13 changes: 13 additions & 0 deletions crates/config/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ fn test_llm_config_roundtrip_from_legacy_json() {
Some("You are a helpful assistant.")
);

assert_eq!(config.logging.level, LogLevel::Info);

let toml_str = toml::to_string_pretty(&config).expect("AppConfig → TOML should work");
assert!(toml_str.contains("connections"));
assert!(toml_str.contains("active_connection"));
Expand Down Expand Up @@ -269,6 +271,17 @@ fn test_option_none_omitted_from_toml() {
);
}

#[test]
fn test_logging_level_deserializes_from_config() {
let toml_str = r#"
[logging]
level = "debug"
"#;

let config: AppConfig = toml::from_str(toml_str).unwrap();
assert_eq!(config.logging.level, LogLevel::Debug);
}

/// Verify that when the frontend sends `null` for an Option<String> field,
/// it correctly deserializes to None.
#[test]
Expand Down
Loading
Loading