From e83da300487d75a78d5b7f7c78045b6f37675f2d Mon Sep 17 00:00:00 2001 From: Laan Tungir Date: Thu, 26 Mar 2026 21:51:02 -0400 Subject: [PATCH] Add tabbed Whisper settings page with persisted decode params and live runtime updates --- config.ini | 5 +- src/config.c | 169 ++++++++++++++++ src/config.h | 38 ++++ src/gui_main.c | 510 +++++++++++++++++++++++++++++++++++++++++++---- src/transcribe.c | 61 +++++- src/transcribe.h | 38 ++++ 6 files changed, 780 insertions(+), 41 deletions(-) diff --git a/config.ini b/config.ini index 42a4145..287f751 100644 --- a/config.ini +++ b/config.ini @@ -2,7 +2,7 @@ # Hotkey format supports Ctrl, Alt, Shift, Super + final key symbol # Example: Ctrl+Alt+V -model_path=./models/ggml-base.en.bin +model_path=./models/ggml-medium.en.bin use_gpu=1 audio_device=alsa_input.usb-Shure_Inc_Shure_MV6_MV6_5-ed2d37fa14d3e65d8d477cacf910d93c-01.mono-fallback sample_rate=16000 @@ -16,7 +16,7 @@ always_on=1 always_on_window_ms=1400 # live level threshold (0.0 - 1.0) -vad_peak_threshold=0.006 +vad_peak_threshold=0.0060 # level must stay above threshold this long before speech starts vad_trigger_ms=100 # level must stay below threshold this long before speech ends @@ -29,3 +29,4 @@ vad_min_speech_ms=250 vad_max_speech_ms=15000 autotype_enabled=1 +filter_bracketed_tags=1 diff --git a/src/config.c b/src/config.c index a132092..cdc5ed0 100644 --- a/src/config.c +++ b/src/config.c @@ -39,6 +39,43 @@ void config_set_defaults(voice_config_t *cfg) { cfg->vad_max_speech_ms = 15000; cfg->autotype_enabled = 1; cfg->filter_bracketed_tags = 1; + + cfg->whisper_sampling_strategy = 0; + cfg->whisper_n_max_text_ctx = 16384; + cfg->whisper_offset_ms = 0; + cfg->whisper_duration_ms = 0; + + cfg->whisper_translate = 0; + cfg->whisper_detect_language = 0; + cfg->whisper_no_context = 0; + cfg->whisper_no_timestamps = 1; + cfg->whisper_single_segment = 0; + + cfg->whisper_token_timestamps = 0; + cfg->whisper_thold_pt = 0.01f; + cfg->whisper_thold_ptsum = 0.01f; + cfg->whisper_max_len = 0; + cfg->whisper_split_on_word = 0; + cfg->whisper_max_tokens = 0; + + cfg->whisper_audio_ctx = 0; + cfg->whisper_tdrz_enable = 0; + + cfg->whisper_suppress_blank = 1; + cfg->whisper_suppress_nst = 0; + + cfg->whisper_temperature = 0.0f; + cfg->whisper_max_initial_ts = 1.0f; + cfg->whisper_length_penalty = -1.0f; + + cfg->whisper_temperature_inc = 0.2f; + cfg->whisper_entropy_thold = 2.4f; + cfg->whisper_logprob_thold = -1.0f; + cfg->whisper_no_speech_thold = 0.6f; + + cfg->whisper_greedy_best_of = 1; + cfg->whisper_beam_size = 5; + cfg->whisper_beam_patience = -1.0f; } int config_load_file(const char *path, voice_config_t *cfg) { @@ -97,9 +134,141 @@ int config_load_file(const char *path, voice_config_t *cfg) { cfg->autotype_enabled = atoi(value); } else if (strcmp(key, "filter_bracketed_tags") == 0) { cfg->filter_bracketed_tags = atoi(value); + } else if (strcmp(key, "whisper_sampling_strategy") == 0) { + cfg->whisper_sampling_strategy = atoi(value); + } else if (strcmp(key, "whisper_n_max_text_ctx") == 0) { + cfg->whisper_n_max_text_ctx = atoi(value); + } else if (strcmp(key, "whisper_offset_ms") == 0) { + cfg->whisper_offset_ms = atoi(value); + } else if (strcmp(key, "whisper_duration_ms") == 0) { + cfg->whisper_duration_ms = atoi(value); + } else if (strcmp(key, "whisper_translate") == 0) { + cfg->whisper_translate = atoi(value); + } else if (strcmp(key, "whisper_detect_language") == 0) { + cfg->whisper_detect_language = atoi(value); + } else if (strcmp(key, "whisper_no_context") == 0) { + cfg->whisper_no_context = atoi(value); + } else if (strcmp(key, "whisper_no_timestamps") == 0) { + cfg->whisper_no_timestamps = atoi(value); + } else if (strcmp(key, "whisper_single_segment") == 0) { + cfg->whisper_single_segment = atoi(value); + } else if (strcmp(key, "whisper_token_timestamps") == 0) { + cfg->whisper_token_timestamps = atoi(value); + } else if (strcmp(key, "whisper_thold_pt") == 0) { + cfg->whisper_thold_pt = (float)atof(value); + } else if (strcmp(key, "whisper_thold_ptsum") == 0) { + cfg->whisper_thold_ptsum = (float)atof(value); + } else if (strcmp(key, "whisper_max_len") == 0) { + cfg->whisper_max_len = atoi(value); + } else if (strcmp(key, "whisper_split_on_word") == 0) { + cfg->whisper_split_on_word = atoi(value); + } else if (strcmp(key, "whisper_max_tokens") == 0) { + cfg->whisper_max_tokens = atoi(value); + } else if (strcmp(key, "whisper_audio_ctx") == 0) { + cfg->whisper_audio_ctx = atoi(value); + } else if (strcmp(key, "whisper_tdrz_enable") == 0) { + cfg->whisper_tdrz_enable = atoi(value); + } else if (strcmp(key, "whisper_suppress_blank") == 0) { + cfg->whisper_suppress_blank = atoi(value); + } else if (strcmp(key, "whisper_suppress_nst") == 0) { + cfg->whisper_suppress_nst = atoi(value); + } else if (strcmp(key, "whisper_temperature") == 0) { + cfg->whisper_temperature = (float)atof(value); + } else if (strcmp(key, "whisper_max_initial_ts") == 0) { + cfg->whisper_max_initial_ts = (float)atof(value); + } else if (strcmp(key, "whisper_length_penalty") == 0) { + cfg->whisper_length_penalty = (float)atof(value); + } else if (strcmp(key, "whisper_temperature_inc") == 0) { + cfg->whisper_temperature_inc = (float)atof(value); + } else if (strcmp(key, "whisper_entropy_thold") == 0) { + cfg->whisper_entropy_thold = (float)atof(value); + } else if (strcmp(key, "whisper_logprob_thold") == 0) { + cfg->whisper_logprob_thold = (float)atof(value); + } else if (strcmp(key, "whisper_no_speech_thold") == 0) { + cfg->whisper_no_speech_thold = (float)atof(value); + } else if (strcmp(key, "whisper_greedy_best_of") == 0) { + cfg->whisper_greedy_best_of = atoi(value); + } else if (strcmp(key, "whisper_beam_size") == 0) { + cfg->whisper_beam_size = atoi(value); + } else if (strcmp(key, "whisper_beam_patience") == 0) { + cfg->whisper_beam_patience = (float)atof(value); } } fclose(f); return 0; } + +int config_save_file(const char *path, const voice_config_t *cfg) { + if (!path || !cfg) return -1; + + FILE *f = fopen(path, "w"); + if (!f) return -2; + + fprintf(f, "# voice_linux runtime configuration\n"); + fprintf(f, "# Hotkey format supports Ctrl, Alt, Shift, Super + final key symbol\n"); + fprintf(f, "# Example: Ctrl+Alt+V\n\n"); + + fprintf(f, "model_path=%s\n", cfg->model_path); + fprintf(f, "use_gpu=%d\n", cfg->use_gpu ? 1 : 0); + fprintf(f, "audio_device=%s\n", cfg->audio_device); + fprintf(f, "sample_rate=%d\n", cfg->sample_rate); + fprintf(f, "language=%s\n", cfg->language); + fprintf(f, "hotkey=%s\n", cfg->hotkey); + fprintf(f, "type_delay_us=%d\n", cfg->type_delay_us); + fprintf(f, "max_record_seconds=%d\n", cfg->max_record_seconds); + fprintf(f, "# always-on VAD controls\n"); + fprintf(f, "always_on=%d\n", cfg->always_on ? 1 : 0); + fprintf(f, "# legacy window size (unused by new VAD pipeline, kept for compatibility)\n"); + fprintf(f, "always_on_window_ms=%d\n\n", cfg->always_on_window_ms); + + fprintf(f, "# live level threshold (0.0 - 1.0)\n"); + fprintf(f, "vad_peak_threshold=%.4f\n", cfg->vad_peak_threshold); + fprintf(f, "# level must stay above threshold this long before speech starts\n"); + fprintf(f, "vad_trigger_ms=%d\n", cfg->vad_trigger_ms); + fprintf(f, "# level must stay below threshold this long before speech ends\n"); + fprintf(f, "vad_release_ms=%d\n", cfg->vad_release_ms); + fprintf(f, "# include this much pre-trigger audio in transcript segment\n"); + fprintf(f, "vad_preroll_ms=%d\n", cfg->vad_preroll_ms); + fprintf(f, "# discard segments shorter than this\n"); + fprintf(f, "vad_min_speech_ms=%d\n", cfg->vad_min_speech_ms); + fprintf(f, "# hard cap for one speech segment\n"); + fprintf(f, "vad_max_speech_ms=%d\n\n", cfg->vad_max_speech_ms); + + fprintf(f, "autotype_enabled=%d\n", cfg->autotype_enabled ? 1 : 0); + fprintf(f, "filter_bracketed_tags=%d\n\n", cfg->filter_bracketed_tags ? 1 : 0); + + fprintf(f, "# whisper decode controls\n"); + fprintf(f, "whisper_sampling_strategy=%d\n", cfg->whisper_sampling_strategy); + fprintf(f, "whisper_n_max_text_ctx=%d\n", cfg->whisper_n_max_text_ctx); + fprintf(f, "whisper_offset_ms=%d\n", cfg->whisper_offset_ms); + fprintf(f, "whisper_duration_ms=%d\n", cfg->whisper_duration_ms); + fprintf(f, "whisper_translate=%d\n", cfg->whisper_translate ? 1 : 0); + fprintf(f, "whisper_detect_language=%d\n", cfg->whisper_detect_language ? 1 : 0); + fprintf(f, "whisper_no_context=%d\n", cfg->whisper_no_context ? 1 : 0); + fprintf(f, "whisper_no_timestamps=%d\n", cfg->whisper_no_timestamps ? 1 : 0); + fprintf(f, "whisper_single_segment=%d\n", cfg->whisper_single_segment ? 1 : 0); + fprintf(f, "whisper_token_timestamps=%d\n", cfg->whisper_token_timestamps ? 1 : 0); + fprintf(f, "whisper_thold_pt=%.4f\n", cfg->whisper_thold_pt); + fprintf(f, "whisper_thold_ptsum=%.4f\n", cfg->whisper_thold_ptsum); + fprintf(f, "whisper_max_len=%d\n", cfg->whisper_max_len); + fprintf(f, "whisper_split_on_word=%d\n", cfg->whisper_split_on_word ? 1 : 0); + fprintf(f, "whisper_max_tokens=%d\n", cfg->whisper_max_tokens); + fprintf(f, "whisper_audio_ctx=%d\n", cfg->whisper_audio_ctx); + fprintf(f, "whisper_tdrz_enable=%d\n", cfg->whisper_tdrz_enable ? 1 : 0); + fprintf(f, "whisper_suppress_blank=%d\n", cfg->whisper_suppress_blank ? 1 : 0); + fprintf(f, "whisper_suppress_nst=%d\n", cfg->whisper_suppress_nst ? 1 : 0); + fprintf(f, "whisper_temperature=%.4f\n", cfg->whisper_temperature); + fprintf(f, "whisper_max_initial_ts=%.4f\n", cfg->whisper_max_initial_ts); + fprintf(f, "whisper_length_penalty=%.4f\n", cfg->whisper_length_penalty); + fprintf(f, "whisper_temperature_inc=%.4f\n", cfg->whisper_temperature_inc); + fprintf(f, "whisper_entropy_thold=%.4f\n", cfg->whisper_entropy_thold); + fprintf(f, "whisper_logprob_thold=%.4f\n", cfg->whisper_logprob_thold); + fprintf(f, "whisper_no_speech_thold=%.4f\n", cfg->whisper_no_speech_thold); + fprintf(f, "whisper_greedy_best_of=%d\n", cfg->whisper_greedy_best_of); + fprintf(f, "whisper_beam_size=%d\n", cfg->whisper_beam_size); + fprintf(f, "whisper_beam_patience=%.4f\n", cfg->whisper_beam_patience); + + fclose(f); + return 0; +} diff --git a/src/config.h b/src/config.h index 37d6ba3..70c9a21 100644 --- a/src/config.h +++ b/src/config.h @@ -22,9 +22,47 @@ typedef struct { int vad_max_speech_ms; int autotype_enabled; int filter_bracketed_tags; + + int whisper_sampling_strategy; // 0=greedy, 1=beam search + int whisper_n_max_text_ctx; + int whisper_offset_ms; + int whisper_duration_ms; + + int whisper_translate; + int whisper_detect_language; + int whisper_no_context; + int whisper_no_timestamps; + int whisper_single_segment; + + int whisper_token_timestamps; + float whisper_thold_pt; + float whisper_thold_ptsum; + int whisper_max_len; + int whisper_split_on_word; + int whisper_max_tokens; + + int whisper_audio_ctx; + int whisper_tdrz_enable; + + int whisper_suppress_blank; + int whisper_suppress_nst; + + float whisper_temperature; + float whisper_max_initial_ts; + float whisper_length_penalty; + + float whisper_temperature_inc; + float whisper_entropy_thold; + float whisper_logprob_thold; + float whisper_no_speech_thold; + + int whisper_greedy_best_of; + int whisper_beam_size; + float whisper_beam_patience; } voice_config_t; void config_set_defaults(voice_config_t *cfg); int config_load_file(const char *path, voice_config_t *cfg); +int config_save_file(const char *path, const voice_config_t *cfg); #endif diff --git a/src/gui_main.c b/src/gui_main.c index c666f01..77cad21 100644 --- a/src/gui_main.c +++ b/src/gui_main.c @@ -10,6 +10,7 @@ #include #include #include +#include #include #include @@ -22,6 +23,7 @@ typedef struct { GtkWidget *type_check; GtkWidget *filter_tags_check; GtkWidget *transcript_view; + GtkWidget *gpu_light; GtkWidget *record_light; GtkWidget *transcribe_light; @@ -34,6 +36,38 @@ typedef struct { GtkWidget *max_speech_scale; GtkWidget *debug_label; + GtkWidget *sampling_combo; + GtkWidget *translate_check; + GtkWidget *detect_language_check; + GtkWidget *no_context_check; + GtkWidget *no_timestamps_check; + GtkWidget *single_segment_check; + GtkWidget *token_timestamps_check; + GtkWidget *split_on_word_check; + GtkWidget *suppress_blank_check; + GtkWidget *suppress_nst_check; + GtkWidget *tdrz_enable_check; + + GtkWidget *n_max_text_ctx_spin; + GtkWidget *offset_ms_spin; + GtkWidget *duration_ms_spin; + GtkWidget *max_len_spin; + GtkWidget *max_tokens_spin; + GtkWidget *audio_ctx_spin; + GtkWidget *greedy_best_of_spin; + GtkWidget *beam_size_spin; + + GtkWidget *thold_pt_spin; + GtkWidget *thold_ptsum_spin; + GtkWidget *temperature_spin; + GtkWidget *max_initial_ts_spin; + GtkWidget *length_penalty_spin; + GtkWidget *temperature_inc_spin; + GtkWidget *entropy_thold_spin; + GtkWidget *logprob_thold_spin; + GtkWidget *no_speech_thold_spin; + GtkWidget *beam_patience_spin; + voice_config_t cfg; int use_gpu; int recording; @@ -45,6 +79,7 @@ typedef struct { int runtime_ready; char last_transcript[4096]; double last_transcript_time; + char cfg_path[512]; } gui_state_t; static float clampf(float v, float lo, float hi) { @@ -71,8 +106,74 @@ static void append_transcript(gui_state_t *s, const char *text) { gtk_text_buffer_insert(buf, &end, text, -1); } +static void save_config_best_effort(gui_state_t *s) { + if (!s || s->cfg_path[0] == '\0') return; + if (config_save_file(s->cfg_path, &s->cfg) != 0) { + fprintf(stderr, "gui: warning: failed to save config to %s\n", s->cfg_path); + } +} + +static void fill_transcribe_params_from_cfg(const gui_state_t *s, transcribe_params_t *tp) { + if (!s || !tp) return; + + memset(tp, 0, sizeof(*tp)); + snprintf(tp->model_path, sizeof(tp->model_path), "%s", s->cfg.model_path); + snprintf(tp->language, sizeof(tp->language), "%s", s->cfg.language); + tp->use_gpu = s->use_gpu; + tp->n_threads = (int)sysconf(_SC_NPROCESSORS_ONLN); + tp->filter_bracketed_tags = s->cfg.filter_bracketed_tags; + + tp->sampling_strategy = s->cfg.whisper_sampling_strategy; + tp->n_max_text_ctx = s->cfg.whisper_n_max_text_ctx; + tp->offset_ms = s->cfg.whisper_offset_ms; + tp->duration_ms = s->cfg.whisper_duration_ms; + + tp->translate = s->cfg.whisper_translate; + tp->detect_language = s->cfg.whisper_detect_language; + tp->no_context = s->cfg.whisper_no_context; + tp->no_timestamps = s->cfg.whisper_no_timestamps; + tp->single_segment = s->cfg.whisper_single_segment; + + tp->token_timestamps = s->cfg.whisper_token_timestamps; + tp->thold_pt = s->cfg.whisper_thold_pt; + tp->thold_ptsum = s->cfg.whisper_thold_ptsum; + tp->max_len = s->cfg.whisper_max_len; + tp->split_on_word = s->cfg.whisper_split_on_word; + tp->max_tokens = s->cfg.whisper_max_tokens; + + tp->audio_ctx = s->cfg.whisper_audio_ctx; + tp->tdrz_enable = s->cfg.whisper_tdrz_enable; + + tp->suppress_blank = s->cfg.whisper_suppress_blank; + tp->suppress_nst = s->cfg.whisper_suppress_nst; + + tp->temperature = s->cfg.whisper_temperature; + tp->max_initial_ts = s->cfg.whisper_max_initial_ts; + tp->length_penalty = s->cfg.whisper_length_penalty; + + tp->temperature_inc = s->cfg.whisper_temperature_inc; + tp->entropy_thold = s->cfg.whisper_entropy_thold; + tp->logprob_thold = s->cfg.whisper_logprob_thold; + tp->no_speech_thold = s->cfg.whisper_no_speech_thold; + + tp->greedy_best_of = s->cfg.whisper_greedy_best_of; + tp->beam_size = s->cfg.whisper_beam_size; + tp->beam_patience = s->cfg.whisper_beam_patience; +} + +static void push_transcribe_runtime_params(gui_state_t *s) { + if (!s || !s->runtime_ready) return; + transcribe_params_t tp; + fill_transcribe_params_from_cfg(s, &tp); + transcribe_update_params(&tp); +} + static void update_activity_lights(gui_state_t *s) { - if (!s->record_light || !s->transcribe_light) return; + if (!s || !s->gpu_light || !s->record_light || !s->transcribe_light) return; + + const char *gpu_markup = s->use_gpu + ? "● GPU" + : "● CPU"; const int rec_on = (s->recording || s->always_on_running) ? 1 : 0; const char *rec_markup = rec_on @@ -83,6 +184,7 @@ static void update_activity_lights(gui_state_t *s) { ? "● Transcribing" : "● Transcribing"; + gtk_label_set_markup(GTK_LABEL(s->gpu_light), gpu_markup); gtk_label_set_markup(GTK_LABEL(s->record_light), rec_markup); gtk_label_set_markup(GTK_LABEL(s->transcribe_light), tr_markup); } @@ -126,6 +228,12 @@ static void update_always_on_button(gui_state_t *s) { gtk_button_set_label(GTK_BUTTON(s->always_on_btn), s->always_on_running ? "Stop Always-On" : "Start Always-On"); } +static void on_autotype_toggled(GtkToggleButton *btn, gpointer user_data) { + gui_state_t *s = (gui_state_t *)user_data; + s->cfg.autotype_enabled = gtk_toggle_button_get_active(btn) ? 1 : 0; + save_config_best_effort(s); +} + static void log_audio_stats(const float *samples, size_t count, int sample_rate, const char *device_name, char *out, size_t out_sz) { if (!samples || count == 0 || sample_rate <= 0) { snprintf(out, out_sz, "Empty capture"); @@ -291,6 +399,7 @@ static void on_vad_value_changed(GtkRange *range, gpointer user_data) { (void)range; gui_state_t *s = (gui_state_t *)user_data; apply_vad_params(s); + save_config_best_effort(s); } static gboolean on_level_draw(GtkWidget *widget, cairo_t *cr, gpointer user_data) { @@ -428,6 +537,8 @@ static void on_always_on_clicked(GtkButton *btn, gpointer user_data) { update_always_on_button(s); update_record_button(s); set_status(s, "Always-on stopped"); + s->cfg.always_on = 0; + save_config_best_effort(s); return; } @@ -437,9 +548,12 @@ static void on_always_on_clicked(GtkButton *btn, gpointer user_data) { update_always_on_button(s); update_record_button(s); set_status(s, "Always-on active (continuous monitor)"); + s->cfg.always_on = 1; + save_config_best_effort(s); } static GtkWidget *add_scale_row(GtkWidget *grid, + int col, int row, const char *label_text, double min, @@ -451,14 +565,14 @@ static GtkWidget *add_scale_row(GtkWidget *grid, GtkWidget **out_scale) { GtkWidget *label = gtk_label_new(label_text); gtk_label_set_xalign(GTK_LABEL(label), 0.0f); - gtk_grid_attach(GTK_GRID(grid), label, 0, row, 1, 1); + gtk_grid_attach(GTK_GRID(grid), label, col, row, 1, 1); GtkWidget *scale = gtk_scale_new_with_range(GTK_ORIENTATION_HORIZONTAL, min, max, step); gtk_scale_set_digits(GTK_SCALE(scale), digits); gtk_scale_set_draw_value(GTK_SCALE(scale), TRUE); gtk_range_set_value(GTK_RANGE(scale), value); gtk_widget_set_hexpand(scale, TRUE); - gtk_grid_attach(GTK_GRID(grid), scale, 1, row, 1, 1); + gtk_grid_attach(GTK_GRID(grid), scale, col + 1, row, 1, 1); g_signal_connect(scale, "value-changed", G_CALLBACK(on_vad_value_changed), s); *out_scale = scale; @@ -471,6 +585,227 @@ static void on_filter_tags_toggled(GtkToggleButton *btn, gpointer user_data) { int enabled = gtk_toggle_button_get_active(btn) ? 1 : 0; s->cfg.filter_bracketed_tags = enabled; transcribe_set_filter_bracketed_tags(enabled); + push_transcribe_runtime_params(s); + save_config_best_effort(s); +} + +static int read_spin_int(GtkWidget *spin) { + return gtk_spin_button_get_value_as_int(GTK_SPIN_BUTTON(spin)); +} + +static float read_spin_float(GtkWidget *spin) { + return (float)gtk_spin_button_get_value(GTK_SPIN_BUTTON(spin)); +} + +static void apply_whisper_settings_from_widgets(gui_state_t *s) { + if (!s || !s->sampling_combo) return; + + s->cfg.whisper_sampling_strategy = gtk_combo_box_get_active(GTK_COMBO_BOX(s->sampling_combo)) == 1 ? 1 : 0; + + s->cfg.whisper_translate = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(s->translate_check)) ? 1 : 0; + s->cfg.whisper_detect_language = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(s->detect_language_check)) ? 1 : 0; + s->cfg.whisper_no_context = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(s->no_context_check)) ? 1 : 0; + s->cfg.whisper_no_timestamps = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(s->no_timestamps_check)) ? 1 : 0; + s->cfg.whisper_single_segment = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(s->single_segment_check)) ? 1 : 0; + + s->cfg.whisper_token_timestamps = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(s->token_timestamps_check)) ? 1 : 0; + s->cfg.whisper_split_on_word = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(s->split_on_word_check)) ? 1 : 0; + s->cfg.whisper_suppress_blank = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(s->suppress_blank_check)) ? 1 : 0; + s->cfg.whisper_suppress_nst = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(s->suppress_nst_check)) ? 1 : 0; + s->cfg.whisper_tdrz_enable = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(s->tdrz_enable_check)) ? 1 : 0; + + s->cfg.whisper_n_max_text_ctx = read_spin_int(s->n_max_text_ctx_spin); + s->cfg.whisper_offset_ms = read_spin_int(s->offset_ms_spin); + s->cfg.whisper_duration_ms = read_spin_int(s->duration_ms_spin); + s->cfg.whisper_max_len = read_spin_int(s->max_len_spin); + s->cfg.whisper_max_tokens = read_spin_int(s->max_tokens_spin); + s->cfg.whisper_audio_ctx = read_spin_int(s->audio_ctx_spin); + s->cfg.whisper_greedy_best_of = read_spin_int(s->greedy_best_of_spin); + s->cfg.whisper_beam_size = read_spin_int(s->beam_size_spin); + + s->cfg.whisper_thold_pt = read_spin_float(s->thold_pt_spin); + s->cfg.whisper_thold_ptsum = read_spin_float(s->thold_ptsum_spin); + s->cfg.whisper_temperature = read_spin_float(s->temperature_spin); + s->cfg.whisper_max_initial_ts = read_spin_float(s->max_initial_ts_spin); + s->cfg.whisper_length_penalty = read_spin_float(s->length_penalty_spin); + s->cfg.whisper_temperature_inc = read_spin_float(s->temperature_inc_spin); + s->cfg.whisper_entropy_thold = read_spin_float(s->entropy_thold_spin); + s->cfg.whisper_logprob_thold = read_spin_float(s->logprob_thold_spin); + s->cfg.whisper_no_speech_thold = read_spin_float(s->no_speech_thold_spin); + s->cfg.whisper_beam_patience = read_spin_float(s->beam_patience_spin); + + push_transcribe_runtime_params(s); + save_config_best_effort(s); +} + +static void on_whisper_toggle_changed(GtkToggleButton *btn, gpointer user_data) { + (void)btn; + gui_state_t *s = (gui_state_t *)user_data; + apply_whisper_settings_from_widgets(s); +} + +static void on_whisper_spin_changed(GtkSpinButton *btn, gpointer user_data) { + (void)btn; + gui_state_t *s = (gui_state_t *)user_data; + apply_whisper_settings_from_widgets(s); +} + +static void on_whisper_combo_changed(GtkComboBox *box, gpointer user_data) { + (void)box; + gui_state_t *s = (gui_state_t *)user_data; + apply_whisper_settings_from_widgets(s); +} + +static GtkWidget *add_spin_row(GtkWidget *grid, + int col, + int row, + const char *label_text, + double min, + double max, + double step, + double value, + int digits, + gui_state_t *s, + GtkWidget **out_spin) { + GtkWidget *label = gtk_label_new(label_text); + gtk_label_set_xalign(GTK_LABEL(label), 0.0f); + gtk_grid_attach(GTK_GRID(grid), label, col, row, 1, 1); + + GtkWidget *spin = gtk_spin_button_new_with_range(min, max, step); + gtk_spin_button_set_digits(GTK_SPIN_BUTTON(spin), digits); + gtk_spin_button_set_value(GTK_SPIN_BUTTON(spin), value); + gtk_widget_set_hexpand(spin, TRUE); + gtk_grid_attach(GTK_GRID(grid), spin, col + 1, row, 1, 1); + + g_signal_connect(spin, "value-changed", G_CALLBACK(on_whisper_spin_changed), s); + *out_spin = spin; + return spin; +} + +static GtkWidget *build_whisper_settings_tab(gui_state_t *s) { + GtkWidget *scroll = gtk_scrolled_window_new(NULL, NULL); + gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); + + GtkWidget *box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8); + gtk_container_set_border_width(GTK_CONTAINER(box), 10); + gtk_container_add(GTK_CONTAINER(scroll), box); + + GtkWidget *hint = gtk_label_new( + "Whisper decode settings apply globally across model sizes.\n" + "Changes are saved immediately and applied to new transcriptions."); + gtk_label_set_xalign(GTK_LABEL(hint), 0.0f); + gtk_box_pack_start(GTK_BOX(box), hint, FALSE, FALSE, 0); + + GtkWidget *checks = gtk_grid_new(); + gtk_grid_set_row_spacing(GTK_GRID(checks), 6); + gtk_grid_set_column_spacing(GTK_GRID(checks), 12); + gtk_box_pack_start(GTK_BOX(box), checks, FALSE, FALSE, 0); + + GtkWidget *sampling_label = gtk_label_new("Sampling strategy:"); + gtk_label_set_xalign(GTK_LABEL(sampling_label), 0.0f); + gtk_grid_attach(GTK_GRID(checks), sampling_label, 0, 0, 1, 1); + + s->sampling_combo = gtk_combo_box_text_new(); + gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(s->sampling_combo), "Greedy"); + gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(s->sampling_combo), "Beam Search"); + gtk_combo_box_set_active(GTK_COMBO_BOX(s->sampling_combo), s->cfg.whisper_sampling_strategy ? 1 : 0); + g_signal_connect(s->sampling_combo, "changed", G_CALLBACK(on_whisper_combo_changed), s); + gtk_grid_attach(GTK_GRID(checks), s->sampling_combo, 1, 0, 1, 1); + + s->translate_check = gtk_check_button_new_with_label("Translate to English"); + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(s->translate_check), s->cfg.whisper_translate ? TRUE : FALSE); + g_signal_connect(s->translate_check, "toggled", G_CALLBACK(on_whisper_toggle_changed), s); + gtk_grid_attach(GTK_GRID(checks), s->translate_check, 0, 1, 1, 1); + + s->detect_language_check = gtk_check_button_new_with_label("Auto detect language"); + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(s->detect_language_check), s->cfg.whisper_detect_language ? TRUE : FALSE); + g_signal_connect(s->detect_language_check, "toggled", G_CALLBACK(on_whisper_toggle_changed), s); + gtk_grid_attach(GTK_GRID(checks), s->detect_language_check, 1, 1, 1, 1); + + s->no_context_check = gtk_check_button_new_with_label("No context"); + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(s->no_context_check), s->cfg.whisper_no_context ? TRUE : FALSE); + g_signal_connect(s->no_context_check, "toggled", G_CALLBACK(on_whisper_toggle_changed), s); + gtk_grid_attach(GTK_GRID(checks), s->no_context_check, 0, 2, 1, 1); + + s->no_timestamps_check = gtk_check_button_new_with_label("No timestamps"); + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(s->no_timestamps_check), s->cfg.whisper_no_timestamps ? TRUE : FALSE); + g_signal_connect(s->no_timestamps_check, "toggled", G_CALLBACK(on_whisper_toggle_changed), s); + gtk_grid_attach(GTK_GRID(checks), s->no_timestamps_check, 1, 2, 1, 1); + + s->single_segment_check = gtk_check_button_new_with_label("Single segment"); + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(s->single_segment_check), s->cfg.whisper_single_segment ? TRUE : FALSE); + g_signal_connect(s->single_segment_check, "toggled", G_CALLBACK(on_whisper_toggle_changed), s); + gtk_grid_attach(GTK_GRID(checks), s->single_segment_check, 0, 3, 1, 1); + + s->token_timestamps_check = gtk_check_button_new_with_label("Token timestamps"); + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(s->token_timestamps_check), s->cfg.whisper_token_timestamps ? TRUE : FALSE); + g_signal_connect(s->token_timestamps_check, "toggled", G_CALLBACK(on_whisper_toggle_changed), s); + gtk_grid_attach(GTK_GRID(checks), s->token_timestamps_check, 1, 3, 1, 1); + + s->split_on_word_check = gtk_check_button_new_with_label("Split on word"); + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(s->split_on_word_check), s->cfg.whisper_split_on_word ? TRUE : FALSE); + g_signal_connect(s->split_on_word_check, "toggled", G_CALLBACK(on_whisper_toggle_changed), s); + gtk_grid_attach(GTK_GRID(checks), s->split_on_word_check, 0, 4, 1, 1); + + s->suppress_blank_check = gtk_check_button_new_with_label("Suppress blank tokens"); + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(s->suppress_blank_check), s->cfg.whisper_suppress_blank ? TRUE : FALSE); + g_signal_connect(s->suppress_blank_check, "toggled", G_CALLBACK(on_whisper_toggle_changed), s); + gtk_grid_attach(GTK_GRID(checks), s->suppress_blank_check, 1, 4, 1, 1); + + s->suppress_nst_check = gtk_check_button_new_with_label("Suppress non-speech tokens"); + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(s->suppress_nst_check), s->cfg.whisper_suppress_nst ? TRUE : FALSE); + g_signal_connect(s->suppress_nst_check, "toggled", G_CALLBACK(on_whisper_toggle_changed), s); + gtk_grid_attach(GTK_GRID(checks), s->suppress_nst_check, 0, 5, 1, 1); + + s->tdrz_enable_check = gtk_check_button_new_with_label("Enable tinydiarize (TDRZ)"); + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(s->tdrz_enable_check), s->cfg.whisper_tdrz_enable ? TRUE : FALSE); + g_signal_connect(s->tdrz_enable_check, "toggled", G_CALLBACK(on_whisper_toggle_changed), s); + gtk_grid_attach(GTK_GRID(checks), s->tdrz_enable_check, 1, 5, 1, 1); + + GtkWidget *grid = gtk_grid_new(); + gtk_grid_set_row_spacing(GTK_GRID(grid), 6); + gtk_grid_set_column_spacing(GTK_GRID(grid), 10); + gtk_box_pack_start(GTK_BOX(box), grid, FALSE, FALSE, 0); + + add_spin_row(grid, 0, 0, "n_max_text_ctx", 0.0, 32768.0, 1.0, + (double)s->cfg.whisper_n_max_text_ctx, 0, s, &s->n_max_text_ctx_spin); + add_spin_row(grid, 2, 0, "offset_ms", 0.0, 60000.0, 10.0, + (double)s->cfg.whisper_offset_ms, 0, s, &s->offset_ms_spin); + add_spin_row(grid, 0, 1, "duration_ms (0=all)", 0.0, 600000.0, 100.0, + (double)s->cfg.whisper_duration_ms, 0, s, &s->duration_ms_spin); + add_spin_row(grid, 2, 1, "max_len", 0.0, 1024.0, 1.0, + (double)s->cfg.whisper_max_len, 0, s, &s->max_len_spin); + add_spin_row(grid, 0, 2, "max_tokens", 0.0, 1024.0, 1.0, + (double)s->cfg.whisper_max_tokens, 0, s, &s->max_tokens_spin); + add_spin_row(grid, 2, 2, "audio_ctx", 0.0, 4096.0, 1.0, + (double)s->cfg.whisper_audio_ctx, 0, s, &s->audio_ctx_spin); + + add_spin_row(grid, 0, 3, "thold_pt", 0.0, 1.0, 0.001, + (double)s->cfg.whisper_thold_pt, 3, s, &s->thold_pt_spin); + add_spin_row(grid, 2, 3, "thold_ptsum", 0.0, 1.0, 0.001, + (double)s->cfg.whisper_thold_ptsum, 3, s, &s->thold_ptsum_spin); + add_spin_row(grid, 0, 4, "temperature", 0.0, 2.0, 0.01, + (double)s->cfg.whisper_temperature, 2, s, &s->temperature_spin); + add_spin_row(grid, 2, 4, "max_initial_ts", 0.0, 5.0, 0.01, + (double)s->cfg.whisper_max_initial_ts, 2, s, &s->max_initial_ts_spin); + add_spin_row(grid, 0, 5, "length_penalty", -2.0, 2.0, 0.01, + (double)s->cfg.whisper_length_penalty, 2, s, &s->length_penalty_spin); + add_spin_row(grid, 2, 5, "temperature_inc", 0.0, 1.0, 0.01, + (double)s->cfg.whisper_temperature_inc, 2, s, &s->temperature_inc_spin); + add_spin_row(grid, 0, 6, "entropy_thold", -5.0, 10.0, 0.01, + (double)s->cfg.whisper_entropy_thold, 2, s, &s->entropy_thold_spin); + add_spin_row(grid, 2, 6, "logprob_thold", -5.0, 1.0, 0.01, + (double)s->cfg.whisper_logprob_thold, 2, s, &s->logprob_thold_spin); + add_spin_row(grid, 0, 7, "no_speech_thold", 0.0, 1.0, 0.01, + (double)s->cfg.whisper_no_speech_thold, 2, s, &s->no_speech_thold_spin); + add_spin_row(grid, 2, 7, "greedy_best_of", 1.0, 10.0, 1.0, + (double)s->cfg.whisper_greedy_best_of, 0, s, &s->greedy_best_of_spin); + add_spin_row(grid, 0, 8, "beam_size", 1.0, 20.0, 1.0, + (double)s->cfg.whisper_beam_size, 0, s, &s->beam_size_spin); + add_spin_row(grid, 2, 8, "beam_patience", -1.0, 5.0, 0.01, + (double)s->cfg.whisper_beam_patience, 2, s, &s->beam_patience_spin); + + return scroll; } static int cmp_str_ptr(const void *a, const void *b) { @@ -517,6 +852,89 @@ static const char *base_name(const char *path) { return slash ? (slash + 1) : path; } +static long long model_size_bytes(const char *path) { + struct stat st; + if (!path) return -1; + if (stat(path, &st) != 0) return -1; + return (long long)st.st_size; +} + +static void format_size_human(long long bytes, char *out, size_t out_sz) { + static const char *units[] = { "B", "KB", "MB", "GB", "TB" }; + double v = (double)(bytes < 0 ? 0 : bytes); + int unit = 0; + while (v >= 1024.0 && unit < 4) { + v /= 1024.0; + unit++; + } + snprintf(out, out_sz, "%.1f %s", v, units[unit]); +} + +typedef struct { + transcribe_params_t tp; + int rc; + volatile gint done; +} transcribe_init_worker_t; + +static gpointer transcribe_init_worker(gpointer user_data) { + transcribe_init_worker_t *w = (transcribe_init_worker_t *)user_data; + w->rc = transcribe_init(&w->tp); + g_atomic_int_set(&w->done, 1); + return NULL; +} + +static int run_model_init_with_progress(gui_state_t *s) { + GtkWidget *dialog = gtk_dialog_new(); + gtk_window_set_title(GTK_WINDOW(dialog), "Loading Model"); + gtk_window_set_modal(GTK_WINDOW(dialog), TRUE); + gtk_window_set_resizable(GTK_WINDOW(dialog), FALSE); + + GtkWidget *content = gtk_dialog_get_content_area(GTK_DIALOG(dialog)); + GtkWidget *box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8); + gtk_container_set_border_width(GTK_CONTAINER(box), 12); + gtk_box_pack_start(GTK_BOX(content), box, TRUE, TRUE, 0); + + char msg[640]; + snprintf(msg, sizeof(msg), "Loading %s (%s mode)...", + base_name(s->cfg.model_path), s->use_gpu ? "GPU" : "CPU"); + GtkWidget *label = gtk_label_new(msg); + gtk_label_set_xalign(GTK_LABEL(label), 0.0f); + gtk_box_pack_start(GTK_BOX(box), label, FALSE, FALSE, 0); + + GtkWidget *bar = gtk_progress_bar_new(); + gtk_progress_bar_set_show_text(GTK_PROGRESS_BAR(bar), TRUE); + gtk_progress_bar_set_text(GTK_PROGRESS_BAR(bar), "Initializing whisper context..."); + gtk_box_pack_start(GTK_BOX(box), bar, FALSE, FALSE, 0); + + gtk_widget_show_all(dialog); + + transcribe_init_worker_t worker; + memset(&worker, 0, sizeof(worker)); + fill_transcribe_params_from_cfg(s, &worker.tp); + worker.done = 0; + + GThread *thread = g_thread_new("transcribe-init", transcribe_init_worker, &worker); + + int ticks = 0; + while (!g_atomic_int_get(&worker.done)) { + while (gtk_events_pending()) gtk_main_iteration(); + gtk_progress_bar_pulse(GTK_PROGRESS_BAR(bar)); + + ticks++; + if ((ticks % 20) == 0) { + char txt[128]; + snprintf(txt, sizeof(txt), "Loading... %ds", ticks / 20); + gtk_progress_bar_set_text(GTK_PROGRESS_BAR(bar), txt); + } + + g_usleep(50000); + } + + g_thread_join(thread); + gtk_widget_destroy(dialog); + return worker.rc; +} + static int run_startup_dialog(gui_state_t *s, GtkWindow *parent) { char model_paths[64][512]; int model_count = scan_model_paths(model_paths, 64); @@ -570,7 +988,17 @@ static int run_startup_dialog(gui_state_t *s, GtkWindow *parent) { int active_model_index = 0; for (int i = 0; i < model_count; ++i) { const char *path = model_paths[i]; - gtk_combo_box_text_append(GTK_COMBO_BOX_TEXT(model_combo), path, base_name(path)); + char entry[256]; + long long sz = model_size_bytes(path); + if (sz >= 0) { + char pretty[32]; + format_size_human(sz, pretty, sizeof(pretty)); + snprintf(entry, sizeof(entry), "%.220s (%s)", base_name(path), pretty); + } else { + snprintf(entry, sizeof(entry), "%.250s", base_name(path)); + } + + gtk_combo_box_text_append(GTK_COMBO_BOX_TEXT(model_combo), path, entry); if (strcmp(path, s->cfg.model_path) == 0) { active_model_index = i; } @@ -589,6 +1017,7 @@ static int run_startup_dialog(gui_state_t *s, GtkWindow *parent) { snprintf(s->cfg.model_path, sizeof(s->cfg.model_path), "%s", active_model); } s->cfg.use_gpu = s->use_gpu; + save_config_best_effort(s); gtk_widget_destroy(dialog); return 0; } @@ -606,14 +1035,7 @@ static void on_app_activate(GtkApplication *app, gpointer user_data) { return; } - transcribe_params_t tp = {0}; - snprintf(tp.model_path, sizeof(tp.model_path), "%s", s->cfg.model_path); - snprintf(tp.language, sizeof(tp.language), "%s", s->cfg.language); - tp.use_gpu = s->use_gpu; - tp.n_threads = (int)sysconf(_SC_NPROCESSORS_ONLN); - tp.filter_bracketed_tags = s->cfg.filter_bracketed_tags; - - if (transcribe_init(&tp) != 0) { + if (run_model_init_with_progress(s) != 0) { GtkWidget *msg = gtk_message_dialog_new( NULL, GTK_DIALOG_MODAL, @@ -644,32 +1066,36 @@ static void on_app_activate(GtkApplication *app, gpointer user_data) { } s->window = gtk_application_window_new(app); - gtk_window_set_title(GTK_WINDOW(s->window), "voice_linux"); - gtk_window_set_default_size(GTK_WINDOW(s->window), 760, 640); + char title[128]; + snprintf(title, sizeof(title), "voice_linux %s", VOICE_LINUX_VERSION); + gtk_window_set_title(GTK_WINDOW(s->window), title); + gtk_window_set_default_size(GTK_WINDOW(s->window), 760, 780); GtkWidget *outer = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8); gtk_container_set_border_width(GTK_CONTAINER(outer), 12); gtk_container_add(GTK_CONTAINER(s->window), outer); - char info[1024]; - snprintf(info, sizeof(info), "Version: %s\nDevice: %s\nModel: %s\nLanguage: %s\nGPU: %s", - VOICE_LINUX_VERSION, s->cfg.audio_device, s->cfg.model_path, s->cfg.language, s->use_gpu ? "on" : "off"); - GtkWidget *info_label = gtk_label_new(info); - gtk_label_set_xalign(GTK_LABEL(info_label), 0.0f); - gtk_box_pack_start(GTK_BOX(outer), info_label, FALSE, FALSE, 0); - s->status_label = gtk_label_new("Idle"); gtk_label_set_xalign(GTK_LABEL(s->status_label), 0.0f); gtk_box_pack_start(GTK_BOX(outer), s->status_label, FALSE, FALSE, 0); + GtkWidget *notebook = gtk_notebook_new(); + gtk_box_pack_start(GTK_BOX(outer), notebook, TRUE, TRUE, 0); + + GtkWidget *main_page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8); + gtk_container_set_border_width(GTK_CONTAINER(main_page), 6); + GtkWidget *lights_row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 14); + s->gpu_light = gtk_label_new(NULL); s->record_light = gtk_label_new(NULL); s->transcribe_light = gtk_label_new(NULL); + gtk_label_set_use_markup(GTK_LABEL(s->gpu_light), TRUE); gtk_label_set_use_markup(GTK_LABEL(s->record_light), TRUE); gtk_label_set_use_markup(GTK_LABEL(s->transcribe_light), TRUE); + gtk_box_pack_start(GTK_BOX(lights_row), s->gpu_light, FALSE, FALSE, 0); gtk_box_pack_start(GTK_BOX(lights_row), s->record_light, FALSE, FALSE, 0); gtk_box_pack_start(GTK_BOX(lights_row), s->transcribe_light, FALSE, FALSE, 0); - gtk_box_pack_start(GTK_BOX(outer), lights_row, FALSE, FALSE, 0); + gtk_box_pack_start(GTK_BOX(main_page), lights_row, FALSE, FALSE, 0); s->record_btn = gtk_button_new_with_label("Start Recording"); gtk_widget_set_name(s->record_btn, "record-btn"); @@ -694,24 +1120,31 @@ static void on_app_activate(GtkApplication *app, gpointer user_data) { update_record_button(s); g_signal_connect(s->record_btn, "clicked", G_CALLBACK(on_record_clicked), s); - gtk_box_pack_start(GTK_BOX(outer), s->record_btn, FALSE, FALSE, 0); - update_activity_lights(s); s->always_on_btn = gtk_button_new_with_label("Start Always-On"); g_signal_connect(s->always_on_btn, "clicked", G_CALLBACK(on_always_on_clicked), s); - gtk_box_pack_start(GTK_BOX(outer), s->always_on_btn, FALSE, FALSE, 0); + GtkWidget *btn_row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8); + gtk_box_pack_start(GTK_BOX(btn_row), s->record_btn, TRUE, TRUE, 0); + gtk_box_pack_start(GTK_BOX(btn_row), s->always_on_btn, TRUE, TRUE, 0); + gtk_box_pack_start(GTK_BOX(main_page), btn_row, FALSE, FALSE, 0); + update_activity_lights(s); + + GtkWidget *checks_row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 12); s->type_check = gtk_check_button_new_with_label("Type transcript into focused window"); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(s->type_check), s->cfg.autotype_enabled ? TRUE : FALSE); - gtk_box_pack_start(GTK_BOX(outer), s->type_check, FALSE, FALSE, 0); + g_signal_connect(s->type_check, "toggled", G_CALLBACK(on_autotype_toggled), s); + gtk_box_pack_start(GTK_BOX(checks_row), s->type_check, TRUE, TRUE, 0); s->filter_tags_check = gtk_check_button_new_with_label("Filter bracketed tags like [laughs]"); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(s->filter_tags_check), s->cfg.filter_bracketed_tags ? TRUE : FALSE); g_signal_connect(s->filter_tags_check, "toggled", G_CALLBACK(on_filter_tags_toggled), s); - gtk_box_pack_start(GTK_BOX(outer), s->filter_tags_check, FALSE, FALSE, 0); + gtk_box_pack_start(GTK_BOX(checks_row), s->filter_tags_check, TRUE, TRUE, 0); + + gtk_box_pack_start(GTK_BOX(main_page), checks_row, FALSE, FALSE, 0); GtkWidget *meter_frame = gtk_frame_new("Live Microphone Level"); - gtk_box_pack_start(GTK_BOX(outer), meter_frame, FALSE, FALSE, 0); + gtk_box_pack_start(GTK_BOX(main_page), meter_frame, FALSE, FALSE, 0); GtkWidget *meter_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6); gtk_container_set_border_width(GTK_CONTAINER(meter_box), 8); @@ -727,17 +1160,17 @@ static void on_app_activate(GtkApplication *app, gpointer user_data) { gtk_grid_set_column_spacing(GTK_GRID(grid), 10); gtk_box_pack_start(GTK_BOX(meter_box), grid, FALSE, FALSE, 0); - add_scale_row(grid, 0, "Threshold (0.0-1.0)", 0.0, 1.0, 0.001, + add_scale_row(grid, 0, 0, "Threshold (0.0-1.0)", 0.0, 1.0, 0.001, s->cfg.vad_peak_threshold, 3, s, &s->threshold_scale); - add_scale_row(grid, 1, "Trigger ms (above threshold)", 10.0, 2000.0, 10.0, + add_scale_row(grid, 2, 0, "Trigger ms (above threshold)", 10.0, 2000.0, 10.0, (double)s->cfg.vad_trigger_ms, 0, s, &s->trigger_scale); - add_scale_row(grid, 2, "Release ms (below threshold)", 10.0, 4000.0, 10.0, + add_scale_row(grid, 0, 1, "Release ms (below threshold)", 10.0, 4000.0, 10.0, (double)s->cfg.vad_release_ms, 0, s, &s->release_scale); - add_scale_row(grid, 3, "Preroll ms (audio before trigger)", 0.0, 3000.0, 10.0, + add_scale_row(grid, 2, 1, "Preroll ms (audio before trigger)", 0.0, 3000.0, 10.0, (double)s->cfg.vad_preroll_ms, 0, s, &s->preroll_scale); - add_scale_row(grid, 4, "Min speech ms", 50.0, 5000.0, 10.0, + add_scale_row(grid, 0, 2, "Min speech ms", 50.0, 5000.0, 10.0, (double)s->cfg.vad_min_speech_ms, 0, s, &s->min_speech_scale); - add_scale_row(grid, 5, "Max speech ms", 200.0, 60000.0, 100.0, + add_scale_row(grid, 2, 2, "Max speech ms", 200.0, 60000.0, 100.0, (double)s->cfg.vad_max_speech_ms, 0, s, &s->max_speech_scale); GtkWidget *hint = gtk_label_new( @@ -751,9 +1184,10 @@ static void on_app_activate(GtkApplication *app, gpointer user_data) { gtk_box_pack_start(GTK_BOX(meter_box), s->debug_label, FALSE, FALSE, 0); GtkWidget *frame = gtk_frame_new("Transcript Log"); - gtk_box_pack_start(GTK_BOX(outer), frame, TRUE, TRUE, 0); + gtk_box_pack_start(GTK_BOX(main_page), frame, TRUE, TRUE, 0); GtkWidget *scroll = gtk_scrolled_window_new(NULL, NULL); + gtk_widget_set_size_request(scroll, -1, 320); gtk_container_add(GTK_CONTAINER(frame), scroll); s->transcript_view = gtk_text_view_new(); @@ -762,6 +1196,11 @@ static void on_app_activate(GtkApplication *app, gpointer user_data) { apply_vad_params(s); update_always_on_button(s); + + gtk_notebook_append_page(GTK_NOTEBOOK(notebook), main_page, gtk_label_new("Main")); + GtkWidget *whisper_page = build_whisper_settings_tab(s); + gtk_notebook_append_page(GTK_NOTEBOOK(notebook), whisper_page, gtk_label_new("Whisper")); + gtk_widget_show_all(s->window); s->meter_timer_id = g_timeout_add(60, meter_tick_cb, s); @@ -801,6 +1240,7 @@ int main(int argc, char **argv) { const char *cfg_path = arg_value(argc, argv, "--config"); if (!cfg_path) cfg_path = "./config.ini"; + snprintf(st.cfg_path, sizeof(st.cfg_path), "%s", cfg_path); if (config_load_file(cfg_path, &st.cfg) != 0) { fprintf(stderr, "warning: failed to load %s, using defaults\n", cfg_path); } diff --git a/src/transcribe.c b/src/transcribe.c index 47b4f3c..ed3ba8a 100644 --- a/src/transcribe.c +++ b/src/transcribe.c @@ -80,6 +80,49 @@ void transcribe_set_filter_bracketed_tags(int enabled) { static struct whisper_context *g_ctx = NULL; static transcribe_params_t g_params; +static void apply_full_params_from_config(struct whisper_full_params *dst, const transcribe_params_t *src) { + if (!dst || !src) return; + + dst->n_threads = src->n_threads > 0 ? src->n_threads : 4; + dst->n_max_text_ctx = src->n_max_text_ctx; + dst->offset_ms = src->offset_ms; + dst->duration_ms = src->duration_ms; + + dst->translate = src->translate ? true : false; + dst->detect_language = src->detect_language ? true : false; + dst->no_context = src->no_context ? true : false; + dst->no_timestamps = src->no_timestamps ? true : false; + dst->single_segment = src->single_segment ? true : false; + + dst->token_timestamps = src->token_timestamps ? true : false; + dst->thold_pt = src->thold_pt; + dst->thold_ptsum = src->thold_ptsum; + dst->max_len = src->max_len; + dst->split_on_word = src->split_on_word ? true : false; + dst->max_tokens = src->max_tokens; + + dst->audio_ctx = src->audio_ctx; + dst->tdrz_enable = src->tdrz_enable ? true : false; + + dst->suppress_blank = src->suppress_blank ? true : false; + dst->suppress_nst = src->suppress_nst ? true : false; + + dst->temperature = src->temperature; + dst->max_initial_ts = src->max_initial_ts; + dst->length_penalty = src->length_penalty; + + dst->temperature_inc = src->temperature_inc; + dst->entropy_thold = src->entropy_thold; + dst->logprob_thold = src->logprob_thold; + dst->no_speech_thold = src->no_speech_thold; + + dst->greedy.best_of = src->greedy_best_of; + dst->beam_search.beam_size = src->beam_size; + dst->beam_search.patience = src->beam_patience; + + dst->language = src->language[0] ? src->language : "en"; +} + int transcribe_init(const transcribe_params_t *params) { if (!params) return -1; g_params = *params; @@ -95,17 +138,22 @@ int transcribe_init(const transcribe_params_t *params) { return 0; } +void transcribe_update_params(const transcribe_params_t *params) { + if (!params) return; + g_params = *params; + transcribe_set_filter_bracketed_tags(g_params.filter_bracketed_tags); +} + char *transcribe_buffer(const float *samples, size_t count) { if (!g_ctx || !samples || count == 0) return NULL; - struct whisper_full_params params = whisper_full_default_params(WHISPER_SAMPLING_GREEDY); + enum whisper_sampling_strategy strategy = g_params.sampling_strategy ? WHISPER_SAMPLING_BEAM_SEARCH : WHISPER_SAMPLING_GREEDY; + struct whisper_full_params params = whisper_full_default_params(strategy); params.print_progress = false; params.print_special = false; params.print_realtime = false; params.print_timestamps = false; - params.translate = false; - params.n_threads = g_params.n_threads > 0 ? g_params.n_threads : 4; - params.language = g_params.language[0] ? g_params.language : "en"; + apply_full_params_from_config(¶ms, &g_params); if (whisper_full(g_ctx, params, samples, (int)count) != 0) { fprintf(stderr, "transcribe: whisper_full failed\n"); @@ -161,6 +209,11 @@ int transcribe_init(const transcribe_params_t *params) { return 0; } +void transcribe_update_params(const transcribe_params_t *params) { + if (!params) return; + g_params = *params; +} + char *transcribe_buffer(const float *samples, size_t count) { (void)samples; (void)count; diff --git a/src/transcribe.h b/src/transcribe.h index 1e9faf5..048af65 100644 --- a/src/transcribe.h +++ b/src/transcribe.h @@ -9,9 +9,47 @@ typedef struct { int use_gpu; int n_threads; int filter_bracketed_tags; + + int sampling_strategy; // 0=greedy, 1=beam search + int n_max_text_ctx; + int offset_ms; + int duration_ms; + + int translate; + int detect_language; + int no_context; + int no_timestamps; + int single_segment; + + int token_timestamps; + float thold_pt; + float thold_ptsum; + int max_len; + int split_on_word; + int max_tokens; + + int audio_ctx; + int tdrz_enable; + + int suppress_blank; + int suppress_nst; + + float temperature; + float max_initial_ts; + float length_penalty; + + float temperature_inc; + float entropy_thold; + float logprob_thold; + float no_speech_thold; + + int greedy_best_of; + int beam_size; + float beam_patience; } transcribe_params_t; int transcribe_init(const transcribe_params_t *params); +void transcribe_update_params(const transcribe_params_t *params); void transcribe_set_filter_bracketed_tags(int enabled); char *transcribe_buffer(const float *samples, size_t count); void transcribe_cleanup(void);