From 8c1418bd80bffdf825f8cf54253046c3b40060cb Mon Sep 17 00:00:00 2001 From: Andrii Ryzhkov Date: Sat, 11 Apr 2026 09:51:17 +0200 Subject: [PATCH 1/3] Add model card dialog to AI preferences --- src/common/ai_models.c | 89 +++++++++++++++++++ src/common/ai_models.h | 34 +++++++ src/gui/preferences_ai.c | 186 ++++++++++++++++++++++++++++++++++++++- 3 files changed, 308 insertions(+), 1 deletion(-) diff --git a/src/common/ai_models.c b/src/common/ai_models.c index 3905951a69db..cd4d7e65e6ab 100644 --- a/src/common/ai_models.c +++ b/src/common/ai_models.c @@ -1842,6 +1842,95 @@ void dt_ai_models_get_spatial_dims(dt_ai_registry_t *registry, g_mutex_unlock(®istry->lock); } +// read an optional string from a JSON object, returns +// g_strdup'd copy or NULL if missing/empty +static char *_card_str(JsonObject *obj, const char *key) +{ + if(!obj || !json_object_has_member(obj, key)) + return NULL; + const char *val = json_object_get_string_member(obj, key); + return (val && val[0]) ? g_strdup(val) : NULL; +} + +dt_ai_model_card_t *dt_ai_models_get_card( + dt_ai_registry_t *registry, const char *model_id) +{ + char *model_path = dt_ai_models_get_path(registry, model_id); + if(!model_path) + { + dt_print(DT_DEBUG_AI, + "[ai_models] model card: %s not on disk", model_id); + return NULL; + } + + char *config_path = g_build_filename(model_path, "config.json", NULL); + g_free(model_path); + + JsonParser *parser = json_parser_new(); + if(!json_parser_load_from_file(parser, config_path, NULL)) + { + g_free(config_path); + g_object_unref(parser); + return NULL; + } + g_free(config_path); + + JsonNode *root = json_parser_get_root(parser); + if(!root || !JSON_NODE_HOLDS_OBJECT(root)) + { + g_object_unref(parser); + return NULL; + } + + JsonObject *config = json_node_get_object(root); + dt_ai_model_card_t *card = g_new0(dt_ai_model_card_t, 1); + + // read name from top level + card->name = _card_str(config, "name"); + + // card fields live under the "model_card" nested object + JsonObject *mc = NULL; + if(json_object_has_member(config, "model_card")) + { + JsonNode *cn = json_object_get_member(config, "model_card"); + if(cn && JSON_NODE_HOLDS_OBJECT(cn)) + mc = json_node_get_object(cn); + } + + card->long_description = _card_str(mc, "long_description"); + card->scope = _card_str(mc, "scope"); + card->author = _card_str(mc, "author"); + card->source = _card_str(mc, "source"); + card->paper = _card_str(mc, "paper"); + card->license = _card_str(mc, "license"); + card->training_data = _card_str(mc, "training_data"); + card->training_data_license = _card_str(mc, "training_data_license"); + card->notes = _card_str(mc, "notes"); + + // fall back to top-level description + if(!card->long_description) + card->long_description = _card_str(config, "description"); + + g_object_unref(parser); + return card; +} + +void dt_ai_model_card_free(dt_ai_model_card_t *card) +{ + if(!card) return; + g_free(card->name); + g_free(card->long_description); + g_free(card->scope); + g_free(card->author); + g_free(card->source); + g_free(card->paper); + g_free(card->license); + g_free(card->training_data); + g_free(card->training_data_license); + g_free(card->notes); + g_free(card); +} + // clang-format off // modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py // vim: shiftwidth=2 expandtab tabstop=2 cindent diff --git a/src/common/ai_models.h b/src/common/ai_models.h index b8a39b854bcc..e12649ed1b2b 100644 --- a/src/common/ai_models.h +++ b/src/common/ai_models.h @@ -314,3 +314,37 @@ void dt_ai_models_get_spatial_dims(dt_ai_registry_t *registry, const char *model_id, const char **out_h, const char **out_w); + +/** + * @brief Model card — provenance and transparency info read + * from config.json on demand. All fields are optional; + * NULL means the field was not present + */ +typedef struct dt_ai_model_card_t +{ + char *name; + char *long_description; + char *scope; + char *author; + char *source; + char *paper; + char *license; + char *training_data; + char *training_data_license; + char *notes; +} dt_ai_model_card_t; + +/** + * @brief Read model card from config.json on disk + * @param registry The registry + * @param model_id The model identifier + * @return Card struct (caller must free with dt_ai_model_card_free), + * or NULL if model directory not found + */ +dt_ai_model_card_t *dt_ai_models_get_card( + dt_ai_registry_t *registry, const char *model_id); + +/** + * @brief Free a model card returned by dt_ai_models_get_card + */ +void dt_ai_model_card_free(dt_ai_model_card_t *card); diff --git a/src/gui/preferences_ai.c b/src/gui/preferences_ai.c index 5ab38104b2ea..7da00b534878 100644 --- a/src/gui/preferences_ai.c +++ b/src/gui/preferences_ai.c @@ -77,6 +77,7 @@ enum COL_STATUS, COL_DEFAULT, COL_ID, + COL_INFO, // info icon column (static "ℹ" text) NUM_COLS }; @@ -98,6 +99,7 @@ typedef struct dt_prefs_ai_data_t GtkWidget *delete_selected_btn; GtkWidget *parent_dialog; GtkWidget *select_all_toggle; + GtkTreeViewColumn *info_col; GtkWidget *controls_box; // container for all controls below the enable toggle GtkWidget *ort_path_entry; GtkWidget *ort_path_indicator; @@ -235,6 +237,8 @@ static void _refresh_model_list(dt_prefs_ai_data_t *data) ? (model->version ? model->version : "0.0") : "–", COL_ID, model->id, + COL_INFO, + "\xe2\x84\xb9", // ℹ (U+2139) -1); dt_ai_model_free(model); } @@ -887,6 +891,167 @@ static void _on_delete_selected(GtkButton *button, gpointer user_data) g_list_free_full(to_delete, g_free); } +// show model card dialog for the given model_id +static void _show_model_card(dt_prefs_ai_data_t *data, + const char *model_id) +{ + if(!model_id || !model_id[0]) return; + + const char *dash = "\xe2\x80\x93"; // en dash for missing fields + dt_ai_model_card_t *card = dt_ai_models_get_card(darktable.ai_registry, model_id); + + const char *name = (card && card->name) + ? card->name : model_id; + const char *desc = (card && card->long_description) + ? card->long_description : dash; + + GtkWidget *dlg = gtk_message_dialog_new( + GTK_WINDOW(data->parent_dialog), + GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT, + GTK_MESSAGE_INFO, + GTK_BUTTONS_CLOSE, + "%s", desc); + gtk_window_set_title(GTK_WINDOW(dlg), name); + + // field grid in the message area below the description + GtkWidget *grid = gtk_grid_new(); + gtk_grid_set_row_spacing(GTK_GRID(grid), 4); + gtk_grid_set_column_spacing(GTK_GRID(grid), 12); + gtk_widget_set_margin_top(grid, 12); + + GtkWidget *msg_area + = gtk_message_dialog_get_message_area(GTK_MESSAGE_DIALOG(dlg)); + gtk_widget_set_margin_top(msg_area, 8); + gtk_container_add(GTK_CONTAINER(msg_area), grid); + + const char *labels[] = { + N_("scope"), N_("author"), + N_("source"), N_("paper"), + N_("license"), N_("training data"), + N_("data license"), N_("notes") + }; + const char *values[] = { + card ? card->scope : NULL, + card ? card->author : NULL, + card ? card->source : NULL, + card ? card->paper : NULL, + card ? card->license : NULL, + card ? card->training_data : NULL, + card ? card->training_data_license : NULL, + card ? card->notes : NULL + }; + const int n_fields = (int)(sizeof(labels) / sizeof(labels[0])); + + for(int i = 0; i < n_fields; i++) + { + GtkWidget *lbl = gtk_label_new(_(labels[i])); + gtk_label_set_xalign(GTK_LABEL(lbl), 1.0f); + gtk_grid_attach(GTK_GRID(grid), lbl, 0, i, 1, 1); + + const char *v = values[i] ? values[i] : dash; + GtkWidget *val; + // render URLs as clickable links + if(g_str_has_prefix(v, "http://") + || g_str_has_prefix(v, "https://")) + { + gchar *markup = g_markup_printf_escaped( + "%s", v, v); + val = gtk_label_new(NULL); + gtk_label_set_markup(GTK_LABEL(val), markup); + g_free(markup); + } + else + { + val = gtk_label_new(v); + } + gtk_label_set_xalign(GTK_LABEL(val), 0.0f); + gtk_label_set_line_wrap(GTK_LABEL(val), TRUE); + gtk_label_set_max_width_chars(GTK_LABEL(val), 50); + gtk_label_set_selectable(GTK_LABEL(val), TRUE); + gtk_grid_attach(GTK_GRID(grid), val, 1, i, 1, 1); + } + + gtk_widget_show_all(dlg); + gtk_dialog_run(GTK_DIALOG(dlg)); + gtk_widget_destroy(dlg); + + dt_ai_model_card_free(card); +} + +// show tooltip and hand cursor over the info column +static gboolean _on_query_tooltip(GtkWidget *widget, + gint x, gint y, + gboolean keyboard_mode, + GtkTooltip *tooltip, + gpointer user_data) +{ + (void)keyboard_mode; + dt_prefs_ai_data_t *data = (dt_prefs_ai_data_t *)user_data; + GtkTreeView *tv = GTK_TREE_VIEW(widget); + GtkTreeViewColumn *column = NULL; + gint bx, by; + gtk_tree_view_convert_widget_to_bin_window_coords( + tv, x, y, &bx, &by); + + if(!gtk_tree_view_get_path_at_pos( + tv, bx, by, NULL, &column, NULL, NULL)) + return FALSE; + + GdkWindow *win = gtk_tree_view_get_bin_window(tv); + if(column == data->info_col) + { + gdk_window_set_cursor(win, + gdk_cursor_new_from_name(gdk_window_get_display(win), + "pointer")); + gtk_tooltip_set_text(tooltip, _("click for model details")); + return TRUE; + } + + gdk_window_set_cursor(win, NULL); + return FALSE; +} + +// click on the ℹ info column opens the model card +static gboolean _on_info_button_press(GtkWidget *widget, + GdkEventButton *event, + gpointer user_data) +{ + if(event->type != GDK_BUTTON_PRESS + || event->button != 1) + return FALSE; + + dt_prefs_ai_data_t *data = (dt_prefs_ai_data_t *)user_data; + GtkTreeView *tv = GTK_TREE_VIEW(widget); + GtkTreePath *path = NULL; + GtkTreeViewColumn *column = NULL; + + if(!gtk_tree_view_get_path_at_pos(tv, (gint)event->x, (gint)event->y, + &path, &column, NULL, NULL)) + return FALSE; + + // only react to clicks on the info column + if(column != data->info_col) + { + gtk_tree_path_free(path); + return FALSE; + } + + GtkTreeIter iter; + if(gtk_tree_model_get_iter(GTK_TREE_MODEL(data->model_store), &iter, path)) + { + gchar *model_id = NULL; + gtk_tree_model_get(GTK_TREE_MODEL(data->model_store), + &iter, COL_ID, &model_id, -1); + if(model_id) + { + _show_model_card(data, model_id); + g_free(model_id); + } + } + gtk_tree_path_free(path); + return TRUE; +} + #if !defined(__APPLE__) static void _show_ort_probe_result(GtkWindow *parent, const char *path, const char *version) { @@ -1280,7 +1445,8 @@ void init_tab_ai(GtkWidget *dialog, GtkWidget *stack) G_TYPE_BOOLEAN, // enabled_sensitive G_TYPE_STRING, // status G_TYPE_STRING, // default - G_TYPE_STRING); // id + G_TYPE_STRING, // id + G_TYPE_STRING); // info icon // sort by task, then default, then name gtk_tree_sortable_set_default_sort_func( @@ -1403,6 +1569,24 @@ void init_tab_ai(GtkWidget *dialog, GtkWidget *stack) NULL); gtk_tree_view_append_column(GTK_TREE_VIEW(data->model_list), default_col); + // info icon column — click opens model card + GtkCellRenderer *info_renderer = gtk_cell_renderer_text_new(); + data->info_col = gtk_tree_view_column_new_with_attributes( + "", + info_renderer, + "text", + COL_INFO, + NULL); + gtk_tree_view_column_set_clickable(data->info_col, FALSE); + gtk_tree_view_append_column(GTK_TREE_VIEW(data->model_list), + data->info_col); + + gtk_widget_set_has_tooltip(data->model_list, TRUE); + g_signal_connect(data->model_list, "query-tooltip", + G_CALLBACK(_on_query_tooltip), data); + g_signal_connect(data->model_list, "button-press-event", + G_CALLBACK(_on_info_button_press), data); + // scrolled window for the list GtkWidget *scroll = gtk_scrolled_window_new(NULL, NULL); gtk_scrolled_window_set_policy( From fd1a7e4dfd56e5f90e237771f820382bdafb982b Mon Sep 17 00:00:00 2001 From: Andrii Ryzhkov Date: Mon, 13 Apr 2026 09:34:13 +0200 Subject: [PATCH 2/3] Remove description column from model table, move info column after name --- src/gui/preferences_ai.c | 52 +++++++++++++++------------------------- 1 file changed, 19 insertions(+), 33 deletions(-) diff --git a/src/gui/preferences_ai.c b/src/gui/preferences_ai.c index 7da00b534878..bec6ed7624a4 100644 --- a/src/gui/preferences_ai.c +++ b/src/gui/preferences_ai.c @@ -69,15 +69,14 @@ enum { COL_SELECTED, COL_NAME, + COL_INFO, // info icon column (static "ℹ" text) COL_VERSION, COL_TASK, - COL_DESCRIPTION, COL_ENABLED, COL_ENABLED_SENSITIVE, // whether the enabled checkbox is clickable COL_STATUS, COL_DEFAULT, COL_ID, - COL_INFO, // info icon column (static "ℹ" text) NUM_COLS }; @@ -224,8 +223,6 @@ static void _refresh_model_list(dt_prefs_ai_data_t *data) model->name ? model->name : model->id, COL_TASK, model->task ? model->task : "", - COL_DESCRIPTION, - model->description ? model->description : "", COL_STATUS, _status_to_string(model->status), COL_DEFAULT, @@ -1000,9 +997,9 @@ static gboolean _on_query_tooltip(GtkWidget *widget, GdkWindow *win = gtk_tree_view_get_bin_window(tv); if(column == data->info_col) { - gdk_window_set_cursor(win, - gdk_cursor_new_from_name(gdk_window_get_display(win), - "pointer")); + GdkCursor *cursor = gdk_cursor_new_from_name(gdk_window_get_display(win), "pointer"); + gdk_window_set_cursor(win, cursor); + g_object_unref(cursor); gtk_tooltip_set_text(tooltip, _("click for model details")); return TRUE; } @@ -1438,15 +1435,14 @@ void init_tab_ai(GtkWidget *dialog, GtkWidget *stack) NUM_COLS, G_TYPE_BOOLEAN, // selected G_TYPE_STRING, // name + G_TYPE_STRING, // info icon G_TYPE_STRING, // version G_TYPE_STRING, // task - G_TYPE_STRING, // description G_TYPE_BOOLEAN, // enabled G_TYPE_BOOLEAN, // enabled_sensitive G_TYPE_STRING, // status G_TYPE_STRING, // default - G_TYPE_STRING, // id - G_TYPE_STRING); // info icon + G_TYPE_STRING); // id // sort by task, then default, then name gtk_tree_sortable_set_default_sort_func( @@ -1504,9 +1500,21 @@ void init_tab_ai(GtkWidget *dialog, GtkWidget *stack) "text", COL_NAME, NULL); - gtk_tree_view_column_set_expand(name_col, FALSE); + gtk_tree_view_column_set_expand(name_col, TRUE); gtk_tree_view_append_column(GTK_TREE_VIEW(data->model_list), name_col); + // info icon column — click opens model card + GtkCellRenderer *info_renderer = gtk_cell_renderer_text_new(); + data->info_col = gtk_tree_view_column_new_with_attributes( + "", + info_renderer, + "text", + COL_INFO, + NULL); + gtk_tree_view_column_set_clickable(data->info_col, FALSE); + gtk_tree_view_append_column(GTK_TREE_VIEW(data->model_list), + data->info_col); + // version column GtkTreeViewColumn *version_col = gtk_tree_view_column_new_with_attributes( _("version"), @@ -1525,16 +1533,6 @@ void init_tab_ai(GtkWidget *dialog, GtkWidget *stack) NULL); gtk_tree_view_append_column(GTK_TREE_VIEW(data->model_list), task_col); - // description column - GtkTreeViewColumn *desc_col = gtk_tree_view_column_new_with_attributes( - _("description"), - text_renderer, - "text", - COL_DESCRIPTION, - NULL); - gtk_tree_view_column_set_expand(desc_col, TRUE); - gtk_tree_view_append_column(GTK_TREE_VIEW(data->model_list), desc_col); - // enabled checkbox column (radio-button behavior per task) GtkCellRenderer *enabled_renderer = gtk_cell_renderer_toggle_new(); g_signal_connect( @@ -1569,18 +1567,6 @@ void init_tab_ai(GtkWidget *dialog, GtkWidget *stack) NULL); gtk_tree_view_append_column(GTK_TREE_VIEW(data->model_list), default_col); - // info icon column — click opens model card - GtkCellRenderer *info_renderer = gtk_cell_renderer_text_new(); - data->info_col = gtk_tree_view_column_new_with_attributes( - "", - info_renderer, - "text", - COL_INFO, - NULL); - gtk_tree_view_column_set_clickable(data->info_col, FALSE); - gtk_tree_view_append_column(GTK_TREE_VIEW(data->model_list), - data->info_col); - gtk_widget_set_has_tooltip(data->model_list, TRUE); g_signal_connect(data->model_list, "query-tooltip", G_CALLBACK(_on_query_tooltip), data); From e1c314257ea3362abc1432b42e2f907e8b25c6a7 Mon Sep 17 00:00:00 2001 From: Andrii Ryzhkov Date: Mon, 13 Apr 2026 19:13:30 +0200 Subject: [PATCH 3/3] Fix code style in dt_ai_models_get_card --- src/common/ai_models.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/common/ai_models.c b/src/common/ai_models.c index cd4d7e65e6ab..6722debec5d9 100644 --- a/src/common/ai_models.c +++ b/src/common/ai_models.c @@ -1852,8 +1852,8 @@ static char *_card_str(JsonObject *obj, const char *key) return (val && val[0]) ? g_strdup(val) : NULL; } -dt_ai_model_card_t *dt_ai_models_get_card( - dt_ai_registry_t *registry, const char *model_id) +dt_ai_model_card_t *dt_ai_models_get_card(dt_ai_registry_t *registry, + const char *model_id) { char *model_path = dt_ai_models_get_path(registry, model_id); if(!model_path)