diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 725bc8dd..1e868887 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -208,6 +208,8 @@ jobs: # (#956). A PR that touches the sidecar runs the suite again against the locked dependency # set, with a missing fastembed a failure rather than a skip. About 550 MB and a minute, and # the embedding test downloads the 67 MB English model, so only when sidecar/ changed. + # Installed exactly as the app installs it, `--no-deps` included (sidecar.rs `provision` + # says why: the lock overrides fastembed's pillow cap). - name: Sidecar unit tests (the locked dependencies, sidecar changes only) env: BASE_SHA: ${{ github.event.pull_request.base.sha }} @@ -219,7 +221,7 @@ jobs: fi python -m venv "$RUNNER_TEMP/sidecar-venv" "$RUNNER_TEMP/sidecar-venv/bin/python" -m pip install --disable-pip-version-check \ - --no-cache-dir --require-hashes -r sidecar/requirements.lock + --no-cache-dir --require-hashes --no-deps -r sidecar/requirements.lock "$RUNNER_TEMP/sidecar-venv/bin/python" sidecar/test_pm_sidecar.py # --- secrets: gitleaks over the working tree AND git history ------------ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3509694f..7ca34ae7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -339,10 +339,13 @@ jobs: # interpreter is a prebuilt binary, so the build runners still need no Python # toolchain to compile.) # - # Installs exactly as the app does — `--require-hashes -r sidecar/requirements.lock` - # — so this is also the proof that the universal lock's markers select correctly on - # all three platforms, and that every artifact each one needs is covered by a hash. - # It used to install requirements.txt, which left the shipped resolution untested. + # Installs exactly as the app does — `--require-hashes --no-deps -r + # sidecar/requirements.lock` — so this is also the proof that the universal lock's + # markers select correctly on all three platforms, and that every artifact each one + # needs is covered by a hash. It used to install requirements.txt, which left the + # shipped resolution untested. `--no-deps` because the lock overrides fastembed's + # pillow cap (sidecar.rs `provision` says why); it also means pip no longer reports a + # dependency the markers failed to select, so the import line is what catches one. python-smoke: needs: guards strategy: @@ -370,7 +373,7 @@ jobs: source .smoke-venv/bin/activate fi python -m pip install --upgrade pip - pip install --require-hashes -r sidecar/requirements.lock + pip install --require-hashes --no-deps -r sidecar/requirements.lock python -c "import markitdown, fastembed, faster_whisper; print('sidecar deps import OK')" publish: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 20d8003c..bff02392 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -78,7 +78,7 @@ prior exposure. ## 3. Set up your environment Full prerequisites are in the [README](README.md#prerequisites). In short: **Node 24+**, -**Rust (stable)**, **Python 3.10+** on your PATH, plus your platform's Tauri build +**Rust (stable)**, **Python 3.11+** on your PATH, plus your platform's Tauri build prerequisites. ```bash diff --git a/README.md b/README.md index 36eabc69..94a07d49 100644 --- a/README.md +++ b/README.md @@ -128,12 +128,13 @@ For a full tour of the architecture and the rules for working in this repo, see ## Prerequisites - **Node.js** 24+ (LTS) and **Rust** (stable) -- **Python** 3.10+ — needed for development (`tauri dev`) and as the base for the +- **Python** 3.11+ — needed for development (`tauri dev`) and as the base for the document sidecar's managed virtual environment, built on first use. PM finds a suitable - interpreter automatically: `PM_PYTHON`, then versioned names (`python3.12` … `python3.10`), - `python3`/`python`, and common macOS locations (Homebrew, python.org) — so even a - Finder-launched app finds one off its stripped PATH. It also rebuilds the venv if that - interpreter is older than 3.10. Set `PM_PYTHON` only for an unusual setup. (Packaged + interpreter automatically: `PM_PYTHON`, then versioned names newest-first (`python3.16` … + `python3.11`), `python3`/`python`, and common macOS locations (Homebrew, python.org) — so even + a Finder-launched app finds one off its stripped PATH. It also rebuilds the venv if that + interpreter is older than 3.11 (the floor was 3.10 until Pillow 13, which needs 3.11). Set + `PM_PYTHON` only for an unusual setup. (Packaged **Windows and Linux** release builds **bundle** a standalone interpreter at build time — `npm run tauri build` fetches it via [`scripts/fetch-python.mjs`](scripts/fetch-python.mjs) — so end users need no Python install. On **macOS**, packaged apps instead **download** a @@ -143,7 +144,7 @@ For a full tour of the architecture and the rules for working in this repo, see build-time bundling into the signed `.app` is gated on the Apple-signing pipeline, whereas a runtime download picks the right per-arch build and isn't gated on signing — see [`docs/MACOS-SIGNING.md`](docs/MACOS-SIGNING.md). Development from source still needs Python - 3.10+ on your machine on every platform.) + 3.11+ on your machine on every platform.) - **Tauri OS prerequisites:** see - Windows: Microsoft C++ Build Tools + WebView2 (preinstalled on Win 11) - macOS: Xcode Command Line Tools diff --git a/justfile b/justfile index 8b9117d0..f8d09dfd 100644 --- a/justfile +++ b/justfile @@ -136,8 +136,15 @@ deny: # transitive ones included, on the base venv AND both optional components (L-6). It used to scan # requirements.txt, i.e. the six top-level pins only, so a CVE in (say) pdfminer.six or lxml — both # of which parse untrusted input — was never looked at. +# +# `--disable-pip`: audit the pinned versions as written instead of pip-installing the locks into a +# throwaway venv first. pip-audit allows it only for hashed requirements, which every lock line is, +# and it does not resolve anything, so it audits exactly the lock. Installing them was also how the +# audit broke: the base lock overrides fastembed's `pillow<13` (sidecar/requirements-override.txt), +# which pip's resolver refuses. Like the install it replaces, it reads the lines whose markers +# match the auditing interpreter (3.12 in CI). pip-audit: - pip-audit -r sidecar/requirements.lock -r sidecar/requirements-ocr.lock -r sidecar/requirements-tsne.lock + pip-audit --disable-pip -r sidecar/requirements.lock -r sidecar/requirements-ocr.lock -r sidecar/requirements-tsne.lock # JS dependency CVE audit against the npm lockfile. `moderate` (not `high`) so a moderate-rated # sanitizer / proto-pollution advisory in the render path can't pass green (L-7). diff --git a/package-lock.json b/package-lock.json index c19663f4..aca65b81 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pm", - "version": "3.148.6-alpha", + "version": "3.148.7-alpha", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pm", - "version": "3.148.6-alpha", + "version": "3.148.7-alpha", "license": "AGPL-3.0-or-later", "dependencies": { "@fontsource/atkinson-hyperlegible": "^5.3.0", diff --git a/package.json b/package.json index 160ebf99..2c6d52ab 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "pm", "private": true, - "version": "3.148.6-alpha", + "version": "3.148.7-alpha", "license": "AGPL-3.0-or-later", "type": "module", "engines": { diff --git a/scripts/check-requirements-lock.mjs b/scripts/check-requirements-lock.mjs index e679effb..d431318c 100644 --- a/scripts/check-requirements-lock.mjs +++ b/scripts/check-requirements-lock.mjs @@ -16,6 +16,8 @@ // What it proves: // * every lock is stamped for the CURRENT sidecar/requirements.txt, requirements-optional.txt // and (for the optional components) the current base lock; +// * the base lock was resolved WITH the current sidecar/requirements-override.txt, and that file +// only overrides packages requirements.txt pins at the same version; // * the resolution targets the MIN_PYTHON floor Rust actually enforces; // * every entry is pinned with `==` — no range can slip in; // * every entry carries at least one sha256, so `--require-hashes` has something to check; @@ -32,6 +34,7 @@ import { fileURLToPath, pathToFileURL } from "node:url"; const SIDECAR_RS = "src-tauri/src/sidecar.rs"; const BASE_INPUT = "sidecar/requirements.txt"; +const BASE_OVERRIDE = "sidecar/requirements-override.txt"; const BASE_LOCK = "sidecar/requirements.lock"; const OPTIONAL_INPUT = "sidecar/requirements-optional.txt"; @@ -81,16 +84,59 @@ export function parseHeader(text) { const rawConstraint = stamp("constraint"); const constraint = rawConstraint ? source(rawConstraint) : null; if (rawConstraint && !constraint) problems.push("malformed `# pm-constraint:` stamp"); + const rawOverride = stamp("override"); + const override = rawOverride ? source(rawOverride) : null; + if (rawOverride && !override) problems.push("malformed `# pm-override:` stamp"); const rawPins = stamp("pins"); return { floor: stamp("python-floor"), input, constraint, + override, pins: rawPins ? rawPins.split(/\s+/).filter(Boolean) : null, problems, }; } +/** + * The base lock's override file: stamped, and narrow. Each line of it REPLACES every requirement on + * that package in the resolution, a dependency's declared bounds included — that is its whole job + * (fastembed's `pillow<13`). So a base lock resolved without it must fail, and the file may only + * name packages requirements.txt pins at the same version: a range, or a package nobody pins by + * name, would be a silent hole in bounds that nobody reviews. + */ +export function overrideProblems({ header, overrideText, requirementsText }) { + const problems = []; + if (!header.override) { + problems.push( + `no \`# pm-override:\` stamp — this lock was resolved without ${BASE_OVERRIDE}; run \`just lock-regen\``, + ); + } else if (header.override.path !== BASE_OVERRIDE) { + problems.push( + `\`# pm-override:\` names ${header.override.path}, but the base lock's override is ${BASE_OVERRIDE}`, + ); + } + const pinned = new Map( + parseRequirements(requirementsText).map((p) => [normalise(p.name), p.version]), + ); + for (const line of overrideText.replace(/\r\n/g, "\n").split("\n")) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith("#")) continue; + const [pin] = parseRequirements(trimmed); + if (!pin) { + problems.push( + `${BASE_OVERRIDE}: \`${trimmed}\` is not an exact \`==\` pin — an override may only pin`, + ); + } else if (pinned.get(normalise(pin.name)) !== pin.version) { + problems.push( + `${BASE_OVERRIDE}: \`${pin.raw}\` is not pinned at ${pin.version} in ${BASE_INPUT} — an override ` + + `lifts every declared bound on a package, so it may only name one PM pins at that version`, + ); + } + } + return problems; +} + /** Every pinned entry in a lock, with the hashes attached to it. */ export function parseEntries(text) { const lines = text.replace(/\r\n/g, "\n").split("\n"); @@ -162,7 +208,7 @@ export function problemsForLock({ path, text, floor, inputs, expectedPins }) { ); } - for (const key of ["input", "constraint"]) { + for (const key of ["input", "constraint", "override"]) { const stamped = header[key]; if (!stamped) continue; const actual = inputs[stamped.path]; @@ -226,14 +272,16 @@ export function problemsForLock({ path, text, floor, inputs, expectedPins }) { return problems; } -export function scan(root) { - const read = (rel) => readFileSync(join(root, rel), "utf8"); +/** `read` is injectable so a test can run the whole scan over the real tree with one file altered. */ +export function scan(root, read = (rel) => readFileSync(join(root, rel), "utf8")) { const { floor, pins } = readRustFacts(read(SIDECAR_RS)); const baseInput = read(BASE_INPUT); + const baseOverride = read(BASE_OVERRIDE); const optionalInput = read(OPTIONAL_INPUT); const baseLock = read(BASE_LOCK); const inputs = { [BASE_INPUT]: baseInput, + [BASE_OVERRIDE]: baseOverride, [OPTIONAL_INPUT]: optionalInput, [BASE_LOCK]: baseLock, }; @@ -253,6 +301,14 @@ export function scan(root) { expectedPins: parseRequirements(baseInput), }), ); + record( + BASE_LOCK, + overrideProblems({ + header: parseHeader(baseLock), + overrideText: baseOverride, + requirementsText: baseInput, + }), + ); const optionalPins = parseRequirements(optionalInput); let entryCount = parseEntries(baseLock).length; diff --git a/scripts/check-requirements-lock.test.mjs b/scripts/check-requirements-lock.test.mjs index 0d3befb4..00ad42cb 100644 --- a/scripts/check-requirements-lock.test.mjs +++ b/scripts/check-requirements-lock.test.mjs @@ -10,15 +10,19 @@ // Two details here bit during development and are locked as regressions: // * uv normalises names (`openTSNE` -> `opentsne`) while the Rust pin keeps the original case, so // every comparison has to be PEP 503-normalised; -// * a name can appear MORE THAN ONCE under different markers (numpy resolves three ways across +// * a name can appear MORE THAN ONCE under different markers (numpy resolves two ways across // the supported Python range), so "find the entry" is never a single lookup. // // Importing the module does not run the gate — entry-point guard at the bottom of it. +import { readFileSync } from "node:fs"; +import { join } from "node:path"; + import { describe, expect, it } from "vitest"; import { normalise, + overrideProblems, parseEntries, parseHeader, parseRequirements, @@ -32,7 +36,7 @@ const H = "a".repeat(64); const H2 = "b".repeat(64); /** A minimal but structurally real lock: header stamps, a forked pin, markers, hashes. */ -function lock({ floor = "3.10", inputSha, extra = "", body } = {}) { +function lock({ floor = "3.11", inputSha, extra = "", body } = {}) { return [ "# PM sidecar dependency lock - GENERATED, DO NOT EDIT BY HAND.", "#", @@ -45,7 +49,7 @@ function lock({ floor = "3.10", inputSha, extra = "", body } = {}) { `defusedxml==0.7.1 \\`, ` --hash=sha256:${H}`, ` # via -r sidecar/requirements.txt`, - `numpy==2.2.6 ; python_full_version < '3.11' \\`, + `numpy==2.2.6 ; python_full_version < '3.12' \\`, ` --hash=sha256:${H2}`, `numpy==2.5.1 ; python_full_version >= '3.12' \\`, ` --hash=sha256:${H}`, @@ -141,12 +145,12 @@ describe("parseRequirements", () => { describe("readRustFacts", () => { it("reads the floor and both optional pin constants", () => { const rust = [ - "const MIN_PYTHON: (u32, u32) = (3, 10);", + "const MIN_PYTHON: (u32, u32) = (3, 11);", 'const OPTIONAL_TSNE_PIN: &str = "openTSNE==1.0.4";', 'const OPTIONAL_OCR_PINS: &[&str] = &["rapidocr==3.9.2", "pi-heif==1.4.0"];', ].join("\n"); expect(readRustFacts(rust)).toEqual({ - floor: "3.10", + floor: "3.11", pins: { tsne: ["openTSNE==1.0.4"], ocr: ["rapidocr==3.9.2", "pi-heif==1.4.0"] }, }); }); @@ -164,12 +168,28 @@ describe("parseHeader", () => { }); const header = parseHeader(text); expect(header.problems).toEqual([]); - expect(header.floor).toBe("3.10"); + expect(header.floor).toBe("3.11"); expect(header.input).toEqual({ path: "sidecar/requirements.txt", sha: H }); expect(header.constraint).toEqual({ path: "sidecar/requirements.lock", sha: H2 }); + expect(header.override).toBeNull(); expect(header.pins).toEqual(["rapidocr==3.9.2", "pi-heif==1.4.0"]); }); + it("reads the base lock's override stamp", () => { + const header = parseHeader( + lock({ inputSha: H, extra: `# pm-override: sidecar/requirements-override.txt@sha256:${H2}` }), + ); + expect(header.problems).toEqual([]); + expect(header.override).toEqual({ path: "sidecar/requirements-override.txt", sha: H2 }); + }); + + it("rejects a malformed override stamp rather than reading it as absent", () => { + const header = parseHeader( + lock({ inputSha: H, extra: "# pm-override: sidecar/requirements-override.txt" }), + ); + expect(header.problems.join(" ")).toMatch(/malformed `# pm-override:`/); + }); + it("rejects a hand-written file with no stamps at all", () => { const header = parseHeader("foo==1.0\n"); expect(header.problems.join(" ")).toMatch(/pm-lock/); @@ -182,7 +202,7 @@ describe("problemsForLock", () => { const base = () => ({ path: "sidecar/fixture.lock", // not one of the three real locks, so no entry floor applies text: lock({ inputSha: sha256(REQS) }), - floor: "3.10", + floor: "3.11", inputs, expectedPins: parseRequirements(REQS).filter((p) => p.name === "defusedxml"), }); @@ -199,6 +219,20 @@ describe("problemsForLock", () => { expect(problems.join(" ")).toMatch(/has changed since this lock was generated/); }); + it("catches an override file edited without regenerating", () => { + const OVERRIDE = "sidecar/requirements-override.txt"; + const text = lock({ + inputSha: sha256(REQS), + extra: `# pm-override: ${OVERRIDE}@sha256:${sha256("pillow==13.0.0\n")}`, + }); + const current = { ...inputs, [OVERRIDE]: "pillow==13.0.0\n" }; + expect(problemsForLock({ ...base(), text, inputs: current })).toEqual([]); + const edited = { ...inputs, [OVERRIDE]: "pillow==13.0.1\n" }; + expect(problemsForLock({ ...base(), text, inputs: edited }).join(" ")).toMatch( + /requirements-override\.txt has changed since this lock was generated/, + ); + }); + it("catches a MIN_PYTHON floor that moved without a regeneration", () => { expect(problemsForLock({ ...base(), floor: "3.12" }).join(" ")).toMatch(/MIN_PYTHON/); }); @@ -239,14 +273,99 @@ describe("problemsForLock", () => { }); }); +describe("overrideProblems", () => { + // The override exists to resolve PAST a dependency's declared bound (fastembed 0.9.0's + // `pillow<13`). Two ways that goes wrong quietly: a base lock regenerated without it (pillow drops + // back below 13, or the resolution simply fails somewhere nobody looks), and an override that grows + // into a blanket hole — a range, or a package PM never pins and audits by name. + const OVERRIDE = "sidecar/requirements-override.txt"; + const REQ = "# top-level\nfastembed==0.9.0\npillow==13.0.0\n"; + const stamped = parseHeader( + lock({ inputSha: H, extra: `# pm-override: ${OVERRIDE}@sha256:${H2}` }), + ); + const ok = "# why, and when to remove it\npillow==13.0.0\n"; + + it("passes a stamped base lock whose override pins what requirements.txt pins", () => { + expect(overrideProblems({ header: stamped, overrideText: ok, requirementsText: REQ })).toEqual( + [], + ); + }); + + it("passes an override file with no pins left in it", () => { + // The removal condition empties the file and keeps it, so the stamp still has something to hash. + expect( + overrideProblems({ header: stamped, overrideText: "# none\n", requirementsText: REQ }), + ).toEqual([]); + }); + + it("fails a base lock resolved without the override", () => { + const unstamped = parseHeader(lock({ inputSha: H })); + expect( + overrideProblems({ header: unstamped, overrideText: ok, requirementsText: REQ }).join(" "), + ).toMatch(/no `# pm-override:` stamp/); + }); + + it("fails a stamp that names some other file", () => { + const other = parseHeader( + lock({ inputSha: H, extra: `# pm-override: sidecar/requirements.txt@sha256:${H2}` }), + ); + expect( + overrideProblems({ header: other, overrideText: ok, requirementsText: REQ }).join(" "), + ).toMatch(/the base lock's override is sidecar\/requirements-override\.txt/); + }); + + it("fails an override that is a range rather than a pin", () => { + expect( + overrideProblems({ + header: stamped, + overrideText: "pillow>=13\n", + requirementsText: REQ, + }).join(" "), + ).toMatch(/is not an exact `==` pin/); + }); + + it("fails an override on a package requirements.txt does not pin", () => { + expect( + overrideProblems({ + header: stamped, + overrideText: "numpy==2.3.0\n", + requirementsText: REQ, + }).join(" "), + ).toMatch(/`numpy==2\.3\.0` is not pinned at 2\.3\.0 in sidecar\/requirements\.txt/); + }); + + it("fails an override at a different version from the top-level pin", () => { + expect( + overrideProblems({ + header: stamped, + overrideText: "Pillow==13.0.1\n", + requirementsText: REQ, + }).join(" "), + ).toMatch(/is not pinned at 13\.0\.1/); + }); +}); + describe("scan", () => { + const root = new URL("..", import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, "$1"); + it("passes on the real tree and parses a plausible number of entries", () => { - const { failures, entryCount } = scan( - new URL("..", import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, "$1"), - ); + const { failures, entryCount } = scan(root); expect(failures).toEqual([]); // Sixty-odd base packages plus both optional components. A collapse to a handful means the // parser broke; this is the same floor `problemsForLock` enforces, asserted end to end. expect(entryCount).toBeGreaterThan(80); }); + + it("runs the override check on the real base lock", () => { + // The real tree with only the base lock's override stamp removed: what a regeneration that + // forgot `--override` would leave behind. + const real = (rel) => readFileSync(join(root, rel), "utf8"); + const read = (rel) => + rel === "sidecar/requirements.lock" + ? real(rel).replace(/^# pm-override:.*\n/m, "") + : real(rel); + expect(scan(root, read).failures.join("\n")).toMatch( + /sidecar\/requirements\.lock: no `# pm-override:` stamp/, + ); + }); }); diff --git a/scripts/regen-sidecar-locks.mjs b/scripts/regen-sidecar-locks.mjs index adbea4c0..303f129e 100644 --- a/scripts/regen-sidecar-locks.mjs +++ b/scripts/regen-sidecar-locks.mjs @@ -9,7 +9,7 @@ // Needs `uv` on PATH. uv rather than pip-tools for one reason that decided the whole shape of // this: `--universal` resolves across every platform AND every Python version at once, emitting // environment markers where the resolution forks. One file therefore covers Windows, macOS and -// Linux and every CPython from MIN_PYTHON upwards — which matters because MIN_PYTHON is 3.10 and +// Linux and every CPython from MIN_PYTHON upwards — which matters because MIN_PYTHON is 3.11 and // macOS prefers a system interpreter over PM's downloaded 3.12, so the real deployed range is // much wider than "the version we ship". // @@ -27,6 +27,7 @@ import { fileURLToPath, pathToFileURL } from "node:url"; const ROOT = join(dirname(fileURLToPath(import.meta.url)), ".."); const SIDECAR_RS = "src-tauri/src/sidecar.rs"; const BASE_INPUT = "sidecar/requirements.txt"; +const BASE_OVERRIDE = "sidecar/requirements-override.txt"; const BASE_LOCK = "sidecar/requirements.lock"; const read = (rel) => readFileSync(join(ROOT, rel), "utf8"); @@ -73,7 +74,11 @@ function header(lines) { "#", "# Universal resolution: ONE file covers Windows, macOS and Linux and every CPython from the", "# MIN_PYTHON floor upwards, with environment markers where the resolution forks by interpreter", - "# (numpy, onnxruntime, pandas, av) or by platform (colorama, magika, pyreadline3, hf-xet).", + "# (numpy, and scipy for t-SNE) or by platform (colorama, magika, tzdata, hf-xet).", + "#", + "# The base lock is also resolved with sidecar/requirements-override.txt, which lifts a declared", + "# bound in one dependency's metadata (that file says which, and why), so every install of these", + "# locks passes `--no-deps`: pip would otherwise re-apply the bound and refuse the lock.", "#", ...lines, "", @@ -101,14 +106,19 @@ function main() { const floor = pythonFloor(rust); const pins = optionalPins(rust); const baseInputHash = sha256(read(BASE_INPUT)); + const overrideHash = sha256(read(BASE_OVERRIDE)); + // `--override` on the base compile only: it lifts a dependency's declared bound (fastembed's + // `pillow<13`, see that file's header). The optional locks inherit the result through the + // base-lock constraint below rather than taking the override themselves. const base = compile({ label: BASE_LOCK, - args: [...UV, BASE_INPUT, "--python-version", floor], + args: [...UV, BASE_INPUT, "--override", BASE_OVERRIDE, "--python-version", floor], stamps: [ "# pm-lock: 1", `# pm-python-floor: ${floor}`, `# pm-input: ${BASE_INPUT}@sha256:${baseInputHash}`, + `# pm-override: ${BASE_OVERRIDE}@sha256:${overrideHash}`, ], }); writeFileSync(join(ROOT, BASE_LOCK), base); diff --git a/sidecar/licences.json b/sidecar/licences.json index c2db5a67..8ee22315 100644 --- a/sidecar/licences.json +++ b/sidecar/licences.json @@ -40,9 +40,9 @@ } }, "av": { - "versions": ["17.1.0", "18.1.0"], + "versions": ["18.1.0"], "licence": "BSD-3-Clause", - "stamp": "b2b5d3746cadfbb34e2a29918ad9b713", + "stamp": "6cc25597aa56ec8396d93ec4161b64f5", "evidence": { "expression": "BSD-3-Clause", "license": null, @@ -60,9 +60,9 @@ } }, "certifi": { - "versions": ["2026.7.22"], + "versions": ["2026.10.19"], "licence": "MPL-2.0", - "stamp": "2f8f467a617c791ee022f6edc5e714e6", + "stamp": "65106fb9319caba4c9e02c330622e097", "evidence": { "expression": null, "license": "MPL-2.0", @@ -121,16 +121,6 @@ }, "note": "`license` is an empty string. LICENSE.txt in the project is the three-clause text." }, - "coloredlogs": { - "versions": ["15.0.1"], - "licence": "MIT", - "stamp": "fd371ec1a116116e68ecb69950224bc3", - "evidence": { - "expression": null, - "license": "MIT", - "classifiers": ["License :: OSI Approved :: MIT License"] - } - }, "colorlog": { "versions": ["6.12.0"], "licence": "MIT", @@ -182,17 +172,6 @@ "classifiers": ["License :: OSI Approved :: MIT License"] } }, - "exceptiongroup": { - "versions": ["1.3.1"], - "licence": "MIT AND PSF-2.0", - "stamp": "a11291e74a36928191e31b0dfd435f34", - "evidence": { - "expression": null, - "license": null, - "classifiers": ["License :: OSI Approved :: MIT License"] - }, - "note": "Classifier says MIT only; the LICENSE file is MIT plus the PSF licence covering the CPython code it backports." - }, "fastembed": { "versions": ["0.9.0"], "licence": "Apache-2.0", @@ -295,16 +274,6 @@ "classifiers": ["License :: OSI Approved :: Apache Software License"] } }, - "humanfriendly": { - "versions": ["10.0"], - "licence": "MIT", - "stamp": "5a29e2c972e72da4620c7c2813d89ed6", - "evidence": { - "expression": null, - "license": "MIT", - "classifiers": ["License :: OSI Approved :: MIT License"] - } - }, "idna": { "versions": ["3.19"], "licence": "BSD-3-Clause", @@ -398,17 +367,6 @@ }, "note": "`license` is the entire MIT text pasted into the one-line field." }, - "mpmath": { - "versions": ["1.3.0"], - "licence": "BSD-3-Clause", - "stamp": "d310b6402ae542a51cd242229fb3d68c", - "evidence": { - "expression": null, - "license": "BSD", - "classifiers": ["License :: OSI Approved :: BSD License"] - }, - "note": "`license: \"BSD\"`; the project's LICENSE is the three-clause text." - }, "narwhals": { "versions": ["2.25.0"], "licence": "MIT", @@ -420,27 +378,15 @@ } }, "numpy": { - "versions": ["2.2.6", "2.4.6", "2.5.2"], + "versions": ["2.4.6", "2.5.2"], "licence": "BSD-3-Clause AND 0BSD AND MIT AND Zlib AND CC0-1.0", - "stamp": "2588cc6c0587e23a6376427e4dc878e1", + "stamp": "f8e3aa1fc386934e02a8c0fc40167240", "evidence": { - "2.2.6": { - "expression": null, - "license": "Copyright (c) 2005-2024, NumPy Developers. All rights reserved. Redistribution and use in source and binary forms, wi...", - "classifiers": ["License :: OSI Approved :: BSD License"] - }, - "2.4.6": { - "expression": "BSD-3-Clause AND 0BSD AND MIT AND Zlib AND CC0-1.0", - "license": null, - "classifiers": [] - }, - "2.5.2": { - "expression": "BSD-3-Clause AND 0BSD AND MIT AND Zlib AND CC0-1.0", - "license": null, - "classifiers": [] - } + "expression": "BSD-3-Clause AND 0BSD AND MIT AND Zlib AND CC0-1.0", + "license": null, + "classifiers": [] }, - "note": "The three pinned versions do not declare the same thing. 2.2.6 predates PEP 639 and pastes the bare BSD-3-Clause text into `license`; 2.4.6 and 2.5.1 declare the full expression, which names the licences of the code numpy vendors (0BSD, MIT, Zlib, CC0-1.0) alongside its own BSD-3-Clause. The vendored code is in 2.2.6 too — only the declaration is new — so the fuller expression is recorded for all three." + "note": "Both pinned versions declare the full expression, which names the licences of the code numpy vendors (0BSD, MIT, Zlib, CC0-1.0) alongside its own BSD-3-Clause. (2.2.6, which pasted only the BSD-3-Clause text, left the lock with Python 3.10.)" }, "olefile": { "versions": ["0.47"], @@ -465,9 +411,9 @@ "note": "Classifier only; the project's LICENSE is the three-clause text." }, "onnxruntime": { - "versions": ["1.23.2", "1.29.0"], + "versions": ["1.29.0"], "licence": "MIT", - "stamp": "88f848740d394d4012bfefd146fd81c7", + "stamp": "0871ccf3fe3dfbda712227de8ff73e63", "evidence": { "expression": null, "license": "MIT License", @@ -516,20 +462,13 @@ } }, "pandas": { - "versions": ["2.3.3", "3.0.5"], + "versions": ["3.0.5"], "licence": "BSD-3-Clause", - "stamp": "4ef320aea45e48b06dc0b13a82a70145", + "stamp": "9c95aeca723365bb75ebab485d8733c0", "evidence": { - "2.3.3": { - "expression": null, - "license": "BSD 3-Clause License Copyright (c) 2008-2011, AQR Capital Management, LLC, Lambda Foundry, Inc. and PyData Developmen...", - "classifiers": ["License :: OSI Approved :: BSD License"] - }, - "3.0.5": { - "expression": null, - "license": "BSD 3-Clause License Copyright (c) 2008-2011, AQR Capital Management, LLC, Lambda Foundry, Inc. and PyData Developmen...", - "classifiers": ["License :: OSI Approved :: BSD License"] - } + "expression": null, + "license": "BSD 3-Clause License Copyright (c) 2008-2011, AQR Capital Management, LLC, Lambda Foundry, Inc. and PyData Developmen...", + "classifiers": ["License :: OSI Approved :: BSD License"] }, "note": "`license` is the entire licence text pasted into the one-line field; it is the three-clause text." }, @@ -566,14 +505,15 @@ "note": "The bindings are BSD-3-Clause (`license`), but the wheel is not only the bindings. Its own dist-info/licenses/LICENSES_bundled.txt states: \"License for pi-heif binary wheels: LGPLv3, due to base library licenses\" — libheif and libde265, both LGPLv3, shipped as DLLs/shared objects alongside the Python code. That is what the LGPLv3 classifier is about, and why the compound expression is the honest record. Recorded as -only rather than -or-later because upstream writes \"LGPLv3\" without stating \"or later\", and the stricter reading is the safe one. This is also why PM pins pi-heif and not pillow-heif: same author, same repository, same API, but pillow-heif's wheels add the x265 HEVC encoder and are GPLv2 as a whole." }, "pillow": { - "versions": ["12.3.0"], + "versions": ["13.0.0"], "licence": "MIT-CMU", - "stamp": "76971e148716b4dbfdde09739531af2a", + "stamp": "e971bc4ca21449fdd2f778c70d99be28", "evidence": { "expression": "MIT-CMU", "license": null, "classifiers": [] - } + }, + "note": "Read in the 13.0.0 wheels (cp312 Linux x86_64, macOS arm64, Windows amd64): dist-info/licenses/LICENSE is Pillow's MIT-CMU text followed by the licences of the native libraries the wheel bundles, all permissive. FreeType offers its own FTL or GPL-2.0-or-later, the FTL being the permissive choice; liblzma is public domain. The Linux and macOS files are byte-identical to 12.3.0's; the Windows one differs only in the harfbuzz and libjpeg-turbo versions and libjpeg-turbo's note on the libspng code its cjpeg/djpeg tools use (BSD-2-Clause and libpng-2.0, subsumed by its IJG and BSD-3-Clause terms)." }, "protobuf": { "versions": ["7.36.0"], @@ -628,17 +568,6 @@ }, "note": "`license` reads \"BSD-3-Clause, Apache-2.0, dependency licenses\" — the bindings are dual-licensed (either satisfies), and the bundled PDFium is itself BSD-3-Clause." }, - "pyreadline3": { - "versions": ["3.5.6"], - "licence": "BSD-3-Clause", - "stamp": "03a421d0f90d9e6bd75a36f865d6ada5", - "evidence": { - "expression": null, - "license": "BSD", - "classifiers": ["License :: OSI Approved :: BSD License"] - }, - "note": "`license: \"BSD\"`; LICENSE.md is the three-clause text." - }, "python-dateutil": { "versions": ["2.9.0.post0"], "licence": "Apache-2.0 OR BSD-3-Clause", @@ -673,16 +602,6 @@ "classifiers": ["License :: OSI Approved :: MIT License"] } }, - "pytz": { - "versions": ["2026.3.post1"], - "licence": "MIT", - "stamp": "5912a8fdc1f876cdc07af2c37a10d1ce", - "evidence": { - "expression": null, - "license": "MIT", - "classifiers": ["License :: OSI Approved :: MIT License"] - } - }, "pyyaml": { "versions": ["6.0.3"], "licence": "MIT", @@ -714,9 +633,9 @@ } }, "scikit-learn": { - "versions": ["1.7.2", "1.9.0"], + "versions": ["1.9.0"], "licence": "BSD-3-Clause", - "stamp": "baa048ca53b05ce71759838320e944ef", + "stamp": "57e96c78232c9c9eac4fc44f3047aa31", "evidence": { "expression": "BSD-3-Clause", "license": null, @@ -724,15 +643,10 @@ } }, "scipy": { - "versions": ["1.15.3", "1.17.1", "1.18.1"], + "versions": ["1.17.1", "1.18.1"], "licence": "BSD-3-Clause", - "stamp": "bf0d942ba99183242f795defc7b10773", + "stamp": "daed36ea6c2fb70ef7b9f5d9641cfc51", "evidence": { - "1.15.3": { - "expression": null, - "license": "Copyright (c) 2001-2002 Enthought, Inc. 2003-2024, SciPy Developers. All rights reserved. Redistribution and use in s...", - "classifiers": ["License :: OSI Approved :: BSD License"] - }, "1.17.1": { "expression": null, "license": "Copyright (c) 2001-2002 Enthought, Inc. 2003, SciPy Developers. All rights reserved. Redistribution and use in source...", @@ -787,17 +701,6 @@ "classifiers": ["License :: OSI Approved :: MIT License"] } }, - "sympy": { - "versions": ["1.14.0"], - "licence": "BSD-3-Clause", - "stamp": "f58ba55b49068fa960372ebe5205a771", - "evidence": { - "expression": null, - "license": "BSD", - "classifiers": ["License :: OSI Approved :: BSD License"] - }, - "note": "`license: \"BSD\"`. The LICENSE file is the three-clause text, followed by MIT notices for third-party code sympy vendors." - }, "threadpoolctl": { "versions": ["3.6.0"], "licence": "BSD-3-Clause", diff --git a/sidecar/pm_sidecar.py b/sidecar/pm_sidecar.py index c4ce2d1d..cb165af8 100644 --- a/sidecar/pm_sidecar.py +++ b/sidecar/pm_sidecar.py @@ -1215,10 +1215,37 @@ def get_ocr_engine(): return _ocr_engine +# The photo extensions (PHOTO_EXTS in ingest.rs routes exactly these to `analyze_image`), each with +# the Pillow format it names, and together the only formats a photo is ever decoded as. Left to +# itself, `Image.open` picks the decoder from the file's CONTENT out of every format Pillow knows, +# so a TIFF, FlashPix, EPS or any of its ~40 others renamed `holiday.jpg` reached that format's +# decoder, and most of the advisories published against Pillow from 2026-10-17 are in exactly +# those decoders. A photo is therefore opened as one of these four photo formats, the one its own +# extension names tried first. Not as that one alone: a PNG or a WebP saved as .jpg, or a JPEG as +# .png, is common, refusing it would block nothing (naming the file correctly reaches the same +# decoder), and it would stay unread for good, because ingest skips content it has indexed already. +# MPO, the multi-picture JPEG some cameras write, is not listed because it cannot be: Pillow +# registers no opener of its own for it (asking for "MPO" raises KeyError), and its JPEG opener +# hands a multi-picture file over to MPO by itself. HEIF exists only once pi-heif has registered +# it; without it a .heic is tried as the other three, so a JPEG saved as .heic still reads, and a +# real HEIC matches none of them and is refused. +_PHOTO_FORMATS = { + ".jpg": ("JPEG",), + ".jpeg": ("JPEG",), + ".png": ("PNG",), + ".webp": ("WEBP",), + ".heic": ("HEIF",), +} +_ANY_PHOTO_FORMAT = tuple(dict.fromkeys(f for own in _PHOTO_FORMATS.values() for f in own)) + + def _open_image(path): - """Open an image with Pillow, registering the HEIC opener if pi-heif is available. Pillow is - already present (a markitdown dependency); pi-heif ships in the optional OCR component, so - HEIC decoding needs that component installed — a non-HEIC image opens regardless. + """Open a photo with Pillow, ONLY as a photo format (see `_PHOTO_FORMATS`), the one its + extension names first, registering the HEIC opener if pi-heif is available. Pillow is already + present (a markitdown dependency); pi-heif ships in the optional OCR component, so HEIC decoding + needs that component installed — a non-HEIC image opens regardless. Raises on anything else: an + extension that is not a photo's, and a file whose content is not one of the photo formats + registered (a TIFF named .jpg, or a real HEIC without pi-heif). pi-heif rather than pillow-heif: same bindings, same author, same repository, but the decode-only build. See OPTIONAL_OCR_PINS in sidecar.rs for why the encoder build had to go. @@ -1234,9 +1261,21 @@ def _open_image(path): register_heif_opener() except Exception: - pass # pi-heif absent → HEIC won't open, every other format still does + pass # pi-heif absent → HEIC won't open (Pillow knows no "HEIF"), the rest still do _heif_registered = True - return Image.open(path) + # Every plugin's opener registered (Pillow loads WEBP's lazily), so a format missing from OPEN + # now is one Pillow cannot open at all: HEIF without pi-heif. + Image.init() + ext = os.path.splitext(str(path))[1].lower() + # An extension that is not a photo's is refused here, by name. A photo's own format may be + # missing (HEIF without pi-heif): it is then tried as the formats that are registered, never as + # Pillow's every format. Never `Image.open` without a list: that is the very thing this + # function exists to refuse. + if ext not in _PHOTO_FORMATS: + raise ValueError("not a photo type PM can open") + own = tuple(f for f in _PHOTO_FORMATS[ext] if f in Image.OPEN) + others = tuple(f for f in _ANY_PHOTO_FORMAT if f in Image.OPEN and f not in own) + return Image.open(path, formats=own + others) def _gps_to_decimal(value, ref): @@ -1297,23 +1336,30 @@ def _exif_meta(img): return capture_date, lat, lon -def _run_ocr(engine, img, path): - """Recognise text in an image, returning the joined lines (newest rapidocr returns an object - with a `.txts` tuple; tolerate the older list-of-[box,text,score] shape too). Pass a decoded RGB - array when we already opened the image (HEIC works); else hand rapidocr the path directly. +def _decode_pixels(img): + """The RGB pixels of an image `_open_image` opened, as the array rapidocr takes. Opening reads + only the header, so this is where a damaged file (a JPEG cut off by an interrupted download or a + half-synced folder) first fails, with Pillow's "image file is truncated".""" + import numpy as np + + return np.array(img.convert("RGB")) + + +def _run_ocr(engine, pixels): + """Recognise text in pixels PM decoded itself (`_decode_pixels`). Never a path: given one, + rapidocr opens the file itself with a bare `Image.open`, which is every decoder Pillow has — the + route `_PHOTO_FORMATS` exists to close. rapidocr prints init/inference chatter, which would corrupt the reply channel. `main()` now redirects stdout around EVERY handler, so this call — and the engine construction — no longer carry a guard of their own. """ - if img is not None: - import numpy as np + return _ocr_lines(engine(pixels)) - target = np.array(img.convert("RGB")) - else: - target = path # let rapidocr read the file itself (non-HEIC) - result = engine(target) +def _ocr_lines(result): + """rapidocr's result as the joined lines: newest rapidocr returns an object with a `.txts` + tuple; tolerate the older list-of-[box,text,score] shape too.""" txts = getattr(result, "txts", None) if txts: return "\n".join(t for t in txts if t) @@ -1331,8 +1377,11 @@ def do_analyze_image(params): """Analyse one image for photo ingestion: EXIF capture metadata (always) + OCR text (only when `run_ocr`). The Rust side sets `run_ocr` from whether the optional component is installed, so a user who declined OCR still gets dimensions + EXIF (date/location) for the metadata chunk. - OCR output is untrusted text — `clean_text`-ed like every other string we return. An unreadable - image (e.g. HEIC without pi-heif) yields nulls; Rust falls back to a filename/ingest date. + OCR output is untrusted text — `clean_text`-ed like every other string we return. An image PM + cannot open (damaged, not a photo format, HEIC without pi-heif) yields null metadata and no OCR + at all; Rust reads the null size as "could not open", tells the user, and falls back to a + filename/ingest date. One that opens but whose pixels will not decode for OCR comes back the + same way, keeping any EXIF its header held. """ path = params["path"] _guard_file_size(path) @@ -1345,13 +1394,32 @@ def do_analyze_image(params): width, height = img.size capture_date, lat, lon = _exif_meta(img) except Exception: - pass # unreadable / HEIC without codec — metadata stays null, OCR may still try the path + pass # could not open it as a photo — metadata stays null, and OCR is skipped below ocr_text = "" ocr_ran = False - if run_ocr: + pixels = None + # Only an image PM itself opened goes to OCR. There used to be a fallback that handed rapidocr + # the path when PM's own open failed, and rapidocr opens a path with no format restriction. + if run_ocr and img is not None: + # Decoded on its own, before the engine is even loaded: a file whose pixels will not decode + # is the FILE's fault, and inside the OCR call below it read as ocr_ran=false, so the row + # blamed text recognition and sent the user to repair a component that works. No size goes + # back instead, which Rust reports as "could not open"; the EXIF from the header stays. + # With OCR off nothing needs the pixels, so nothing decodes them, as before. + try: + pixels = _decode_pixels(img) + except Exception as exc: + sys.stderr.write(f"pm_sidecar: could not decode {path!r}, so no OCR: {exc}\n") + sys.stderr.flush() + width = height = None + if img is not None: + # Everything is read from the file by now. Pillow keeps it open until the pixels are, which + # with OCR off is never, so it lingered until garbage collection. + img.close() + if pixels is not None: try: - ocr_text = _run_ocr(get_ocr_engine(), img, path) + ocr_text = _run_ocr(get_ocr_engine(), pixels) ocr_ran = True except ModelNotCached: # NOT a broken component: the offline worker just needs the fetcher to download diff --git a/sidecar/requirements-ocr.lock b/sidecar/requirements-ocr.lock index 149496e7..53c5c115 100644 --- a/sidecar/requirements-ocr.lock +++ b/sidecar/requirements-ocr.lock @@ -13,19 +13,23 @@ # # Universal resolution: ONE file covers Windows, macOS and Linux and every CPython from the # MIN_PYTHON floor upwards, with environment markers where the resolution forks by interpreter -# (numpy, onnxruntime, pandas, av) or by platform (colorama, magika, pyreadline3, hf-xet). +# (numpy, and scipy for t-SNE) or by platform (colorama, magika, tzdata, hf-xet). +# +# The base lock is also resolved with sidecar/requirements-override.txt, which lifts a declared +# bound in one dependency's metadata (that file says which, and why), so every install of these +# locks passes `--no-deps`: pip would otherwise re-apply the bound and refuse the lock. # # pm-lock: 1 -# pm-python-floor: 3.10 +# pm-python-floor: 3.11 # pm-input: sidecar/requirements-optional.txt@sha256:bbfc9c5804fc0d4f7c282c010593a6968eec9738d2847a9deb41fc6a9d84486c -# pm-constraint: sidecar/requirements.lock@sha256:e4a2c8e1f26574688977e9c57a62e12c147b129f3e83241a377f02e14f639f00 +# pm-constraint: sidecar/requirements.lock@sha256:5c7a00de93e84c336c436ca72a2bde9e536f858ced0df80821a03f86908e1e94 # pm-pins: rapidocr==3.10.0 pi-heif==1.4.0 antlr4-python3-runtime==4.9.3 \ --hash=sha256:f224469b4168294902bb1efa80a8bf7855f24c99aef99cbefc1bcd3cce77881b # via omegaconf -certifi==2026.7.22 \ - --hash=sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775 \ - --hash=sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55 +certifi==2026.10.19 \ + --hash=sha256:035c1e85572b141bf1c3a3eff0e33a8eb74917522f2559075ff8d2fea780a1ae \ + --hash=sha256:42d35340cc5b9ba78e818ddbff0c5b1c2b779de448a340fd1b989f5fa8670e1b # via # -c sidecar/requirements.lock # requests @@ -222,68 +226,7 @@ idna==3.19 \ # via # -c sidecar/requirements.lock # requests -numpy==2.2.6 ; python_full_version < '3.11' \ - --hash=sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff \ - --hash=sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47 \ - --hash=sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84 \ - --hash=sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d \ - --hash=sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6 \ - --hash=sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f \ - --hash=sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b \ - --hash=sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49 \ - --hash=sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163 \ - --hash=sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571 \ - --hash=sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42 \ - --hash=sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff \ - --hash=sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491 \ - --hash=sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4 \ - --hash=sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566 \ - --hash=sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf \ - --hash=sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40 \ - --hash=sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd \ - --hash=sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06 \ - --hash=sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282 \ - --hash=sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680 \ - --hash=sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db \ - --hash=sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3 \ - --hash=sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90 \ - --hash=sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1 \ - --hash=sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289 \ - --hash=sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab \ - --hash=sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c \ - --hash=sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d \ - --hash=sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb \ - --hash=sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d \ - --hash=sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a \ - --hash=sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf \ - --hash=sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1 \ - --hash=sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2 \ - --hash=sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a \ - --hash=sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543 \ - --hash=sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00 \ - --hash=sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c \ - --hash=sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f \ - --hash=sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd \ - --hash=sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868 \ - --hash=sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303 \ - --hash=sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83 \ - --hash=sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3 \ - --hash=sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d \ - --hash=sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87 \ - --hash=sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa \ - --hash=sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f \ - --hash=sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae \ - --hash=sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda \ - --hash=sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915 \ - --hash=sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249 \ - --hash=sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de \ - --hash=sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8 - # via - # -c sidecar/requirements.lock - # opencv-python-headless - # rapidocr - # shapely -numpy==2.4.6 ; python_full_version == '3.11.*' \ +numpy==2.4.6 ; python_full_version < '3.12' \ --hash=sha256:001fbb8e08d942dd57599e781f2472269ee7f2755fae407b4f67b2f0b17da3f1 \ --hash=sha256:0280e0356c0829a18d9de1cb7eee50ec22ca639878d7240307ca0943d73cd2c4 \ --hash=sha256:043191bfa8eab18c776647b62723ac9dddece59743b13f49b2016094129c2b3f \ @@ -497,94 +440,90 @@ pi-heif==1.4.0 \ --hash=sha256:eeea6d0291bb16f674b2de5088630c580a101b90bcca4990a7dca236959cbf9d \ --hash=sha256:f5ecc06e86a0170ed3782ec27c854b5b334d44d7651d5c0bbff0c123ea44d5f9 \ --hash=sha256:fcfb9422c9c9c8f84d6a18ebf256b872556bf4f05697430e790a16bb197a8f26 -pillow==12.3.0 \ - --hash=sha256:00808c5e14ef63ac5161091d242999076604ff74b883423a11e5d7bbb38bf756 \ - --hash=sha256:04f01d28a6aaff387bf842a13be313df23ba0597a44f1a976c9feb3c6ff4711a \ - --hash=sha256:06ff022112bc9cbf83b60f8e028d94ad87b60621706487e65f673de61610ab59 \ - --hash=sha256:0740a512dc522224c77d9aa5a8d70d8b7d73fb91f2c21125d8d025d3b8990e45 \ - --hash=sha256:0847a763afefb695bc912d7c131e7e0632d4edc1d8698f58ddabec8e46b8b6d3 \ - --hash=sha256:0dd2064cbc55aaec028ef5fbb60fa47bb6c3e7918e07ff17935284b227a9d2df \ - --hash=sha256:0feb2e9d6ad6c9e3c06effe9d00f3f1e618a6643273576b016f591e9315a7139 \ - --hash=sha256:10e41f0fbf1eec8cfd234b8fe17a4caac7c9d0db4c204d3c173a8f9f6ef3232b \ - --hash=sha256:1182d52bc2d5e5d7d0949503aa7e36d12f42205dc287e4883f407b1988820d39 \ - --hash=sha256:164b31cd1a0490ab6efae01aa5df49da7061be0af1b30e035b6e9a1bfe34ee6e \ - --hash=sha256:1657923d2d45afb66526e5b933e5b3052e6bdea196c90d3abb2424e18c77dae8 \ - --hash=sha256:186941b6aef820ad110fb01fb06eb925374dc3a21b17e37ec9a53b250c6fe2d1 \ - --hash=sha256:1cca606cd25738df4ed873d5ad46bbdb3d83b5cbca291f6b4ff13a4df6b0bbe8 \ - --hash=sha256:21900ce7ba264168cd50defae43cd75d25c833ad4ad6e73ffc5596d12e25ac89 \ - --hash=sha256:236ff70b9312fb68943c703aa842ca6a758abfa45ac187a5e7c1452e96ef72b5 \ - --hash=sha256:23aceaa007d6172b02c277f0cd359c79492bbb14f7072b4ede9fbcaf20648130 \ - --hash=sha256:23d27a3e0307ec2244cc51e7287b919aa68d097504ebe19df4e76a98a3eea5bd \ - --hash=sha256:24870b09b224f7ae3c39ed07d10e819d06f8720bc551847b1d623832b5b0e28d \ - --hash=sha256:251bf95b67017e27b13d82f5b326234ca62d70f9cf4c2b9032de2358a3b12c7b \ - --hash=sha256:25b9b82bb22e6e2b3cd07b39c68b7b862001226cb3dff7130d1cb914121b39ed \ - --hash=sha256:28ce87c5ab450a9dd970b52e5aca5fe63ed432d18a2eaddd1979a00a1ba24ace \ - --hash=sha256:300557495eb45ebb8aec96c2da9c4be642fbf7cd937278b4013ba894ea8eb0eb \ - --hash=sha256:30f2aa603c41533cc25c05acd0da21636e84a315768feb631c937177db558931 \ - --hash=sha256:331b624368d4f1d069149002f25f44bc61c8919ce8ddb3c45bdad8f6e2d89510 \ - --hash=sha256:37d6d0a00072fd2948eb22bce7e1475f34569d90c87c59f7a2ec59541b77f7a6 \ - --hash=sha256:37dc8f7bbb66efe481bb60defacef820c950c24713fb44962ed6aa2a50966de1 \ - --hash=sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce \ - --hash=sha256:3edce1d53195db527e0191f84b71d02022de0540bf43a16ed734ed7537b07385 \ - --hash=sha256:446c34dcc4324b084a53b705127dc15717b22c5e140ae0a3c38349d4efec071e \ - --hash=sha256:4998562bf62a445225f22e07c896bb04b35b1b1f2eb6d760584c9c51d7a5f78c \ - --hash=sha256:4b0a7fe987b14c31ebda6083f74f22b561fd3739bc0ac51e019622e3d72668c7 \ - --hash=sha256:4e8c2a84d977f50b9daed6eeaf3baef67d00d5d74d932288f02cb94518ee3ace \ - --hash=sha256:4f883547d4b7f0495ebe7056b0cc2aea76094e7a4abc8e933540f3271df27d9c \ - --hash=sha256:514435a37670e3e5e08f3945b68718b6ed329bb84367777e16f9f4dfe1e61a0f \ - --hash=sha256:53aa02d20d10c3d814d536aa4e5ac9b84ca0ff5a88377963b085ad6822f93e64 \ - --hash=sha256:5594fc43d548a7ed94949d139aa1341b270f1863f11cfd37f5a6c8b778a6b67f \ - --hash=sha256:571b9fcb07b97ef3a492028fb3d2dc0993ca23a06138b0315286566d29ef718a \ - --hash=sha256:57b3d78c95ba9059768b10e28b813002261d3f3dfc55cc48b0c988f625175827 \ - --hash=sha256:5afb51d599ea772b8365ae807ae557f18bccfe46ab261fd1c2a9ed700fc6eb17 \ - --hash=sha256:6b02afb9b97f65fbca5f31db6a2a3ba21aa93030225f150fa3f249717e938fb4 \ - --hash=sha256:6c0016e7b354317c4e9e525b937ac8596c38d2d232b419529b9cd7a1cd46e39a \ - --hash=sha256:71d6097b330eea8fd15097780c8e89cb1a8ce7838669f48c5bacd6f663dd4701 \ - --hash=sha256:756c768d0c9c2955feb7a56c37ea24aea2e369f8d36a88da270b6a9f19e62b5e \ - --hash=sha256:78cb2c6865a35ab8ff8b75fd122f6033b92a62c82801110e48ddd6c936a45d91 \ - --hash=sha256:7a743ff716f746fc19a9557f60dab1600d4613255f8a7aeb3cdde4db7eb15a66 \ - --hash=sha256:85f998ea1848bc6757289e739cfbdda3a04adfd58b02fc018ce54d754a5ce468 \ - --hash=sha256:8728f216dcdb6e6d555cf971cb34076139ad74b31fc2c14da4fafc741c5f6217 \ - --hash=sha256:877c3f311ff35410f690861c4409e7ccbf0cd2f878e50628a28e5a0bb689e658 \ - --hash=sha256:8cd2f7bdda092d99c9fc2fb7391354f306d01443d22785d0cbfafa2e2c8bb418 \ - --hash=sha256:8e95e1385e4998ae9694eeaa4730ba5457ff61185b3a55e2e7bea0880aef452a \ - --hash=sha256:962864dc93511324d51ddbb5b9f8731bf71675b93ca612a07441896f4688fb8c \ - --hash=sha256:9cf95fe4d0f84c82d282745d9bb08ad9f926efa00be4697e767b814ce40d4330 \ - --hash=sha256:9e881fca225083806662a5c43d627d215f258ff43c890f831966c7d7ba9c7402 \ - --hash=sha256:a2b55dd6b2a4c4b7d87ffa56bdb33fdc5fdb9a462173861a7bc097f17d91cb09 \ - --hash=sha256:a45650e8ce7fafffd731db8550230db6b0d306d181a90b67d3e6bca2f1990930 \ - --hash=sha256:a876864214e136f0eb367788dbd7df045f4806801518e2cfe9e13229cfe06d8f \ - --hash=sha256:ae26d61dfa7a47befdc7572b521024e8745f3d809bd95ca9505a7bba9ef849ec \ - --hash=sha256:af8d94b0db561cf68b88a267c5c44b49e134f525d0dc2cb7ed413a66bc23559a \ - --hash=sha256:b343699e8308bdc51978310e1c959c584e7869cc8c40780058c87da7781a1e94 \ - --hash=sha256:b3c777e849237620b022f7f297dd67705f9f5cf1685f09f02e46f93e92725468 \ - --hash=sha256:b629de27fda84b42cde7edef0d85f13b958b47f6e9bbcbba9b673c562a89bd8b \ - --hash=sha256:ba09209fbe443b4acccebe845d8a138b89a8f4fbaeedd44953490b5315d5e965 \ - --hash=sha256:ba54cfebe86920a559a7c4d6b9050791c20513650a1952ebe3368c7dc70306f8 \ - --hash=sha256:bcb46e2f9feff8d06323983bd83ed00c201fdcab3d74973e7072a889b3979fcd \ - --hash=sha256:bcc33feacfaefce60c12fd500a277533bdc02b10a19f7f6d348763d8140bbba7 \ - --hash=sha256:bf16ba1b4d0b6b7c8e534936632270cf70eb00dbe09005bc345b2677b726855c \ - --hash=sha256:cf1845d02ad822a369a49f2bb9345b1614744267682e7a03527dc3bf6eea1777 \ - --hash=sha256:d69141514cc30b774ceea5e3ed3a6635c8d8a96edf664689b890f4089111fb35 \ - --hash=sha256:d9c7f76c0673154f044e9d78c8655fb4213f6ca31a836df48b40fe5d187717b9 \ - --hash=sha256:dbce0b29841537a2fa4a214c2bbf14de3587c9680caa9b4e217568472490b28f \ - --hash=sha256:dc624f6bc473dacdf7ef7eb8678d0d08edf15cd94fad6ae5c7d6cc67a4e4902f \ - --hash=sha256:e158cb00350dc278f3b91551101aa7d12415a66ebf2c91d8d5ac14e56ddd3ad0 \ - --hash=sha256:e491916b378fba47242221bb9ead245211b70d504f495d105d17b14a24b4907c \ - --hash=sha256:e795b7eb908249c4e43c7c99fac7c2c75dab0c43566e37db472a355f63693d71 \ - --hash=sha256:e7e480451b9fa137494bccd3a7d69adbe8ac65a87d97be61e11f1b1050a5bac3 \ - --hash=sha256:e91206ee562682b51b98ef4b26a6ef48fd84e15fd4c4bc5ec768eb641d206838 \ - --hash=sha256:e9871b1ffbfa9656b60aeee92ed5136a5742696006fa322b29ea3d8da0ecc9cf \ - --hash=sha256:e9aeb04d6aef139de265b29683e119b638208f88cf73cdd1658aa07221165321 \ - --hash=sha256:ebaea975e03d3141d9d3a507df75c9b3ec90fa9d2ffd07567b3a978d9d790b26 \ - --hash=sha256:f0606c8bf2cdefea14a43530f7657cbbb7ecf1c4222512492ef4a4434a9501ec \ - --hash=sha256:f13c32a3abd6079a66d9526e18dad9b6d280384d49d7c54040cd57b6424041d9 \ - --hash=sha256:f7401aebd7f581d7f83a439d87d474999317ee099218e5ad25d125290990ba65 \ - --hash=sha256:fa4ecea169a355be7a3ade2c783e2ed12f0e40d2c5621cda8b3297faf7fbb9f5 \ - --hash=sha256:fbd139c8447d25dd750ab79ee274cc5e1fe80fc56340ab10b18a195e1b6eca3e \ - --hash=sha256:fdafc9cce40277e0f7a0feabce0ee50dd2fa1800f3b38015e51296b5e814048d \ - --hash=sha256:fe3cca2e4e8a592be0f269a1ca4835c25199d9f3ce815c8491048f785b0a0198 \ - --hash=sha256:ffd0c5368496f41b0944be820fcb7a838aa6e623d250b01acf2643939c3f99d7 +pillow==13.0.0 \ + --hash=sha256:02464da86f561f410e13b061ec37c3068a9cb557a095494157742d07b3c24a27 \ + --hash=sha256:04a63720f79e22fd54d897c233a68314c50ca0603360f54ce2bd5db8f0f43782 \ + --hash=sha256:05cd2cd39055d0b9650060e4513ef4a9a0aea870083950bd046a0d6490f93a22 \ + --hash=sha256:06903aac074d00c988e77a1fc435286dc498506d4ca8d6f69df9babddf4ecc15 \ + --hash=sha256:08b826a9bf1265c76de04a42061d61ea6b109418b7826f28ff1ecdfaa26c5502 \ + --hash=sha256:0d1a9cf7bc6917538b20f8d06b9ab2147e542f68849e96fc58eb66bdf6acc692 \ + --hash=sha256:10b5019883c98f1dd819ca48ffc6f33a91cee04948fe637c605f4345c7cafd6f \ + --hash=sha256:12b0fa15879fe5a9d9eebe66e4e2d74128a773b1a4448010c9c714f45e04b9d8 \ + --hash=sha256:12fbb851cf323cc19aac3eea1df5ad17b57e91e9fe3cfd06100dba7bb8208c13 \ + --hash=sha256:149a1c323ba366f90b48e6f1a6db6582eb50512df374588de56fc909abc0f0f3 \ + --hash=sha256:19b66ca0a4d25cc0200b381ce824c7ea090db61427315e73cfb05dbf1b35c369 \ + --hash=sha256:1c774b9c0ba0d0b356f71530677b39028878a2cae5a93e0a13779cfd17120f88 \ + --hash=sha256:20722cd5375533ad754053363a80b8fb30c79d37930a6f7228030e237ce3bd5d \ + --hash=sha256:20822140c9a2ba9c5e42df342b0a892ab2e102fc0e3db967a133bb9ce3f898bb \ + --hash=sha256:21c32061a5512dc13f87cc7303e120e4d77c2f70df89a35af0e3ce246e5ac57f \ + --hash=sha256:2310e2adfc13c6d457d7d0f2ffdd5c56a74a317650cf765887df8d515a647ac0 \ + --hash=sha256:23edf4ec4b6f4f240e8df6e4985e9ee2ced86474632433e55fb1e2548744bf40 \ + --hash=sha256:2d50500a4a3bc6469adcdb319cf05d688b08ca3bf0f22a05339f3b25b433a599 \ + --hash=sha256:2e31c4e68cffd7b1f116127626813b82fcba19edf6b940a7b0c266d6484d0055 \ + --hash=sha256:34fb3db1149c879343861831728c243c432d4a5afc8f1878ca7612ef8de2a033 \ + --hash=sha256:37290fbef0c73e2d51d594eb78fded7dc7170d1c3ad0a890269e6fd8c8c4e061 \ + --hash=sha256:405be6058e206a86fedce2f715ecc2343f94072d7925251632dffa2c05f70dca \ + --hash=sha256:40a266371ece581e5de13b6635ef78d7487824af80561b6ca27694a88c19bc0f \ + --hash=sha256:42d63be3af4f9b951b199b0a9d23365d37ab14687e6d77b74940432c8515c4ca \ + --hash=sha256:44629d4498c9cb6b2051806155f22141df0d8a929c56134b166655232dfb2f8c \ + --hash=sha256:49c12af515073e9bf511e4ef736743bf70d531a643668722279314b937742ba4 \ + --hash=sha256:4a25756244bdcce3b50580baadf320fa48297f0eeb8e114ba64139a40d4d0fc8 \ + --hash=sha256:4bd024d567eca931b668978d672dba914d4b8e2628e2c3073a2ad178de8a2620 \ + --hash=sha256:4f8db54177035a2b5f717f94475b74907e12a96dc74f6d487f3966b0a4694071 \ + --hash=sha256:51ebb529fc5e8abdd55302f6c3f8943037348ec2384329a3e55677f69ae2fdfc \ + --hash=sha256:567f20f1c75897d7663830da6ea8af719e03d712b1d9ee7f813319f9ece42e5a \ + --hash=sha256:5933fcea5a5ba64c686f55c886a115cbfd92f2900460784a1e79909fdbafd7b6 \ + --hash=sha256:5bdb38ba220b61b4ec31dd1046dea6b016d3c6850a23f43ed8b58332b2fc668e \ + --hash=sha256:5f56499c1a74d2b862beb0c33b8016f99d095858455df9d21583bbbc1c15b582 \ + --hash=sha256:67871a13cf47275201553b536c025da777f997fa562a19eb798af4ad4b97c18f \ + --hash=sha256:67c8d44ad89fa7eb88de5d6ef6199b1e03dd24974f85a67639fc8badb801fe79 \ + --hash=sha256:6a0920cb55df5a4d3f6862964908078c0bd59332bdd1bcd20d14e8c70ef27cc5 \ + --hash=sha256:6bce6a29c3898191f4c9998e4d4e8f69790caeabde46e54da4bc235ce5bde683 \ + --hash=sha256:6f38f3f885a09cafc93fd667b1173a92f379e43785d635e7983c893f0da319fb \ + --hash=sha256:73ee25cf8cf8b75d5a5aeed7cb6588f5e5dbfbb0fde51707b630bbb23897645c \ + --hash=sha256:7b999816b5850a35ada08443e6e03945f1f6488144a83afe2d37b240a552aafa \ + --hash=sha256:7c88c10dd432f3b39ccdafc9ed81d0c21b8fd634502574da76d1bfa4fe2c76d0 \ + --hash=sha256:7d5e097eabd5d4915c27ac9f6e9325bb1e01a7c94d7348b5c4451d3fc30e8521 \ + --hash=sha256:7e91e97a929232cdb828889ee6ad0e48d7f40f4ec38818f4e4a8968b2b1af22e \ + --hash=sha256:7f6299e494e46253d40c34653cac369412f44ca53b5f85d848d3f95b1ed0546c \ + --hash=sha256:816752a5ef76a80484c8c441c85593b7ea9304f6231fc5776ad1fbff7e773058 \ + --hash=sha256:83b37abd87b590acd8f47ca8a61f70893322b11834d36e07c2298dacad4d14d2 \ + --hash=sha256:8619685cad8efc4c38c66d3b36aad854523297c43d748b3b6f0cc03c90ba3463 \ + --hash=sha256:8bf295fe459343bc6093c8e5e0df60bfa189e0fcb775a22e2c2488fdeccc6c0d \ + --hash=sha256:8e459b613cf31f8980f584387b4fe47bf08e076e288d61aa8eba1f7fd4ddbb81 \ + --hash=sha256:8f36aa77f74b14a2103622b196402c71330e4e7cf4122ee45c854e9a01e0eecf \ + --hash=sha256:93ff9c9c3663ce23aaa5d0b6b6abac55359e2a0c06c7e30f3771829af816be63 \ + --hash=sha256:9a635068d9793c7f0d929f39f18b58f1bd86c236e8e7b64167b86af9232ebca0 \ + --hash=sha256:9dda0788a9932776a75d8bcc020076edc308ce2508b51f6739a45e3158b3fc42 \ + --hash=sha256:a192360d512305fe92d3053e4f7627bbad1f7708f1f4ad38cccd9336acabe9ad \ + --hash=sha256:a2a14cd73d5e6c927f644b0110ee81f59c4ef506259ec9183904747f2caacc81 \ + --hash=sha256:a625df9a2a5597fa5e5266e01aefc82604bb85d291e3547981e863560d75da6c \ + --hash=sha256:a6aaa7871aebca081eb5c45f83e845bbd9712ae99fa9286dbfdcee835e63f4b4 \ + --hash=sha256:aa694c49787040943570d61c9b8fc01271bd2a2cfdd55f0d896f8aecdce1a981 \ + --hash=sha256:adb3c2124c68e480f4632d06cb7caa3b48a51678475f483b4edb1c6dee29ea43 \ + --hash=sha256:b84be41387fc00db28c13c02e30d42b02d8d8f6a4a741a1aee6012ca7f1f6039 \ + --hash=sha256:bf97ba1b5542050e9b0c74e632323b6d8136cc99d64e193abebee22fe6013c76 \ + --hash=sha256:c3bc8ee0271387e0b1e3ad0ff893645990a656e79bf9c42f4f734a3642b11d51 \ + --hash=sha256:c50c80e3591e5cf9a8dfa6007d4f10732f02e32263850999286e56ccd650cfb2 \ + --hash=sha256:c6fd031333dedcad7ce3e03d244093f43504f209e86925ebdb2b868fa4037630 \ + --hash=sha256:c87a04e7f152ee4db5dbe15f0bfa3fe628a1aeedbe66b81c43b87f52c813f9ac \ + --hash=sha256:cdcd1ba258178f7356d74170c9a81ba4eab6a5d55748bc402a5a2a6fe6be3510 \ + --hash=sha256:d4737f50d23ddd11d6685faacbb9252887dde0bc7997127757247983d74399d6 \ + --hash=sha256:d7710eb676e9bb68965cb045be0d333b4c0d7f9dfbaa7fbf121385b1588683d3 \ + --hash=sha256:d9b0a2bee3f3e03e9f29057aedb807dbf1e1d96ee1b9d0f422d9db6c8583271d \ + --hash=sha256:e65dd6e75de67b2362a4fa3e16aa627595f65054dc4ec73d64680c84d592fa61 \ + --hash=sha256:e892354a424b1e82a78ba16785822bdd40de06f19446a3d4a28047df1a4f8932 \ + --hash=sha256:e8ca8f69b7c77fa96dad99f8638c1fc4f1c0bed4acaa1baf7a3c31bdb80a0ab1 \ + --hash=sha256:e91c2b1cbeae768c98752e67e717e283d68dae45717b41f2d91eaee648e37b7d \ + --hash=sha256:e98b4f91f99b24b1da72e10621ba181f7281922384a7403094debb4199de6c00 \ + --hash=sha256:ef5fe6b7db3f501c56000ba8fe7c7e1a35ca1d45824fbeb95a67daa85cff4dbe \ + --hash=sha256:f11a3c1358bb3c68012a7b1f41b223c3db9f7bfde90b481df028cfe1c3363313 \ + --hash=sha256:f391dc92a015c478745192fe4f2e120c2af2aba18d9fe0616341cbf537599806 \ + --hash=sha256:f7cbb7a64e7ec664775195e4f81923bc8a93d9451b963f844e59118f63147a7a \ + --hash=sha256:f9bd7d260d986f3553518c5d723544346fa48031f8aef3265d786aeb59845bc0 \ + --hash=sha256:fc481f742ccda20e9ed760b8f978dff50f2a4e9bc253ac167fb0a05b0783ef52 \ + --hash=sha256:ff3fe9eb621fbb9db4c4be33ab946f6bac5f98ab50ca8d3260503653656c961b \ + --hash=sha256:ffea6b2986a797c84b6d1d27d4f43e0b83c73fc16281a930891cd237cf0d08fe # via # -c sidecar/requirements.lock # pi-heif diff --git a/sidecar/requirements-override.txt b/sidecar/requirements-override.txt new file mode 100644 index 00000000..3bb9a5a9 --- /dev/null +++ b/sidecar/requirements-override.txt @@ -0,0 +1,20 @@ +# Overrides for the base lock: each line REPLACES every requirement on that package in the +# resolution, a dependency's declared bounds included. `just lock-regen` passes this file to +# `uv pip compile --override` for sidecar/requirements.lock only, and stamps its hash in the lock's +# header, so `just requirements-lock` fails if it changes without a regeneration. +# +# An override is a hole in a dependency's own metadata, so keep this file to the pins that need +# one, each also pinned at the same version in requirements.txt (the gate checks that), with the +# reason and the condition for removing it. This is NOT requirements.txt: overriding with every +# top-level pin would silently replace every transitive bound on them. +# +# Because the lock is resolved past a declared bound, pip installs it with `--no-deps` (sidecar.rs, +# pr.yml, release.yml): pip's own resolver would read fastembed's cap again and refuse the lock. +# The lock is still complete, since uv resolved every dependency, and every line is still +# hash-checked. +# +# pillow — 13.0.0 carries the fixes for the image-decoder advisories published from 2026-10-17; +# fastembed 0.9.0 declares `pillow<13` on every Python, though its code runs on 13 and PM calls +# only its text models. Remove this line once a fastembed release allows pillow 13 +# (qdrant/fastembed#830, open and unmerged at 0.9.0). Keep the file even when it is empty. +pillow==13.0.0 diff --git a/sidecar/requirements-tsne.lock b/sidecar/requirements-tsne.lock index 48e7f89c..99d3bf9d 100644 --- a/sidecar/requirements-tsne.lock +++ b/sidecar/requirements-tsne.lock @@ -13,83 +13,26 @@ # # Universal resolution: ONE file covers Windows, macOS and Linux and every CPython from the # MIN_PYTHON floor upwards, with environment markers where the resolution forks by interpreter -# (numpy, onnxruntime, pandas, av) or by platform (colorama, magika, pyreadline3, hf-xet). +# (numpy, and scipy for t-SNE) or by platform (colorama, magika, tzdata, hf-xet). +# +# The base lock is also resolved with sidecar/requirements-override.txt, which lifts a declared +# bound in one dependency's metadata (that file says which, and why), so every install of these +# locks passes `--no-deps`: pip would otherwise re-apply the bound and refuse the lock. # # pm-lock: 1 -# pm-python-floor: 3.10 +# pm-python-floor: 3.11 # pm-input: sidecar/requirements-optional.txt@sha256:bbfc9c5804fc0d4f7c282c010593a6968eec9738d2847a9deb41fc6a9d84486c -# pm-constraint: sidecar/requirements.lock@sha256:e4a2c8e1f26574688977e9c57a62e12c147b129f3e83241a377f02e14f639f00 +# pm-constraint: sidecar/requirements.lock@sha256:5c7a00de93e84c336c436ca72a2bde9e536f858ced0df80821a03f86908e1e94 # pm-pins: openTSNE==1.0.4 joblib==1.5.3 \ --hash=sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713 \ --hash=sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3 # via scikit-learn -narwhals==2.25.0 ; python_full_version >= '3.11' \ +narwhals==2.25.0 \ --hash=sha256:1f0f403e8c7e4463cde9bfe78b12fdd809e3ae3dda6d9b2f802934fb9c7a6a8f \ --hash=sha256:62c036c810662bf7820b7737077176313bc59350eeeefb808510f388c743e4b2 # via scikit-learn -numpy==2.2.6 ; python_full_version < '3.11' \ - --hash=sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff \ - --hash=sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47 \ - --hash=sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84 \ - --hash=sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d \ - --hash=sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6 \ - --hash=sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f \ - --hash=sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b \ - --hash=sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49 \ - --hash=sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163 \ - --hash=sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571 \ - --hash=sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42 \ - --hash=sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff \ - --hash=sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491 \ - --hash=sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4 \ - --hash=sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566 \ - --hash=sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf \ - --hash=sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40 \ - --hash=sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd \ - --hash=sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06 \ - --hash=sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282 \ - --hash=sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680 \ - --hash=sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db \ - --hash=sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3 \ - --hash=sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90 \ - --hash=sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1 \ - --hash=sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289 \ - --hash=sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab \ - --hash=sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c \ - --hash=sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d \ - --hash=sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb \ - --hash=sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d \ - --hash=sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a \ - --hash=sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf \ - --hash=sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1 \ - --hash=sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2 \ - --hash=sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a \ - --hash=sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543 \ - --hash=sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00 \ - --hash=sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c \ - --hash=sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f \ - --hash=sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd \ - --hash=sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868 \ - --hash=sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303 \ - --hash=sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83 \ - --hash=sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3 \ - --hash=sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d \ - --hash=sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87 \ - --hash=sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa \ - --hash=sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f \ - --hash=sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae \ - --hash=sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda \ - --hash=sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915 \ - --hash=sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249 \ - --hash=sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de \ - --hash=sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8 - # via - # -c sidecar/requirements.lock - # opentsne - # scikit-learn - # scipy -numpy==2.4.6 ; python_full_version == '3.11.*' \ +numpy==2.4.6 ; python_full_version < '3.12' \ --hash=sha256:001fbb8e08d942dd57599e781f2472269ee7f2755fae407b4f67b2f0b17da3f1 \ --hash=sha256:0280e0356c0829a18d9de1cb7eee50ec22ca639878d7240307ca0943d73cd2c4 \ --hash=sha256:043191bfa8eab18c776647b62723ac9dddece59743b13f49b2016094129c2b3f \ @@ -256,40 +199,7 @@ opentsne==1.0.4 \ --hash=sha256:e90bf612be94fcbe06e3cab9531a58e4824661f38dd7c2e934569820d15c82ab \ --hash=sha256:f2c4670461372880ecddbe839245faab30f2472d3d42ca0c52c6e302d4b459fa \ --hash=sha256:f681ed5957e99af9500538384bfc15b50697f99c7cd057cfe8863d50248cc228 -scikit-learn==1.7.2 ; python_full_version < '3.11' \ - --hash=sha256:0486c8f827c2e7b64837c731c8feff72c0bd2b998067a8a9cbc10643c31f0fe1 \ - --hash=sha256:0b7dacaa05e5d76759fb071558a8b5130f4845166d88654a0f9bdf3eb57851b7 \ - --hash=sha256:191e5550980d45449126e23ed1d5e9e24b2c68329ee1f691a3987476e115e09c \ - --hash=sha256:20e9e49ecd130598f1ca38a1d85090e1a600147b9c02fa6f15d69cb53d968fda \ - --hash=sha256:2a41e2a0ef45063e654152ec9d8bcfc39f7afce35b08902bfe290c2498a67a6a \ - --hash=sha256:36749fb62b3d961b1ce4fedf08fa57a1986cd409eff2d783bca5d4b9b5fce51c \ - --hash=sha256:4a847fea807e278f821a0406ca01e387f97653e284ecbd9750e3ee7c90347f18 \ - --hash=sha256:502c18e39849c0ea1a5d681af1dbcf15f6cce601aebb657aabbfe84133c1907f \ - --hash=sha256:57dc4deb1d3762c75d685507fbd0bc17160144b2f2ba4ccea5dc285ab0d0e973 \ - --hash=sha256:6088aa475f0785e01bcf8529f55280a3d7d298679f50c0bb70a2364a82d0b290 \ - --hash=sha256:63a9afd6f7b229aad94618c01c252ce9e6fa97918c5ca19c9a17a087d819440c \ - --hash=sha256:6b33579c10a3081d076ab403df4a4190da4f4432d443521674637677dc91e61f \ - --hash=sha256:7a4c328a71785382fe3fe676a9ecf2c86189249beff90bf85e22bdb7efaf9ae0 \ - --hash=sha256:7a58814265dfc52b3295b1900cfb5701589d30a8bb026c7540f1e9d3499d5ec8 \ - --hash=sha256:89877e19a80c7b11a2891a27c21c4894fb18e2c2e077815bcade10d34287b20d \ - --hash=sha256:8d91a97fa2b706943822398ab943cde71858a50245e31bc71dba62aab1d60a96 \ - --hash=sha256:8da8bf89d4d79aaec192d2bda62f9b56ae4e5b4ef93b6a56b5de4977e375c1f1 \ - --hash=sha256:9656e4a53e54578ad10a434dc1f993330568cfee176dff07112b8785fb413106 \ - --hash=sha256:96dc05a854add0e50d3f47a1ef21a10a595016da5b007c7d9cd9d0bffd1fcc61 \ - --hash=sha256:98335fb98509b73385b3ab2bd0639b1f610541d3988ee675c670371d6a87aa7c \ - --hash=sha256:9acb6c5e867447b4e1390930e3944a005e2cb115922e693c08a323421a6966e8 \ - --hash=sha256:9b7ed8d58725030568523e937c43e56bc01cadb478fc43c042a9aca1dacb3ba1 \ - --hash=sha256:abebbd61ad9e1deed54cca45caea8ad5f79e1b93173dece40bb8e0c658dbe6fe \ - --hash=sha256:acbc0f5fd2edd3432a22c69bed78e837c70cf896cd7993d71d51ba6708507476 \ - --hash=sha256:b4d6e9deed1a47aca9fe2f267ab8e8fe82ee20b4526b2c0cd9e135cea10feb44 \ - --hash=sha256:bb24510ed3f9f61476181e4db51ce801e2ba37541def12dc9333b946fc7a9cf8 \ - --hash=sha256:c7509693451651cd7361d30ce4e86a1347493554f172b1c72a39300fa2aea79e \ - --hash=sha256:ca250e6836d10e6f402436d6463d6c0e4d8e0234cfb6a9a47835bd392b852ce5 \ - --hash=sha256:e5bf3d930aee75a65478df91ac1225ff89cd28e9ac7bd1196853a9229b6adb0b \ - --hash=sha256:f95dc55b7902b91331fa4e5845dd5bde0580c9cd9612b1b2791b7e80c3d32615 \ - --hash=sha256:fa8f63940e29c82d1e67a45d5297bdebbcb585f5a5a50c4914cc2e852ab77f33 - # via opentsne -scikit-learn==1.9.0 ; python_full_version >= '3.11' \ +scikit-learn==1.9.0 \ --hash=sha256:051075bda8b7aab87b1906ab3d4740a1e1224a19d7b3781a576736edc94e76aa \ --hash=sha256:056c92bb67ad4c28463c2f2653d9701449201e7e7a9e94e321be0f71c4fef2b8 \ --hash=sha256:147e9329ef0e39f75d4cffa02b2aa48d827832684926cd5210d9a2cb5c57246b \ @@ -322,57 +232,7 @@ scikit-learn==1.9.0 ; python_full_version >= '3.11' \ --hash=sha256:f7e254636164090da847715a27f8e5478feb98c40a9e0ee90cbd277de9e5ceb8 \ --hash=sha256:fd3a8ef0c758555a3b23c03adaa858af32f7736785ded50ad5991f59c4ed03fa # via opentsne -scipy==1.15.3 ; python_full_version < '3.11' \ - --hash=sha256:05dc6abcd105e1a29f95eada46d4a3f251743cfd7d3ae8ddb4088047f24ea477 \ - --hash=sha256:06efcba926324df1696931a57a176c80848ccd67ce6ad020c810736bfd58eb1c \ - --hash=sha256:0a769105537aa07a69468a0eefcd121be52006db61cdd8cac8a0e68980bbb723 \ - --hash=sha256:0bdd905264c0c9cfa74a4772cdb2070171790381a5c4d312c973382fc6eaf730 \ - --hash=sha256:0ff17c0bb1cb32952c09217d8d1eed9b53d1463e5f1dd6052c7857f83127d539 \ - --hash=sha256:14ed70039d182f411ffc74789a16df3835e05dc469b898233a245cdfd7f162cb \ - --hash=sha256:185cd3d6d05ca4b44a8f1595af87f9c372bb6acf9c808e99aa3e9aa03bd98cf6 \ - --hash=sha256:18aaacb735ab38b38db42cb01f6b92a2d0d4b6aabefeb07f02849e47f8fb3594 \ - --hash=sha256:1c832e1bd78dea67d5c16f786681b28dd695a8cb1fb90af2e27580d3d0967e92 \ - --hash=sha256:263961f658ce2165bbd7b99fa5135195c3a12d9bef045345016b8b50c315cb82 \ - --hash=sha256:271e3713e645149ea5ea3e97b57fdab61ce61333f97cfae392c28ba786f9bb49 \ - --hash=sha256:2c620736bcc334782e24d173c0fdbb7590a0a436d2fdf39310a8902505008759 \ - --hash=sha256:34716e281f181a02341ddeaad584205bd2fd3c242063bd3423d61ac259ca7eba \ - --hash=sha256:39cb9c62e471b1bb3750066ecc3a3f3052b37751c7c3dfd0fd7e48900ed52982 \ - --hash=sha256:3ac07623267feb3ae308487c260ac684b32ea35fd81e12845039952f558047b8 \ - --hash=sha256:3b0334816afb8b91dab859281b1b9786934392aa3d527cd847e41bb6f45bee65 \ - --hash=sha256:40e54d5c7e7ebf1aa596c374c49fa3135f04648a0caabcb66c52884b943f02b4 \ - --hash=sha256:50f9e62461c95d933d5c5ef4a1f2ebf9a2b4e83b0db374cb3f1de104d935922e \ - --hash=sha256:52092bc0472cfd17df49ff17e70624345efece4e1a12b23783a1ac59a1b728ed \ - --hash=sha256:5380741e53df2c566f4d234b100a484b420af85deb39ea35a1cc1be84ff53a5c \ - --hash=sha256:5e721fed53187e71d0ccf382b6bf977644c533e506c4d33c3fb24de89f5c3ed5 \ - --hash=sha256:6487aa99c2a3d509a5227d9a5e889ff05830a06b2ce08ec30df6d79db5fcd5c5 \ - --hash=sha256:6ac6310fdbfb7aa6612408bd2f07295bcbd3fda00d2d702178434751fe48e019 \ - --hash=sha256:6cfd56fc1a8e53f6e89ba3a7a7251f7396412d655bca2aa5611c8ec9a6784a1e \ - --hash=sha256:6db907c7368e3092e24919b5e31c76998b0ce1684d51a90943cb0ed1b4ffd6c1 \ - --hash=sha256:721d6b4ef5dc82ca8968c25b111e307083d7ca9091bc38163fb89243e85e3889 \ - --hash=sha256:76ad1fb5f8752eabf0fa02e4cc0336b4e8f021e2d5f061ed37d6d264db35e3ca \ - --hash=sha256:79167bba085c31f38603e11a267d862957cbb3ce018d8b38f79ac043bc92d825 \ - --hash=sha256:795c46999bae845966368a3c013e0e00947932d68e235702b5c3f6ea799aa8c9 \ - --hash=sha256:7e11270a000969409d37ed399585ee530b9ef6aa99d50c019de4cb01e8e54e62 \ - --hash=sha256:8c9ed3ba2c8a2ce098163a9bdb26f891746d02136995df25227a20e71c396ebb \ - --hash=sha256:993439ce220d25e3696d1b23b233dd010169b62f6456488567e830654ee37a6b \ - --hash=sha256:9d61e97b186a57350f6d6fd72640f9e99d5a4a2b8fbf4b9ee9a841eab327dc13 \ - --hash=sha256:9db984639887e3dffb3928d118145ffe40eff2fa40cb241a306ec57c219ebbbb \ - --hash=sha256:9e2abc762b0811e09a0d3258abee2d98e0c703eee49464ce0069590846f31d40 \ - --hash=sha256:a345928c86d535060c9c2b25e71e87c39ab2f22fc96e9636bd74d1dbf9de448c \ - --hash=sha256:ad3432cb0f9ed87477a8d97f03b763fd1d57709f1bbde3c9369b1dff5503b253 \ - --hash=sha256:ae48a786a28412d744c62fd7816a4118ef97e5be0bee968ce8f0a2fba7acf3bb \ - --hash=sha256:aef683a9ae6eb00728a542b796f52a5477b78252edede72b8327a886ab63293f \ - --hash=sha256:b90ab29d0c37ec9bf55424c064312930ca5f4bde15ee8619ee44e69319aab163 \ - --hash=sha256:c05045d8b9bfd807ee1b9f38761993297b10b245f012b11b13b91ba8945f7e45 \ - --hash=sha256:c9deabd6d547aee2c9a81dee6cc96c6d7e9a9b1953f74850c179f91fdc729cb7 \ - --hash=sha256:dde4fc32993071ac0c7dd2d82569e544f0bdaff66269cb475e0f369adad13f11 \ - --hash=sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf \ - --hash=sha256:ed7284b21a7a0c8f1b6e5977ac05396c0d008b89e05498c8b7e8f4a1423bba0e \ - --hash=sha256:f77f853d584e72e874d87357ad70f44b437331507d1c311457bed8ed2b956126 - # via - # opentsne - # scikit-learn -scipy==1.17.1 ; python_full_version == '3.11.*' \ +scipy==1.17.1 ; python_full_version < '3.12' \ --hash=sha256:010f4333c96c9bb1a4516269e33cb5917b08ef2166d5556ca2fd9f082a9e6ea0 \ --hash=sha256:02ae3b274fde71c5e92ac4d54bc06c42d80e399fec704383dcd99b301df37458 \ --hash=sha256:08b900519463543aa604a06bec02461558a6e1cef8fdbb8098f77a48a83c8118 \ diff --git a/sidecar/requirements.lock b/sidecar/requirements.lock index 0a75af68..41f93518 100644 --- a/sidecar/requirements.lock +++ b/sidecar/requirements.lock @@ -13,50 +13,21 @@ # # Universal resolution: ONE file covers Windows, macOS and Linux and every CPython from the # MIN_PYTHON floor upwards, with environment markers where the resolution forks by interpreter -# (numpy, onnxruntime, pandas, av) or by platform (colorama, magika, pyreadline3, hf-xet). +# (numpy, and scipy for t-SNE) or by platform (colorama, magika, tzdata, hf-xet). +# +# The base lock is also resolved with sidecar/requirements-override.txt, which lifts a declared +# bound in one dependency's metadata (that file says which, and why), so every install of these +# locks passes `--no-deps`: pip would otherwise re-apply the bound and refuse the lock. # # pm-lock: 1 -# pm-python-floor: 3.10 -# pm-input: sidecar/requirements.txt@sha256:6b02e41fb547232397875e631dbe4e407db0cc92d9b1ae074eaf34414143f5b4 +# pm-python-floor: 3.11 +# pm-input: sidecar/requirements.txt@sha256:30c86bfc9acd7aec0b87b8642b409887618b6a6e216442b06547adf379a45106 +# pm-override: sidecar/requirements-override.txt@sha256:0ad677b8821f238240c27ef7f64a9d1b4972ddd7bc97df048dc8601feedd9bd3 anyio==4.14.2 \ --hash=sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494 \ --hash=sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f # via httpx -av==17.1.0 ; python_full_version < '3.11' \ - --hash=sha256:1284addf3c0dd939887a9722dc30df2241a97471ad52c3c507e31583ae22ff02 \ - --hash=sha256:1370b11a697eb3f2555906f8ab3519b0cfe48425d7830a3996ad42e6bffafda5 \ - --hash=sha256:19264c9bb4bee404accc7ce9ec461f2044b7f577a70234d29aafde31ed17de46 \ - --hash=sha256:19c84fd72af5ef81a20f18fbc6f9aedff9e1455e53a7062c1d4c95926d73da4e \ - --hash=sha256:22dff0ae582d10ef08c75c2150a4fd27cfc26653b54930c7c27b9f7b3aa20723 \ - --hash=sha256:3453b06075c7bb973fdb6de52563f7692ff05cbc64c0bb45f4fd6e8709131f2f \ - --hash=sha256:3dcd41e53f53f9a3260751d9c3c11d34e93d70d61e506c81f13dbc1e3606e07b \ - --hash=sha256:43ebbe977f19a7f2d2bd1a4e119675a0b15e05852cf7309846b6ab922ba7ffe9 \ - --hash=sha256:5327807c1219293803ef0c5d1578ff3ae1cf638c09e5998962026e1a554ec240 \ - --hash=sha256:58f7593726437cda5bd19793027e027768450b5c4a594777bf487798a33db702 \ - --hash=sha256:5df5c1172ef1cf65a1529d612f7da7798ce2cf82c1ff7212466b538a6cc7214c \ - --hash=sha256:6a20658ec7d96a70e14b1196eff00b7cdd8831ac3b99868e16b8ba8b24090847 \ - --hash=sha256:6c9b71fe5c0c5a8d303b1588d4d8ce9397d6b023f467cfef95000ba1f75507fa \ - --hash=sha256:7f1e71ff621b66253333926f948e00faae11d855b2442133c65128bca64cdeb3 \ - --hash=sha256:90c49bc9608377d01e82e747377505419a229464873341db18202d5dddecce5a \ - --hash=sha256:9514cfda85180554c430695282faf4be3ffdf95775d8519733821244eecb58e0 \ - --hash=sha256:ad7b4aa011093324b7118245f50ac6db244cfe9900d4072508a5245a2b0d3f41 \ - --hash=sha256:b41647e42884bf543b8e8d0a1dabd4d1b006c99183eb1a2d7afc5b01f73eeff4 \ - --hash=sha256:bbab058bd965309f39962e53caac8126987c68c0be094fc4f9427e5615b0218f \ - --hash=sha256:bff8896454b38fcb785a70e5ae0485d7021cb776303a5849393128a30b8f850b \ - --hash=sha256:cc5a5247622cb77e24c342364eb68f88c1442ddfaab60c1f1f483359d3cc7879 \ - --hash=sha256:e1c90f85cd7431ede95b11e8e711571a896ebea433f298849c2c0f1594c8d86e \ - --hash=sha256:ec630be6321b04e317862f6082e84812bbd801e55a3c2298312e3fc8a0a4af4f \ - --hash=sha256:ee98534242a74da847af78624779ac5a3177dc7c69f956a4da9e6f0fdb37d7f6 \ - --hash=sha256:efe9b1397300b67b644ad220c89df4892a76f2debe70f16bae1749fa20526e63 \ - --hash=sha256:f997e3351bdf51127c07a74e21741a2996e9230cbeb2d81c14acde761b116c9c \ - --hash=sha256:f9a65d1f48b818323fb411e80358f89d77dec340b01d27c6b2dfbb9cbf4b779f \ - --hash=sha256:fa64e1f1500d01c4a98e7a41dc1a9a35fb4dfe71f5de0389264ec1192200c76a \ - --hash=sha256:ff457ed419348e5b8e8c811d341389b052c5e4d5839da3794d019b125b9fe830 \ - --hash=sha256:ffbd78d73d2c9bf31e9a007c992faec3991428b2941a3b085b84fb82e8c32d19 - # via - # -r sidecar/requirements.txt - # faster-whisper -av==18.1.0 ; python_full_version >= '3.11' \ +av==18.1.0 \ --hash=sha256:025f84494cb23278498f03b0d8117d3e47a1cbc9c44b97eb31875cf02251e46b \ --hash=sha256:08a9ae288299cfcbf739dba4ad0c53b9b71f45184303dd45947920d022fed695 \ --hash=sha256:191224788d87af06c31784a395bb73f14b72f33d7f4871ace0157de2abdc6276 \ @@ -85,9 +56,9 @@ beautifulsoup4==4.15.0 \ # via # markdownify # markitdown -certifi==2026.7.22 \ - --hash=sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775 \ - --hash=sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55 +certifi==2026.10.19 \ + --hash=sha256:035c1e85572b141bf1c3a3eff0e33a8eb74917522f2559075ff8d2fea780a1ae \ + --hash=sha256:42d35340cc5b9ba78e818ddbff0c5b1c2b779de448a340fd1b989f5fa8670e1b # via # httpcore # httpx @@ -388,10 +359,6 @@ colorama==0.4.6 ; sys_platform == 'win32' \ # click # loguru # tqdm -coloredlogs==15.0.1 ; python_full_version < '3.11' \ - --hash=sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934 \ - --hash=sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0 - # via onnxruntime cryptography==50.0.1 \ --hash=sha256:01f41478cf33fc605a6a089cd56d28b45c6c0b45a1928b61797f2621a04bac71 \ --hash=sha256:05ba322c4da95b262a212c345af888ef2c37c88c0509756ea00a0e6d68850f23 \ @@ -487,10 +454,6 @@ et-xmlfile==2.0.0 \ --hash=sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa \ --hash=sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54 # via openpyxl -exceptiongroup==1.3.1 ; python_full_version < '3.11' \ - --hash=sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219 \ - --hash=sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598 - # via anyio fastembed==0.9.0 \ --hash=sha256:273d408edec8c0f161711d8f6e44e4a5b559d18e8edf6bf805415d55dc772846 \ --hash=sha256:bc3beadb46ecb3580ab832d12670be7ecb937f80adfcb7b77b03f7eef76c394a @@ -547,10 +510,6 @@ huggingface-hub==1.28.0 \ # fastembed # faster-whisper # tokenizers -humanfriendly==10.0 ; python_full_version < '3.11' \ - --hash=sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477 \ - --hash=sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc - # via coloredlogs idna==3.19 \ --hash=sha256:5e0811a4383b21dc5838069f801c4fb62113b7447663d2530d2bd6e77b49bf15 \ --hash=sha256:815e7be7a7806d54abb586dc943addc79e8b2ee16915059658cbeff4b1b43bf4 @@ -914,73 +873,7 @@ mmh3==5.3.0 \ --hash=sha256:fcd32858eb0df02dd0210523f12e1dabbae1a8d2d74b58ba40aabf2ca75ef872 \ --hash=sha256:fe9b3b53b0688e9e5d7358e934e87c2da5ed34d997d0ef1ec403024ea760215b # via fastembed -mpmath==1.3.0 ; python_full_version < '3.11' \ - --hash=sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f \ - --hash=sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c - # via sympy -numpy==2.2.6 ; python_full_version < '3.11' \ - --hash=sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff \ - --hash=sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47 \ - --hash=sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84 \ - --hash=sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d \ - --hash=sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6 \ - --hash=sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f \ - --hash=sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b \ - --hash=sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49 \ - --hash=sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163 \ - --hash=sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571 \ - --hash=sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42 \ - --hash=sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff \ - --hash=sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491 \ - --hash=sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4 \ - --hash=sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566 \ - --hash=sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf \ - --hash=sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40 \ - --hash=sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd \ - --hash=sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06 \ - --hash=sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282 \ - --hash=sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680 \ - --hash=sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db \ - --hash=sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3 \ - --hash=sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90 \ - --hash=sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1 \ - --hash=sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289 \ - --hash=sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab \ - --hash=sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c \ - --hash=sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d \ - --hash=sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb \ - --hash=sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d \ - --hash=sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a \ - --hash=sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf \ - --hash=sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1 \ - --hash=sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2 \ - --hash=sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a \ - --hash=sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543 \ - --hash=sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00 \ - --hash=sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c \ - --hash=sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f \ - --hash=sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd \ - --hash=sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868 \ - --hash=sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303 \ - --hash=sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83 \ - --hash=sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3 \ - --hash=sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d \ - --hash=sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87 \ - --hash=sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa \ - --hash=sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f \ - --hash=sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae \ - --hash=sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda \ - --hash=sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915 \ - --hash=sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249 \ - --hash=sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de \ - --hash=sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8 - # via - # ctranslate2 - # fastembed - # magika - # onnxruntime - # pandas -numpy==2.4.6 ; python_full_version == '3.11.*' \ +numpy==2.4.6 ; python_full_version < '3.12' \ --hash=sha256:001fbb8e08d942dd57599e781f2472269ee7f2755fae407b4f67b2f0b17da3f1 \ --hash=sha256:0280e0356c0829a18d9de1cb7eee50ec22ca639878d7240307ca0943d73cd2c4 \ --hash=sha256:043191bfa8eab18c776647b62723ac9dddece59743b13f49b2016094129c2b3f \ @@ -1136,34 +1029,7 @@ olefile==0.47 \ --hash=sha256:543c7da2a7adadf21214938bb79c83ea12b473a4b6ee4ad4bf854e7715e13d1f \ --hash=sha256:599383381a0bf3dfbd932ca0ca6515acd174ed48870cbf7fee123d698c192c1c # via markitdown -onnxruntime==1.23.2 ; python_full_version < '3.11' \ - --hash=sha256:0be6a37a45e6719db5120e9986fcd30ea205ac8103fd1fb74b6c33348327a0cc \ - --hash=sha256:0f9b4ae77f8e3c9bee50c27bc1beede83f786fe1d52e99ac85aa8d65a01e9b77 \ - --hash=sha256:162f4ca894ec3de1a6fd53589e511e06ecdc3ff646849b62a9da7489dee9ce95 \ - --hash=sha256:1f9cc0a55349c584f083c1c076e611a7c35d5b867d5d6e6d6c823bf821978088 \ - --hash=sha256:218295a8acae83905f6f1aed8cacb8e3eb3bd7513a13fe4ba3b2664a19fc4a6b \ - --hash=sha256:25de5214923ce941a3523739d34a520aac30f21e631de53bba9174dc9c004435 \ - --hash=sha256:2ff531ad8496281b4297f32b83b01cdd719617e2351ffe0dba5684fb283afa1f \ - --hash=sha256:45d127d6e1e9b99d1ebeae9bcd8f98617a812f53f46699eafeb976275744826b \ - --hash=sha256:4ca88747e708e5c67337b0f65eed4b7d0dd70d22ac332038c9fc4635760018f7 \ - --hash=sha256:6f91d2c9b0965e86827a5ba01531d5b669770b01775b23199565d6c1f136616c \ - --hash=sha256:76ff670550dc23e58ea9bc53b5149b99a44e63b34b524f7b8547469aaa0dcb8c \ - --hash=sha256:87d8b6eaf0fbeb6835a60a4265fde7a3b60157cf1b2764773ac47237b4d48612 \ - --hash=sha256:8bace4e0d46480fbeeb7bbe1ffe1f080e6663a42d1086ff95c1551f2d39e7872 \ - --hash=sha256:8f7d1fe034090a1e371b7f3ca9d3ccae2fabae8c1d8844fb7371d1ea38e8e8d2 \ - --hash=sha256:902c756d8b633ce0dedd889b7c08459433fbcf35e9c38d1c03ddc020f0648c6e \ - --hash=sha256:9d2385e774f46ac38f02b3a91a91e30263d41b2f1f4f26ae34805b2a9ddef466 \ - --hash=sha256:a7730122afe186a784660f6ec5807138bf9d792fa1df76556b27307ea9ebcbe3 \ - --hash=sha256:b28740f4ecef1738ea8f807461dd541b8287d5650b5be33bca7b474e3cbd1f36 \ - --hash=sha256:b8f029a6b98d3cf5be564d52802bb50a8489ab73409fa9db0bf583eabb7c2321 \ - --hash=sha256:bbfd2fca76c855317568c1b36a885ddea2272c13cb0e395002c402f2360429a6 \ - --hash=sha256:da44b99206e77734c5819aa2142c69e64f3b46edc3bd314f6a45a932defc0b3e \ - --hash=sha256:e2b9233c4947907fd1818d0e581c049c41ccc39b2856cc942ff6d26317cee145 - # via - # fastembed - # faster-whisper - # magika -onnxruntime==1.29.0 ; python_full_version >= '3.11' \ +onnxruntime==1.29.0 \ --hash=sha256:07c5907474dec4a2792fd7626b753dc66707808385a6d9eecf993db0066a9d0f \ --hash=sha256:0d4f427afac434b0070fe992b540ddf20a7aff2265f760f314d91331935b6b98 \ --hash=sha256:11264bb58f7b7cf6af835ab10d36838d73680580820fd6f51d90124a1ca8f449 \ @@ -1204,64 +1070,7 @@ packaging==26.3 \ # via # huggingface-hub # onnxruntime -pandas==2.3.3 ; python_full_version < '3.11' \ - --hash=sha256:0242fe9a49aa8b4d78a4fa03acb397a58833ef6199e9aa40a95f027bb3a1b6e7 \ - --hash=sha256:1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593 \ - --hash=sha256:1b07204a219b3b7350abaae088f451860223a52cfb8a6c53358e7948735158e5 \ - --hash=sha256:1d37b5848ba49824e5c30bedb9c830ab9b7751fd049bc7914533e01c65f79791 \ - --hash=sha256:23ebd657a4d38268c7dfbdf089fbc31ea709d82e4923c5ffd4fbd5747133ce73 \ - --hash=sha256:2462b1a365b6109d275250baaae7b760fd25c726aaca0054649286bcfbb3e8ec \ - --hash=sha256:28083c648d9a99a5dd035ec125d42439c6c1c525098c58af0fc38dd1a7a1b3d4 \ - --hash=sha256:2e3ebdb170b5ef78f19bfb71b0dc5dc58775032361fa188e814959b74d726dd5 \ - --hash=sha256:318d77e0e42a628c04dc56bcef4b40de67918f7041c2b061af1da41dcff670ac \ - --hash=sha256:371a4ab48e950033bcf52b6527eccb564f52dc826c02afd9a1bc0ab731bba084 \ - --hash=sha256:376c6446ae31770764215a6c937f72d917f214b43560603cd60da6408f183b6c \ - --hash=sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87 \ - --hash=sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35 \ - --hash=sha256:4793891684806ae50d1288c9bae9330293ab4e083ccd1c5e383c34549c6e4250 \ - --hash=sha256:4e0a175408804d566144e170d0476b15d78458795bb18f1304fb94160cabf40c \ - --hash=sha256:503cf027cf9940d2ceaa1a93cfb5f8c8c7e6e90720a2850378f0b3f3b1e06826 \ - --hash=sha256:5554c929ccc317d41a5e3d1234f3be588248e61f08a74dd17c9eabb535777dc9 \ - --hash=sha256:56851a737e3470de7fa88e6131f41281ed440d29a9268dcbf0002da5ac366713 \ - --hash=sha256:5caf26f64126b6c7aec964f74266f435afef1c1b13da3b0636c7518a1fa3e2b1 \ - --hash=sha256:602b8615ebcc4a0c1751e71840428ddebeb142ec02c786e8ad6b1ce3c8dec523 \ - --hash=sha256:6253c72c6a1d990a410bc7de641d34053364ef8bcd3126f7e7450125887dffe3 \ - --hash=sha256:6435cb949cb34ec11cc9860246ccb2fdc9ecd742c12d3304989017d53f039a78 \ - --hash=sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53 \ - --hash=sha256:6d2cefc361461662ac48810cb14365a365ce864afe85ef1f447ff5a1e99ea81c \ - --hash=sha256:74ecdf1d301e812db96a465a525952f4dde225fdb6d8e5a521d47e1f42041e21 \ - --hash=sha256:75ea25f9529fdec2d2e93a42c523962261e567d250b0013b16210e1d40d7c2e5 \ - --hash=sha256:854d00d556406bffe66a4c0802f334c9ad5a96b4f1f868adf036a21b11ef13ff \ - --hash=sha256:8fe25fc7b623b0ef6b5009149627e34d2a4657e880948ec3c840e9402e5c1b45 \ - --hash=sha256:900f47d8f20860de523a1ac881c4c36d65efcb2eb850e6948140fa781736e110 \ - --hash=sha256:93c2d9ab0fc11822b5eece72ec9587e172f63cff87c00b062f6e37448ced4493 \ - --hash=sha256:a16dcec078a01eeef8ee61bf64074b4e524a2a3f4b3be9326420cabe59c4778b \ - --hash=sha256:a21d830e78df0a515db2b3d2f5570610f5e6bd2e27749770e8bb7b524b89b450 \ - --hash=sha256:a45c765238e2ed7d7c608fc5bc4a6f88b642f2f01e70c0c23d2224dd21829d86 \ - --hash=sha256:a637c5cdfa04b6d6e2ecedcb81fc52ffb0fd78ce2ebccc9ea964df9f658de8c8 \ - --hash=sha256:a68e15f780eddf2b07d242e17a04aa187a7ee12b40b930bfdd78070556550e98 \ - --hash=sha256:b3d11d2fda7eb164ef27ffc14b4fcab16a80e1ce67e9f57e19ec0afaf715ba89 \ - --hash=sha256:b468d3dad6ff947df92dcb32ede5b7bd41a9b3cceef0a30ed925f6d01fb8fa66 \ - --hash=sha256:b98560e98cb334799c0b07ca7967ac361a47326e9b4e5a7dfb5ab2b1c9d35a1b \ - --hash=sha256:bdcd9d1167f4885211e401b3036c0c8d9e274eee67ea8d0758a256d60704cfe8 \ - --hash=sha256:bf1f8a81d04ca90e32a0aceb819d34dbd378a98bf923b6398b9a3ec0bf44de29 \ - --hash=sha256:c46467899aaa4da076d5abc11084634e2d197e9460643dd455ac3db5856b24d6 \ - --hash=sha256:c4fc4c21971a1a9f4bdb4c73978c7f7256caa3e62b323f70d6cb80db583350bc \ - --hash=sha256:c503ba5216814e295f40711470446bc3fd00f0faea8a086cbc688808e26f92a2 \ - --hash=sha256:d051c0e065b94b7a3cea50eb1ec32e912cd96dba41647eb24104b6c6c14c5788 \ - --hash=sha256:d3e28b3e83862ccf4d85ff19cf8c20b2ae7e503881711ff2d534dc8f761131aa \ - --hash=sha256:db4301b2d1f926ae677a751eb2bd0e8c5f5319c9cb3f88b0becbbb0b07b34151 \ - --hash=sha256:dd7478f1463441ae4ca7308a70e90b33470fa593429f9d4c578dd00d1fa78838 \ - --hash=sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b \ - --hash=sha256:e19d192383eab2f4ceb30b412b22ea30690c9e618f78870357ae1d682912015a \ - --hash=sha256:e32e7cc9af0f1cc15548288a51a3b681cc2a219faa838e995f7dc53dbab1062d \ - --hash=sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908 \ - --hash=sha256:ee15f284898e7b246df8087fc82b87b01686f98ee67d85a17b7ab44143a3a9a0 \ - --hash=sha256:ee67acbbf05014ea6c763beb097e03cd629961c8a632075eeb34247120abcb4b \ - --hash=sha256:f086f6fe114e19d92014a1966f43a3e62285109afe874f067f5abbdcbb10e59c \ - --hash=sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee - # via markitdown -pandas==3.0.5 ; python_full_version >= '3.11' \ +pandas==3.0.5 \ --hash=sha256:08d24fe11a17dc33bd6e937dc9c665f9cba08fbdc9f657f405713515febe300d \ --hash=sha256:0d298e951f23016ce4699951d044ae6418dbc91bf68cefca0f77666fcbb4e5c6 \ --hash=sha256:0fac0010c75e4efb6b99e249c183a8993ce0dc95c240f9b120a5e67c727b7928 \ @@ -1315,95 +1124,92 @@ pdfplumber==0.11.10 \ --hash=sha256:7741ea81bf165b474b153e6789d10d18e06b6ddcf3ec84289c3ef2fed6802580 \ --hash=sha256:b95b2d28c66efb0a794a83b88c6c6aea5987532a445d20a1cbcfa657022e6e57 # via markitdown -pillow==12.3.0 \ - --hash=sha256:00808c5e14ef63ac5161091d242999076604ff74b883423a11e5d7bbb38bf756 \ - --hash=sha256:04f01d28a6aaff387bf842a13be313df23ba0597a44f1a976c9feb3c6ff4711a \ - --hash=sha256:06ff022112bc9cbf83b60f8e028d94ad87b60621706487e65f673de61610ab59 \ - --hash=sha256:0740a512dc522224c77d9aa5a8d70d8b7d73fb91f2c21125d8d025d3b8990e45 \ - --hash=sha256:0847a763afefb695bc912d7c131e7e0632d4edc1d8698f58ddabec8e46b8b6d3 \ - --hash=sha256:0dd2064cbc55aaec028ef5fbb60fa47bb6c3e7918e07ff17935284b227a9d2df \ - --hash=sha256:0feb2e9d6ad6c9e3c06effe9d00f3f1e618a6643273576b016f591e9315a7139 \ - --hash=sha256:10e41f0fbf1eec8cfd234b8fe17a4caac7c9d0db4c204d3c173a8f9f6ef3232b \ - --hash=sha256:1182d52bc2d5e5d7d0949503aa7e36d12f42205dc287e4883f407b1988820d39 \ - --hash=sha256:164b31cd1a0490ab6efae01aa5df49da7061be0af1b30e035b6e9a1bfe34ee6e \ - --hash=sha256:1657923d2d45afb66526e5b933e5b3052e6bdea196c90d3abb2424e18c77dae8 \ - --hash=sha256:186941b6aef820ad110fb01fb06eb925374dc3a21b17e37ec9a53b250c6fe2d1 \ - --hash=sha256:1cca606cd25738df4ed873d5ad46bbdb3d83b5cbca291f6b4ff13a4df6b0bbe8 \ - --hash=sha256:21900ce7ba264168cd50defae43cd75d25c833ad4ad6e73ffc5596d12e25ac89 \ - --hash=sha256:236ff70b9312fb68943c703aa842ca6a758abfa45ac187a5e7c1452e96ef72b5 \ - --hash=sha256:23aceaa007d6172b02c277f0cd359c79492bbb14f7072b4ede9fbcaf20648130 \ - --hash=sha256:23d27a3e0307ec2244cc51e7287b919aa68d097504ebe19df4e76a98a3eea5bd \ - --hash=sha256:24870b09b224f7ae3c39ed07d10e819d06f8720bc551847b1d623832b5b0e28d \ - --hash=sha256:251bf95b67017e27b13d82f5b326234ca62d70f9cf4c2b9032de2358a3b12c7b \ - --hash=sha256:25b9b82bb22e6e2b3cd07b39c68b7b862001226cb3dff7130d1cb914121b39ed \ - --hash=sha256:28ce87c5ab450a9dd970b52e5aca5fe63ed432d18a2eaddd1979a00a1ba24ace \ - --hash=sha256:300557495eb45ebb8aec96c2da9c4be642fbf7cd937278b4013ba894ea8eb0eb \ - --hash=sha256:30f2aa603c41533cc25c05acd0da21636e84a315768feb631c937177db558931 \ - --hash=sha256:331b624368d4f1d069149002f25f44bc61c8919ce8ddb3c45bdad8f6e2d89510 \ - --hash=sha256:37d6d0a00072fd2948eb22bce7e1475f34569d90c87c59f7a2ec59541b77f7a6 \ - --hash=sha256:37dc8f7bbb66efe481bb60defacef820c950c24713fb44962ed6aa2a50966de1 \ - --hash=sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce \ - --hash=sha256:3edce1d53195db527e0191f84b71d02022de0540bf43a16ed734ed7537b07385 \ - --hash=sha256:446c34dcc4324b084a53b705127dc15717b22c5e140ae0a3c38349d4efec071e \ - --hash=sha256:4998562bf62a445225f22e07c896bb04b35b1b1f2eb6d760584c9c51d7a5f78c \ - --hash=sha256:4b0a7fe987b14c31ebda6083f74f22b561fd3739bc0ac51e019622e3d72668c7 \ - --hash=sha256:4e8c2a84d977f50b9daed6eeaf3baef67d00d5d74d932288f02cb94518ee3ace \ - --hash=sha256:4f883547d4b7f0495ebe7056b0cc2aea76094e7a4abc8e933540f3271df27d9c \ - --hash=sha256:514435a37670e3e5e08f3945b68718b6ed329bb84367777e16f9f4dfe1e61a0f \ - --hash=sha256:53aa02d20d10c3d814d536aa4e5ac9b84ca0ff5a88377963b085ad6822f93e64 \ - --hash=sha256:5594fc43d548a7ed94949d139aa1341b270f1863f11cfd37f5a6c8b778a6b67f \ - --hash=sha256:571b9fcb07b97ef3a492028fb3d2dc0993ca23a06138b0315286566d29ef718a \ - --hash=sha256:57b3d78c95ba9059768b10e28b813002261d3f3dfc55cc48b0c988f625175827 \ - --hash=sha256:5afb51d599ea772b8365ae807ae557f18bccfe46ab261fd1c2a9ed700fc6eb17 \ - --hash=sha256:6b02afb9b97f65fbca5f31db6a2a3ba21aa93030225f150fa3f249717e938fb4 \ - --hash=sha256:6c0016e7b354317c4e9e525b937ac8596c38d2d232b419529b9cd7a1cd46e39a \ - --hash=sha256:71d6097b330eea8fd15097780c8e89cb1a8ce7838669f48c5bacd6f663dd4701 \ - --hash=sha256:756c768d0c9c2955feb7a56c37ea24aea2e369f8d36a88da270b6a9f19e62b5e \ - --hash=sha256:78cb2c6865a35ab8ff8b75fd122f6033b92a62c82801110e48ddd6c936a45d91 \ - --hash=sha256:7a743ff716f746fc19a9557f60dab1600d4613255f8a7aeb3cdde4db7eb15a66 \ - --hash=sha256:85f998ea1848bc6757289e739cfbdda3a04adfd58b02fc018ce54d754a5ce468 \ - --hash=sha256:8728f216dcdb6e6d555cf971cb34076139ad74b31fc2c14da4fafc741c5f6217 \ - --hash=sha256:877c3f311ff35410f690861c4409e7ccbf0cd2f878e50628a28e5a0bb689e658 \ - --hash=sha256:8cd2f7bdda092d99c9fc2fb7391354f306d01443d22785d0cbfafa2e2c8bb418 \ - --hash=sha256:8e95e1385e4998ae9694eeaa4730ba5457ff61185b3a55e2e7bea0880aef452a \ - --hash=sha256:962864dc93511324d51ddbb5b9f8731bf71675b93ca612a07441896f4688fb8c \ - --hash=sha256:9cf95fe4d0f84c82d282745d9bb08ad9f926efa00be4697e767b814ce40d4330 \ - --hash=sha256:9e881fca225083806662a5c43d627d215f258ff43c890f831966c7d7ba9c7402 \ - --hash=sha256:a2b55dd6b2a4c4b7d87ffa56bdb33fdc5fdb9a462173861a7bc097f17d91cb09 \ - --hash=sha256:a45650e8ce7fafffd731db8550230db6b0d306d181a90b67d3e6bca2f1990930 \ - --hash=sha256:a876864214e136f0eb367788dbd7df045f4806801518e2cfe9e13229cfe06d8f \ - --hash=sha256:ae26d61dfa7a47befdc7572b521024e8745f3d809bd95ca9505a7bba9ef849ec \ - --hash=sha256:af8d94b0db561cf68b88a267c5c44b49e134f525d0dc2cb7ed413a66bc23559a \ - --hash=sha256:b343699e8308bdc51978310e1c959c584e7869cc8c40780058c87da7781a1e94 \ - --hash=sha256:b3c777e849237620b022f7f297dd67705f9f5cf1685f09f02e46f93e92725468 \ - --hash=sha256:b629de27fda84b42cde7edef0d85f13b958b47f6e9bbcbba9b673c562a89bd8b \ - --hash=sha256:ba09209fbe443b4acccebe845d8a138b89a8f4fbaeedd44953490b5315d5e965 \ - --hash=sha256:ba54cfebe86920a559a7c4d6b9050791c20513650a1952ebe3368c7dc70306f8 \ - --hash=sha256:bcb46e2f9feff8d06323983bd83ed00c201fdcab3d74973e7072a889b3979fcd \ - --hash=sha256:bcc33feacfaefce60c12fd500a277533bdc02b10a19f7f6d348763d8140bbba7 \ - --hash=sha256:bf16ba1b4d0b6b7c8e534936632270cf70eb00dbe09005bc345b2677b726855c \ - --hash=sha256:cf1845d02ad822a369a49f2bb9345b1614744267682e7a03527dc3bf6eea1777 \ - --hash=sha256:d69141514cc30b774ceea5e3ed3a6635c8d8a96edf664689b890f4089111fb35 \ - --hash=sha256:d9c7f76c0673154f044e9d78c8655fb4213f6ca31a836df48b40fe5d187717b9 \ - --hash=sha256:dbce0b29841537a2fa4a214c2bbf14de3587c9680caa9b4e217568472490b28f \ - --hash=sha256:dc624f6bc473dacdf7ef7eb8678d0d08edf15cd94fad6ae5c7d6cc67a4e4902f \ - --hash=sha256:e158cb00350dc278f3b91551101aa7d12415a66ebf2c91d8d5ac14e56ddd3ad0 \ - --hash=sha256:e491916b378fba47242221bb9ead245211b70d504f495d105d17b14a24b4907c \ - --hash=sha256:e795b7eb908249c4e43c7c99fac7c2c75dab0c43566e37db472a355f63693d71 \ - --hash=sha256:e7e480451b9fa137494bccd3a7d69adbe8ac65a87d97be61e11f1b1050a5bac3 \ - --hash=sha256:e91206ee562682b51b98ef4b26a6ef48fd84e15fd4c4bc5ec768eb641d206838 \ - --hash=sha256:e9871b1ffbfa9656b60aeee92ed5136a5742696006fa322b29ea3d8da0ecc9cf \ - --hash=sha256:e9aeb04d6aef139de265b29683e119b638208f88cf73cdd1658aa07221165321 \ - --hash=sha256:ebaea975e03d3141d9d3a507df75c9b3ec90fa9d2ffd07567b3a978d9d790b26 \ - --hash=sha256:f0606c8bf2cdefea14a43530f7657cbbb7ecf1c4222512492ef4a4434a9501ec \ - --hash=sha256:f13c32a3abd6079a66d9526e18dad9b6d280384d49d7c54040cd57b6424041d9 \ - --hash=sha256:f7401aebd7f581d7f83a439d87d474999317ee099218e5ad25d125290990ba65 \ - --hash=sha256:fa4ecea169a355be7a3ade2c783e2ed12f0e40d2c5621cda8b3297faf7fbb9f5 \ - --hash=sha256:fbd139c8447d25dd750ab79ee274cc5e1fe80fc56340ab10b18a195e1b6eca3e \ - --hash=sha256:fdafc9cce40277e0f7a0feabce0ee50dd2fa1800f3b38015e51296b5e814048d \ - --hash=sha256:fe3cca2e4e8a592be0f269a1ca4835c25199d9f3ce815c8491048f785b0a0198 \ - --hash=sha256:ffd0c5368496f41b0944be820fcb7a838aa6e623d250b01acf2643939c3f99d7 +pillow==13.0.0 \ + --hash=sha256:02464da86f561f410e13b061ec37c3068a9cb557a095494157742d07b3c24a27 \ + --hash=sha256:04a63720f79e22fd54d897c233a68314c50ca0603360f54ce2bd5db8f0f43782 \ + --hash=sha256:05cd2cd39055d0b9650060e4513ef4a9a0aea870083950bd046a0d6490f93a22 \ + --hash=sha256:06903aac074d00c988e77a1fc435286dc498506d4ca8d6f69df9babddf4ecc15 \ + --hash=sha256:08b826a9bf1265c76de04a42061d61ea6b109418b7826f28ff1ecdfaa26c5502 \ + --hash=sha256:0d1a9cf7bc6917538b20f8d06b9ab2147e542f68849e96fc58eb66bdf6acc692 \ + --hash=sha256:10b5019883c98f1dd819ca48ffc6f33a91cee04948fe637c605f4345c7cafd6f \ + --hash=sha256:12b0fa15879fe5a9d9eebe66e4e2d74128a773b1a4448010c9c714f45e04b9d8 \ + --hash=sha256:12fbb851cf323cc19aac3eea1df5ad17b57e91e9fe3cfd06100dba7bb8208c13 \ + --hash=sha256:149a1c323ba366f90b48e6f1a6db6582eb50512df374588de56fc909abc0f0f3 \ + --hash=sha256:19b66ca0a4d25cc0200b381ce824c7ea090db61427315e73cfb05dbf1b35c369 \ + --hash=sha256:1c774b9c0ba0d0b356f71530677b39028878a2cae5a93e0a13779cfd17120f88 \ + --hash=sha256:20722cd5375533ad754053363a80b8fb30c79d37930a6f7228030e237ce3bd5d \ + --hash=sha256:20822140c9a2ba9c5e42df342b0a892ab2e102fc0e3db967a133bb9ce3f898bb \ + --hash=sha256:21c32061a5512dc13f87cc7303e120e4d77c2f70df89a35af0e3ce246e5ac57f \ + --hash=sha256:2310e2adfc13c6d457d7d0f2ffdd5c56a74a317650cf765887df8d515a647ac0 \ + --hash=sha256:23edf4ec4b6f4f240e8df6e4985e9ee2ced86474632433e55fb1e2548744bf40 \ + --hash=sha256:2d50500a4a3bc6469adcdb319cf05d688b08ca3bf0f22a05339f3b25b433a599 \ + --hash=sha256:2e31c4e68cffd7b1f116127626813b82fcba19edf6b940a7b0c266d6484d0055 \ + --hash=sha256:34fb3db1149c879343861831728c243c432d4a5afc8f1878ca7612ef8de2a033 \ + --hash=sha256:37290fbef0c73e2d51d594eb78fded7dc7170d1c3ad0a890269e6fd8c8c4e061 \ + --hash=sha256:405be6058e206a86fedce2f715ecc2343f94072d7925251632dffa2c05f70dca \ + --hash=sha256:40a266371ece581e5de13b6635ef78d7487824af80561b6ca27694a88c19bc0f \ + --hash=sha256:42d63be3af4f9b951b199b0a9d23365d37ab14687e6d77b74940432c8515c4ca \ + --hash=sha256:44629d4498c9cb6b2051806155f22141df0d8a929c56134b166655232dfb2f8c \ + --hash=sha256:49c12af515073e9bf511e4ef736743bf70d531a643668722279314b937742ba4 \ + --hash=sha256:4a25756244bdcce3b50580baadf320fa48297f0eeb8e114ba64139a40d4d0fc8 \ + --hash=sha256:4bd024d567eca931b668978d672dba914d4b8e2628e2c3073a2ad178de8a2620 \ + --hash=sha256:4f8db54177035a2b5f717f94475b74907e12a96dc74f6d487f3966b0a4694071 \ + --hash=sha256:51ebb529fc5e8abdd55302f6c3f8943037348ec2384329a3e55677f69ae2fdfc \ + --hash=sha256:567f20f1c75897d7663830da6ea8af719e03d712b1d9ee7f813319f9ece42e5a \ + --hash=sha256:5933fcea5a5ba64c686f55c886a115cbfd92f2900460784a1e79909fdbafd7b6 \ + --hash=sha256:5bdb38ba220b61b4ec31dd1046dea6b016d3c6850a23f43ed8b58332b2fc668e \ + --hash=sha256:5f56499c1a74d2b862beb0c33b8016f99d095858455df9d21583bbbc1c15b582 \ + --hash=sha256:67871a13cf47275201553b536c025da777f997fa562a19eb798af4ad4b97c18f \ + --hash=sha256:67c8d44ad89fa7eb88de5d6ef6199b1e03dd24974f85a67639fc8badb801fe79 \ + --hash=sha256:6a0920cb55df5a4d3f6862964908078c0bd59332bdd1bcd20d14e8c70ef27cc5 \ + --hash=sha256:6bce6a29c3898191f4c9998e4d4e8f69790caeabde46e54da4bc235ce5bde683 \ + --hash=sha256:6f38f3f885a09cafc93fd667b1173a92f379e43785d635e7983c893f0da319fb \ + --hash=sha256:73ee25cf8cf8b75d5a5aeed7cb6588f5e5dbfbb0fde51707b630bbb23897645c \ + --hash=sha256:7b999816b5850a35ada08443e6e03945f1f6488144a83afe2d37b240a552aafa \ + --hash=sha256:7c88c10dd432f3b39ccdafc9ed81d0c21b8fd634502574da76d1bfa4fe2c76d0 \ + --hash=sha256:7d5e097eabd5d4915c27ac9f6e9325bb1e01a7c94d7348b5c4451d3fc30e8521 \ + --hash=sha256:7e91e97a929232cdb828889ee6ad0e48d7f40f4ec38818f4e4a8968b2b1af22e \ + --hash=sha256:7f6299e494e46253d40c34653cac369412f44ca53b5f85d848d3f95b1ed0546c \ + --hash=sha256:816752a5ef76a80484c8c441c85593b7ea9304f6231fc5776ad1fbff7e773058 \ + --hash=sha256:83b37abd87b590acd8f47ca8a61f70893322b11834d36e07c2298dacad4d14d2 \ + --hash=sha256:8619685cad8efc4c38c66d3b36aad854523297c43d748b3b6f0cc03c90ba3463 \ + --hash=sha256:8bf295fe459343bc6093c8e5e0df60bfa189e0fcb775a22e2c2488fdeccc6c0d \ + --hash=sha256:8e459b613cf31f8980f584387b4fe47bf08e076e288d61aa8eba1f7fd4ddbb81 \ + --hash=sha256:8f36aa77f74b14a2103622b196402c71330e4e7cf4122ee45c854e9a01e0eecf \ + --hash=sha256:93ff9c9c3663ce23aaa5d0b6b6abac55359e2a0c06c7e30f3771829af816be63 \ + --hash=sha256:9a635068d9793c7f0d929f39f18b58f1bd86c236e8e7b64167b86af9232ebca0 \ + --hash=sha256:9dda0788a9932776a75d8bcc020076edc308ce2508b51f6739a45e3158b3fc42 \ + --hash=sha256:a192360d512305fe92d3053e4f7627bbad1f7708f1f4ad38cccd9336acabe9ad \ + --hash=sha256:a2a14cd73d5e6c927f644b0110ee81f59c4ef506259ec9183904747f2caacc81 \ + --hash=sha256:a625df9a2a5597fa5e5266e01aefc82604bb85d291e3547981e863560d75da6c \ + --hash=sha256:a6aaa7871aebca081eb5c45f83e845bbd9712ae99fa9286dbfdcee835e63f4b4 \ + --hash=sha256:aa694c49787040943570d61c9b8fc01271bd2a2cfdd55f0d896f8aecdce1a981 \ + --hash=sha256:adb3c2124c68e480f4632d06cb7caa3b48a51678475f483b4edb1c6dee29ea43 \ + --hash=sha256:b84be41387fc00db28c13c02e30d42b02d8d8f6a4a741a1aee6012ca7f1f6039 \ + --hash=sha256:bf97ba1b5542050e9b0c74e632323b6d8136cc99d64e193abebee22fe6013c76 \ + --hash=sha256:c3bc8ee0271387e0b1e3ad0ff893645990a656e79bf9c42f4f734a3642b11d51 \ + --hash=sha256:c50c80e3591e5cf9a8dfa6007d4f10732f02e32263850999286e56ccd650cfb2 \ + --hash=sha256:c6fd031333dedcad7ce3e03d244093f43504f209e86925ebdb2b868fa4037630 \ + --hash=sha256:c87a04e7f152ee4db5dbe15f0bfa3fe628a1aeedbe66b81c43b87f52c813f9ac \ + --hash=sha256:cdcd1ba258178f7356d74170c9a81ba4eab6a5d55748bc402a5a2a6fe6be3510 \ + --hash=sha256:d4737f50d23ddd11d6685faacbb9252887dde0bc7997127757247983d74399d6 \ + --hash=sha256:d7710eb676e9bb68965cb045be0d333b4c0d7f9dfbaa7fbf121385b1588683d3 \ + --hash=sha256:d9b0a2bee3f3e03e9f29057aedb807dbf1e1d96ee1b9d0f422d9db6c8583271d \ + --hash=sha256:e65dd6e75de67b2362a4fa3e16aa627595f65054dc4ec73d64680c84d592fa61 \ + --hash=sha256:e892354a424b1e82a78ba16785822bdd40de06f19446a3d4a28047df1a4f8932 \ + --hash=sha256:e8ca8f69b7c77fa96dad99f8638c1fc4f1c0bed4acaa1baf7a3c31bdb80a0ab1 \ + --hash=sha256:e91c2b1cbeae768c98752e67e717e283d68dae45717b41f2d91eaee648e37b7d \ + --hash=sha256:e98b4f91f99b24b1da72e10621ba181f7281922384a7403094debb4199de6c00 \ + --hash=sha256:ef5fe6b7db3f501c56000ba8fe7c7e1a35ca1d45824fbeb95a67daa85cff4dbe \ + --hash=sha256:f11a3c1358bb3c68012a7b1f41b223c3db9f7bfde90b481df028cfe1c3363313 \ + --hash=sha256:f391dc92a015c478745192fe4f2e120c2af2aba18d9fe0616341cbf537599806 \ + --hash=sha256:f7cbb7a64e7ec664775195e4f81923bc8a93d9451b963f844e59118f63147a7a \ + --hash=sha256:f9bd7d260d986f3553518c5d723544346fa48031f8aef3265d786aeb59845bc0 \ + --hash=sha256:fc481f742ccda20e9ed760b8f978dff50f2a4e9bc253ac167fb0a05b0783ef52 \ + --hash=sha256:ff3fe9eb621fbb9db4c4be33ab946f6bac5f98ab50ca8d3260503653656c961b \ + --hash=sha256:ffea6b2986a797c84b6d1d27d4f43e0b83c73fc16281a930891cd237cf0d08fe # via + # --override sidecar/requirements-override.txt # -r sidecar/requirements.txt # fastembed # pdfplumber @@ -1503,10 +1309,6 @@ pypdfium2==5.13.0 \ --hash=sha256:d96beb7f379e6c76d874ca93fcd182ac3168dd499056407070f9927fb1061b8e \ --hash=sha256:da5c7b74eebf40b5c1fbe1de01aa1edc8827a79fb1efd999616bc20dcaf77ba4 # via pdfplumber -pyreadline3==3.5.6 ; python_full_version < '3.11' and sys_platform == 'win32' \ - --hash=sha256:61e53218b99656091ddb077df9e71f25850e72e030b6183b39c9b7e6e4f4a9bf \ - --hash=sha256:8449b734232e42a5dcd74048e39b60db2839a4c38cf3ae2bf7707d58b5389c0d - # via humanfriendly python-dateutil==2.9.0.post0 \ --hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \ --hash=sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427 @@ -1519,10 +1321,6 @@ python-pptx==1.0.2 \ --hash=sha256:160838e0b8565a8b1f67947675886e9fea18aa5e795db7ae531606d68e785cba \ --hash=sha256:479a8af0eaf0f0d76b6f00b0887732874ad2e3188230315290cd1f9dd9cc7095 # via markitdown -pytz==2026.3.post1 ; python_full_version < '3.11' \ - --hash=sha256:2211d3fcf9a797d3405cac96ac7f61d80e6a644f72a3309607282fe8a2010c5d \ - --hash=sha256:dd95840dd199baea12d9cc096a1d452caa6596a1c1e4b5f3dbd1541855d5e815 - # via pandas pyyaml==6.0.3 \ --hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \ --hash=sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a \ @@ -1620,10 +1418,6 @@ soupsieve==2.9.2 \ --hash=sha256:4a55d8cf158a9c2e587fa4922f1bbb91d68ac829e2d6f25403a85747c71daf74 \ --hash=sha256:8089a26fd974ca7a1f30276d3d8492ab266ab15af581642dfe8aa162e0c1c823 # via beautifulsoup4 -sympy==1.14.0 ; python_full_version < '3.11' \ - --hash=sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517 \ - --hash=sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5 - # via onnxruntime tokenizers==0.23.1 \ --hash=sha256:120468fb4c24faf0543c835a4fabafa4deb3f20a035c9b6e83d0b553a97615d4 \ --hash=sha256:1974288a609c343774f1b897c8b482c791ab17b75ab5c8c2b1737565c1d82288 \ @@ -1658,11 +1452,9 @@ typing-extensions==4.16.0 \ # via # anyio # beautifulsoup4 - # cryptography - # exceptiongroup # huggingface-hub # python-pptx -tzdata==2026.3 ; python_full_version < '3.11' or sys_platform == 'emscripten' or sys_platform == 'win32' \ +tzdata==2026.3 ; sys_platform == 'emscripten' or sys_platform == 'win32' \ --hash=sha256:4a1518b8993086a7982523e071643f3c0e5f213e75b21318e78bcabfff9d1415 \ --hash=sha256:dc096730c87af6cab1b171c9d532be840741ff5d459015e7f6947bd7d7e54931 # via pandas diff --git a/sidecar/requirements.txt b/sidecar/requirements.txt index 77ae4769..ebc524d4 100644 --- a/sidecar/requirements.txt +++ b/sidecar/requirements.txt @@ -48,6 +48,15 @@ # pillow — pinned explicitly (it arrives transitively via markitdown's image/PDF path and the # sidecar's own EXIF/dimension reader) so a security-patched version is enforced rather than # whatever the resolver happens to pick. +# +# 13.0.0 is the only release carrying the fixes for the decoder advisories published from +# 2026-10-17 (there is no 12.x after 12.3.0), and it needs Python 3.11 or newer, which is why +# MIN_PYTHON in sidecar.rs is (3, 11). fastembed 0.9.0 still declares `pillow<13` on every +# Python although its code runs on 13 (and PM calls only its text models), so the same pin +# also sits in requirements-override.txt: `just lock-regen` passes that file to uv as an +# override, which lifts fastembed's cap, and the sidecar installs the lock with `--no-deps` so +# pip does not put the cap back on the user's machine. Remove the override line once a +# fastembed release allows pillow 13 — qdrant/fastembed#830 (open, unmerged at 0.9.0). # defusedxml — hardens the stdlib XML parser (openpyxl's fallback) against entity-expansion / XXE # from a malicious .xlsx (H-1 subset). Pure-Python; applied once at sidecar import. markitdown[pdf,docx,pptx,xlsx,outlook]==0.1.8 @@ -55,5 +64,5 @@ fastembed==0.9.0 faster-whisper==1.2.1 av<19 openpyxl==3.1.5 -pillow==12.3.0 +pillow==13.0.0 defusedxml==0.7.1 diff --git a/sidecar/test_pm_sidecar.py b/sidecar/test_pm_sidecar.py index 1ebeaa27..e17aea29 100644 --- a/sidecar/test_pm_sidecar.py +++ b/sidecar/test_pm_sidecar.py @@ -38,8 +38,10 @@ def _needs_fastembed(reason): """Skip a real-fastembed test where fastembed is absent (a plain `just sidecar-test` on a system Python, locally or in CI) — unless `PM_REQUIRE_FASTEMBED=1`, which makes the absence a FAILURE instead, so a run meant to exercise the pinned fastembed can't pass by skipping. pr.yml's - python job sets it for its second run, against a venv built from `sidecar/requirements.lock - --require-hashes`, on any PR that touches sidecar/. Locally: + python job sets it for its second run, against a venv built with `pip install --require-hashes + --no-deps -r sidecar/requirements.lock`, on any PR that touches sidecar/. `--no-deps` is not + optional: the lock overrides fastembed's `pillow<13` (sidecar/requirements-override.txt), so + without it pip refuses the lock. Locally, build the venv the same way, then `PM_REQUIRE_FASTEMBED=1 /bin/python sidecar/test_pm_sidecar.py`, inside the capped systemd-run scope on the Linux dev machine.""" if os.environ.get("PM_REQUIRE_FASTEMBED") == "1": @@ -217,10 +219,57 @@ def __call__(self, _target): return self._result +class _RecordingOcrEngine: + """A fake rapidocr that keeps what it was handed: whether OCR ran, and on what.""" + + def __init__(self): + self.targets = [] + + def __call__(self, target): + self.targets.append(target) + return _OcrResultObj(("TOTAL 12.50",)) + + +class _FakeOpenedImage: + """What `_open_image` returns, to the extent `do_analyze_image` reads it — no Pillow needed.""" + + size = (4, 3) + + def getexif(self): + return {} + + def close(self): + pass + + +_needs_pillow = unittest.skipUnless( + importlib.util.find_spec("PIL") is not None, + "Pillow not installed (image decode is a local-only check)", +) + + +def _exif_with_date_and_place(): + """EXIF the way a phone writes it: DateTimeOriginal in the Exif sub-IFD, a position in the GPS + sub-IFD (55°57'0"N, 3°11'24"W).""" + from PIL import Image + from PIL.TiffImagePlugin import IFDRational + + exif = Image.Exif() + exif.get_ifd(0x8769)[0x9003] = "2023:01:02 03:04:05" + gps = exif.get_ifd(0x8825) + gps[1] = "N" + gps[2] = (IFDRational(55, 1), IFDRational(57, 1), IFDRational(0, 1)) + gps[3] = "W" + gps[4] = (IFDRational(3, 1), IFDRational(11, 1), IFDRational(24, 1)) + return exif + + class AnalyzeImageTest(unittest.TestCase): - """Photo analysis: EXIF GPS/date parsing (pure), OCR result-shape tolerance (fake engine), and - the EXIF-only path when OCR is declined. The OCR engine and HEIC codec are OPTIONAL components, - so nothing here installs or downloads anything.""" + """Photo analysis: EXIF GPS/date parsing (pure), OCR result-shape tolerance (fake engine), the + EXIF-only path when OCR is declined, and the format allow-list: a photo opens only as a photo + format, its extension's own first, and OCR never sees an image PM could not open or decode. The + OCR engine and HEIC codec are OPTIONAL components, so nothing here installs or downloads + anything.""" def test_gps_to_decimal_signs_and_malformed(self): # 55°57'00"N, 3°11'24"W (Edinburgh-ish). N/E positive, S/W negated. @@ -253,15 +302,285 @@ def test_a_non_finite_result_fails_one_request_not_the_engine(self): fallback = json.dumps({"id": 1, "ok": False, "error": "non-finite number in result"}) self.assertEqual(json.loads(fallback)["ok"], False) - def test_run_ocr_parses_object_and_list_shapes(self): + def test_ocr_lines_parses_object_and_list_shapes(self): # New rapidocr: object with a .txts tuple. - obj = _OcrResultObj(("hello", "world")) - self.assertEqual(S._run_ocr(_FakeOcrEngine(obj), None, "x.png"), "hello\nworld") + self.assertEqual(S._ocr_lines(_OcrResultObj(("hello", "world"))), "hello\nworld") # Older rapidocr: ([[box, text, score], ...], elapse). old = ([[[0, 0], "line one", 0.9], [[1, 1], "line two", 0.8]], 0.1) - self.assertEqual(S._run_ocr(_FakeOcrEngine(old), None, "x.png"), "line one\nline two") + self.assertEqual(S._ocr_lines(old), "line one\nline two") # Nothing recognised → empty string, not an error. - self.assertEqual(S._run_ocr(_FakeOcrEngine(_OcrResultObj(())), None, "x.png"), "") + self.assertEqual(S._ocr_lines(_OcrResultObj(())), "") + + @_needs_pillow + def test_a_tiff_renamed_jpg_is_refused(self): + # Pillow picks a decoder from the file's CONTENT, so before the allow-list this opened as a + # TIFF — and TIFF, FlashPix, EPS and friends are where most of the 2026-10-17 advisories + # are. Pillow alone still opens it; PM must not. + import tempfile + + from PIL import Image, UnidentifiedImageError + + with tempfile.TemporaryDirectory() as d: + path = f"{d}/holiday.jpg" + Image.new("RGB", (20, 10), (200, 100, 50)).save(path, format="TIFF") + with Image.open(path) as unrestricted: + self.assertEqual(unrestricted.format, "TIFF", "the fixture really is a TIFF") + with self.assertRaises(UnidentifiedImageError): + S._open_image(path) + out = S.do_analyze_image({"path": path, "run_ocr": False}) + self.assertIsNone(out["width"], "no size: Rust reads that as 'could not open'") + self.assertIsNone(out["height"]) + self.assertIsNone(out["capture_date"]) + self.assertEqual(out["ocr_ran"], False) + + @_needs_pillow + def test_a_gif_renamed_png_is_refused(self): + # A real format that is not a photo's stays refused under any photo extension, not just + # under its own: the other photo formats are tried, nothing beyond them. + import tempfile + + from PIL import Image, UnidentifiedImageError + + with tempfile.TemporaryDirectory() as d: + path = f"{d}/meme.png" + Image.new("RGB", (20, 10), (200, 100, 50)).save(path, format="GIF") + with Image.open(path) as unrestricted: + self.assertEqual(unrestricted.format, "GIF", "the fixture really is a GIF") + with self.assertRaises(UnidentifiedImageError): + S._open_image(path) + + @_needs_pillow + def test_a_real_photo_with_the_wrong_photo_extension_still_reads(self): + # A PNG or a WebP saved as .jpg, a JPEG saved as .png: common, and refusing them blocked + # nothing (naming the file correctly reaches the same decoder). Refused, such a photo stayed + # unread for good, because ingest skips content it has indexed already. + import tempfile + + from PIL import Image + + exif = _exif_with_date_and_place().tobytes() + cases = ( + ("screenshot.jpg", "PNG"), + ("download.jpg", "WEBP"), + ("camera.png", "JPEG"), + ("camera.webp", "JPEG"), + ("export.png", "WEBP"), + ) + for name, fmt in cases: + with self.subTest(name=name, content=fmt), tempfile.TemporaryDirectory() as d: + path = f"{d}/{name}" + Image.new("RGB", (40, 30), (10, 20, 30)).save(path, format=fmt, exif=exif) + with S._open_image(path) as img: + self.assertEqual(img.format, fmt) + out = S.do_analyze_image({"path": path, "run_ocr": False}) + self.assertEqual((out["width"], out["height"]), (40, 30)) + self.assertEqual(out["capture_date"], "2023-01-02") + + @_needs_pillow + def test_the_extensions_own_format_is_tried_first(self): + import tempfile + + from PIL import Image + + tried = [] + real_open = Image.open + + def _recording_open(fp, mode="r", formats=None): + tried.append(formats) + return real_open(fp, mode, formats) + + with tempfile.TemporaryDirectory() as d: + for name in ("a.png", "b.webp", "c.jpeg"): + Image.new("RGB", (4, 3)).save(f"{d}/{name}") + with mock.patch.object(Image, "open", _recording_open): + for name in ("a.png", "b.webp", "c.jpeg"): + S._open_image(f"{d}/{name}").close() + self.assertEqual([f[0] for f in tried], ["PNG", "WEBP", "JPEG"]) + for formats in tried: + self.assertEqual(set(formats) - {"JPEG", "PNG", "WEBP", "HEIF"}, set()) + + @_needs_pillow + def test_an_extension_pm_does_not_route_here_opens_as_nothing(self): + # Rust only sends PHOTO_EXTS, but the fallback for an unknown extension must be "refuse", + # never `formats=None` — which is Pillow's "try every format it knows". + import tempfile + + from PIL import Image + + with tempfile.TemporaryDirectory() as d: + path = f"{d}/scan.tif" + Image.new("RGB", (20, 10)).save(path) + with self.assertRaises(ValueError): + S._open_image(path) + + @_needs_pillow + @unittest.skipIf( + importlib.util.find_spec("pi_heif") is not None, + "pi-heif installed (this pins the venv without the OCR component)", + ) + def test_a_heic_without_pi_heif_is_tried_as_the_other_photo_formats(self): + # Pillow raises a bare KeyError when asked for a format nobody registered, so HEIF is left + # out of the list without pi-heif. A JPEG saved as .heic still reads; a TIFF or a GIF saved + # as .heic matches none of the photo formats and is refused, never a KeyError. + import tempfile + + from PIL import Image, UnidentifiedImageError + + with tempfile.TemporaryDirectory() as d: + path = f"{d}/photo.heic" + Image.new("RGB", (20, 10), (200, 100, 50)).save(path, format="JPEG") + with S._open_image(path) as img: + self.assertEqual(img.format, "JPEG") + out = S.do_analyze_image({"path": path, "run_ocr": False}) + self.assertEqual((out["width"], out["height"]), (20, 10)) + for fmt in ("TIFF", "GIF"): + other = f"{d}/{fmt.lower()}.heic" + Image.new("RGB", (20, 10)).save(other, format=fmt) + with self.assertRaises(UnidentifiedImageError): + S._open_image(other) + + @_needs_pillow + def test_a_real_png_and_jpeg_still_read(self): + import tempfile + + from PIL import Image + + # As bytes: PNG's writer drops an Exif whose tags are all in sub-IFDs (the object is falsy). + exif = _exif_with_date_and_place().tobytes() + for name, fmt in (("shot.jpg", "JPEG"), ("SHOT.JPEG", "JPEG"), ("shot.png", "PNG")): + with self.subTest(name), tempfile.TemporaryDirectory() as d: + path = f"{d}/{name}" + Image.new("RGB", (40, 30), (10, 20, 30)).save(path, format=fmt, exif=exif) + out = S.do_analyze_image({"path": path, "run_ocr": False}) + self.assertEqual((out["width"], out["height"]), (40, 30)) + self.assertEqual(out["capture_date"], "2023-01-02") + self.assertAlmostEqual(out["lat"], 55.95, places=4) + self.assertAlmostEqual(out["lon"], -3.19, places=4) + + @_needs_pillow + def test_a_multi_picture_jpeg_still_reads(self): + # MPO is not in the allow-list (Pillow has no opener for it to name); it arrives through + # JPEG's, which hands a multi-picture file over by itself. Some cameras write these as .jpg. + import tempfile + + from PIL import Image + + with tempfile.TemporaryDirectory() as d: + path = f"{d}/stereo.jpg" + left, right = Image.new("RGB", (32, 24), "red"), Image.new("RGB", (32, 24), "blue") + left.save(path, format="MPO", save_all=True, append_images=[right]) + with S._open_image(path) as img: + self.assertEqual(img.format, "MPO") + out = S.do_analyze_image({"path": path, "run_ocr": False}) + self.assertEqual((out["width"], out["height"]), (32, 24)) + + @_needs_pillow + def test_ocr_is_not_attempted_on_an_image_pm_could_not_open(self): + # The old fallback handed rapidocr the PATH when PM's own open failed, and rapidocr opens a + # path with a bare `Image.open` — every decoder, the allow-list undone one call later. + import tempfile + + from PIL import Image + + engine = _RecordingOcrEngine() + asked = [] + + def _engine(): + asked.append(True) + return engine + + with tempfile.TemporaryDirectory() as d: + path = f"{d}/receipt.jpg" + Image.new("RGB", (20, 10)).save(path, format="TIFF") + with mock.patch.object(S, "get_ocr_engine", _engine): + out = S.do_analyze_image({"path": path, "run_ocr": True}) + self.assertEqual(asked, [], "the OCR engine was not even loaded") + self.assertEqual(engine.targets, []) + self.assertEqual(out["ocr_ran"], False) + self.assertEqual(out["ocr_text"], "") + self.assertIsNone(out["width"]) + + @unittest.skipUnless( + importlib.util.find_spec("PIL") is not None + and importlib.util.find_spec("numpy") is not None, + "Pillow/numpy not installed (image decode is a local-only check)", + ) + def test_ocr_reads_the_pixels_pm_opened(self): + import tempfile + + import numpy as np + from PIL import Image + + engine = _RecordingOcrEngine() + with tempfile.TemporaryDirectory() as d: + path = f"{d}/receipt.png" + Image.new("RGB", (40, 30), (255, 255, 255)).save(path) + with mock.patch.object(S, "get_ocr_engine", lambda: engine): + out = S.do_analyze_image({"path": path, "run_ocr": True}) + self.assertEqual(out["ocr_ran"], True) + self.assertEqual(out["ocr_text"], "TOTAL 12.50") + (target,) = engine.targets + self.assertIsInstance(target, np.ndarray, "pixels, never a path rapidocr would open itself") + self.assertEqual(target.shape, (30, 40, 3)) + + @staticmethod + def _half_a_jpeg(d): + """A JPEG cut off halfway, as an interrupted download or a half-synced folder leaves one: + the header (size, EXIF) is whole, the pixels are not. Noise, so the cut lands in the scan + data.""" + from PIL import Image + + path = f"{d}/receipt.jpg" + noise = Image.frombytes("RGB", (160, 120), os.urandom(160 * 120 * 3)) + noise.save(path, format="JPEG", exif=_exif_with_date_and_place().tobytes()) + with open(path, "rb") as f: + whole = f.read() + with open(path, "wb") as f: + f.write(whole[: len(whole) // 2]) + return path + + @unittest.skipUnless( + importlib.util.find_spec("PIL") is not None + and importlib.util.find_spec("numpy") is not None, + "Pillow/numpy not installed (image decode is a local-only check)", + ) + def test_a_truncated_photo_is_the_files_fault_not_ocrs(self): + # Opening reads only the header, so this used to come back WITH a size and fail inside the + # OCR call instead, as ocr_ran=false: the row then blamed text recognition, a component that + # works. No size is what Rust reports as "could not open"; the header's EXIF is still true. + import tempfile + + engine = _RecordingOcrEngine() + asked = [] + + def _engine(): + asked.append(True) + return engine + + with tempfile.TemporaryDirectory() as d: + path = self._half_a_jpeg(d) + with S._open_image(path) as img: + self.assertEqual(img.size, (160, 120), "the fixture's header is whole") + with mock.patch.object(S, "get_ocr_engine", _engine): + out = S.do_analyze_image({"path": path, "run_ocr": True}) + self.assertIsNone(out["width"], "no size: Rust reads that as 'could not open'") + self.assertIsNone(out["height"]) + self.assertEqual(out["ocr_ran"], False) + self.assertEqual(asked, [], "a file that will not decode never loads the OCR engine") + self.assertEqual(out["capture_date"], "2023-01-02", "the header's EXIF is kept") + self.assertAlmostEqual(out["lat"], 55.95, places=4) + + @_needs_pillow + def test_a_truncated_photo_with_ocr_off_reads_its_header_as_before(self): + # With OCR off nothing needs the pixels, so nothing decodes them: the size and EXIF in the + # header are what PM read before, and no new note appears on the row. + import tempfile + + with tempfile.TemporaryDirectory() as d: + out = S.do_analyze_image({"path": self._half_a_jpeg(d), "run_ocr": False}) + self.assertEqual((out["width"], out["height"]), (160, 120)) + self.assertEqual(out["capture_date"], "2023-01-02") + self.assertEqual(out["ocr_ran"], False) @unittest.skipUnless( importlib.util.find_spec("PIL") is not None, @@ -284,6 +603,26 @@ def test_analyze_image_exif_only_when_ocr_declined(self): self.assertIsNone(out["capture_date"]) self.assertIsNone(out["lat"]) + @_needs_pillow + def test_the_photo_is_closed_when_analysis_returns(self): + # With OCR off nothing reads the pixels, and Pillow holds the file open until something + # does, so every photo's handle lingered until garbage collection. + import gc + import tempfile + import warnings + + from PIL import Image + + with tempfile.TemporaryDirectory() as d: + path = f"{d}/shot.jpg" + Image.new("RGB", (8, 6)).save(path) + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always", ResourceWarning) + S.do_analyze_image({"path": path, "run_ocr": False}) + gc.collect() + leaks = [w for w in caught if issubclass(w.category, ResourceWarning)] + self.assertEqual(leaks, [], "the photo's file was left open") + def test_analyze_image_unreadable_path_is_null_not_crash(self): out = S.do_analyze_image({"path": "/no/such/image.png", "run_ocr": False}) self.assertEqual(out["ocr_ran"], False) @@ -291,12 +630,15 @@ def test_analyze_image_unreadable_path_is_null_not_crash(self): self.assertIsNone(out["capture_date"]) @unittest.skipUnless( - importlib.util.find_spec("PIL") is not None, - "Pillow not installed (image decode is a local-only check)", + importlib.util.find_spec("PIL") is not None + and importlib.util.find_spec("numpy") is not None, + "Pillow/numpy not installed (image decode is a local-only check)", ) def test_broken_ocr_degrades_to_exif_only(self): # F-56: if the OCR component is broken/offline (engine load or call raises), photo ingest - # must still return dimensions/EXIF with ocr_ran=false, not fail the whole file. + # must still return dimensions/EXIF with ocr_ran=false, not fail the whole file. The pixels + # are decoded (into a numpy array) before the engine is asked for, so numpy is needed to + # reach the engine at all; every venv has it (fastembed and rapidocr both depend on it). import tempfile from PIL import Image @@ -321,9 +663,13 @@ def test_a_cold_model_cache_is_not_swallowed_as_exif_only(self): def _cold(): raise S.ModelNotCached("rapidocr models are not downloaded yet") - with mock.patch.object(S, "get_ocr_engine", _cold): - with self.assertRaises(S.ModelNotCached): - S.do_analyze_image({"path": "/no/such/image.png", "run_ocr": True}) + # OCR only runs on an image PM opened and decoded, so stand both in (no Pillow or numpy + # needed for that). + with mock.patch.object(S, "_open_image", lambda _path: _FakeOpenedImage()): + with mock.patch.object(S, "_decode_pixels", lambda _img: object()): + with mock.patch.object(S, "get_ocr_engine", _cold): + with self.assertRaises(S.ModelNotCached): + S.do_analyze_image({"path": "/no/such/image.png", "run_ocr": True}) def _fake_rapidocr(captured, model_file="/pm/models/rapidocr/det.onnx", lazy=True): diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 113e6964..18126a0e 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -3072,7 +3072,7 @@ dependencies = [ [[package]] name = "pm" -version = "3.148.6-alpha" +version = "3.148.7-alpha" dependencies = [ "argon2", "base64 0.23.1", @@ -3709,9 +3709,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] name = "rustls-webpki" -version = "0.103.15" +version = "0.103.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" +checksum = "4443b1b6d39d7c663139942cc4558262562d2c84123bb6282ef177009ca8b281" dependencies = [ "ring", "rustls-pki-types", @@ -5447,18 +5447,18 @@ dependencies = [ [[package]] name = "webpki-root-certs" -version = "1.0.7" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c" +checksum = "58ec80e73fd8baf57bf0d9615554c75348bda990d6d5bbeab964dd068319fecb" dependencies = [ "rustls-pki-types", ] [[package]] name = "webpki-roots" -version = "1.0.7" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" +checksum = "1c1c8e20c8a38fc7d65abfe74eb7280e4aa667e8eec8bc84996d2874810748d9" dependencies = [ "rustls-pki-types", ] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 4ba4ce9d..746079b0 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pm" -version = "3.148.6-alpha" +version = "3.148.7-alpha" description = "PM — a local-first personal manager (Archivist + Personal Assistant)" authors = ["Bobby Yu"] edition = "2021" diff --git a/src-tauri/src/ingest.rs b/src-tauri/src/ingest.rs index 062dfed2..665556db 100644 --- a/src-tauri/src/ingest.rs +++ b/src-tauri/src/ingest.rs @@ -719,17 +719,9 @@ fn ingest_photo( // come back either way. No DB lock held across this call. let run_ocr = state.sidecar.optional_ocr_ready(); let analysis = state.sidecar.analyze_image(path, run_ocr)?; - // `ocr_ran` is the only thing that tells "OCR was asked for and broke" from "this image holds - // no text" — the two produce a byte-identical document otherwise, so a receipt indexed with no - // searchable text looked entirely normal. A cold model cache no longer lands here (the sidecar - // reports that as a miss and the fetcher fills it), so what is left is a genuinely broken - // component, and the user is the only one who can act on it. - let ocr_warning = (run_ocr && !analysis.ocr_ran).then(|| { - eprintln!( - "ingest: photo indexed without OCR (the text-recognition component did not run): {}", - path.display() - ); - "text recognition did not run, so any text in this image is not searchable".to_string() + let ocr_warning = photo_read_warning(ext, run_ocr, &analysis).map(|note| { + eprintln!("ingest: a photo was indexed with a note: {note}"); + note.to_string() }); let capture_date = @@ -874,6 +866,40 @@ fn ingest_photo( }) } +/// The note a photo's row carries when something in it was not read, or `None`. Pure, so every case +/// is pinned by a test rather than by a real broken component. +/// +/// Two causes, kept apart because what the user can do about them differs: +/// - The image did not open (the sidecar reports no size). It opens a photo only as a photo format +/// (JPEG, PNG, WebP, HEIC), and with OCR on it decodes the pixels before the engine runs, so this +/// is a damaged or cut-off file, or another kind of image renamed — the FILE, which no reinstall +/// fixes. Blaming the text-recognition component would send the user to repair something that +/// works. The one silent case is a HEIC with OCR off: decoding HEIC needs pi-heif, which ships +/// with the OCR component the UI already offers on a photo drop. +/// - It opened, OCR was asked for, and did not run. `ocr_ran` is the only thing that tells that +/// apart from "this image holds no text": the two produce a byte-identical document otherwise, so +/// a receipt indexed with no searchable text looked entirely normal. A cold model cache no longer +/// lands here (the sidecar reports that as a miss and the fetcher fills it), so what is left is a +/// genuinely broken component, and the user is the only one who can act on it. +fn photo_read_warning( + ext: Option<&str>, + run_ocr: bool, + analysis: &photos::ImageAnalysis, +) -> Option<&'static str> { + let opened = analysis.width.is_some() || analysis.height.is_some(); + if !opened { + if ext == Some("heic") && !run_ocr { + return None; + } + return Some( + "PM could not read this image (it may be damaged or cut off, or not the kind of image \ + its name says), so its picture and any text in it were not read", + ); + } + (run_ocr && !analysis.ocr_ran) + .then_some("text recognition did not run, so any text in this image is not searchable") +} + /// Fold several optional notes into the one `warning` slot [`Outcome::Indexed`] carries, dropping the /// empties. Separate function so "both things went wrong" cannot be lost to a plain assignment. fn join_warnings(parts: impl IntoIterator>) -> Option { @@ -4904,6 +4930,49 @@ mod tests { ); } + #[test] + fn a_photo_that_did_not_open_blames_the_file_not_the_ocr_component() { + let opened = photos::ImageAnalysis { + width: Some(1200), + height: Some(800), + ..Default::default() + }; + let read = photos::ImageAnalysis { + ocr_ran: true, + ..opened.clone() + }; + let unopened = photos::ImageAnalysis::default(); + let file_note = Some( + "PM could not read this image (it may be damaged or cut off, or not the kind of image \ + its name says), so its picture and any text in it were not read", + ); + let ocr_note = + Some("text recognition did not run, so any text in this image is not searchable"); + + // Nothing went wrong, OCR on or off. + assert_eq!(photo_read_warning(Some("jpg"), true, &read), None); + assert_eq!(photo_read_warning(Some("jpg"), false, &opened), None); + // It opened and the component did not run: the component's note, as before. + assert_eq!(photo_read_warning(Some("jpg"), true, &opened), ocr_note); + // It did not open — a TIFF renamed .jpg, say, which the sidecar now refuses. OCR was never + // attempted, so `ocr_ran` is false here too, and the old note blamed the component. + assert_eq!(photo_read_warning(Some("jpg"), true, &unopened), file_note); + // …and the user hears about it with OCR off as well. + assert_eq!(photo_read_warning(Some("png"), false, &unopened), file_note); + // A JPEG cut off halfway, with OCR on: its pixels would not decode, so the sidecar sends no + // size but keeps the EXIF from the header. The size decides it, and the file is blamed. + let cut_off = photos::ImageAnalysis { + capture_date: Some("2023-01-02".to_string()), + lat: Some(55.95), + lon: Some(-3.19), + ..Default::default() + }; + assert_eq!(photo_read_warning(Some("jpg"), true, &cut_off), file_note); + // The one silent case: HEIC needs pi-heif, which comes with the OCR component. + assert_eq!(photo_read_warning(Some("heic"), false, &unopened), None); + assert_eq!(photo_read_warning(Some("heic"), true, &unopened), file_note); + } + #[test] fn snapshot_counts_completed_files_not_started_ones() { // This is what a tab returning mid-rebuild renders, so the count has to match the views': diff --git a/src-tauri/src/photos.rs b/src-tauri/src/photos.rs index da4d36d1..f027bc82 100644 --- a/src-tauri/src/photos.rs +++ b/src-tauri/src/photos.rs @@ -225,7 +225,10 @@ pub fn ocr_text_from_body(body: &str) -> Option { /// run) plus best-effort EXIF capture metadata. `ocr_ran` distinguishes "OCR ran and found nothing" /// from "OCR was not requested" (the user declined the optional component). Any EXIF field is `None` /// when absent or unreadable (e.g. a HEIC opened without the pi-heif codec) — the caller then -/// falls back to a filename- or ingest-time capture date. +/// falls back to a filename- or ingest-time capture date. The sidecar opens a photo only as a photo +/// format, and an image it could not open comes back with no size and no OCR attempted, as does one +/// whose pixels would not decode for OCR (a file cut off halfway; its header's EXIF is kept): ingest +/// reads the missing size as "could not open" and says so on the file's row. #[derive(Debug, Clone, Default, PartialEq)] pub struct ImageAnalysis { /// Recognised text, joined by newlines. Empty string when OCR found nothing or was not run. diff --git a/src-tauri/src/python_fetch.rs b/src-tauri/src/python_fetch.rs index 68c01a78..d7d43f97 100644 --- a/src-tauri/src/python_fetch.rs +++ b/src-tauri/src/python_fetch.rs @@ -9,7 +9,7 @@ //! and bundling unsigned dylibs inside the signed `.app` needs the Apple-signing //! pipeline (see `docs/MACOS-SIGNING.md`). So the fallback here is the other half //! of the same idea: when [`crate::sidecar::SidecarManager::resolve_base_python`] -//! probes the machine and finds **no** interpreter ≥ 3.10, it downloads one at +//! probes the machine and finds **no** interpreter ≥ 3.11, it downloads one at //! runtime into PM's data-home (a subprocess download into a user directory, not //! a build-time bundle into the signed app — so it isn't gated on signing, and a //! `reqwest` download carries no `com.apple.quarantine` flag). @@ -19,7 +19,7 @@ //! before it is unpacked (no trust-on-first-use); a stamp file makes a second run //! skip the work. The pinned interpreter version is deliberately exact — it's the //! one validated in CI against markitdown/fastembed/faster-whisper — unlike the -//! *probe*, which accepts any ≥ 3.10 already on the machine. +//! *probe*, which accepts any ≥ 3.11 already on the machine. //! //! macOS-only in practice: [`fetch_macos_python`] is only ever called from the //! `cfg!(target_os = "macos")` arm of `resolve_base_python`. The pure helpers are diff --git a/src-tauri/src/sidecar.rs b/src-tauri/src/sidecar.rs index 63b1b997..13f54a38 100644 --- a/src-tauri/src/sidecar.rs +++ b/src-tauri/src/sidecar.rs @@ -1054,8 +1054,9 @@ impl SidecarManager { // Tear down a venv we can't trust — a previous attempt that died before // stamping the marker, or one whose interpreter is now too old (e.g. it - // was first built against macOS's system 3.9, then the user installed - // 3.10+). This is what removes the manual "delete the venv and retry". + // was first built against macOS's system 3.9, or on a 3.10 from before + // the floor rose to 3.11). This is what removes the manual "delete the + // venv and retry". let venv_python_exists = self.paths.venv_python().exists(); let detected = venv_python_exists .then(|| detect_python_version(&self.paths.venv_python())) @@ -1135,16 +1136,18 @@ impl SidecarManager { // whatever PyPI served that day — in the one process that opens untrusted PDFs, Office // documents and images. The flag is also fail-closed in a second, useful way: it makes pip // reject a lock that is not FULLY pinned, so a partially-regenerated file cannot install. - pip.args([ - "-m", - "pip", - "install", - "--disable-pip-version-check", - "--no-cache-dir", - "--require-hashes", - "-r", - ]) - .arg(&lock); + // `--no-deps`: the lock is resolved past one dependency's declared bound. fastembed 0.9.0 + // says `pillow<13`, but only pillow 13 carries the decoder fixes published from 2026-10-17 + // (see sidecar/requirements-override.txt), so pip's own resolver would refuse the lock. The + // lock needs no resolving here anyway: uv resolved every dependency into it, and + // `just requirements-lock` fails one that has drifted from its inputs. Every line is still + // pinned and hash-checked; what pip no longer does is notice a dependency MISSING from the + // lock, which only a hand-edited lock could produce. The optional installs and CI's + // installs of the lock (pr.yml, release.yml) take the same flag. + pip.args(["-m", "pip", "install"]) + .args(LOCK_INSTALL_FLAGS) + .arg("-r") + .arg(&lock); clean_python_env(&mut pip); no_window(&mut pip); run_command(&mut pip, "pip install requirements").map_err(|e| ProvisionError { @@ -1246,9 +1249,11 @@ impl SidecarManager { kind: SidecarErrorKind::PythonDownloadFailed, source: Error::Other(format!( "PM downloaded a Python interpreter to {} but it did not report a \ - usable version (3.10+). This is a problem with PM's build, not your \ + usable version ({}.{}+). This is a problem with PM's build, not your \ computer.", - p.display() + p.display(), + MIN_PYTHON.0, + MIN_PYTHON.1 )), }); } @@ -1303,7 +1308,7 @@ impl SidecarManager { return Ok(false); } // The lock can be unchanged yet the venv's interpreter be too old - // (built against macOS system 3.9, then the user installed 3.10+). Don't + // (built against macOS system 3.9, then the user installed 3.11+). Don't // report Ready in that case — let provision() rebuild it. This probe runs // only here, in the otherwise-ready branch, not on every status() poll. Ok(detect_python_version(&self.paths.venv_python()).is_some_and(meets_min)) @@ -1739,7 +1744,9 @@ impl SidecarManager { // arrive with its whole transitive tree (opencv, shapely, pyclipper) free-resolved at install // time. The lock is also resolved against the BASE lock, so an on-demand install can no // longer move a package the running venv depends on — pip was previously free to satisfy - // rapidocr by swapping the numpy that fastembed and onnxruntime are using. + // rapidocr by swapping the numpy that fastembed and onnxruntime are using. `--no-deps` as + // in the base install (see `provision`): the component's lock is just as complete, so + // every lock installs by one rule — pip resolves nothing on the user's machine. let lock = self.paths.optional_lock(component); if !lock.exists() { return Err(Error::Other(format!( @@ -1769,16 +1776,9 @@ impl SidecarManager { )?; } let lock_arg = lock.to_string_lossy().into_owned(); - let args: Vec<&str> = vec![ - "install", - "--disable-pip-version-check", - "--no-cache-dir", - "--progress-bar", - "off", - "--require-hashes", - "-r", - &lock_arg, - ]; + let mut args: Vec<&str> = vec!["install"]; + args.extend(LOCK_INSTALL_FLAGS); + args.extend(["--progress-bar", "off", "-r", &lock_arg]); run_pip_streaming(&py, &args, |line| { if let Some(f) = pip_phase_fraction(line, &mut downloads) { if f > last { @@ -2699,9 +2699,23 @@ fn request_timeout(method: &str) -> std::time::Duration { } } -/// markitdown (pinned in requirements.txt) needs Python >= 3.10, so the venv's -/// base interpreter must too. -const MIN_PYTHON: (u32, u32) = (3, 10); +/// The flags every pip install of a lock passes, the base venv's and each optional component's. +/// `--require-hashes` and `--no-deps` are the two that carry weight; `provision` says why each. +const LOCK_INSTALL_FLAGS: [&str; 4] = [ + "--disable-pip-version-check", + "--no-cache-dir", + "--require-hashes", + "--no-deps", +]; + +/// pillow 13 (pinned in requirements.txt: the first release with the decoder fixes +/// published from 2026-10-17) needs Python >= 3.11, so the venv's base interpreter +/// must too. It was 3.10, markitdown's floor, until then; 3.10 reaches end of life +/// in October 2026. Every installer already provides 3.12 (bundled on Windows and +/// Linux, downloaded on macOS when the machine has nothing new enough), so a venv +/// left on 3.10 is rebuilt once on the next launch. `just lock-regen` reads this +/// value as the floor the locks resolve from. +const MIN_PYTHON: (u32, u32) = (3, 11); /// Highest `python3.N` minor we probe by versioned name. Gives headroom past /// today's releases; anything newer is still found via the bare `python3` name. @@ -2716,7 +2730,7 @@ fn meets_min(v: (u32, u32)) -> bool { /// - no venv yet → false (the normal create path handles it) /// - venv but no marker → true (a previous attempt died before stamping) /// - venv + marker but the interpreter is too old / unprobeable → true (e.g. -/// built against macOS system 3.9, then the user installed 3.10+) +/// built against macOS system 3.9, or on a 3.10 from before the floor rose) fn should_rebuild_venv( venv_python_exists: bool, marker_present: bool, @@ -2895,9 +2909,9 @@ fn unknown(e: impl Into) -> ProvisionError { } } -/// Map a pip / `run_command` failure message to a cause. The common one is -/// markitdown needing Python >= 3.10, which pip reports as "No matching -/// distribution found" / "requires-python". +/// Map a pip / `run_command` failure message to a cause. The common one is a +/// pinned package needing a newer Python than the venv's (pillow 13 needs 3.11), +/// which pip reports as "No matching distribution found" / "requires-python". fn classify_pip_failure(msg: &str) -> SidecarErrorKind { let m = msg.to_ascii_lowercase(); if m.contains("no matching distribution") @@ -2910,16 +2924,21 @@ fn classify_pip_failure(msg: &str) -> SidecarErrorKind { } } +// Both messages name the floor from MIN_PYTHON itself, so raising it cannot leave them asking for +// a version PM then refuses. fn missing_message() -> String { - "No Python interpreter was found. PM's document engine needs Python 3.10 or newer — \ - install it and make sure it's on your PATH, or set PM_PYTHON to its full path." - .to_string() + let (major, minor) = MIN_PYTHON; + format!( + "No Python interpreter was found. PM's document engine needs Python {major}.{minor} or \ + newer — install it and make sure it's on your PATH, or set PM_PYTHON to its full path." + ) } fn too_old_message() -> String { - let mut m = String::from( - "The Python that was found is older than 3.10, which PM's document engine requires. \ - Install Python 3.10 or newer, then retry.", + let (major, minor) = MIN_PYTHON; + let mut m = format!( + "The Python that was found is older than {major}.{minor}, which PM's document engine \ + requires. Install Python {major}.{minor} or newer, then retry." ); if std::env::var_os("PM_PYTHON").is_some() { m.push_str(" (PM_PYTHON points to an interpreter that is too old.)"); @@ -4164,13 +4183,34 @@ mod tests { #[test] fn enforces_minimum_python() { - assert!(meets_min((3, 10))); + assert!(meets_min((3, 11))); assert!(meets_min((3, 14))); assert!(meets_min((4, 0))); + // pillow 13 has no 3.10 wheels and no 3.10 in its Requires-Python. + assert!(!meets_min((3, 10))); assert!(!meets_min((3, 9))); assert!(!meets_min((2, 7))); } + #[test] + fn every_lock_install_is_hash_checked_and_resolves_nothing() { + // `--require-hashes`: an artifact whose digest is not in the lock is refused. `--no-deps`: + // pip must not re-resolve, because the base lock overrides fastembed 0.9.0's `pillow<13` + // and pip's resolver refuses it (ResolutionImpossible) — every install would fail. + assert!(LOCK_INSTALL_FLAGS.contains(&"--require-hashes")); + assert!(LOCK_INSTALL_FLAGS.contains(&"--no-deps")); + } + + #[test] + fn python_guidance_names_the_floor_pm_enforces() { + // The messages used to spell the version out, so raising MIN_PYTHON would have left them + // asking for a Python that PM then turns down. + assert!(missing_message().contains("needs Python 3.11 or newer")); + let too_old = too_old_message(); + assert!(too_old.contains("older than 3.11")); + assert!(too_old.contains("Install Python 3.11 or newer")); + } + #[test] fn rebuilds_only_stale_or_old_venvs() { // No venv yet → the normal create path handles it. @@ -4182,8 +4222,10 @@ mod tests { // Venv + marker but the interpreter is too old / unprobeable. assert!(should_rebuild_venv(true, true, Some((3, 9)))); assert!(should_rebuild_venv(true, true, None)); + // A venv an older PM built on 3.10, which the floor accepted until pillow 13: rebuilt once. + assert!(should_rebuild_venv(true, true, Some((3, 10)))); // Venv + marker + new enough → keep it. - assert!(!should_rebuild_venv(true, true, Some((3, 10)))); + assert!(!should_rebuild_venv(true, true, Some((3, 11)))); assert!(!should_rebuild_venv(true, true, Some((3, 12)))); } @@ -4204,6 +4246,11 @@ mod tests { classify_pip_failure("x requires a different Python: 3.9.6 not in '>=3.10'"), SidecarErrorKind::PythonTooOld ); + // What a 3.10 interpreter meets now: pillow 13 publishes no wheel it can install. + assert_eq!( + classify_pip_failure("ERROR: No matching distribution found for pillow==13.0.0"), + SidecarErrorKind::PythonTooOld + ); assert_eq!( classify_pip_failure("Failed to establish a new connection"), SidecarErrorKind::PipFailed @@ -4337,10 +4384,8 @@ mod tests { "Current".to_string(), "3.10".to_string(), ]; - assert_eq!( - pick_framework_versions(&names), - vec![(3, 12), (3, 11), (3, 10)] - ); + // 3.10 is listed and dropped: it is below the floor now, like 3.9. + assert_eq!(pick_framework_versions(&names), vec![(3, 12), (3, 11)]); let empty: Vec = Vec::new(); assert!(pick_framework_versions(&empty).is_empty()); diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 408b701b..8fe60465 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "PM", - "version": "3.148.6-alpha", + "version": "3.148.7-alpha", "identifier": "org.itsatlas.pm", "build": { "beforeDevCommand": "npm run dev", diff --git a/src/lib/changelog.ts b/src/lib/changelog.ts index 6f25924b..335e29a5 100644 --- a/src/lib/changelog.ts +++ b/src/lib/changelog.ts @@ -23,6 +23,14 @@ export interface ChangelogEntry { } export const CHANGELOG: ChangelogEntry[] = [ + { + version: "3.148.7-alpha", + date: "2026-10-21", + highlights: [ + "Security fixes for photos. This week the image library PM uses to read your photos fixed a long list of flaws in how it decodes pictures, some serious enough that a booby-trapped image could have done real harm, and PM now runs the fixed version. PM also reads a photo only as one of the kinds of photo it expects (JPEG, PNG, WebP or HEIC), so a file of some other kind wearing a photo's name is turned away, while a real photo with the wrong ending, like a PNG saved as .jpg, still reads as before. When PM can't read a photo, because it's damaged, cut off partway through a download, or not a photo at all, the file's row now says so instead of blaming text recognition. The fixed library needs Python 3.11 or newer. PM's Windows and Linux installers bring their own, and on a Mac PM uses yours if it's new enough or fetches one by itself, so most people won't notice a thing: PM updates its document engine by itself the next time it starts, and if it had been running on an older Python of yours, it moves to a newer one once.", + "Security fixes for connections. This week a certificate authority lost control of some of its signing keys, which would let whoever holds them pass for almost any website on a network they control. PM no longer trusts anything signed with those keys, both when the app talks to services such as Google and when its document engine downloads what it needs.", + ], + }, { version: "3.148.6-alpha", date: "2026-10-18", diff --git a/src/lib/setupGuide.ts b/src/lib/setupGuide.ts index 6285df18..a1891a16 100644 --- a/src/lib/setupGuide.ts +++ b/src/lib/setupGuide.ts @@ -63,14 +63,14 @@ export function guideFor(mode: SetupGuideMode, platform: SetupPlatform): SetupGu case "python_missing": { const summary = mode === "python_too_old" - ? "PM's document engine needs Python 3.10 or newer, and the Python on this computer is older. (macOS ships an old Python, so this is common on a fresh Mac.)" - : "PM's document engine needs Python 3.10 or newer, and PM couldn't find a suitable one on this computer."; + ? "PM's document engine needs Python 3.11 or newer, and the Python on this computer is older. (macOS ships an old Python, so this is common on a fresh Mac.)" + : "PM's document engine needs Python 3.11 or newer, and PM couldn't find a suitable one on this computer."; if (platform === "mac") { return { title: "PM needs a newer Python", summary, steps: [ - "Install Python 3.10 or newer — run `brew install python@3.12`, or download it from python.org and run the installer.", + "Install Python 3.11 or newer — run `brew install python@3.12`, or download it from python.org and run the installer.", "Click Retry below. PM checks the usual install locations automatically — you don't need the Terminal or any environment variables.", "Still stuck? Open Technical details below to see exactly what PM found.", ], @@ -83,7 +83,7 @@ export function guideFor(mode: SetupGuideMode, platform: SetupPlatform): SetupGu title: "PM needs a newer Python", summary, steps: [ - "Install Python 3.10 or newer with your package manager — Fedora: `sudo dnf install python3`; Debian/Ubuntu: `sudo apt install python3`.", + "Install Python 3.11 or newer with your package manager — Fedora: `sudo dnf install python3`; Debian/Ubuntu: `sudo apt install python3` (Ubuntu 22.04's is 3.10, so there: `sudo apt install python3.11 python3.11-venv`).", "Click Retry below.", "Unusual setup? You can point PM at a specific interpreter with the PM_PYTHON environment variable.", ], @@ -93,7 +93,7 @@ export function guideFor(mode: SetupGuideMode, platform: SetupPlatform): SetupGu title: "PM needs a newer Python", summary, steps: [ - "Install Python 3.10 or newer from python.org — tick “Add python.exe to PATH” in the installer.", + "Install Python 3.11 or newer from python.org — tick “Add python.exe to PATH” in the installer.", "Click Retry below.", "Unusual setup? You can point PM at a specific interpreter with the PM_PYTHON environment variable.", ],