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.
This commit is contained in:
Djeex
2026-08-01 15:26:18 +02:00
parent d67a2569f4
commit 974690132e
15 changed files with 29 additions and 23 deletions
+125
View File
@@ -0,0 +1,125 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Instagram HDR Assembler</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head>
<body>
<div class="wrap">
<p class="eyebrow">Instagram HDR Assembler</p>
<h1>Build your HDR JPEG for Instagram</h1>
<p class="subtitle">Drop your SDR export and your HDR export from Lightroom / Camera Raw. The script does the rest — gain map, 4:2:0 subsampling, Instagram metadata injection. Add more pairs to process several photos in one go.</p>
<div class="card">
{% if error %}
<div class="error-banner">{{ error }}</div>
{% endif %}
<form action="/convert" method="post" enctype="multipart/form-data">
<div id="duos">
<div class="duo">
<button type="button" class="remove-duo" title="Remove this pair" aria-label="Remove this pair">&times;</button>
<div class="dropzones">
<div class="dropzone" data-zone="sdr">
<span class="tag">SDR fallback</span>
<div class="hint">.jpg / .jpeg</div>
<div class="filename"></div>
<input type="file" name="sdr[]" accept=".jpg,.jpeg" required>
</div>
<div class="dropzone" data-zone="hdr">
<span class="tag">HDR export</span>
<div class="hint">.jpg (gain map), or 32-bit float .tif</div>
<div class="filename"></div>
<input type="file" name="hdr[]" accept=".jpg,.jpeg,.tif,.tiff" required>
</div>
</div>
</div>
</div>
<button type="button" class="add-duo" id="add-duo">+ Add another pair</button>
<label class="checkbox-row">
<input type="checkbox" name="ignore_ig_resolution">
Process anyway if the resolution doesn't match Instagram's feed sizes
</label>
<button class="run" type="submit">Assemble the HDR</button>
</form>
</div>
<p class="footnote">
Each pair's files must have identical dimensions. By default that must also match one of
Instagram's feed resolutions exactly: <strong>1080x1080</strong> (1:1),
<strong>1080x1350</strong> (4:5), or <strong>1080x566</strong> (1.91:1); check the box above
to skip that check for results meant for somewhere other than Instagram. Assembling more
than one pair downloads as a <code>.zip</code>. HDR can be a regular gain-map JPEG or
Lightroom's 32-bit float linear HDR TIFF export; a TIFF HDR file gives a more precise result
than the JPEG gain-map route.
</p>
<p class="version">Instagram HDR Assembler v{{ app_version }}</p>
<div class="repo-links">
<a href="https://git.djeex.fr/Djeex/insta-hdr-converter" target="_blank" rel="noopener">
<span class="icon"><img src="{{ url_for('static', filename='img/gitea.svg') }}" alt=""></span>
Gitea
</a>
<a href="https://github.com/Djeex/insta-hdr-converter" target="_blank" rel="noopener">
<span class="icon"><img src="{{ url_for('static', filename='img/github.svg') }}" alt=""></span>
GitHub
</a>
</div>
</div>
<script>
(function () {
var duosContainer = document.getElementById('duos');
var addBtn = document.getElementById('add-duo');
function updateFilename(zone) {
var input = zone.querySelector('input[type="file"]');
var filenameEl = zone.querySelector('.filename');
if (input.files && input.files.length > 0) {
zone.classList.add('has-file');
filenameEl.textContent = input.files[0].name;
filenameEl.style.display = 'block';
} else {
zone.classList.remove('has-file');
filenameEl.style.display = 'none';
}
}
duosContainer.addEventListener('change', function (e) {
var zone = e.target.closest('.dropzone');
if (zone) updateFilename(zone);
});
['dragover', 'dragenter'].forEach(function (evt) {
duosContainer.addEventListener(evt, function (e) {
var zone = e.target.closest('.dropzone');
if (zone) { e.preventDefault(); zone.classList.add('has-file'); }
});
});
duosContainer.addEventListener('click', function (e) {
if (!e.target.classList.contains('remove-duo')) return;
var duos = duosContainer.querySelectorAll('.duo');
if (duos.length > 1) {
e.target.closest('.duo').remove();
}
});
addBtn.addEventListener('click', function () {
var clone = duosContainer.querySelector('.duo').cloneNode(true);
clone.querySelectorAll('input[type="file"]').forEach(function (input) { input.value = ''; });
clone.querySelectorAll('.filename').forEach(function (el) {
el.textContent = '';
el.style.display = 'none';
});
clone.querySelectorAll('.dropzone').forEach(function (zone) { zone.classList.remove('has-file'); });
duosContainer.appendChild(clone);
});
})();
</script>
</body>
</html>