docs: deploy from e588597

This commit is contained in:
2026-07-21 09:12:46 +02:00
parent 34b1f2c58f
commit 9c029d43ca
9 changed files with 35 additions and 29 deletions
+2 -2
View File
@@ -835,7 +835,7 @@ image count.</p>
<h2 id="first-signal-calibration-curves">First signal: calibration curves<a class="headerlink" href="#first-signal-calibration-curves" title="Permanent link">&para;</a></h2>
<p>Each gallery carries a fitted Platt sigmoid <code>P(match | cosine similarity) =
σ(a·sim + b)</code>, stored directly in the gallery HDF5
(<a href="https://gitea.tourolle.paris/dtourolle/scene-actor-extraction/raw/commit/0bd27470698c45cab21935d636a04512360e1008/src/gallery/gallery_calibration.hpp"><code>src/gallery/gallery_calibration.hpp</code></a>).
(<a href="https://gitea.tourolle.paris/dtourolle/scene-actor-extraction/raw/commit/e5885977dfd04741a0d1bb7aaf0270d9c40a2fa8/src/gallery/gallery_calibration.hpp"><code>src/gallery/gallery_calibration.hpp</code></a>).
This is a property of the embedding space alone, computed from intra- and
inter-actor reference-image pairs with no tracking or scene logic involved,
so it is a clean first read on discriminative power before running a
@@ -1032,7 +1032,7 @@ lead here is a precision-and-recall lead, not a misID one.</p>
is model-specific, since embeddings from different models are not
comparable. Any existing gallery built against a different model must be
rebuilt from source images before the new default takes effect.
<a href="https://gitea.tourolle.paris/dtourolle/scene-actor-extraction/raw/commit/0bd27470698c45cab21935d636a04512360e1008/scripts/optimizer/reembed_gallery.py"><code>scripts/optimizer/reembed_gallery.py</code></a>
<a href="https://gitea.tourolle.paris/dtourolle/scene-actor-extraction/raw/commit/e5885977dfd04741a0d1bb7aaf0270d9c40a2fa8/scripts/optimizer/reembed_gallery.py"><code>scripts/optimizer/reembed_gallery.py</code></a>
does this from a reference gallery's cached source images without
re-downloading anything.</p>
+3 -3
View File
@@ -835,7 +835,7 @@ model-choice effect: LVFace beats r18 by 6.2pp in full mode but beats mbf by
<h2 id="why-this-is-not-the-shipped-default">Why this is not the shipped default<a class="headerlink" href="#why-this-is-not-the-shipped-default" title="Permanent link">&para;</a></h2>
<p>Cast restriction is implemented today only as an offline optimizer
technique
(<a href="https://gitea.tourolle.paris/dtourolle/scene-actor-extraction/raw/commit/0bd27470698c45cab21935d636a04512360e1008/scripts/optimizer/cast_restrict.py"><code>scripts/optimizer/cast_restrict.py</code></a>):
(<a href="https://gitea.tourolle.paris/dtourolle/scene-actor-extraction/raw/commit/e5885977dfd04741a0d1bb7aaf0270d9c40a2fa8/scripts/optimizer/cast_restrict.py"><code>scripts/optimizer/cast_restrict.py</code></a>):
it pre-builds a filtered gallery file per film using Jellyfin's cast list
before the benchmark calls the matcher. There is no runtime "restrict to
this title's credited cast" switch in the shipped application;
@@ -844,7 +844,7 @@ given.</p>
<p>Building this as a real feature requires:</p>
<ul>
<li>A live Jellyfin cast lookup at analysis time. The title is already known,
and <a href="https://gitea.tourolle.paris/dtourolle/scene-actor-extraction/raw/commit/0bd27470698c45cab21935d636a04512360e1008/scripts/run_from_jellyfin.py"><code>scripts/run_from_jellyfin.py</code></a>
and <a href="https://gitea.tourolle.paris/dtourolle/scene-actor-extraction/raw/commit/e5885977dfd04741a0d1bb7aaf0270d9c40a2fa8/scripts/run_from_jellyfin.py"><code>scripts/run_from_jellyfin.py</code></a>
already performs this lookup for its own <code>filter_gallery</code>-based
restriction path; it is not wired into <code>scene_analyze</code> as a first-class
option.</li>
@@ -855,7 +855,7 @@ given.</p>
<li>Regenerating the restricted-gallery cache whenever a title's Jellyfin
cast list changes.</li>
</ul>
<p>The shipped <a href="https://gitea.tourolle.paris/dtourolle/scene-actor-extraction/raw/commit/0bd27470698c45cab21935d636a04512360e1008/src/config.hpp"><code>src/config.hpp</code></a> defaults use
<p>The shipped <a href="https://gitea.tourolle.paris/dtourolle/scene-actor-extraction/raw/commit/e5885977dfd04741a0d1bb7aaf0270d9c40a2fa8/src/config.hpp"><code>src/config.hpp</code></a> defaults use
the full-mode winner (<code>LVFace-B_Glint360K_full_exp</code>, F1 75.3% training,
67.4% held-out macro) rather than the higher-scoring <code>restricted_exp</code>
(78.3%), because 78.3% describes a capability the application does not
+2 -2
View File
@@ -855,7 +855,7 @@ credited cast.</p>
four pages above, including how replaying against cached embeddings
inside the same KPN network makes a full model and configuration
comparison practical, the full results table, and every caveat. This is
where the shipped <a href="https://gitea.tourolle.paris/dtourolle/scene-actor-extraction/raw/commit/0bd27470698c45cab21935d636a04512360e1008/src/config.hpp"><code>src/config.hpp</code></a>
where the shipped <a href="https://gitea.tourolle.paris/dtourolle/scene-actor-extraction/raw/commit/e5885977dfd04741a0d1bb7aaf0270d9c40a2fa8/src/config.hpp"><code>src/config.hpp</code></a>
defaults come from.</li>
<li><strong><a href="service-conversion/">Service conversion (proposal)</a></strong>: design
sketch for a native idle-GPU worker gated on screen lock, not yet built.</li>
@@ -868,7 +868,7 @@ pushed to the Gitea package registry and pulled on demand:</p>
</span><span id="__span-0-2"><a id="__codelineno-0-2" name="__codelineno-0-2" href="#__codelineno-0-2"></a>scripts/artifacts/pull_artifacts.sh<span class="w"> </span>experiment-data
</span><span id="__span-0-3"><a id="__codelineno-0-3" name="__codelineno-0-3" href="#__codelineno-0-3"></a>scripts/artifacts/pull_artifacts.sh<span class="w"> </span>montage-frames<span class="w"> </span>&lt;film-slug&gt;
</span></code></pre></div>
<p>See <a href="https://gitea.tourolle.paris/dtourolle/scene-actor-extraction/raw/commit/0bd27470698c45cab21935d636a04512360e1008/scripts/artifacts/push_artifacts.sh"><code>scripts/artifacts/push_artifacts.sh</code></a>
<p>See <a href="https://gitea.tourolle.paris/dtourolle/scene-actor-extraction/raw/commit/e5885977dfd04741a0d1bb7aaf0270d9c40a2fa8/scripts/artifacts/push_artifacts.sh"><code>scripts/artifacts/push_artifacts.sh</code></a>
for the upload side, which requires a <code>GITEA_TOKEN</code> with package write
scope.</p>
+13 -7
View File
@@ -1165,7 +1165,7 @@ same span (verified for Hugh Bonneville: bbox <code>(1743.2, 0.0, 171.3, 317.8)<
unchanged to the pixel, at every sampled second for 57 seconds). The
staircase at the right edge is the extinction window expiring, actor by
actor. This is <code>SceneTrackerFunc::active_[actor_idx].last_bbox</code>
(<a href="https://gitea.tourolle.paris/dtourolle/scene-actor-extraction/raw/commit/0bd27470698c45cab21935d636a04512360e1008/src/nodes/scene_tracker_node.hpp"><code>src/nodes/scene_tracker_node.hpp</code></a>)
(<a href="https://gitea.tourolle.paris/dtourolle/scene-actor-extraction/raw/commit/e5885977dfd04741a0d1bb7aaf0270d9c40a2fa8/src/nodes/scene_tracker_node.hpp"><code>src/nodes/scene_tracker_node.hpp</code></a>)
re-emitted as designed. <code>extinction_sec=57.4</code> was tuned long because
bridging is correct on most footage, as in the polygraph scene above. The
training films did not contain a faceless stretch long enough to expose the
@@ -1245,17 +1245,23 @@ pipeline has no way to draw that distinction from a face crop alone.</p>
the edge of frame. A marginal, low-confidence lookalike confusion.</p>
<h3 id="lovelace-1-name">Lovelace: 1 name<a class="headerlink" href="#lovelace-1-name" title="Permanent link">&para;</a></h3>
<p><img alt="Chloë Sevigny, out-of-cast name in Lovelace" src="../assets/images/lovelace_fpi_sevigny.jpg" /></p>
<p>Chloë Sevigny, t=2451s, 100% confidence. Two boxes are drawn on the same
face: one correctly labeled Amanda Seyfried, one incorrectly labeled Chloë
Sevigny, both at 100%. A single detection producing two competing high-
confidence identities on the same crop.</p>
<p>Chloë Sevigny, t=2451s, 100% confidence. Amanda Seyfried's track is real and
well-tracked through most of this shot, but her bbox is frozen at the exact
same coordinates for t=2450 and t=2451, one second where her box stopped
updating from a fresh detection. Only one real face is detected at t=2451
(confirmed against the dump's own per-frame detections), and it is a tight
IoU-1.0 fit under the Chloë Sevigny box, not the Seyfried one. So the green
Seyfried box in this frame is a ghost, re-emitting her last known position
for that one second, and the fresh, wrong detection is Sevigny, landing on
top of it. Not two competing fresh identities on one crop: one ghost and
one fresh misidentification happening to overlap.</p>
<h3 id="scarface-1-name">Scarface: 1 name<a class="headerlink" href="#scarface-1-name" title="Permanent link">&para;</a></h3>
<p><img alt="Kirstie Alley, out-of-cast name in Scarface" src="../assets/images/scarface_fpi_alley.jpg" /></p>
<p>Kirstie Alley, t=2451s, 89% confidence. Al Pacino is correctly identified in
the foreground at 100%; a background face in the same shot is wrongly
labeled Kirstie Alley. (The t=2451s here and the Lovelace Chloë Sevigny case
above landing on the identical second is a genuine coincidence, verified from
each film's raw stream by <a href="https://gitea.tourolle.paris/dtourolle/scene-actor-extraction/raw/commit/0bd27470698c45cab21935d636a04512360e1008/scripts/docs/first_fpi_frames.py"><code>first_fpi_frames.py</code></a>,
each film's raw stream by <a href="https://gitea.tourolle.paris/dtourolle/scene-actor-extraction/raw/commit/e5885977dfd04741a0d1bb7aaf0270d9c40a2fa8/scripts/docs/first_fpi_frames.py"><code>first_fpi_frames.py</code></a>,
not a transcription slip, two unrelated films whose <em>first</em> out-of-cast name
happens to fall at the same timestamp.)</p>
<h3 id="summary-of-the-nine">Summary of the nine<a class="headerlink" href="#summary-of-the-nine" title="Permanent link">&para;</a></h3>
@@ -1342,7 +1348,7 @@ bridging: every one of these nine is a fresh detection on a real face crop
at the second it first appears.</p>
<h2 id="where-lvface-beat-x-ray">Where LVFace beat X-Ray<a class="headerlink" href="#where-lvface-beat-x-ray" title="Permanent link">&para;</a></h2>
<p>Not every name marked wrong is actually wrong.
<a href="https://gitea.tourolle.paris/dtourolle/scene-actor-extraction/raw/commit/0bd27470698c45cab21935d636a04512360e1008/scripts/optimizer/second_score.py"><code>scripts/optimizer/second_score.py</code></a>
<a href="https://gitea.tourolle.paris/dtourolle/scene-actor-extraction/raw/commit/e5885977dfd04741a0d1bb7aaf0270d9c40a2fa8/scripts/optimizer/second_score.py"><code>scripts/optimizer/second_score.py</code></a>
scores strictly against X-Ray, and X-Ray has gaps of its own.</p>
<p><img alt="LVFace correctly identifies Germar Terrell Gardner, uncredited by X-Ray" src="../assets/images/germar_beats_xray.jpg" /></p>
<p>Germar Terrell Gardner, the same name from the table above, does not appear
+1 -1
View File
@@ -917,7 +917,7 @@ not in X-Ray's cast for this second. Split into two categories:</p>
<p>The precision reported throughout this report, and therefore the F1
derived from it, puts each <code>FPI_misid</code> into the denominator <strong>10 times</strong>
(<code>precision = TPI / (TPI + FPI_incast + 10·FPI_misid)</code>,
<a href="https://gitea.tourolle.paris/dtourolle/scene-actor-extraction/raw/commit/0bd27470698c45cab21935d636a04512360e1008/scripts/optimizer/second_score.py"><code>second_score.py</code></a>).
<a href="https://gitea.tourolle.paris/dtourolle/scene-actor-extraction/raw/commit/e5885977dfd04741a0d1bb7aaf0270d9c40a2fa8/scripts/optimizer/second_score.py"><code>second_score.py</code></a>).
This is deliberate: the whole point is to punish naming an out-of-film
actor far harder than a scene-boundary slip. But it means the <code>P</code> column
is not raw precision, and a misID-heavy film's <code>P</code> is depressed
+7 -7
View File
@@ -1039,7 +1039,7 @@ caveat needed to trust the numbers.</p>
<p>Read <a href="../methodology/">How we score against X-Ray</a> first for what F1,
precision, recall, and misID mean in this report. All numbers below use the
per-second metric
(<a href="https://gitea.tourolle.paris/dtourolle/scene-actor-extraction/raw/commit/0bd27470698c45cab21935d636a04512360e1008/scripts/optimizer/second_score.py"><code>scripts/optimizer/second_score.py</code></a>).</p>
(<a href="https://gitea.tourolle.paris/dtourolle/scene-actor-extraction/raw/commit/e5885977dfd04741a0d1bb7aaf0270d9c40a2fa8/scripts/optimizer/second_score.py"><code>scripts/optimizer/second_score.py</code></a>).</p>
<p>r50 (ArcFace w600k-R50) is excluded from the detailed comparison below. Its
gallery was built with roughly 30% fewer reference images per actor than the
other three models on the identical source photos (10808 vs 15055 total
@@ -1057,8 +1057,8 @@ output, and the cheap stage can be re-run against that cache as many times
as needed with different Config values.</p>
<p><code>scene_analyze --dump-embeddings out.h5</code> runs the expensive half once per
film and writes per-frame face detections and embeddings to HDF5
(<a href="https://gitea.tourolle.paris/dtourolle/scene-actor-extraction/raw/commit/0bd27470698c45cab21935d636a04512360e1008/scripts/optimizer/SCHEMA.md"><code>scripts/optimizer/SCHEMA.md</code></a>).
<a href="https://gitea.tourolle.paris/dtourolle/scene-actor-extraction/raw/commit/0bd27470698c45cab21935d636a04512360e1008/scripts/optimizer/replay.py"><code>scripts/optimizer/replay.py</code></a>
(<a href="https://gitea.tourolle.paris/dtourolle/scene-actor-extraction/raw/commit/e5885977dfd04741a0d1bb7aaf0270d9c40a2fa8/scripts/optimizer/SCHEMA.md"><code>scripts/optimizer/SCHEMA.md</code></a>).
<a href="https://gitea.tourolle.paris/dtourolle/scene-actor-extraction/raw/commit/e5885977dfd04741a0d1bb7aaf0270d9c40a2fa8/scripts/optimizer/replay.py"><code>scripts/optimizer/replay.py</code></a>
then re-assembles the real C++ <code>face_tracker</code>, <code>identity_matcher</code>, and
<code>scene_tracker</code> nodes into a Python-driven KPN network and replays a
film's cached embeddings through them, varying <code>prob_threshold</code>,
@@ -1507,7 +1507,7 @@ runtime feature of the application; see
into the application would require.</p>
<p><strong>Pose expansion</strong> (promoting a confidently identified track's novel-pose
views into a per-film gallery annex,
<a href="https://gitea.tourolle.paris/dtourolle/scene-actor-extraction/raw/commit/0bd27470698c45cab21935d636a04512360e1008/src/gallery/track_gallery.hpp"><code>src/gallery/track_gallery.hpp</code></a>):</p>
<a href="https://gitea.tourolle.paris/dtourolle/scene-actor-extraction/raw/commit/e5885977dfd04741a0d1bb7aaf0270d9c40a2fa8/src/gallery/track_gallery.hpp"><code>src/gallery/track_gallery.hpp</code></a>):</p>
<table>
<thead>
<tr>
@@ -1563,7 +1563,7 @@ and the two methodology bugs caught while checking it.</p>
<h2 id="calibration-curves">Calibration curves<a class="headerlink" href="#calibration-curves" title="Permanent link">&para;</a></h2>
<p>Each gallery carries a fitted Platt sigmoid <code>P(match | sim) = σ(a·sim + b)</code>,
stored directly in the gallery HDF5
(<a href="https://gitea.tourolle.paris/dtourolle/scene-actor-extraction/raw/commit/0bd27470698c45cab21935d636a04512360e1008/src/gallery/gallery_calibration.hpp"><code>src/gallery/gallery_calibration.hpp</code></a>).
(<a href="https://gitea.tourolle.paris/dtourolle/scene-actor-extraction/raw/commit/e5885977dfd04741a0d1bb7aaf0270d9c40a2fa8/src/gallery/gallery_calibration.hpp"><code>src/gallery/gallery_calibration.hpp</code></a>).
This measures discriminative power independent of whatever
<code>prob_threshold</code> a given run used:</p>
<p><img alt="Calibrated P(match|similarity) for all four models" src="../assets/images/calibration_curves.png" /></p>
@@ -1572,7 +1572,7 @@ variants) and the lowest P=0.5 decision boundary (similarity 0.23 vs
0.27-0.31), separating same-actor from different-actor pairs more
confidently at a lower similarity than any ArcFace variant tested,
including r50. Generated by
<a href="https://gitea.tourolle.paris/dtourolle/scene-actor-extraction/raw/commit/0bd27470698c45cab21935d636a04512360e1008/scripts/docs/calibration_chart.py"><code>scripts/docs/calibration_chart.py</code></a>.</p>
<a href="https://gitea.tourolle.paris/dtourolle/scene-actor-extraction/raw/commit/e5885977dfd04741a0d1bb7aaf0270d9c40a2fa8/scripts/docs/calibration_chart.py"><code>scripts/docs/calibration_chart.py</code></a>.</p>
<h2 id="extinction-and-anneal-window-search">Extinction and anneal window search<a class="headerlink" href="#extinction-and-anneal-window-search" title="Permanent link">&para;</a></h2>
<p>Every one of the 512 DE evaluations for the winning LVFace <code>full_exp</code>
combo, plotted over the <code>prob_threshold</code> × <code>extinction_sec</code> plane:</p>
@@ -1632,7 +1632,7 @@ past 60s, or does it plateau there. Not chased further this pass.</p>
</span><span id="__span-0-24"><a id="__codelineno-0-24" name="__codelineno-0-24" href="#__codelineno-0-24"></a>python3<span class="w"> </span>scripts/docs/first_fpi_frames.py
</span></code></pre></div>
<p>See also the session log
<a href="https://gitea.tourolle.paris/dtourolle/scene-actor-extraction/raw/commit/0bd27470698c45cab21935d636a04512360e1008/experiments/SESSION_STATE.md"><code>experiments/SESSION_STATE.md</code></a>.</p>
<a href="https://gitea.tourolle.paris/dtourolle/scene-actor-extraction/raw/commit/e5885977dfd04741a0d1bb7aaf0270d9c40a2fa8/experiments/SESSION_STATE.md"><code>experiments/SESSION_STATE.md</code></a>.</p>
+2 -2
View File
@@ -826,7 +826,7 @@
<h1 id="pose-expansion-does-promoting-new-poses-mid-film-help">Pose expansion: does promoting new poses mid-film help?<a class="headerlink" href="#pose-expansion-does-promoting-new-poses-mid-film-help" title="Permanent link">&para;</a></h1>
<p><code>expand_gallery</code>
(<a href="https://gitea.tourolle.paris/dtourolle/scene-actor-extraction/raw/commit/0bd27470698c45cab21935d636a04512360e1008/src/gallery/track_gallery.hpp"><code>src/gallery/track_gallery.hpp</code></a>)
(<a href="https://gitea.tourolle.paris/dtourolle/scene-actor-extraction/raw/commit/e5885977dfd04741a0d1bb7aaf0270d9c40a2fa8/src/gallery/track_gallery.hpp"><code>src/gallery/track_gallery.hpp</code></a>)
promotes a confidently identified track's novel-pose reference views into a
per-film, in-memory gallery annex. The idea: once the pipeline is confident
about an identity, a pose it has not seen before (turned head, different
@@ -982,7 +982,7 @@ inert config look consequential; both are reasons to distrust a dramatic
training-set delta until it survives on held-out films, which this one did
not. This does not mean <code>expand_gallery</code> never does anything: the
mechanism is real, and
<a href="https://gitea.tourolle.paris/dtourolle/scene-actor-extraction/raw/commit/0bd27470698c45cab21935d636a04512360e1008/src/gallery/track_gallery.hpp"><code>track_gallery.hpp</code></a>'s
<a href="https://gitea.tourolle.paris/dtourolle/scene-actor-extraction/raw/commit/e5885977dfd04741a0d1bb7aaf0270d9c40a2fa8/src/gallery/track_gallery.hpp"><code>track_gallery.hpp</code></a>'s
promotion logging confirms tracks get confirmed and views get promoted
into the annex on every film tested. It means whatever effect expansion
has on final per-second identification was too small to detect against 5
File diff suppressed because one or more lines are too long
+4 -4
View File
@@ -1035,7 +1035,7 @@ lock-gating, not new pipeline logic.</p>
</tr>
<tr>
<td>Backend selection</td>
<td><a href="https://gitea.tourolle.paris/dtourolle/scene-actor-extraction/raw/commit/0bd27470698c45cab21935d636a04512360e1008/CMakeLists.txt"><code>CMakeLists.txt</code></a> (<code>SAE_INFERENCE_BACKEND</code>, <code>SAE_GEMM_BACKEND</code>)</td>
<td><a href="https://gitea.tourolle.paris/dtourolle/scene-actor-extraction/raw/commit/e5885977dfd04741a0d1bb7aaf0270d9c40a2fa8/CMakeLists.txt"><code>CMakeLists.txt</code></a> (<code>SAE_INFERENCE_BACKEND</code>, <code>SAE_GEMM_BACKEND</code>)</td>
<td>ORT/TRT + ROCm/CUDA, chosen <strong>at build time</strong></td>
</tr>
<tr>
@@ -1045,7 +1045,7 @@ lock-gating, not new pipeline logic.</p>
</tr>
<tr>
<td>Worker loop</td>
<td><a href="https://gitea.tourolle.paris/dtourolle/scene-actor-extraction/raw/commit/0bd27470698c45cab21935d636a04512360e1008/scripts/run_from_jellyfin.py"><code>scripts/run_from_jellyfin.py</code></a><code>--worker</code></td>
<td><a href="https://gitea.tourolle.paris/dtourolle/scene-actor-extraction/raw/commit/e5885977dfd04741a0d1bb7aaf0270d9c40a2fa8/scripts/run_from_jellyfin.py"><code>scripts/run_from_jellyfin.py</code></a><code>--worker</code></td>
<td>Poll Pending → run <code>scene_analyze</code> → push results</td>
</tr>
<tr>
@@ -1055,12 +1055,12 @@ lock-gating, not new pipeline logic.</p>
</tr>
<tr>
<td>Incremental gallery</td>
<td><a href="https://gitea.tourolle.paris/dtourolle/scene-actor-extraction/raw/commit/0bd27470698c45cab21935d636a04512360e1008/scripts/make_jellyfin_gallery.py"><code>scripts/make_jellyfin_gallery.py</code></a><code>--merge</code></td>
<td><a href="https://gitea.tourolle.paris/dtourolle/scene-actor-extraction/raw/commit/e5885977dfd04741a0d1bb7aaf0270d9c40a2fa8/scripts/make_jellyfin_gallery.py"><code>scripts/make_jellyfin_gallery.py</code></a><code>--merge</code></td>
<td>Embeds only cast not already in the gallery</td>
</tr>
<tr>
<td>Secrets loader</td>
<td><code>.env</code> via <a href="https://gitea.tourolle.paris/dtourolle/scene-actor-extraction/raw/commit/0bd27470698c45cab21935d636a04512360e1008/scripts/sae_env.py"><code>scripts/sae_env.py</code></a></td>
<td><code>.env</code> via <a href="https://gitea.tourolle.paris/dtourolle/scene-actor-extraction/raw/commit/e5885977dfd04741a0d1bb7aaf0270d9c40a2fa8/scripts/sae_env.py"><code>scripts/sae_env.py</code></a></td>
<td><code>JELLYFIN_URL</code>, <code>JELLYFIN_API_KEY</code>, <code>TMDB_API_KEY</code></td>
</tr>
</tbody>