feat: shortcut

This commit is contained in:
intelligence committed 2026-08-14 14:00:52 +02:00
1 parent 05a7c1cd83
commit 37fbe5d787
2 files changed
+15 -61

No files matched your search

+2 -2
View File
@@ -35,8 +35,8 @@ transui # same as record
``` ```
Bind `transui record` to a shortcut in your desktop environment. Transui does Bind `transui record` to a shortcut in your desktop environment. Transui does
not register a global shortcut itself. Recording stops automatically after two not register a global shortcut itself. Press the same shortcut again to stop
continuous seconds without detected speech. and transcribe the current recording.
The **Hosted first, local fallback** setting tries the Hosted endpoint, then The **Hosted first, local fallback** setting tries the Hosted endpoint, then
the Local endpoint when the first request fails. With it disabled, Transui uses the Local endpoint when the first request fails. With it disabled, Transui uses
+13 -59
View File
@@ -19,8 +19,6 @@ namespace {
constexpr const char* kAppId = "io.github.transui"; constexpr const char* kAppId = "io.github.transui";
constexpr const char* kConfigGroup = "transui"; constexpr const char* kConfigGroup = "transui";
constexpr gint64 kSilenceTimeoutUs = 2 * G_USEC_PER_SEC;
constexpr double kSoundThresholdDb = -42.0;
const SecretSchema kSecretSchema = { const SecretSchema kSecretSchema = {
"io.github.transui.credentials", SECRET_SCHEMA_NONE, "io.github.transui.credentials", SECRET_SCHEMA_NONE,
@@ -45,10 +43,7 @@ struct App {
GtkWidget* processing = nullptr; GtkWidget* processing = nullptr;
GtkWidget* elapsed_label = nullptr; GtkWidget* elapsed_label = nullptr;
guint elapsed_source = 0; guint elapsed_source = 0;
guint silence_source = 0;
guint level_watch = 0;
guint elapsed_seconds = 0; guint elapsed_seconds = 0;
gint64 last_sound_us = 0;
GstElement* pipeline = nullptr; GstElement* pipeline = nullptr;
std::string recording_path; std::string recording_path;
std::atomic<bool> cancelled{false}; std::atomic<bool> cancelled{false};
@@ -290,53 +285,11 @@ gboolean tick_elapsed(gpointer data) {
return G_SOURCE_CONTINUE; return G_SOURCE_CONTINUE;
} }
void stop_recording(GtkButton*, gpointer data); void stop_elapsed_timer(App* app) {
gboolean monitor_audio_level(GstBus*, GstMessage* message, gpointer data) {
if (GST_MESSAGE_TYPE(message) != GST_MESSAGE_ELEMENT) return G_SOURCE_CONTINUE;
const GstStructure* structure = gst_message_get_structure(message);
if (!structure || !gst_structure_has_name(structure, "level")) return G_SOURCE_CONTINUE;
const GValue* rms = gst_structure_get_value(structure, "rms");
if (!rms || !GST_VALUE_HOLDS_LIST(rms)) return G_SOURCE_CONTINUE;
auto* app = static_cast<App*>(data);
for (guint index = 0; index < gst_value_list_get_size(rms); ++index) {
const GValue* channel = gst_value_list_get_value(rms, index);
if (G_VALUE_HOLDS_DOUBLE(channel) && g_value_get_double(channel) > kSoundThresholdDb) {
app->last_sound_us = g_get_monotonic_time();
break;
}
}
return G_SOURCE_CONTINUE;
}
gboolean detect_silence(gpointer data) {
auto* app = static_cast<App*>(data);
if (!app->pipeline) {
app->silence_source = 0;
return G_SOURCE_REMOVE;
}
if (g_get_monotonic_time() - app->last_sound_us < kSilenceTimeoutUs)
return G_SOURCE_CONTINUE;
app->silence_source = 0;
stop_recording(nullptr, app);
return G_SOURCE_REMOVE;
}
void remove_recording_sources(App* app) {
if (app->elapsed_source) { if (app->elapsed_source) {
g_source_remove(app->elapsed_source); g_source_remove(app->elapsed_source);
app->elapsed_source = 0; app->elapsed_source = 0;
} }
if (app->silence_source) {
g_source_remove(app->silence_source);
app->silence_source = 0;
}
if (app->level_watch) {
g_source_remove(app->level_watch);
app->level_watch = 0;
}
} }
void remove_recording(App* app) { void remove_recording(App* app) {
@@ -347,7 +300,7 @@ void remove_recording(App* app) {
} }
void close_recorder(App* app) { void close_recorder(App* app) {
remove_recording_sources(app); stop_elapsed_timer(app);
if (app->pipeline) { if (app->pipeline) {
gst_element_set_state(app->pipeline, GST_STATE_NULL); gst_element_set_state(app->pipeline, GST_STATE_NULL);
gst_object_unref(app->pipeline); gst_object_unref(app->pipeline);
@@ -557,7 +510,7 @@ void show_processing(App* app) {
void stop_recording(GtkButton*, gpointer data) { void stop_recording(GtkButton*, gpointer data) {
auto* app = static_cast<App*>(data); auto* app = static_cast<App*>(data);
if (!app->pipeline) return; if (!app->pipeline) return;
remove_recording_sources(app); stop_elapsed_timer(app);
gst_element_send_event(app->pipeline, gst_event_new_eos()); gst_element_send_event(app->pipeline, gst_event_new_eos());
GstBus* bus = gst_element_get_bus(app->pipeline); GstBus* bus = gst_element_get_bus(app->pipeline);
GstMessage* message = gst_bus_timed_pop_filtered(bus, 3 * GST_SECOND, GstMessage* message = gst_bus_timed_pop_filtered(bus, 3 * GST_SECOND,
@@ -585,7 +538,7 @@ void cancel_recording(GtkButton*, gpointer data) {
void recorder_closed(GtkWidget*, gpointer data) { void recorder_closed(GtkWidget*, gpointer data) {
auto* app = static_cast<App*>(data); auto* app = static_cast<App*>(data);
app->recorder = nullptr; app->recorder = nullptr;
remove_recording_sources(app); stop_elapsed_timer(app);
if (app->pipeline) { if (app->pipeline) {
gst_element_set_state(app->pipeline, GST_STATE_NULL); gst_element_set_state(app->pipeline, GST_STATE_NULL);
gst_object_unref(app->pipeline); gst_object_unref(app->pipeline);
@@ -595,7 +548,14 @@ void recorder_closed(GtkWidget*, gpointer data) {
} }
void start_recording(App* app) { void start_recording(App* app) {
if (app->recorder) { gtk_window_present(GTK_WINDOW(app->recorder)); return; } if (app->recorder) {
stop_recording(nullptr, app);
return;
}
if (app->processing) {
gtk_window_present(GTK_WINDOW(app->processing));
return;
}
if (app->config.local_url.empty() && (!app->config.hosted_first || app->config.hosted_url.empty())) { if (app->config.local_url.empty() && (!app->config.hosted_first || app->config.hosted_url.empty())) {
open_settings(app); open_settings(app);
show_error(GTK_WINDOW(app->settings), "Set an endpoint first."); show_error(GTK_WINDOW(app->settings), "Set an endpoint first.");
@@ -608,8 +568,7 @@ void start_recording(App* app) {
app->recording_path = template_path; app->recording_path = template_path;
g_free(template_path); g_free(template_path);
gchar* pipeline_text = g_strdup_printf( gchar* pipeline_text = g_strdup_printf(
"autoaudiosrc ! audioconvert ! audioresample ! " "autoaudiosrc ! audioconvert ! audioresample ! wavenc ! filesink location=\"%s\"",
"level interval=100000000 post-messages=true ! wavenc ! filesink location=\"%s\"",
app->recording_path.c_str()); app->recording_path.c_str());
GError* error = nullptr; GError* error = nullptr;
app->pipeline = gst_parse_launch(pipeline_text, &error); app->pipeline = gst_parse_launch(pipeline_text, &error);
@@ -620,9 +579,6 @@ void start_recording(App* app) {
remove_recording(app); remove_recording(app);
return; return;
} }
GstBus* level_bus = gst_element_get_bus(app->pipeline);
app->level_watch = gst_bus_add_watch(level_bus, monitor_audio_level, app);
gst_object_unref(level_bus);
if (gst_element_set_state(app->pipeline, GST_STATE_PLAYING) == GST_STATE_CHANGE_FAILURE) { if (gst_element_set_state(app->pipeline, GST_STATE_PLAYING) == GST_STATE_CHANGE_FAILURE) {
show_error(nullptr, "Could not access an audio input."); show_error(nullptr, "Could not access an audio input.");
close_recorder(app); close_recorder(app);
@@ -659,9 +615,7 @@ void start_recording(App* app) {
gtk_box_pack_start(GTK_BOX(actions), cancel, false, false, 0); gtk_box_pack_start(GTK_BOX(actions), cancel, false, false, 0);
gtk_box_pack_start(GTK_BOX(actions), stop, false, false, 0); gtk_box_pack_start(GTK_BOX(actions), stop, false, false, 0);
app->elapsed_seconds = 0; app->elapsed_seconds = 0;
app->last_sound_us = g_get_monotonic_time();
app->elapsed_source = g_timeout_add_seconds(1, tick_elapsed, app); app->elapsed_source = g_timeout_add_seconds(1, tick_elapsed, app);
app->silence_source = g_timeout_add(100, detect_silence, app);
g_signal_connect(cancel, "clicked", G_CALLBACK(cancel_recording), app); g_signal_connect(cancel, "clicked", G_CALLBACK(cancel_recording), app);
g_signal_connect(stop, "clicked", G_CALLBACK(stop_recording), app); g_signal_connect(stop, "clicked", G_CALLBACK(stop_recording), app);
g_signal_connect(window, "destroy", G_CALLBACK(recorder_closed), app); g_signal_connect(window, "destroy", G_CALLBACK(recorder_closed), app);