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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
168 changes: 100 additions & 68 deletions src/common/ai_models.c
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,8 @@ static char *_find_latest_compatible_release(const char *repository, char **erro
curl_easy_setopt(curl, CURLOPT_URL, url);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, _curl_write_string);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, response);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L);
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 5L);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 5L);

CURLcode res = curl_easy_perform(curl);
long http_code = 0;
Expand Down Expand Up @@ -300,7 +301,8 @@ static char *_fetch_asset_digest(
curl_easy_setopt(curl, CURLOPT_URL, url);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, _curl_write_string);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, response);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L);
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 5L);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 5L);

CURLcode res = curl_easy_perform(curl);
long http_code = 0;
Expand Down Expand Up @@ -822,27 +824,78 @@ void dt_ai_models_refresh_status(void)
g_mutex_unlock(&registry->lock);
}

void dt_ai_models_check_updates(void)
// takes ownership of `parser`
static gboolean _apply_updates_idle(gpointer user_data)
{
JsonParser *parser = (JsonParser *)user_data;
dt_ai_registry_t *registry = darktable.ai_registry;
if(!registry) return;
if(!registry)
{
g_object_unref(parser);
return G_SOURCE_REMOVE;
}

// only check once per session
g_mutex_lock(&registry->lock);
if(registry->updates_checked)
JsonNode *root = json_parser_get_root(parser);
JsonObject *root_obj = json_node_get_object(root);
JsonObject *models_obj
= json_object_has_member(root_obj, "models")
? json_object_get_object_member(root_obj, "models")
: NULL;

if(!models_obj)
{
g_mutex_unlock(&registry->lock);
return;
g_object_unref(parser);
dt_print(DT_DEBUG_AI,
"[ai_models] check_updates: no 'models' object in "
"versions.json");
return G_SOURCE_REMOVE;
}
registry->updates_checked = TRUE;
const char *repository = g_strdup(registry->repository);
g_mutex_unlock(&registry->lock);

if(!repository || !repository[0])
// populate model->checksum from sha256 so downloads skip re-fetching
g_mutex_lock(&registry->lock);
for(GList *l = registry->models; l; l = g_list_next(l))
{
g_free((char *)repository);
return;
dt_ai_model_t *model = (dt_ai_model_t *)l->data;
if(!json_object_has_member(models_obj, model->id)) continue;

JsonNode *m_node = json_object_get_member(models_obj, model->id);
if(!m_node || !JSON_NODE_HOLDS_OBJECT(m_node)) continue;
JsonObject *m_obj = json_node_get_object(m_node);

const char *remote_version = json_object_has_member(m_obj, "version")
? json_object_get_string_member(m_obj, "version") : NULL;
const char *remote_sha256 = json_object_has_member(m_obj, "sha256")
? json_object_get_string_member(m_obj, "sha256") : NULL;

if(remote_sha256 && g_str_has_prefix(remote_sha256, "sha256:"))
{
g_free(model->checksum);
model->checksum = g_strdup(remote_sha256);
}

if(remote_version
&& model->status == DT_AI_MODEL_DOWNLOADED
&& _version_compare(model->version, remote_version) < 0)
{
model->status = DT_AI_MODEL_UPDATE_AVAILABLE;
dt_print(DT_DEBUG_AI,
"[ai_models] update available for %s: v%s -> v%s",
model->id,
model->version ? model->version : "?",
remote_version);
}
}
g_mutex_unlock(&registry->lock);
g_object_unref(parser);

DT_CONTROL_SIGNAL_RAISE(DT_SIGNAL_AI_MODELS_CHANGED);
return G_SOURCE_REMOVE;
}

// takes ownership of `repository`; no registry access
static gpointer _check_updates_worker(gpointer data)
{
char *repository = (char *)data;

// find the latest compatible release tag
char *error_msg = NULL;
Expand All @@ -854,8 +907,8 @@ void dt_ai_models_check_updates(void)
"[ai_models] check_updates: no compatible release found%s%s",
error_msg ? ": " : "", error_msg ? error_msg : "");
g_free(error_msg);
g_free((char *)repository);
return;
g_free(repository);
return NULL;
}
g_free(error_msg);

Expand All @@ -869,16 +922,17 @@ void dt_ai_models_check_updates(void)
{
g_free(url);
g_free(release_tag);
g_free((char *)repository);
return;
g_free(repository);
return NULL;
}
dt_curl_init(curl, FALSE);

GString *response = g_string_new(NULL);
curl_easy_setopt(curl, CURLOPT_URL, url);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, _curl_write_string);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, response);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 15L);
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 5L);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 5L);
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);

CURLcode res = curl_easy_perform(curl);
Expand All @@ -887,7 +941,7 @@ void dt_ai_models_check_updates(void)
curl_easy_cleanup(curl);
g_free(url);
g_free(release_tag);
g_free((char *)repository);
g_free(repository);

if(res != CURLE_OK || http_code != 200)
{
Expand All @@ -896,7 +950,7 @@ void dt_ai_models_check_updates(void)
" (curl=%d, http=%ld)",
res, http_code);
g_string_free(response, TRUE);
return;
return NULL;
}

// parse versions.json
Expand All @@ -908,62 +962,40 @@ void dt_ai_models_check_updates(void)
g_string_free(response, TRUE);
dt_print(DT_DEBUG_AI,
"[ai_models] check_updates: failed to parse versions.json");
return;
return NULL;
}
g_string_free(response, TRUE);

JsonNode *root = json_parser_get_root(parser);
JsonObject *root_obj = json_node_get_object(root);
JsonObject *models_obj
= json_object_has_member(root_obj, "models")
? json_object_get_object_member(root_obj, "models")
: NULL;
g_idle_add(_apply_updates_idle, parser);
return NULL;
}

if(!models_obj)
void dt_ai_models_check_updates(void)
{
dt_ai_registry_t *registry = darktable.ai_registry;
if(!registry) return;

// only check once per session
g_mutex_lock(&registry->lock);
if(registry->updates_checked)
{
g_object_unref(parser);
dt_print(DT_DEBUG_AI,
"[ai_models] check_updates: no 'models' object in "
"versions.json");
g_mutex_unlock(&registry->lock);
return;
}
registry->updates_checked = TRUE;
char *repository = g_strdup(registry->repository);
g_mutex_unlock(&registry->lock);

// populate model->checksum from sha256 so downloads skip re-fetching
g_mutex_lock(&registry->lock);
for(GList *l = registry->models; l; l = g_list_next(l))
if(!repository || !repository[0])
{
dt_ai_model_t *model = (dt_ai_model_t *)l->data;
if(!json_object_has_member(models_obj, model->id)) continue;

JsonNode *m_node = json_object_get_member(models_obj, model->id);
if(!m_node || !JSON_NODE_HOLDS_OBJECT(m_node)) continue;
JsonObject *m_obj = json_node_get_object(m_node);

const char *remote_version = json_object_has_member(m_obj, "version")
? json_object_get_string_member(m_obj, "version") : NULL;
const char *remote_sha256 = json_object_has_member(m_obj, "sha256")
? json_object_get_string_member(m_obj, "sha256") : NULL;

if(remote_sha256 && g_str_has_prefix(remote_sha256, "sha256:"))
{
g_free(model->checksum);
model->checksum = g_strdup(remote_sha256);
}

if(remote_version
&& model->status == DT_AI_MODEL_DOWNLOADED
&& _version_compare(model->version, remote_version) < 0)
{
model->status = DT_AI_MODEL_UPDATE_AVAILABLE;
dt_print(DT_DEBUG_AI,
"[ai_models] update available for %s: v%s -> v%s",
model->id,
model->version ? model->version : "?",
remote_version);
}
g_free(repository);
return;
}
g_mutex_unlock(&registry->lock);
g_object_unref(parser);

// detached: worker owns `repository`, off the GTK main thread
GThread *t = g_thread_new("ai-model-updates",
_check_updates_worker, repository);
g_thread_unref(t);
}

void dt_ai_models_cleanup(void)
Expand Down
20 changes: 18 additions & 2 deletions src/gui/preferences_ai.c
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,19 @@ static void _refresh_model_list(dt_prefs_ai_data_t *data)
#endif
}

static void _ai_models_changed_cb(gpointer instance, gpointer user_data)
{
dt_prefs_ai_data_t *data = (dt_prefs_ai_data_t *)user_data;
if(data) _refresh_model_list(data);
}

// disconnect before free so a late signal dispatch can't touch freed data
static void _prefs_ai_data_free(gpointer user_data)
{
DT_CONTROL_SIGNAL_DISCONNECT(_ai_models_changed_cb, user_data);
g_free(user_data);
}

static void _update_controls_sensitivity(dt_prefs_ai_data_t *data, gboolean enabled)
{
// grey out settings grid rows below the enable toggle
Expand Down Expand Up @@ -1931,8 +1944,11 @@ void init_tab_ai(GtkWidget *dialog, GtkWidget *stack)
// populate model list
_refresh_model_list(data);

// store data pointer for cleanup (attach to container)
g_object_set_data_full(G_OBJECT(tab_box), "prefs-ai-data", data, g_free);
DT_CONTROL_SIGNAL_CONNECT(DT_SIGNAL_AI_MODELS_CHANGED,
_ai_models_changed_cb, data);

g_object_set_data_full(G_OBJECT(tab_box), "prefs-ai-data",
data, _prefs_ai_data_free);
}

// clang-format off
Expand Down
Loading