[0.0.6] Erase OCR readings, or labels and vectors, from the whole library (for testing)

This commit is contained in:
Musa Mahmood 2026-10-08 11:47:30 +01:00
parent cf60dfa535
commit bedeaebe41
9 changed files with 199 additions and 7 deletions

View File

@ -47,7 +47,10 @@ and everything around them, ready for an app to be written into `src/app/app_ui.
- **OCR** (`modules/OCR`): the OCR window shows whether it runs on the GPU and, if not, what to install, - **OCR** (`modules/OCR`): the OCR window shows whether it runs on the GPU and, if not, what to install,
and switches model (kept as `model` under `[ocr]` in the settings file). **Scan the library** reads and switches model (kept as `model` under `[ocr]` in the settings file). **Scan the library** reads
every capture shown that the model has not read yet, storing each reading in its file as it goes, every capture shown that the model has not read yet, storing each reading in its file as it goes,
so it can pause, cancel and pick up again; with **Labels too** it labels them in the same pass. The so it can pause, cancel and pick up again; with **Labels too** it labels them in the same pass.
For testing, the OCR window's **Erase OCR readings** and the Labels window's **Erase labels and
vectors** take them out of every file in the library again (after asking); `-erase ocr|labels|all`
does the same without the UI and exits. The
Capture window's **OCR** button reads one capture; a capture read by several models keeps each Capture window's **OCR** button reads one capture; a capture read by several models keeps each
reading, and the window can show any of them. `-library FOLDER` shows another folder of `.mcap` files; reading, and the window can show any of them. `-library FOLDER` shows another folder of `.mcap` files;
`-scan` scans it with the chosen model once it has loaded and exits (0, or 1 if any capture `-scan` scans it with the chosen model once it has loaded and exits (0, or 1 if any capture

View File

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

View File

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

View File

@ -321,6 +321,40 @@ mcap_store :: (path: string, ocr: *Mcap_Ocr_Section = null, front: [] Mcap_Chunk
return written, write_error; return written, write_error;
} }
// Takes out of the .mcap at path: with ocr, every OCR section; and every chunk, front or back, whose
// tag is one of tags (whatever its key). Everything else stays as it is. The file is rewritten (and
// upgraded to the current version) only when there was something to take out; changed says whether.
mcap_erase :: (path: string, ocr := false, tags: [] string = .[]) -> ok: bool, error: string, changed: bool {
data, read_ok := read_entire_file(path, log_errors = false);
if !read_ok return false, "cannot be read", false;
defer free(data);
m, parsed, parse_error := mcap_parse(cast([] u8) data);
if !parsed return false, parse_error, false;
push_allocator(temp);
changed := false;
if ocr && m.ocr.count { m.ocr = ""; changed = true; }
without :: (chunks: [] Mcap_Chunk, tags: [] string, changed: *bool) -> [] Mcap_Chunk {
kept: [..] Mcap_Chunk;
for chunks {
erase := false;
for tag: tags if mcap_same_tag(it.tag, mcap_tag(tag)) erase = true;
if erase changed.* = true;
else array_add(*kept, it);
}
return kept;
}
m.front_chunks = without(m.front_chunks, tags, *changed);
m.back_chunks = without(m.back_chunks, tags, *changed);
if !changed return true, "", false;
m.version = MCAP_VERSION;
serializer, serialized, serialize_error := mcap_serialize(m);
if !serialized return false, serialize_error, false;
written, write_error := mcap_write(path, serializer);
return written, write_error, written;
}
// Stores chunk alone: in front, or with back after the images. See mcap_store. // Stores chunk alone: in front, or with back after the images. See mcap_store.
mcap_store_chunk :: (path: string, chunk: Mcap_Chunk, back := false) -> ok: bool, error: string { mcap_store_chunk :: (path: string, chunk: Mcap_Chunk, back := false) -> ok: bool, error: string {
if !back { if !back {

View File

@ -184,6 +184,7 @@ App :: struct {
scan_and_exit: bool; // -scan: scan the library with the chosen OCR model once it has loaded, then exit (0, or 1 if any capture failed) scan_and_exit: bool; // -scan: scan the library with the chosen OCR model once it has loaded, then exit (0, or 1 if any capture failed)
stats_and_exit: bool; // -stats: log the Statistics window's numbers once the catalog has loaded, then exit stats_and_exit: bool; // -stats: log the Statistics window's numbers once the catalog has loaded, then exit
start_search: string; // -search QUERY: the search box's text at startup start_search: string; // -search QUERY: the search box's text at startup
erase_and_exit: string; // -erase ocr|labels|all: erase that from the whole library once it has loaded, then exit
last_window_restore_time: Apollo_Time; last_window_restore_time: Apollo_Time;
imgui_context: *ImGui.ImGuiContext; imgui_context: *ImGui.ImGuiContext;

View File

@ -290,6 +290,14 @@ labels_window_contents :: () { using ImGui;
} }
SeparatorText("The library"); SeparatorText("The library");
Text("% of % captures labelled; % with this list.", labels_ui.labelled_count, viewer_capture_count(), labels_ui.current_count); Text("% of % captures labelled; % with this list.", labels_ui.labelled_count, viewer_capture_count(), labels_ui.current_count);
if ocr_ui.scan.running {
ocr_scan_progress_ui();
} else {
if busy BeginDisabled();
if SmallButton("Erase labels and vectors from the library...") ocr_ui.erase_asked = .LABELS;
if busy EndDisabled();
if IsItemHovered(.AllowWhenDisabled) SetTooltip("Takes every capture's labels and image vector, of every label model, out of its file (asks first). For testing: a scan with Labels too makes them again.");
}
TextColored(app_theme.palette.dim, "The OCR window's scan labels the rest, and labels again from their vectors those scored with an earlier list."); TextColored(app_theme.palette.dim, "The OCR window's scan labels the rest, and labels again from their vectors those scored with an earlier list.");
if SmallButton("OCR window") ocr_ui.window_open = true; if SmallButton("OCR window") ocr_ui.window_open = true;

View File

@ -21,6 +21,7 @@ main :: () {
app.scan_and_exit = array_find(app.args, "-scan"); app.scan_and_exit = array_find(app.args, "-scan");
app.stats_and_exit = array_find(app.args, "-stats"); app.stats_and_exit = array_find(app.args, "-stats");
for app.args if it == "-search" && it_index + 1 < app.args.count app.start_search = app.args[it_index + 1]; for app.args if it == "-search" && it_index + 1 < app.args.count app.start_search = app.args[it_index + 1];
for app.args if it == "-erase" && it_index + 1 < app.args.count app.erase_and_exit = app.args[it_index + 1];
defer report_memory_leaks(); defer report_memory_leaks();

View File

@ -36,6 +36,7 @@ ocr_ui: struct {
scan: Ocr_Scan; scan: Ocr_Scan;
scan_requested: bool; // -scan has started its scan scan_requested: bool; // -scan has started its scan
erase_asked: Erase_What; // the erase waiting for its confirmation
scan_logged_step: s64; // -scan's last progress line, in twentieths of the scan scan_logged_step: s64; // -scan's last progress line, in twentieths of the scan
// The Capture window. // The Capture window.
@ -48,7 +49,7 @@ ocr_ui: struct {
toast_shown := false; toast_shown := false;
} }
Ocr_Task_Kind :: enum { NONE; CREATE; READ; SCAN; LABELS_CREATE; LABEL_SET; } Ocr_Task_Kind :: enum { NONE; CREATE; READ; SCAN; LABELS_CREATE; LABEL_SET; ERASE; }
Ocr_Task :: struct { Ocr_Task :: struct {
kind: Ocr_Task_Kind; kind: Ocr_Task_Kind;
@ -75,9 +76,12 @@ Ocr_Scan :: struct {
pause_start: Apollo_Time; pause_start: Apollo_Time;
elapsed_seconds: float64; // once it has ended: the time spent scanning, pauses left out elapsed_seconds: float64; // once it has ended: the time spent scanning, pauses left out
label_set: *Label_Set; // labels scored against this; null when the scan does not label label_set: *Label_Set; // labels scored against this; null when the scan does not label
erase_ocr, erase_labels: bool; // an erase, not a scan: what it takes out of each file
} }
// A capture to read, and what it needs. // A capture to read, and what it needs.
Erase_What :: enum u8 { NONE; OCR; LABELS; ALL; }
Scan_Item :: struct { Scan_Item :: struct {
index: s32; index: s32;
path: string; path: string;
@ -211,6 +215,45 @@ ocr_ui_thread_proc :: (thread: *Thread) -> s64 {
log("[OCR scan] % of % captures read with %% in % (% failed)%.", scan.done, scan.total, scan.model, ifx scan.label_set then tprint(" and labelled with %", LABELS_MODEL) else "", log("[OCR scan] % of % captures read with %% in % (% failed)%.", scan.done, scan.total, scan.model, ifx scan.label_set then tprint(" and labelled with %", LABELS_MODEL) else "",
ocr_duration_text(scan.elapsed_seconds), scan.failed, ifx scan.cancel then ", cancelled" else ""); ocr_duration_text(scan.elapsed_seconds), scan.failed, ifx scan.cancel then ", cancelled" else "");
atomic_swap(*scan.running, false); atomic_swap(*scan.running, false);
case .ERASE;
scan := *ocr_ui.scan;
// Not in temporary storage, which is reset after each capture.
label_tags := string.[MCAP_LABELS_TAG, MCAP_EMBEDDING_TAG];
tags: [] string;
if scan.erase_labels tags = label_tags;
erased := 0;
for item: scan.items {
while scan.paused && !scan.cancel sleep_milliseconds(100);
if scan.cancel break;
ok, changed: bool;
error: string;
for 1..OCR_STORE_ATTEMPTS {
ok, error, changed = mcap_erase(item.path, scan.erase_ocr, tags);
if ok break;
sleep_milliseconds(250);
}
if !ok {
atomic_add(*scan.failed, 1);
log_error("[Erase] % %", item.path, error);
} else if changed {
erased += 1;
entry, load_error := catalog_load(item.path);
if entry {
lock(*viewer.mutex);
array_add(*viewer.catalog_updates, .{item.index, entry});
unlock(*viewer.mutex);
} else {
log_error("[Erase] % was erased, but could not be read back: %", item.path, load_error);
}
}
atomic_add(*scan.done, 1);
reset_temporary_storage();
}
scan.elapsed_seconds = to_float64_seconds(current_time_monotonic() - scan.start) - scan.paused_seconds;
log("[Erase] % erased from % of % captures in % (% failed)%.", erase_what_text(scan), erased, scan.total,
ocr_duration_text(scan.elapsed_seconds), scan.failed, ifx scan.cancel then ", cancelled" else "");
atomic_swap(*scan.running, false);
} }
free(task.model); free(task.model);
free(task.path); free(task.path);
@ -349,6 +392,91 @@ ocr_read_and_store :: (items: [] Scan_Item, set: *Label_Set) -> [] string {
// ---------------------------------------------------------------- the scan // ---------------------------------------------------------------- the scan
// -erase: starts the erase once the catalog is loaded, and ends the app when it is done.
ocr_ui_erase_and_exit :: (busy: bool) {
if !ocr_ui.scan_requested {
if busy || viewer.catalog_loaded < viewer.captures.count return;
what: Erase_What;
if app.erase_and_exit == {
case "ocr"; what = .OCR;
case "labels"; what = .LABELS;
case "all"; what = .ALL;
case;
log_error("[Erase] -erase takes ocr, labels or all, not %.", app.erase_and_exit);
app.exit_code = 2; app.should_exit = true; app.erase_and_exit = ""; return;
}
ocr_ui.scan_requested = true;
ocr_ui.window_open = true; // its progress bar
ocr_erase_start(what);
return;
}
if busy || ocr_ui.scan.running return;
log("[Erase] -erase is done: % in %, % failed.", ocr_scan_progress_text(ocr_ui.scan.done, ocr_ui.scan.total), ocr_duration_text(ocr_ui.scan.elapsed_seconds), ocr_ui.scan.failed);
app.exit_code = ifx ocr_ui.scan.failed then cast(s32) 1 else 0;
app.should_exit = true;
app.erase_and_exit = "";
}
// What an erase takes out, for people. Temp.
erase_what_text :: (scan: *Ocr_Scan) -> string {
if scan.erase_ocr && scan.erase_labels return "OCR, labels and vectors";
if scan.erase_ocr return "OCR readings";
return "labels and vectors";
}
// Takes OCR readings, labels and vectors, or both, out of every capture in the library (not only
// those shown) that has any, on the OCR thread, as a scan runs: with its progress, pause and cancel.
ocr_erase_start :: (what: Erase_What) {
if ocr_ui.thread_busy || what == .NONE return;
scan := *ocr_ui.scan;
for scan.items free(it.path);
array_reset(*scan.items);
free(scan.model);
scan.* = .{};
scan.items.allocator = context.default_allocator;
scan.erase_ocr = what == .OCR || what == .ALL;
scan.erase_labels = what == .LABELS || what == .ALL;
for * viewer.captures {
if it.removed continue;
entry := it.catalog;
has := !entry // not known: looked at anyway
|| (scan.erase_ocr && entry.ocr.count > 0)
|| (scan.erase_labels && (entry.labels.count > 0 || entry.embedding.count > 0 || entry.label_list.count > 0));
if !has continue;
array_add(*scan.items, .{index = cast(s32) it_index, path = copy_string(it.path,, context.default_allocator)});
}
scan.total = scan.items.count;
if !scan.total { toast_show(.Success, "No capture has % to erase", erase_what_text(scan)); return; }
scan.running = true;
scan.start = current_time_monotonic();
ocr_ui_start(.{kind = .ERASE});
}
// The confirmation for ocr_ui.erase_asked, as a modal popup; at the top level, each frame.
ocr_erase_popup :: () { using ImGui;
POPUP :: "Erase from the library?";
if ocr_ui.erase_asked != .NONE && !IsPopupOpen(POPUP) OpenPopup(POPUP);
if !BeginPopupModal(POPUP, null, .AlwaysAutoResize) return;
what := ifx ocr_ui.erase_asked == .OCR then "every OCR reading (all models)" else ifx ocr_ui.erase_asked == .LABELS then "every capture's labels and image vectors (all label models)" else "every OCR reading, label and image vector";
Text("Erase % from all % captures in %?", what, viewer_capture_count(), viewer_folder());
TextColored(app_theme.palette.dim, "Each file holding any is rewritten without them; its images, text and preview stay as they are.");
TextColored(app_theme.palette.warning, "This cannot be undone: only scanning the library again brings them back.");
Spacing();
if Button("Erase") {
what_asked := ocr_ui.erase_asked;
ocr_ui.erase_asked = .NONE;
CloseCurrentPopup();
ocr_ui.window_open = true; // its progress bar
ocr_erase_start(what_asked);
}
SameLine();
if Button("Cancel") || IsKeyPressed(.Escape) {
ocr_ui.erase_asked = .NONE;
CloseCurrentPopup();
}
EndPopup();
}
// -scan: starts the scan once the engine and the catalog are ready, and ends the app when it is done. // -scan: starts the scan once the engine and the catalog are ready, and ends the app when it is done.
ocr_ui_scan_and_exit :: (busy: bool) { ocr_ui_scan_and_exit :: (busy: bool) {
finish :: (code: s32, format: string, args: ..Any) { finish :: (code: s32, format: string, args: ..Any) {
@ -432,14 +560,27 @@ ocr_scan_ui :: () { using ImGui;
if !catalog_ready TextColored(app_theme.palette.dim, "(once the catalog has loaded)"); 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."); 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 { if scan.total && scan.done {
TextColored(app_theme.palette.dim, "Last scan: % of % read with % in %, % failed.", scan.done, scan.total, scan.model, ocr_duration_text(scan.elapsed_seconds), scan.failed); if scan.erase_ocr || scan.erase_labels TextColored(app_theme.palette.dim, "Last erase: % from % of % captures in %, % failed.", erase_what_text(scan), scan.done, scan.total, ocr_duration_text(scan.elapsed_seconds), scan.failed);
else TextColored(app_theme.palette.dim, "Last scan: % of % read with % in %, % failed.", scan.done, scan.total, scan.model, ocr_duration_text(scan.elapsed_seconds), scan.failed);
} }
if busy BeginDisabled();
if SmallButton("Erase OCR readings from the library...") ocr_ui.erase_asked = .OCR;
if busy EndDisabled();
if IsItemHovered(.AllowWhenDisabled) SetTooltip("Takes every OCR reading, of every model, out of every capture's file (asks first). For testing: a scan reads them again.");
return; return;
} }
ocr_scan_progress_ui();
}
// The running scan's or erase's progress bar and buttons (the OCR and Labels windows show it).
ocr_scan_progress_ui :: () { using ImGui;
scan := *ocr_ui.scan;
if scan.erase_ocr || scan.erase_labels Text("Erasing %:", erase_what_text(scan));
done, total, failed, rate, left, elapsed := ocr_scan_progress(); done, total, failed, rate, left, elapsed := ocr_scan_progress();
ProgressBar(cast(float) done / max(total, 1), .{-FLOAT32_MIN, 0}, temp_c_string(ocr_scan_progress_text(done, total))); ProgressBar(cast(float) done / max(total, 1), .{-FLOAT32_MIN, 0}, temp_c_string(ocr_scan_progress_text(done, total)));
Text("% elapsed, % left, % captures/s, % failed, on %", ocr_duration_text(elapsed), ocr_duration_text(left), FF(rate, 1), failed, ifx ocr_ui.engine && ocr_ui.engine.provider == .CUDA then "the GPU" else "the CPU"); if scan.erase_ocr || scan.erase_labels Text("% elapsed, % left, % captures/s, % failed", ocr_duration_text(elapsed), ocr_duration_text(left), FF(rate, 1), failed);
else Text("% elapsed, % left, % captures/s, % failed, on %", ocr_duration_text(elapsed), ocr_duration_text(left), FF(rate, 1), failed, ifx ocr_ui.engine && ocr_ui.engine.provider == .CUDA then "the GPU" else "the CPU");
if scan.paused { if scan.paused {
if Button("Resume") { scan.paused_seconds += to_float64_seconds(current_time_monotonic() - scan.pause_start); atomic_swap(*scan.paused, false); } if Button("Resume") { scan.paused_seconds += to_float64_seconds(current_time_monotonic() - scan.pause_start); atomic_swap(*scan.paused, false); }
} else { } else {
@ -483,6 +624,8 @@ ocr_duration_text :: (seconds: float64) -> string {
ocr_ui_frame :: () { using ImGui; ocr_ui_frame :: () { using ImGui;
busy := ocr_ui.thread_busy; busy := ocr_ui.thread_busy;
if app.scan_and_exit ocr_ui_scan_and_exit(busy); 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();
if !busy && ocr_ui.engine && !ocr_ui.toast_shown { if !busy && ocr_ui.engine && !ocr_ui.toast_shown {
ocr_ui.toast_shown = true; ocr_ui.toast_shown = true;
in_use, explanation := ocr_gpu_status(ocr_ui.engine); in_use, explanation := ocr_gpu_status(ocr_ui.engine);

View File

@ -790,7 +790,9 @@ viewer_ui :: () { using ImGui;
if ocr_ui.scan.running { if ocr_ui.scan.running {
done, total, failed, rate, left, elapsed := ocr_scan_progress(); done, total, failed, rate, left, elapsed := ocr_scan_progress();
ProgressBar(cast(float) done / max(total, 1), .{-FLOAT32_MIN, max(2.0, GetFontSize() * 0.15)}, ""); ProgressBar(cast(float) done / max(total, 1), .{-FLOAT32_MIN, max(2.0, GetFontSize() * 0.15)}, "");
if IsItemHovered() SetTooltip("OCR scan with %: %, % elapsed, % left, % failed%", ocr_ui.scan.model, ocr_scan_progress_text(done, total), ocr_duration_text(elapsed), ocr_duration_text(left), failed, ifx ocr_ui.scan.paused then " (paused)" else ""); scan := *ocr_ui.scan;
doing := ifx scan.erase_ocr || scan.erase_labels then tprint("Erasing %", erase_what_text(scan)) else tprint("OCR scan with %", scan.model);
if IsItemHovered() SetTooltip("%: %, % elapsed, % left, % failed%", doing, ocr_scan_progress_text(done, total), ocr_duration_text(elapsed), ocr_duration_text(left), failed, ifx scan.paused then " (paused)" else "");
} }
BeginChild("grid"); BeginChild("grid");