Files
instameex/README.md
T
Djeex 974690132e Reorganize into src/ with script/assets/templates, illustration/ at root
New layout:
  src/script/app/    Python package (was app/)
  src/assets/img/    SVGs (was static/*.svg)
  src/assets/css/    stylesheet (was static/style.css)
  src/templates/     HTML templates (was templates/)
  illustration/      README screenshot (was screenshots/), kept at
                      the repo root since it's a doc asset, not
                      something the app serves
  main.py, jobs/, VERSION, docker/  unchanged locations

main.py stays at the repo root as the entry point (matches how
Docker and local runs already invoke it) and prepends src/script to
sys.path before importing app, rather than moving into src/script/
itself and needing every entry point updated to match.

config.py's path constants are recomputed for app/config.py's new
depth (src/script/app/), with SRC_DIR and REPO_ROOT resolved
explicitly rather than a single ambiguous BASE_DIR, since "templates
and assets live under src/" and "jobs and VERSION live at the repo
root" are no longer the same directory. Flask's static_folder now
points at src/assets, which serves img/ and css/ under it at
whatever URL prefix Flask derives from the folder name (/assets, not
/static); templates already reference assets via the "static"
endpoint name through url_for, so this needed the url_for() calls'
filename argument updated with the img/ or css/ prefix, not a
hardcoded URL change.

Docker copies src/ into the image preserving this same internal
structure, so the path arithmetic in config.py resolves identically
in both a local checkout and the container, no --chdir or other
container-specific path handling needed.

Verified end to end after the move: local Flask dev server, a full
Docker rebuild, and a real HTTP conversion request through the
running container (TIFF HDR pipeline, numpy/tifffile imports
included) all succeed.
2026-08-01 15:26:18 +02:00

71 lines
3.1 KiB
Markdown

> [!NOTE]
> ⚠️ _This is an heavily vibe-coded proof of concept — **do not expose it to the internet** — use it at your own risk._
> _Github repo is a mirror of https://git.djeex.fr/Djeex/insta-hdr-converter. You'll find full package, history and release note there._
# Instagram HDR Assembler
A small web front end for assembling Instagram-compatible HDR JPEGs (gain
map) from an SDR export and an HDR export out of Lightroom or Camera Raw.
The gain-map assembly logic ([src/script/app/assembler.py](src/script/app/assembler.py)) is
adapted from [kostis-kounadis/instagram-hdr-assembler](https://github.com/kostis-kounadis/instagram-hdr-assembler),
itself based on the reverse-engineering work of
[karachungen/instagram-hdr-converter](https://github.com/karachungen/instagram-hdr-converter).
Original MIT license kept in [LICENSE](LICENSE).
![Instagram HDR Assembler screenshot](illustration/insta-hdr-1.png)
## How it works
1. Upload an SDR file (`.jpg`/`.jpeg`) and an HDR file: either a gain-map
JPEG (`.jpg`/`.jpeg`) or Lightroom's 32-bit float linear HDR TIFF
export (`.tif`/`.tiff`).
2. Both files must have identical dimensions and match one of Instagram's
feed resolutions exactly: `1080x1080` (1:1), `1080x1350` (4:5), or
`1080x566` (1.91:1).
3. The real HDR pixel data is obtained from the HDR file: decoded back
out of a gain-map JPEG (`ultrahdr_app`, undoing its own embedded base
image and gain map), or read directly from a linear TIFF and given
HLG's system-gamma OOTF so it's on the same display-referred scale a
gain-map JPEG's HDR intent would be. A gain map is only correct
against the exact base it was computed from, and that's rarely the
SDR file you upload, so reusing an existing one as-is would quietly
reconstruct the wrong HDR look. Your SDR file is cleaned up and
re-encoded at 4:2:0 chroma subsampling (Pillow), then a fresh gain map
is computed against it and packaged into the final UltraHDR JPEG
(`ultrahdr_app`). The output's visible image is exactly your SDR file;
only the gain map is recomputed. The TIFF route skips the gain-map
JPEG's 8-bit-in-8-bit precision ceiling, giving a more accurate result.
4. The validation report is shown on the result page, including the
"GainMapMin is negative" warning when present, with a download link
for the finished JPEG.
Uploads and results don't stick around: SDR/HDR originals are deleted
right after conversion, the result a few seconds after you leave the
result page (heartbeat + background reaper), and `jobs/` is wiped on every
app startup.
## Running locally, without Docker
```bash
pip install -r requirements.txt
# also install exiftool and compile libultrahdr, see docker/Dockerfile
# for the exact packages and commands (validated on Alpine)
PORT=5050 python3 main.py # PORT is optional, defaults to 5000
```
## Running with Docker
```bash
docker compose -f docker/compose.yaml up -d --build
```
Opens on `http://<host>:5050` (port 5000 collides with AirPlay Receiver on
macOS, hence 5050 by default; change it in `docker/compose.yaml` if
needed).
## Running this on the open internet
Don't do it.