1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
use crate;
use crateSemaphore;
use DynamicImage;
use OnceLock;
use Duration;
/// Ceiling for a single `qlmanage` conversion. A disconnected/stale external
/// volume can make `qlmanage`'s own file access block indefinitely on macOS
/// rather than fail fast, which otherwise freezes the calling command with
/// no output; this bounds that to a single conversion's worth of waiting.
const QLMANAGE_TIMEOUT: Duration = from_secs;
/// Default max `qlmanage` processes running at once, process-wide, unless
/// overridden via `set_qlmanage_concurrency` (e.g. `videre faces
/// --qlmanage-concurrency <n>`). QuickLook's thumbnail-generation agent is a
/// single shared per-user service, not something that scales with parallel
/// callers, a UI rendering hundreds of HEIC thumbnails at once (or two
/// videre processes both hitting the same library concurrently) can launch
/// enough simultaneous `qlmanage` processes to make
/// the agent and the source drive's I/O queue up, causing conversions that
/// would normally take under a second to occasionally exceed even
/// `QLMANAGE_TIMEOUT`. Capping concurrency here keeps every caller (axum
/// server, faces/embed/watch, and any other embedder) well behaved without
/// needing to coordinate with each other. Raised from 3 to 6 on 2026-07-29 after real
/// A/B measurement of `videre faces`'s parallel pipeline showed HEIC-heavy
/// runs leaving CPU idle (477% of 1000% on a 10-core machine), a real
/// bottleneck candidate given `--workers` now defaults to 2x cores (up to
/// 20 concurrent workers, all previously queuing on a 3-permit cap).
const QLMANAGE_MAX_CONCURRENT_DEFAULT: usize = 6;
/// Process-wide override for `QLMANAGE_MAX_CONCURRENT_DEFAULT`, set at most
/// once per process (first call wins, matching the underlying semaphore's
/// own `OnceLock` semantics). See `set_qlmanage_concurrency`.
static QLMANAGE_CONCURRENCY_OVERRIDE: = new;
/// Overrides the `qlmanage` concurrency cap for the remainder of this
/// process. Must be called before the first HEIC conversion (i.e. before
/// anything calls `qlmanage_semaphore()`) to take effect, like the
/// semaphore itself, this is a `OnceLock`: the first call sets the value,
/// every later call (including one after the semaphore has already been
/// created with the default) is a no-op. Intended for CLI flags like
/// `videre faces --qlmanage-concurrency <n>` to call once at startup.
/// Pure resolution of the effective concurrency cap, split out from
/// `qlmanage_semaphore` so the "override if present, else default" logic is
/// unit-testable without touching the process-wide `OnceLock` singletons.
/// Shared across every `qlmanage` call site in the codebase (this module's
/// own `heic_via_quicklook` and `videre-ml`'s separate `decode_heic`), so the
/// concurrency cap is process-wide rather than per-call-site.
/// Convert a HEIC file to a `DynamicImage` via QuickLook (`qlmanage -t`).
///
/// `sips -s format jpeg` copies the raw sensor-buffer pixels unrotated for
/// HEIC files where the camera encoded rotation via the HEIF `irot`
/// transform box rather than a classic EXIF Orientation tag, the same
/// rotation Finder/Preview/Photos apply via QuickLook. Using `sips` would
/// produce sideways images (or, for dupe-faces, detect faces and compute
/// bounding boxes against the wrongly oriented image).
///
/// `tag` disambiguates concurrent/repeated conversions of the same path for
/// different purposes (e.g. a 240px thumbnail vs a 1200px lightbox version)
/// so their temp-directory names don't collide.
///
/// Message shared by every QuickLook entry point, so a non-macOS user gets one
/// clear explanation instead of a stream of opaque per-file decode failures.
pub const QUICKLOOK_UNAVAILABLE: &str =
"HEIC images and video frames are decoded via macOS QuickLook (`qlmanage`), \
which has no equivalent on this platform - those files are skipped. \
Scanning, dedupe, and search still work for jpg/jpeg/png/gif/webp/bmp/tiff.";
/// Prints `QUICKLOOK_UNAVAILABLE` at most once per process. Called on the
/// non-macOS path of every QuickLook helper: without it a Linux user just sees
/// each HEIC/video silently fail to decode, with nothing saying why.
/// `max_size` caps `qlmanage -s`'s longest-side render size. `qlmanage -s`
/// only ever caps, never upscales, so `None` (rendered as `10000`, comfortably
/// above any real photo's native resolution) means "give me the native/full
/// resolution", the only safe choice for callers whose output pixels must
/// stay in the same coordinate space as something already stored (face
/// detection bboxes, face-thumbnail crops) or that need true full quality
/// (serving the original image). `Some(n)` is only safe for callers that
/// immediately downscale the result themselves anyway (report.rs's
/// b64/`/api/raw` thumbnails, `watch --heic`'s cache), for them, requesting
/// a smaller render up front avoids qlmanage decoding/resizing/PNG-encoding
/// pixels that get thrown away moments later. Do NOT pass `Some` for the
/// face-detection or face-thumbnail-crop call sites: detection's bbox
/// coordinates are stored in terms of whatever image size detection ran on
/// (see `face_detect.rs`), so shrinking that decode would silently corrupt
/// every later full-res thumbnail crop and the `--min-face-size` quality
/// gate, which measures bbox size in that same (assumed-full-res) space.
// Sibling: `videre_ml::preprocess::decode_via_quicklook` does nearly the same
// thing for the inference path. The two are near-identical and known to be, but
// they are not merged: this one returns `Option` and swallows failures because a
// scan must continue past an unreadable file, while that one returns `Result`
// because a failed decode is a failed embedding. A change to the qlmanage
// invocation, the timeout, or the temp-file handling probably belongs in both.