[0.1.13] Models load when first needed, and unload after 10 minutes unused

- No model is made at startup (private memory there falls from about 4.3 GB to about 420 MB).
  ocr_ui_models_ready makes the OCR engine, then the label engine and the list's set, on the OCR
  thread when a reading, a scan (the button, -scan, or after a capture or an import) needs them;
  the OCR and scan buttons wait for them, showing "Loading the models...". A like: search asks
  for the label engine alone (labels_ui_want) and is worked out again once it is up.
- ocr_ui_unload_idle_models, each frame and also in the tray: an engine (OCR, labels or
  segmentation) unused for MODELS_IDLE_UNLOAD_SECONDS (10 min) is destroyed by an UNLOAD task on
  the OCR thread, never during a scan, a waiting read or scan, or while search jobs hold the label
  engine. The label list's set stays, so loading again does not score the prompts again.
- Choosing another OCR model with none loaded only notes it; -segment no longer waits for the OCR
  and label engines. Some 1.4 GB stays after an unload, the CUDA runtime it loaded.
This commit is contained in:
Musa Mahmood 2026-10-12 21:58:17 +01:00
parent e7229343c7
commit 157888d6d8
10 changed files with 179 additions and 38 deletions

View File

@ -29,7 +29,7 @@ APP_NAME :: "mcap";
APP_FOLDER_NAME :: "mcap";
GIT_BRANCH :: "check";
GIT_HASH :: "-";
VERSION :: "0.1.12";
VERSION :: "0.1.13";
BUILD_DATE :: "-";
DEBUG :: true;
COMPILE_TESTS :: true;

View File

@ -8,7 +8,7 @@ APP_NAME :: "mcap";
APP_FOLDER_NAME :: "mcap";
APP_BUNDLE_IDENTIFIER :: "com.example.mcap";
VERSION :: "0.1.12";
VERSION :: "0.1.13";
// Where everything goes: bin/, or bin-headless/ for builds made with no_crash_dialog, so automated
// builds never write over the ones you run by hand.
build_directory := "bin";

View File

@ -17,6 +17,7 @@ app_ui_startup :: () {
// Every frame, before input is handed to ImGui. Poll background work here.
app_ui_frame_start :: () {
screen_capture_frame(); // also while hidden in the tray, where the frame ends before the UI
ocr_ui_unload_idle_models(); // models unused for a while, also while in the tray
}
// Once, on the shutdown thread after the window is asked to close (exit_shutdown.jai): save and
@ -77,6 +78,7 @@ app_ui_shutdown_status :: () -> string {
case .LABELS_CREATE; text = "Waiting for the label model to finish loading.";
case .LABEL_SET; text = "Waiting for the label list to finish.";
case .SEGMENT; text = "Stopping the segmentation scan: finishing the images it is segmenting.";
case .UNLOAD; text = "Waiting for the models unused for a while to unload.";
case; text = "Stopping OCR.";
}
case .SEGMENT; text = "Closing the segmentation model.";

View File

@ -149,6 +149,7 @@ background_search_start :: () {
likes: [..] string;
likes.allocator = temp;
for request.terms if it.field == .LIKE array_add(*likes, it.text);
if likes.count labels_ui_want(); // when it is not up, the search is worked out again once it is
if likes.count && labels_ui.engine {
request.like_texts = NewArray(likes.count, string);
for likes request.like_texts[it_index] = copy_string(it);

View File

@ -921,7 +921,7 @@ import_frame :: () { using ImGui;
importer.scan_pending = importer.read_after && importer.new_indices.count > 0 && !app.import_source;
importer.segment_pending = library_folder_segments(importer.dest_name) && importer.new_indices.count > 0 && !app.import_source;
}
if importer.scan_pending && !ocr_ui.thread_busy && ocr_ui.engine && labels_ui_settled() {
if importer.scan_pending && ocr_ui_models_ready(labels_ui.scan_labels) && ocr_ui.engine {
importer.scan_pending = false;
ocr_scan_start(importer.new_indices);
}

View File

@ -7,9 +7,10 @@
// vectors and a hash of it, made on the OCR thread (app/ocr_ui.jai) and never changed after, so a
// scan can hold the set it started with while the list is edited.
//
// The engine is made on the OCR thread too, once, after the OCR engine; it is destroyed only at
// shutdown, after the background workers (app/background.jai), which use its text model for like:
// searches, have stopped.
// The engine is made on the OCR thread too, when a reading, a scan or a like: search first needs it
// (ocr_ui_models_ready, labels_ui_want), and destroyed there once it has gone unused for a while
// (ocr_ui_unload_idle_models) with no search job holding it; else at shutdown, after the background
// workers (app/background.jai), which use its text model for like: searches, have stopped.
LABELS_WINDOW :: "Labels";
LABELS_MODEL :: "siglip2-base-patch16-384"; // the one label model; its chunks are keyed by this
@ -29,8 +30,10 @@ labels_ui: struct {
// Owned by the OCR thread while ocr_ui.thread_busy, like ocr_ui.engine.
engine: *Labels_Engine;
engine_error: string;
engine_tried: bool;
set: *Label_Set; // the newest list's
engine_tried: bool; // made, or failed; cleared when it is unloaded
last_used: Apollo_Time; // main thread: when a task or a like: search last used it
wanted: bool; // main thread: a like: search asked for it
set: *Label_Set; // the newest list's
retired_sets: [..] *Label_Set; // replaced while a scan might hold them; freed when the OCR thread is idle
// Main thread.
@ -77,11 +80,11 @@ labels_ui_shutdown :: () {
free(labels_ui.counted_hash,, context.default_allocator);
}
// Whether a scan can start with labels: the engine has been tried and, when it is up, the list's
// set is made.
labels_ui_settled :: () -> bool {
if ocr_ui.thread_busy || !labels_ui.engine_tried return false;
return !labels_ui.engine || (labels_ui.set && labels_ui.set.hash == labels_ui.list_hash);
// A like: search needs the engine: it is made when the OCR thread is free (labels_ui_frame), and the
// search worked out again once it is up.
labels_ui_want :: () {
labels_ui.last_used = current_time_monotonic();
if !labels_ui.engine_tried labels_ui.wanted = true;
}
// ---------------------------------------------------------------- the label list
@ -218,8 +221,9 @@ labels_ui_frame :: () { using ImGui;
if !ocr_ui.thread_busy {
for labels_ui.retired_sets label_set_free(it);
array_reset_keeping_memory(*labels_ui.retired_sets);
if !labels_ui.engine_tried && ocr_ui.engine_tried { // after the OCR engine
ocr_ui_start(.{kind = .LABELS_CREATE});
if labels_ui.wanted {
labels_ui.wanted = false;
if !labels_ui.engine_tried ocr_ui_start(.{kind = .LABELS_CREATE});
} else if labels_ui.engine && (!labels_ui.set || labels_ui.set.hash != labels_ui.list_hash) && labels_ui.list.count {
ocr_ui_start(.{kind = .LABEL_SET});
}
@ -272,7 +276,7 @@ labels_window_contents :: () { using ImGui;
Text("%: images on %, text on the CPU", labels_ui.engine.model.title, ifx in_use then "the GPU (CUDA)" else "the CPU");
if explanation text_with_links(explanation);
} else if !labels_ui.engine_tried {
TextColored(app_theme.palette.dim, "Starts after OCR.");
TextColored(app_theme.palette.dim, "Not loaded: it loads when a capture is read or labelled, or a like: search runs, and unloads after % minutes unused.", MODELS_IDLE_UNLOAD_SECONDS / 60);
}
// How much of the library is labelled, and with which list: counted again only when the catalog

View File

@ -10,11 +10,15 @@
// Making the engines (OCR, then labels), the label list's vectors, reading one capture and the scan
// all run on one background thread, one at a time; the UI only looks at what it has finished, and
// at the scan's progress counters.
//
// No engine is made at startup: each is made when something first needs it (ocr_ui_models_ready),
// and destroyed again once it has gone MODELS_IDLE_UNLOAD_SECONDS unused (ocr_ui_unload_idle_models).
OCR_WINDOW :: "OCR";
OCR_DEFAULT_MODEL :: "ppocr-v5-mobile";
OCR_SCAN_BATCH :: 8; // captures per ocr_process call in a scan
OCR_STORE_ATTEMPTS :: 4; // a file the viewer has open at that moment is tried again, 250 ms apart
MODELS_IDLE_UNLOAD_SECONDS :: 10 * 60; // an engine unused this long is destroyed, giving its memory back
ocr_ui: struct {
models: [] Ocr_Model_Info;
@ -24,7 +28,8 @@ ocr_ui: struct {
// Owned by the main thread while thread_busy is false; by the background thread while it is true.
engine: *Ocr_Engine;
engine_error: string; // why the last engine could not be made
engine_tried: bool; // the first engine has been made, or failed: labels start after it
engine_tried: bool; // the engine has been made, or failed; cleared when it is unloaded
last_used: Apollo_Time; // main thread: when a task last used the engine
thread: Thread;
thread_started: bool;
thread_busy: bool; // set by the main thread, cleared by the background one
@ -33,6 +38,10 @@ ocr_ui: struct {
read_error: string; // why the last single read failed
read_index: s32 = -1;
read_seconds: float64;
read_wanted: bool; // the OCR button was pressed: it reads once the models are up
read_wanted_index: s32;
read_wanted_path: string; // heap
scan_wanted: bool; // the scan button was pressed: it scans once the models are up
scan: Ocr_Scan;
scan_requested: bool; // -scan has started its scan
@ -49,7 +58,7 @@ ocr_ui: struct {
toast_shown := false;
}
Ocr_Task_Kind :: enum { NONE; CREATE; READ; SCAN; LABELS_CREATE; LABEL_SET; ERASE; SEGMENT; }
Ocr_Task_Kind :: enum { NONE; CREATE; READ; SCAN; LABELS_CREATE; LABEL_SET; ERASE; SEGMENT; UNLOAD; }
Ocr_Task :: struct {
kind: Ocr_Task_Kind;
@ -59,6 +68,10 @@ Ocr_Task :: struct {
// LABELS_CREATE and LABEL_SET: the label list as it was when the task started (heap).
label_names, label_prompts: [] string;
label_hash: string;
// UNLOAD: the engines to destroy, already taken out of ocr_ui, labels_ui and segment_ui.
unload_ocr: *Ocr_Engine;
unload_labels: *Labels_Engine;
unload_segment: *Segment_Engine;
}
// The library scan. The counters and flags are shared with the background thread.
@ -103,7 +116,7 @@ ocr_ui_startup :: () {
ocr_ui.models = models;
if error ocr_ui.models_error = copy_string(error);
ocr_ui.model_index = ocr_model_index(app_user_settings.ocr_model);
if ocr_ui.models.count ocr_ui_start(.{kind = .CREATE, model = ocr_ui.models[ocr_ui.model_index].name});
// The engine is made when something first needs it: ocr_ui_models_ready.
}
ocr_ui_shutdown :: () {
@ -111,6 +124,7 @@ ocr_ui_shutdown :: () {
atomic_swap(*ocr_ui.scan.paused, false);
if ocr_ui.thread_started thread_is_done(*ocr_ui.thread, -1);
ocr_destroy(ocr_ui.engine);
free(ocr_ui.read_wanted_path,, context.default_allocator);
}
// The model the settings name, or the default one, or the first; with a log line when the settings
@ -135,7 +149,13 @@ ocr_ui_choose_model :: (index: s32, save := true) {
if ocr_ui.thread_busy return;
ocr_ui.model_index = index;
ocr_ui.toast_shown = true; // said once already, or not needed
ocr_ui_start(.{kind = .CREATE, model = ocr_ui.models[index].name});
if ocr_ui.engine {
ocr_ui_start(.{kind = .CREATE, model = ocr_ui.models[index].name});
} else { // not loaded, or failed: the next use makes the one chosen
ocr_ui.engine_tried = false;
free(ocr_ui.engine_error,, context.default_allocator);
ocr_ui.engine_error = "";
}
if save {
free(app_user_settings.ocr_model,, context.default_allocator);
app_user_settings.ocr_model = copy_string(ocr_ui.models[index].name,, context.default_allocator);
@ -160,6 +180,7 @@ ocr_ui_start :: (task: Ocr_Task) -> bool {
}
ocr_ui.task.label_hash = copy_string(labels_ui.list_hash);
}
ocr_ui_note_use(task.kind, current_time_monotonic());
ocr_ui.thread_busy = true;
if ocr_ui.thread_started thread_deinit(*ocr_ui.thread);
ocr_ui.thread_started = thread_init(*ocr_ui.thread, ocr_ui_thread_proc, temporary_storage_size = 4 * 1024 * 1024, name = "ocr");
@ -168,6 +189,86 @@ ocr_ui_start :: (task: Ocr_Task) -> bool {
return true;
}
// ---------------------------------------------------------------- the models: made when needed, unloaded when idle
// Whether the OCR engine (and, with labels, the label engine and the list's set) are up, or could
// not be made, so a reading or a scan can start. When not, starts making the next of them on the OCR
// thread, if it is free. Called each frame by what waits on them. After it returns true, a null
// ocr_ui.engine means OCR could not start.
ocr_ui_models_ready :: (labels := true) -> bool {
if ocr_ui.thread_busy return false;
if !ocr_ui.engine_tried {
if !ocr_ui.models.count return true; // ocr_ui.models_error says why
ocr_ui_start(.{kind = .CREATE, model = ocr_ui.models[ocr_ui.model_index].name});
return false;
}
if !labels return true;
if !labels_ui.engine_tried {
ocr_ui_start(.{kind = .LABELS_CREATE});
return false;
}
if labels_ui.engine && labels_ui.list.count && (!labels_ui.set || labels_ui.set.hash != labels_ui.list_hash) {
ocr_ui_start(.{kind = .LABEL_SET});
return false;
}
return true;
}
// Each frame, also while the window is in the tray: notes which engines the OCR thread's task uses,
// and destroys those unused for MODELS_IDLE_UNLOAD_SECONDS, on the OCR thread.
ocr_ui_unload_idle_models :: () {
if app_exit_shutdown_started() return; // the shutdown thread destroys them
now := current_time_monotonic();
if ocr_ui.thread_busy { ocr_ui_note_use(ocr_ui.task.kind, now); return; }
if ocr_ui.scan.running || ocr_ui.read_wanted || ocr_ui.scan_wanted return;
idle :: (last_used: Apollo_Time, now: Apollo_Time) -> bool {
return to_float64_seconds(now - last_used) >= MODELS_IDLE_UNLOAD_SECONDS;
}
task := Ocr_Task.{kind = .UNLOAD};
if ocr_ui.engine && idle(ocr_ui.last_used, now) {
task.unload_ocr = ocr_ui.engine;
ocr_ui.engine = null;
ocr_ui.engine_tried = false;
}
// A like: search's jobs hold the label engine until they are back.
if labels_ui.engine && idle(labels_ui.last_used, now) && !background.jobs_out {
task.unload_labels = labels_ui.engine;
labels_ui.engine = null;
labels_ui.engine_tried = false;
labels_ui.search_ready = false; // the list's set stays: the same model makes the same vectors
}
if segment_ui.engine && idle(segment_ui.last_used, now) {
task.unload_segment = segment_ui.engine;
segment_ui.engine = null;
}
if !task.unload_ocr && !task.unload_labels && !task.unload_segment return;
names: [..] string;
names.allocator = temp;
if task.unload_ocr array_add(*names, task.unload_ocr.model.name);
if task.unload_labels array_add(*names, LABELS_MODEL);
if task.unload_segment array_add(*names, SEGMENT_MODEL);
log("[Models] Unloading % after % minutes unused.", join(..names, separator = ", ",, temp), MODELS_IDLE_UNLOAD_SECONDS / 60);
if !ocr_ui_start(task) { // the thread could not start: destroy them here instead
ocr_destroy(task.unload_ocr);
labels_destroy(task.unload_labels);
segment_destroy(task.unload_segment);
}
}
// The engines a task of this kind uses, as used at now: when it starts, and each frame it runs.
ocr_ui_note_use :: (kind: Ocr_Task_Kind, now: Apollo_Time) {
if kind == {
case .CREATE; ocr_ui.last_used = now;
case .LABELS_CREATE; labels_ui.last_used = now;
case .LABEL_SET; labels_ui.last_used = now;
case .READ; ocr_ui.last_used = now; labels_ui.last_used = now;
case .SCAN; ocr_ui.last_used = now; if ocr_ui.scan.label_set labels_ui.last_used = now;
case .SEGMENT; segment_ui.last_used = now;
}
}
// ---------------------------------------------------------------- the background thread
ocr_ui_thread_proc :: (thread: *Thread) -> s64 {
@ -229,6 +330,11 @@ ocr_ui_thread_proc :: (thread: *Thread) -> s64 {
case .SEGMENT;
segment_ui_scan_task(); // app/segment_ui.jai
case .UNLOAD;
ocr_destroy(task.unload_ocr);
labels_destroy(task.unload_labels);
segment_destroy(task.unload_segment);
case .ERASE;
scan := *ocr_ui.scan;
// Not in temporary storage, which is reset after each capture.
@ -512,7 +618,8 @@ ocr_ui_scan_and_exit :: (busy: bool) {
if ocr_ui.models_error { finish(1, "[OCR scan] %", ocr_ui.models_error); return; }
if viewer.scan_error { finish(1, "[OCR scan] %", viewer.scan_error); return; }
if !ocr_ui.scan_requested {
if busy || !ocr_ui.engine || viewer.catalog_loaded < viewer.captures.count || !labels_ui_settled() return;
if busy || viewer.catalog_loaded < viewer.captures.count || !ocr_ui_models_ready(labels_ui.scan_labels) return;
if !ocr_ui.engine { finish(1, "[OCR scan] OCR could not start: %", ocr_ui.engine_error); return; }
ocr_ui.scan_requested = true;
ocr_ui.window_open = true; // its progress bar
ocr_scan_start();
@ -570,17 +677,18 @@ ocr_scan_ui :: () { using ImGui;
catalog_ready := viewer.catalog_loaded >= viewer.captures.count;
if !scan.running {
can_scan := ocr_ui.engine && !busy && catalog_ready;
can_scan := !busy && !ocr_ui.scan_wanted && catalog_ready;
if !can_scan BeginDisabled();
label := ifx viewer.search_terms.count then "Scan the captures shown" else "Scan the library";
if Button(temp_c_string(label)) ocr_scan_start();
if Button(temp_c_string(label)) ocr_ui.scan_wanted = true; // it starts once the models are up: ocr_ui_frame
if !can_scan EndDisabled();
SameLine();
labels_ready := labels_ui.engine && labels_ui.set && labels_ui.set.hash == labels_ui.list_hash;
if !labels_ready BeginDisabled();
labels_failed := labels_ui.engine_tried && !labels_ui.engine;
if labels_failed BeginDisabled();
Checkbox("Labels too", *labels_ui.scan_labels);
if !labels_ready EndDisabled();
if IsItemHovered(.AllowWhenDisabled) SetTooltip(ifx labels_ready then tprint("Also labels each capture with %, storing its labels and image vector", LABELS_MODEL) else "Labelling is not ready: see the Labels window");
if labels_failed EndDisabled();
if IsItemHovered(.AllowWhenDisabled) SetTooltip(ifx labels_failed then "Labelling could not start: see the Labels window" else tprint("Also labels each capture with %, storing its labels and image vector", LABELS_MODEL));
if ocr_ui.scan_wanted TextColored(app_theme.palette.dim, "Loading the models...");
if !catalog_ready TextColored(app_theme.palette.dim, "(once the catalog has loaded)");
else TextColored(app_theme.palette.dim, "Reads every capture this model has not read, and labels those not yet labelled with this list. Each is stored as it is read, so a scan can stop and start again.");
if scan.total && scan.done {
@ -652,6 +760,17 @@ ocr_ui_frame :: () { using ImGui;
if app.scan_and_exit ocr_ui_scan_and_exit(busy);
if app.erase_and_exit ocr_ui_erase_and_exit(busy);
ocr_erase_popup();
// The OCR and scan buttons wait for the models they load.
if ocr_ui.read_wanted && ocr_ui_models_ready() {
ocr_ui.read_wanted = false;
ocr_ui_start(.{kind = .READ, path = ocr_ui.read_wanted_path, index = ocr_ui.read_wanted_index});
busy = ocr_ui.thread_busy;
}
if ocr_ui.scan_wanted && ocr_ui_models_ready(labels_ui.scan_labels) {
ocr_ui.scan_wanted = false;
if ocr_ui.engine ocr_scan_start();
busy = ocr_ui.thread_busy;
}
if !busy && ocr_ui.engine && !ocr_ui.toast_shown {
ocr_ui.toast_shown = true;
in_use, explanation := ocr_gpu_status(ocr_ui.engine);
@ -685,7 +804,11 @@ ocr_ui_frame :: () { using ImGui;
} else if ocr_ui.engine_error {
TextColored(app_theme.palette.warning, "OCR could not start:");
TextWrapped("%", ocr_ui.engine_error);
} else if ocr_ui.engine {
} else if !ocr_ui.engine {
TextColored(app_theme.palette.dim, "Not loaded: it loads when a capture is read or a scan starts, and unloads after % minutes unused.", MODELS_IDLE_UNLOAD_SECONDS / 60);
Separator();
ocr_scan_ui();
} else {
in_use, explanation := ocr_gpu_status(ocr_ui.engine);
Text("Running on: %", ifx in_use then "the GPU (CUDA)" else "the CPU");
SameLine();
@ -750,12 +873,21 @@ ocr_ui_capture_controls :: (index: s32, path: string) { using ImGui;
model := ifx ocr_ui.models.count then ocr_ui.models[ocr_ui.model_index].name else "";
has_reading := entry && catalog_section(entry, model) != null;
can_read := ocr_ui.engine && !busy;
ocr_failed := ocr_ui.engine_tried && !ocr_ui.engine;
can_read := ocr_ui.models.count && !busy && !ocr_ui.read_wanted && !ocr_failed;
if !can_read BeginDisabled();
if SmallButton(temp_c_string(ifx has_reading then "Read again" else "OCR")) ocr_ui_start(.{kind = .READ, path = path, index = index});
if SmallButton(temp_c_string(ifx has_reading then "Read again" else "OCR")) { // it reads once the models are up: ocr_ui_frame
ocr_ui.read_wanted = true;
ocr_ui.read_wanted_index = index;
free(ocr_ui.read_wanted_path,, context.default_allocator);
ocr_ui.read_wanted_path = copy_string(path,, context.default_allocator);
}
if !can_read EndDisabled();
SameLine();
if busy && ocr_ui.task.kind == .READ && ocr_ui.task.index == index {
if ocr_ui.read_wanted && ocr_ui.read_wanted_index == index {
TextColored(app_theme.palette.dim, "Loading the models...");
SameLine();
} else if busy && ocr_ui.task.kind == .READ && ocr_ui.task.index == index {
TextColored(app_theme.palette.dim, "Reading...");
SameLine();
} else if !busy && ocr_ui.read_index == index {
@ -789,8 +921,8 @@ ocr_ui_capture_controls :: (index: s32, path: string) { using ImGui;
}
}
SameLine();
} else if !ocr_ui.engine {
TextColored(app_theme.palette.dim, "(OCR is not running; see the OCR window)");
} else if ocr_failed {
TextColored(app_theme.palette.dim, "(OCR could not start; see the OCR window)");
SameLine();
}
if SmallButton("OCR window") ocr_ui.window_open = true;

View File

@ -112,8 +112,8 @@ screen_capture_frame :: () {
remove job;
}
// As an import's are, once the models are free: OCR and labels, then segmentation.
if screen_capture.scan_pending.count && !ocr_ui.thread_busy && !ocr_ui.scan.running && ocr_ui.engine && labels_ui_settled() {
// As an import's are, once the models are loaded and free: OCR and labels, then segmentation.
if screen_capture.scan_pending.count && !ocr_ui.scan.running && ocr_ui_models_ready(labels_ui.scan_labels) && ocr_ui.engine {
ocr_scan_start(screen_capture.scan_pending);
array_reset_keeping_memory(*screen_capture.scan_pending);
}

View File

@ -6,6 +6,7 @@
// The scan runs on the OCR thread (app/ocr_ui.jai), as a task of its own, so it shares the OCR scan's
// progress, pause and cancel, and one thread uses the GPU at a time. The engine is made by the first
// scan, not at startup: it takes some 2.4 GB of GPU memory, and most sessions never segment anything.
// It is destroyed again once it has gone unused for a while (ocr_ui_unload_idle_models).
SEGMENT_WINDOW :: "Segmentation";
SEGMENT_MODEL :: "mask2former-swin-base-coco-panoptic"; // the one segmentation model; its chunks are keyed by this
@ -15,6 +16,7 @@ segment_ui: struct {
// Owned by the OCR thread while ocr_ui.thread_busy, like ocr_ui.engine.
engine: *Segment_Engine;
engine_error: string;
last_used: Apollo_Time; // main thread: when a scan last used the engine (ocr_ui_unload_idle_models)
// Main thread.
window_open: bool;
@ -290,8 +292,8 @@ segment_ui_segment_and_exit :: (busy: bool) {
}
if viewer.scan_error { finish(1, "[Segment scan] %", viewer.scan_error); return; }
if !segment_ui.scan_requested {
// After the catalog, and after the engines made at startup, which use the same thread.
if busy || viewer.catalog_loaded < viewer.captures.count || !ocr_ui.engine_tried || !labels_ui.engine_tried return;
// After the catalog.
if busy || viewer.catalog_loaded < viewer.captures.count return;
segment_ui.scan_requested = true;
segment_ui.window_open = true; // its progress bar
if !segment_scan_start(quiet = true) finish(0, "[Segment scan] -segment: nothing to segment (no image in a folder set to segment needs it).");

View File

@ -908,7 +908,7 @@ viewer_ui :: () { using ImGui;
engine: *Ocr_Engine;
if !ocr_ui.thread_busy engine = ocr_ui.engine; // the background thread owns it while busy
if engine SetTooltip("OCR with %, on %", engine.model.name, ifx engine.provider == .CUDA then "the GPU" else "the CPU");
else SetTooltip("OCR is starting, or could not start; the OCR window says which");
else SetTooltip("OCR is not loaded, is loading, or could not start; the OCR window says which");
}
// A running OCR scan, as a thin bar under the search, so it shows with the OCR window closed.