Skip to content

feat(preprod): Add odiff server wrapper and Dockerfile binary install#109380

Merged
NicoHinderling merged 12 commits intomasterfrom
create-compare-task-v1-part1
Feb 26, 2026
Merged

feat(preprod): Add odiff server wrapper and Dockerfile binary install#109380
NicoHinderling merged 12 commits intomasterfrom
create-compare-task-v1-part1

Conversation

@NicoHinderling
Copy link
Copy Markdown
Contributor

Summary

  • Add OdiffServer subprocess wrapper that communicates with odiff via JSON-over-stdin/stdout protocol
  • Add DiffResult dataclass for structured diff output
  • Install the odiff binary in the self-hosted Dockerfile (x64 + arm64, SHA256-verified)

Stack: 1/3 — next: image comparison library

&& apt-get update \
&& apt-get install -y --no-install-recommends $buildDeps \
&& uv sync --frozen --quiet --no-install-project \
&& case "$(dpkg --print-architecture)" in \
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should move this into its own layer as it's separate from uv.lock, otherwise we'll do this every time a python dep changes

pretty sure this is all possible with docker primitives + multistage (to handle the docker arch for multiplat builds) at this point too; let me just commit this for you

Copy link
Copy Markdown
Contributor Author

@NicoHinderling NicoHinderling Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you!

Copy link
Copy Markdown
Contributor Author

NicoHinderling commented Feb 25, 2026

@NicoHinderling NicoHinderling force-pushed the create-compare-task-v1-part1 branch from ca48cee to 12f0802 Compare February 25, 2026 23:52
raise RuntimeError("odiff server timed out after 30s")
line = process.stdout.readline()
try:
response = self._read_json(line)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would become something like response = OdiffResponse.model_validate(line) if we used a pydantic model: https://docs.pydantic.dev/latest/concepts/json/

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gotcha, okay let me try that out

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I kept parse_obj(json.loads(line)) rather than model_validate_json since Sentry pins pydantic to v1 (pydantic>=1.10.26,<2 in pyproject.toml). model_validate_json is v2-only and doesn't exist at runtime or in mypy's pydantic plugin unfortunately

"jest-fail-on-console": "3.3.1",
"jest-junit": "16.0.0",
"knip": "5.83.1",
"odiff-bin": "^4.3.2",
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need the package.json change? This is only touching backend

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

right, i removed it initially as you remember, but as it turns out, for local development, this is the easiest way to have access to odiff

'@babel/preset-react', // Still used in jest
'@babel/preset-typescript', // Still used in jest
'@emotion/babel-plugin', // Still used in jest
'odiff-bin', // raw binary consumed by Python backend, not a JS import
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, why is this needed in knip? That's only a frontend check IIUC

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is needed because odiff is used for dev purposes and knip freaks out that we have a package that doesnt seem to be referenced in the frontend code

def __exit__(self, *args: object) -> None:
self.close()

def _read_json(self, line: bytes) -> dict[str, object]:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to remove this entirely, it seems it's only used twice (_read_response and _start). _read_response is already typed and the json.loads should be fine to do inline. If it throws, that's a valid error we want to catch (and catching and rethrowing anyways?)

In _start I feel we should just inline json.loads(). Both errors here are just rethrown, so I don't see what catching and rethrowing is doing in this function

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tried to incorporate this feedback. Removed _read_json entirely and inlined json.loads at both call sites. Kept parse_obj over model_validate_json since we pin pydantic to v1.

Copy link
Copy Markdown
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

@NicoHinderling NicoHinderling merged commit afeb841 into master Feb 26, 2026
87 of 89 checks passed
@NicoHinderling NicoHinderling deleted the create-compare-task-v1-part1 branch February 26, 2026 19:26
NicoHinderling added a commit that referenced this pull request Feb 26, 2026
…109381)

## Summary
- Add `compare_images` and `compare_images_batch` functions using
dual-threshold detection (base + color-sensitive)
- Produces base64-encoded diff mask PNGs with pixel-level change data
- Unit tests covering identical, different, different-size, threshold
sensitivity, bytes input, and batch modes

**Stack**: 2/3 — depends on #109380, next: Celery task

---------

Co-authored-by: Claude <[email protected]>
@github-actions github-actions bot locked and limited conversation to collaborators Mar 18, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

Scope: Backend Automatically applied to PRs that change backend components Scope: Frontend Automatically applied to PRs that change frontend components

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants