649 lines
21 KiB
C
649 lines
21 KiB
C
#include "audio.h"
|
|
#include "config.h"
|
|
#include "transcribe.h"
|
|
#include "typer.h"
|
|
#include "version.h"
|
|
|
|
#include <gtk/gtk.h>
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <unistd.h>
|
|
|
|
typedef struct {
|
|
GtkApplication *app;
|
|
GtkWidget *window;
|
|
GtkWidget *status_label;
|
|
GtkWidget *record_btn;
|
|
GtkWidget *always_on_btn;
|
|
GtkWidget *type_check;
|
|
GtkWidget *transcript_view;
|
|
GtkWidget *record_light;
|
|
GtkWidget *transcribe_light;
|
|
|
|
GtkWidget *level_area;
|
|
GtkWidget *threshold_scale;
|
|
GtkWidget *trigger_scale;
|
|
GtkWidget *release_scale;
|
|
GtkWidget *preroll_scale;
|
|
GtkWidget *min_speech_scale;
|
|
GtkWidget *max_speech_scale;
|
|
GtkWidget *debug_label;
|
|
|
|
voice_config_t cfg;
|
|
int use_gpu;
|
|
int recording;
|
|
int transcribing;
|
|
int always_on_running;
|
|
|
|
unsigned int meter_timer_id;
|
|
float live_peak;
|
|
} gui_state_t;
|
|
|
|
static float clampf(float v, float lo, float hi) {
|
|
if (v < lo) return lo;
|
|
if (v > hi) return hi;
|
|
return v;
|
|
}
|
|
|
|
static void set_status(gui_state_t *s, const char *text) {
|
|
gtk_label_set_text(GTK_LABEL(s->status_label), text);
|
|
}
|
|
|
|
static void append_transcript(gui_state_t *s, const char *text) {
|
|
if (!text || text[0] == '\0') return;
|
|
|
|
GtkTextBuffer *buf = gtk_text_view_get_buffer(GTK_TEXT_VIEW(s->transcript_view));
|
|
GtkTextIter end;
|
|
gtk_text_buffer_get_end_iter(buf, &end);
|
|
|
|
int has_text = gtk_text_buffer_get_char_count(buf) > 0;
|
|
if (has_text) {
|
|
gtk_text_buffer_insert(buf, &end, "\n", -1);
|
|
}
|
|
gtk_text_buffer_insert(buf, &end, text, -1);
|
|
}
|
|
|
|
static void update_activity_lights(gui_state_t *s) {
|
|
if (!s->record_light || !s->transcribe_light) return;
|
|
|
|
const int rec_on = (s->recording || s->always_on_running) ? 1 : 0;
|
|
const char *rec_markup = rec_on
|
|
? "<span foreground='#2e7d32'><b>● Recording</b></span>"
|
|
: "<span foreground='#777777'>● Recording</span>";
|
|
|
|
const char *tr_markup = s->transcribing
|
|
? "<span foreground='#ef6c00'><b>● Transcribing</b></span>"
|
|
: "<span foreground='#777777'>● Transcribing</span>";
|
|
|
|
gtk_label_set_markup(GTK_LABEL(s->record_light), rec_markup);
|
|
gtk_label_set_markup(GTK_LABEL(s->transcribe_light), tr_markup);
|
|
}
|
|
|
|
static void update_record_button(gui_state_t *s) {
|
|
GtkStyleContext *ctx = gtk_widget_get_style_context(s->record_btn);
|
|
gtk_style_context_remove_class(ctx, "idle");
|
|
gtk_style_context_remove_class(ctx, "recording");
|
|
gtk_style_context_remove_class(ctx, "busy");
|
|
|
|
if (s->transcribing) {
|
|
gtk_widget_set_sensitive(s->record_btn, FALSE);
|
|
gtk_button_set_label(GTK_BUTTON(s->record_btn), "Transcribing...");
|
|
gtk_style_context_add_class(ctx, "busy");
|
|
update_activity_lights(s);
|
|
return;
|
|
}
|
|
|
|
if (s->always_on_running) {
|
|
gtk_widget_set_sensitive(s->record_btn, FALSE);
|
|
gtk_button_set_label(GTK_BUTTON(s->record_btn), "Manual Disabled (Always-On)");
|
|
gtk_style_context_add_class(ctx, "busy");
|
|
update_activity_lights(s);
|
|
return;
|
|
}
|
|
|
|
gtk_widget_set_sensitive(s->record_btn, TRUE);
|
|
if (s->recording) {
|
|
gtk_button_set_label(GTK_BUTTON(s->record_btn), "Stop Recording");
|
|
gtk_style_context_add_class(ctx, "recording");
|
|
} else {
|
|
gtk_button_set_label(GTK_BUTTON(s->record_btn), "Start Recording");
|
|
gtk_style_context_add_class(ctx, "idle");
|
|
}
|
|
|
|
update_activity_lights(s);
|
|
}
|
|
|
|
static void update_always_on_button(gui_state_t *s) {
|
|
if (!s->always_on_btn) return;
|
|
gtk_button_set_label(GTK_BUTTON(s->always_on_btn), s->always_on_running ? "Stop Always-On" : "Start Always-On");
|
|
}
|
|
|
|
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");
|
|
return;
|
|
}
|
|
|
|
float peak_abs = 0.0f;
|
|
double sum_abs = 0.0;
|
|
|
|
for (size_t i = 0; i < count; ++i) {
|
|
float v = samples[i];
|
|
float a = v < 0.0f ? -v : v;
|
|
if (a > peak_abs) peak_abs = a;
|
|
sum_abs += (double)a;
|
|
}
|
|
|
|
double mean_abs = sum_abs / (double)count;
|
|
double seconds = (double)count / (double)sample_rate;
|
|
|
|
if (peak_abs < 0.005f) {
|
|
snprintf(out, out_sz,
|
|
"Near-silent audio (%.2fs, peak %.6f) from '%.120s'",
|
|
seconds, peak_abs, device_name ? device_name : "(default)");
|
|
} else {
|
|
snprintf(out, out_sz,
|
|
"Audio OK (%.2fs, peak %.6f, mean %.6f)",
|
|
seconds, peak_abs, mean_abs);
|
|
}
|
|
}
|
|
|
|
static int peak_is_speech(const float *samples, size_t count, float threshold) {
|
|
float peak_abs = 0.0f;
|
|
for (size_t i = 0; i < count; ++i) {
|
|
float v = samples[i];
|
|
float a = v < 0.0f ? -v : v;
|
|
if (a > peak_abs) peak_abs = a;
|
|
}
|
|
return peak_abs >= threshold;
|
|
}
|
|
|
|
static void process_segment(gui_state_t *s, float *samples, size_t count) {
|
|
if (!samples || count == 0) {
|
|
free(samples);
|
|
return;
|
|
}
|
|
|
|
char audio_msg[256];
|
|
log_audio_stats(samples, count, s->cfg.sample_rate, s->cfg.audio_device, audio_msg, sizeof(audio_msg));
|
|
|
|
if (!peak_is_speech(samples, count, s->cfg.vad_peak_threshold)) {
|
|
set_status(s, "Mic muted or near-silent");
|
|
free(samples);
|
|
return;
|
|
}
|
|
|
|
if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(s->type_check))) {
|
|
if (typer_capture_focus() != 0) {
|
|
fprintf(stderr, "gui: failed to capture focus target for autotype\n");
|
|
}
|
|
}
|
|
|
|
s->transcribing = 1;
|
|
update_record_button(s);
|
|
set_status(s, "Transcribing... (please wait)");
|
|
while (gtk_events_pending()) gtk_main_iteration();
|
|
|
|
char *text = transcribe_buffer(samples, count);
|
|
free(samples);
|
|
|
|
s->transcribing = 0;
|
|
update_record_button(s);
|
|
|
|
if (!text) {
|
|
set_status(s, "Transcription failed");
|
|
return;
|
|
}
|
|
|
|
append_transcript(s, text);
|
|
|
|
if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(s->type_check))) {
|
|
if (typer_type_text(text) != 0) {
|
|
fprintf(stderr, "gui: autotype injection failed\n");
|
|
set_status(s, "Autotype failed (see terminal logs)");
|
|
free(text);
|
|
return;
|
|
}
|
|
}
|
|
|
|
set_status(s, audio_msg);
|
|
free(text);
|
|
}
|
|
|
|
static int read_scale_int(GtkWidget *scale) {
|
|
return (int)gtk_range_get_value(GTK_RANGE(scale));
|
|
}
|
|
|
|
static float read_scale_float(GtkWidget *scale) {
|
|
return (float)gtk_range_get_value(GTK_RANGE(scale));
|
|
}
|
|
|
|
static void apply_vad_params(gui_state_t *s) {
|
|
audio_vad_params_t v;
|
|
v.threshold = read_scale_float(s->threshold_scale);
|
|
v.trigger_ms = read_scale_int(s->trigger_scale);
|
|
v.release_ms = read_scale_int(s->release_scale);
|
|
v.preroll_ms = read_scale_int(s->preroll_scale);
|
|
v.min_speech_ms = read_scale_int(s->min_speech_scale);
|
|
v.max_speech_ms = read_scale_int(s->max_speech_scale);
|
|
|
|
if (v.max_speech_ms < v.min_speech_ms) {
|
|
v.max_speech_ms = v.min_speech_ms;
|
|
gtk_range_set_value(GTK_RANGE(s->max_speech_scale), (double)v.max_speech_ms);
|
|
}
|
|
|
|
s->cfg.vad_peak_threshold = v.threshold;
|
|
s->cfg.vad_trigger_ms = v.trigger_ms;
|
|
s->cfg.vad_release_ms = v.release_ms;
|
|
s->cfg.vad_preroll_ms = v.preroll_ms;
|
|
s->cfg.vad_min_speech_ms = v.min_speech_ms;
|
|
s->cfg.vad_max_speech_ms = v.max_speech_ms;
|
|
|
|
audio_set_vad_params(&v);
|
|
gtk_widget_queue_draw(s->level_area);
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
static gboolean on_level_draw(GtkWidget *widget, cairo_t *cr, gpointer user_data) {
|
|
gui_state_t *s = (gui_state_t *)user_data;
|
|
|
|
GtkAllocation a;
|
|
gtk_widget_get_allocation(widget, &a);
|
|
|
|
const double w = (double)a.width;
|
|
const double h = (double)a.height;
|
|
|
|
cairo_set_source_rgb(cr, 0.10, 0.10, 0.10);
|
|
cairo_rectangle(cr, 0.0, 0.0, w, h);
|
|
cairo_fill(cr);
|
|
|
|
double peak = (double)clampf(s->live_peak, 0.0f, 1.0f);
|
|
double threshold = (double)clampf(s->cfg.vad_peak_threshold, 0.0f, 1.0f);
|
|
|
|
double fill_w = peak * w;
|
|
if (fill_w > 0.0) {
|
|
if (peak >= threshold) {
|
|
cairo_set_source_rgb(cr, 0.18, 0.72, 0.22);
|
|
} else {
|
|
cairo_set_source_rgb(cr, 0.20, 0.52, 0.85);
|
|
}
|
|
cairo_rectangle(cr, 0.0, 0.0, fill_w, h);
|
|
cairo_fill(cr);
|
|
}
|
|
|
|
double tx = threshold * w;
|
|
cairo_set_source_rgb(cr, 0.95, 0.25, 0.25);
|
|
cairo_set_line_width(cr, 2.0);
|
|
cairo_move_to(cr, tx, 0.0);
|
|
cairo_line_to(cr, tx, h);
|
|
cairo_stroke(cr);
|
|
|
|
cairo_set_source_rgb(cr, 0.75, 0.75, 0.75);
|
|
cairo_set_line_width(cr, 1.0);
|
|
cairo_rectangle(cr, 0.5, 0.5, w - 1.0, h - 1.0);
|
|
cairo_stroke(cr);
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
static const char *vad_state_text(int s) {
|
|
switch (s) {
|
|
case 0: return "IDLE";
|
|
case 1: return "ONSET";
|
|
case 2: return "SPEECH";
|
|
case 3: return "TRAILING";
|
|
default: return "?";
|
|
}
|
|
}
|
|
|
|
static gboolean meter_tick_cb(gpointer user_data) {
|
|
gui_state_t *s = (gui_state_t *)user_data;
|
|
|
|
audio_debug_info_t di;
|
|
if (audio_get_debug_info(&di) == 0) {
|
|
s->live_peak = di.peak_level;
|
|
|
|
char dbg[256];
|
|
snprintf(dbg, sizeof(dbg),
|
|
"Debug: always_on=%d state=%s peak=%.4f thr=%.4f onset=%d trail=%d speech=%d ready=%zu",
|
|
di.always_on_enabled,
|
|
vad_state_text(di.vad_state),
|
|
di.peak_level,
|
|
di.threshold,
|
|
di.onset_samples,
|
|
di.trailing_samples,
|
|
di.speech_samples,
|
|
di.ready_samples);
|
|
gtk_label_set_text(GTK_LABEL(s->debug_label), dbg);
|
|
} else {
|
|
s->live_peak = audio_get_peak_level();
|
|
}
|
|
|
|
gtk_widget_queue_draw(s->level_area);
|
|
|
|
if (s->always_on_running && !s->transcribing) {
|
|
float *samples = NULL;
|
|
size_t count = 0;
|
|
int rc = audio_get_speech_segment(&samples, &count);
|
|
if (rc == 0) {
|
|
set_status(s, "Speech segment detected, transcribing...");
|
|
process_segment(s, samples, count);
|
|
}
|
|
}
|
|
|
|
return G_SOURCE_CONTINUE;
|
|
}
|
|
|
|
static void on_record_clicked(GtkButton *btn, gpointer user_data) {
|
|
(void)btn;
|
|
gui_state_t *s = (gui_state_t *)user_data;
|
|
|
|
if (s->always_on_running || s->transcribing) {
|
|
return;
|
|
}
|
|
|
|
if (!s->recording) {
|
|
if (audio_start_recording() != 0) {
|
|
set_status(s, "Failed to start recording");
|
|
return;
|
|
}
|
|
|
|
s->recording = 1;
|
|
update_record_button(s);
|
|
set_status(s, "Recording...");
|
|
return;
|
|
}
|
|
|
|
float *samples = NULL;
|
|
size_t count = 0;
|
|
if (audio_stop_recording(&samples, &count) != 0) {
|
|
set_status(s, "Failed to stop recording");
|
|
return;
|
|
}
|
|
|
|
s->recording = 0;
|
|
update_record_button(s);
|
|
|
|
process_segment(s, samples, count);
|
|
}
|
|
|
|
static void on_always_on_clicked(GtkButton *btn, gpointer user_data) {
|
|
(void)btn;
|
|
gui_state_t *s = (gui_state_t *)user_data;
|
|
|
|
if (s->transcribing || s->recording) return;
|
|
|
|
if (s->always_on_running) {
|
|
audio_set_always_on_enabled(0);
|
|
s->always_on_running = 0;
|
|
update_always_on_button(s);
|
|
update_record_button(s);
|
|
set_status(s, "Always-on stopped");
|
|
return;
|
|
}
|
|
|
|
apply_vad_params(s);
|
|
audio_set_always_on_enabled(1);
|
|
s->always_on_running = 1;
|
|
update_always_on_button(s);
|
|
update_record_button(s);
|
|
set_status(s, "Always-on active (continuous monitor)");
|
|
}
|
|
|
|
static GtkWidget *add_scale_row(GtkWidget *grid,
|
|
int row,
|
|
const char *label_text,
|
|
double min,
|
|
double max,
|
|
double step,
|
|
double value,
|
|
int digits,
|
|
gui_state_t *s,
|
|
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);
|
|
|
|
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);
|
|
|
|
g_signal_connect(scale, "value-changed", G_CALLBACK(on_vad_value_changed), s);
|
|
*out_scale = scale;
|
|
|
|
return scale;
|
|
}
|
|
|
|
static void on_app_activate(GtkApplication *app, gpointer user_data) {
|
|
gui_state_t *s = (gui_state_t *)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);
|
|
|
|
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 *lights_row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 14);
|
|
s->record_light = gtk_label_new(NULL);
|
|
s->transcribe_light = gtk_label_new(NULL);
|
|
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->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);
|
|
|
|
s->record_btn = gtk_button_new_with_label("Start Recording");
|
|
gtk_widget_set_name(s->record_btn, "record-btn");
|
|
|
|
GtkCssProvider *provider = gtk_css_provider_new();
|
|
gtk_css_provider_load_from_data(
|
|
provider,
|
|
"#record-btn { font-weight: 700; color: #ffffff; }"
|
|
"#record-btn.idle { background-image: none; background-color: #c62828; }"
|
|
"#record-btn.recording { background-image: none; background-color: #2e7d32; }"
|
|
"#record-btn.busy { background-image: none; background-color: #ef6c00; }"
|
|
"#record-btn:backdrop { opacity: 0.92; }",
|
|
-1,
|
|
NULL);
|
|
gtk_style_context_add_provider_for_screen(
|
|
gdk_screen_get_default(),
|
|
GTK_STYLE_PROVIDER(provider),
|
|
GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
|
|
g_object_unref(provider);
|
|
|
|
s->recording = 0;
|
|
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);
|
|
|
|
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);
|
|
|
|
GtkWidget *meter_frame = gtk_frame_new("Live Microphone Level");
|
|
gtk_box_pack_start(GTK_BOX(outer), 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);
|
|
gtk_container_add(GTK_CONTAINER(meter_frame), meter_box);
|
|
|
|
s->level_area = gtk_drawing_area_new();
|
|
gtk_widget_set_size_request(s->level_area, -1, 30);
|
|
g_signal_connect(s->level_area, "draw", G_CALLBACK(on_level_draw), s);
|
|
gtk_box_pack_start(GTK_BOX(meter_box), s->level_area, FALSE, FALSE, 0);
|
|
|
|
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(meter_box), grid, FALSE, FALSE, 0);
|
|
|
|
add_scale_row(grid, 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,
|
|
(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,
|
|
(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,
|
|
(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,
|
|
(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,
|
|
(double)s->cfg.vad_max_speech_ms, 0, s, &s->max_speech_scale);
|
|
|
|
GtkWidget *hint = gtk_label_new(
|
|
"Green bar means level >= threshold. Red line is threshold.\n"
|
|
"Always-On uses continuous capture + preroll so word starts are not clipped.");
|
|
gtk_label_set_xalign(GTK_LABEL(hint), 0.0f);
|
|
gtk_box_pack_start(GTK_BOX(meter_box), hint, FALSE, FALSE, 0);
|
|
|
|
s->debug_label = gtk_label_new("Debug: waiting for audio...");
|
|
gtk_label_set_xalign(GTK_LABEL(s->debug_label), 0.0f);
|
|
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);
|
|
|
|
GtkWidget *scroll = gtk_scrolled_window_new(NULL, NULL);
|
|
gtk_container_add(GTK_CONTAINER(frame), scroll);
|
|
|
|
s->transcript_view = gtk_text_view_new();
|
|
gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(s->transcript_view), GTK_WRAP_WORD_CHAR);
|
|
gtk_container_add(GTK_CONTAINER(scroll), s->transcript_view);
|
|
|
|
apply_vad_params(s);
|
|
update_always_on_button(s);
|
|
gtk_widget_show_all(s->window);
|
|
|
|
s->meter_timer_id = g_timeout_add(60, meter_tick_cb, s);
|
|
|
|
if (s->cfg.always_on) {
|
|
audio_set_always_on_enabled(1);
|
|
s->always_on_running = 1;
|
|
update_always_on_button(s);
|
|
update_record_button(s);
|
|
set_status(s, "Always-on active (continuous monitor)");
|
|
}
|
|
}
|
|
|
|
static const char *arg_value(int argc, char **argv, const char *name) {
|
|
for (int i = 1; i + 1 < argc; ++i) {
|
|
if (strcmp(argv[i], name) == 0) {
|
|
return argv[i + 1];
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static int arg_flag(int argc, char **argv, const char *name) {
|
|
for (int i = 1; i < argc; ++i) {
|
|
if (strcmp(argv[i], name) == 0) {
|
|
return 1;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
int main(int argc, char **argv) {
|
|
gui_state_t st;
|
|
memset(&st, 0, sizeof(st));
|
|
|
|
config_set_defaults(&st.cfg);
|
|
|
|
const char *cfg_path = arg_value(argc, argv, "--config");
|
|
if (!cfg_path) cfg_path = "./config.ini";
|
|
if (config_load_file(cfg_path, &st.cfg) != 0) {
|
|
fprintf(stderr, "warning: failed to load %s, using defaults\n", cfg_path);
|
|
}
|
|
|
|
st.use_gpu = arg_flag(argc, argv, "--gpu") ? 1 : 0;
|
|
if (arg_flag(argc, argv, "--cpu")) st.use_gpu = 0;
|
|
|
|
audio_params_t ap = {0};
|
|
snprintf(ap.device, sizeof(ap.device), "%s", st.cfg.audio_device);
|
|
ap.sample_rate = st.cfg.sample_rate;
|
|
ap.max_seconds = st.cfg.max_record_seconds;
|
|
|
|
transcribe_params_t tp = {0};
|
|
snprintf(tp.model_path, sizeof(tp.model_path), "%s", st.cfg.model_path);
|
|
snprintf(tp.language, sizeof(tp.language), "%s", st.cfg.language);
|
|
tp.use_gpu = st.use_gpu;
|
|
tp.n_threads = (int)sysconf(_SC_NPROCESSORS_ONLN);
|
|
|
|
if (audio_init(&ap) != 0) {
|
|
fprintf(stderr, "fatal: audio init failed\n");
|
|
return 1;
|
|
}
|
|
|
|
audio_vad_params_t vp = {
|
|
.threshold = st.cfg.vad_peak_threshold,
|
|
.trigger_ms = st.cfg.vad_trigger_ms,
|
|
.release_ms = st.cfg.vad_release_ms,
|
|
.preroll_ms = st.cfg.vad_preroll_ms,
|
|
.min_speech_ms = st.cfg.vad_min_speech_ms,
|
|
.max_speech_ms = st.cfg.vad_max_speech_ms,
|
|
};
|
|
audio_set_vad_params(&vp);
|
|
|
|
if (transcribe_init(&tp) != 0) {
|
|
fprintf(stderr, "fatal: transcribe init failed\n");
|
|
audio_cleanup();
|
|
return 1;
|
|
}
|
|
if (typer_init(st.cfg.type_delay_us) != 0) {
|
|
fprintf(stderr, "fatal: typer init failed\n");
|
|
transcribe_cleanup();
|
|
audio_cleanup();
|
|
return 1;
|
|
}
|
|
|
|
st.app = gtk_application_new("local.voice_linux", G_APPLICATION_DEFAULT_FLAGS);
|
|
g_signal_connect(st.app, "activate", G_CALLBACK(on_app_activate), &st);
|
|
|
|
int gtk_argc = 1;
|
|
char *gtk_argv[] = { argv[0], NULL };
|
|
int rc = g_application_run(G_APPLICATION(st.app), gtk_argc, gtk_argv);
|
|
|
|
if (st.meter_timer_id != 0) {
|
|
g_source_remove(st.meter_timer_id);
|
|
st.meter_timer_id = 0;
|
|
}
|
|
|
|
audio_set_always_on_enabled(0);
|
|
|
|
g_object_unref(st.app);
|
|
typer_cleanup();
|
|
transcribe_cleanup();
|
|
audio_cleanup();
|
|
|
|
return rc;
|
|
}
|