<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Ray Lin</title>
    <description>The latest articles on DEV Community by Ray Lin (@whereisthisplace).</description>
    <link>https://dev.to/whereisthisplace</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4062682%2Fac0081b5-f84c-4f60-b705-a7ba50f3476a.png</url>
      <title>DEV Community: Ray Lin</title>
      <link>https://dev.to/whereisthisplace</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/whereisthisplace"/>
    <language>en</language>
    <item>
      <title>Upscaled 2K and re-generated 2K are not the same file</title>
      <dc:creator>Ray Lin</dc:creator>
      <pubDate>Mon, 24 Aug 2026 03:59:03 +0000</pubDate>
      <link>https://dev.to/whereisthisplace/upscaled-2k-and-re-generated-2k-are-not-the-same-file-110f</link>
      <guid>https://dev.to/whereisthisplace/upscaled-2k-and-re-generated-2k-are-not-the-same-file-110f</guid>
      <description>&lt;p&gt;Two video APIs both advertise a 2K output. You send the same prompt to each, you get back two files with identical dimensions, and your billing line item looks roughly the same. They are not the same product, and the difference is visible in one place: small text.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two pipelines
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Path A — post-hoc super-resolution.&lt;/strong&gt; The model generates at its native resolution. A second network then scales the frames. ESRGAN, a diffusion SR pass, a temporal-consistency variant, it doesn't matter which. What matters is that the upscaler sees pixels and only pixels. It never saw your prompt. It has no idea the scene was supposed to contain a shop sign, so it does the only thing an interpolator can do: it makes edges confident. A blurry six-pixel-tall word becomes a crisp six-pixel-tall word that spells something else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Path B — conditioned re-generation.&lt;/strong&gt; The low-resolution result goes back into the same model along with the original conditioning — prompt, reference images, whatever produced it the first time — and the model generates again at the higher resolution. Nothing is interpolated. The second pass knows the sign reads OPEN because the conditioning still says the sign reads OPEN.&lt;/p&gt;

&lt;p&gt;Both produce a file with the same pixel dimensions. Only one of them can recover information that was never in the low-resolution frames.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to tell which one you actually bought
&lt;/h2&gt;

&lt;p&gt;You don't need the vendor to tell you. Two tests, both cheap.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test 1: put legible text in the scene.&lt;/strong&gt; Generate at the low resolution, then at the high one. Look at the same word in both.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If it's an upscaler, the high-res glyphs are a &lt;em&gt;sharpened version of the same wrong shapes&lt;/em&gt;. The error is preserved and then rendered crisply. This is the tell — interpolation cannot invent a letter that wasn't there, so it commits to the smudge it was given.&lt;/li&gt;
&lt;li&gt;If it's a re-generation, the text is simply different. Often correct, sometimes wrong in a new way, but not a cleaned-up copy of the previous mistake.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Test 2: diff the motion.&lt;/strong&gt; An upscaler operates frame by frame, or over a short temporal window. Frame 40 of the high-res clip corresponds exactly to frame 40 of the low-res one. A re-generation is a fresh sample: same intent, same composition, but the micro-motion drifts — a hand lands a few milliseconds off, the hair moves differently.&lt;/p&gt;

&lt;p&gt;So: &lt;strong&gt;if your high-resolution render has frame-for-frame identical motion to your preview, you bought an upscaler.&lt;/strong&gt; That's not a criticism, it's just a different product, and it should be priced like one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part nobody puts in the docs
&lt;/h2&gt;

&lt;p&gt;Re-generation is not strictly better, and treating it as a free upgrade will break a pipeline.&lt;/p&gt;

&lt;p&gt;The trade-off is exact frame correspondence. If you are building anything that previews at low resolution and then commits at high resolution — an editor with a scrubbing timeline, a batch job that picks a thumbnail frame index, an approval workflow where a human signs off on the cheap render — an upscaler is the thing you want. Your frame 40 stays frame 40. With re-generation, the clip your reviewer approved is not the clip you ship. Same shot, different take.&lt;/p&gt;

&lt;p&gt;I've watched a team lose two days to this. They rendered previews, had a client pick an in-point by frame number, then rendered final at high resolution and cut on that number. The final motion had drifted maybe 80 ms. The edit landed mid-blink on every take.&lt;/p&gt;

&lt;p&gt;The rule I'd write down: &lt;strong&gt;preview-then-commit workflows want deterministic upscaling; one-shot delivery wants re-generation.&lt;/strong&gt; Decide which one you are before you pick the model, not after.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cost follows the pipeline, and per-second pricing hides it
&lt;/h2&gt;

&lt;p&gt;Most of these APIs bill per output second, with a different rate per resolution. That framing quietly conceals which pipeline you're on, because an upscale pass and a full second forward pass have wildly different compute costs but can be sold at similar-looking per-second rates.&lt;/p&gt;

&lt;p&gt;Worth doing the arithmetic yourself before committing to a length. On the model I've been testing most, the high-resolution path runs roughly 60% above the base rate per output second, which is consistent with a real second pass rather than a bolt-on upscaler — someone has written up &lt;a href="https://minimax-h3ai.video/blog/what-a-2k-h3-clip-actually-costs" rel="noopener noreferrer"&gt;what a 2K clip actually costs&lt;/a&gt; once you multiply that out across a realistic set of takes, including the failed ones. The failure rate matters more than the rate card, and almost nobody models it.&lt;/p&gt;

&lt;h2&gt;
  
  
  A concrete example of the difference
&lt;/h2&gt;

&lt;p&gt;MiniMax H3 is the clearest implementation of Path B I've found, mostly because it's documented as such rather than left ambiguous: the 768P result is fed back through the model along with the original context and generated again at 2K. The site I pulled the pricing from also puts &lt;a href="https://minimax-h3ai.video/#2k-output" rel="noopener noreferrer"&gt;the same frame down both paths&lt;/a&gt; side by side, which is the comparison every vendor should publish and almost none do.&lt;/p&gt;

&lt;p&gt;There's an architectural consequence worth noting for anyone planning to self-host: if the high-resolution stage is a separate hosted component, downloading the base weights gets you the low-resolution path and nothing else. Your local output will be correct and will not match the hosted result, and you'll spend a while assuming you configured something wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to ask a vendor
&lt;/h2&gt;

&lt;p&gt;Four questions. Any vendor running a real re-generation path can answer all four in one sentence; any vendor running an upscaler will get vague on the second one.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Is the high-resolution output conditioned on the original prompt, or only on the low-resolution frames?&lt;/li&gt;
&lt;li&gt;Is frame &lt;em&gt;n&lt;/em&gt; of the high-resolution render the same moment as frame &lt;em&gt;n&lt;/em&gt; of the low-resolution one?&lt;/li&gt;
&lt;li&gt;Is the high-resolution stage the same model or a separate one?&lt;/li&gt;
&lt;li&gt;If I self-host, do I get both stages?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The answers determine whether "2K" in your invoice means resolution or means detail. They're not the same thing, and only one of them survives contact with a shop sign.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>video</category>
      <category>api</category>
    </item>
    <item>
      <title>Upscaled 2K and re-generated 2K are not the same file</title>
      <dc:creator>Ray Lin</dc:creator>
      <pubDate>Fri, 21 Aug 2026 17:14:28 +0000</pubDate>
      <link>https://dev.to/whereisthisplace/upscaled-2k-and-re-generated-2k-are-not-the-same-file-4pil</link>
      <guid>https://dev.to/whereisthisplace/upscaled-2k-and-re-generated-2k-are-not-the-same-file-4pil</guid>
      <description>&lt;p&gt;Two video APIs both advertise a 2K output. You send the same prompt to each, you get back two files with identical dimensions, and your billing line item looks roughly the same. They are not the same product, and the difference is visible in one place: small text.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two pipelines
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Path A — post-hoc super-resolution.&lt;/strong&gt; The model generates at its native resolution. A second network then scales the frames. ESRGAN, a diffusion SR pass, a temporal-consistency variant, it doesn't matter which. What matters is that the upscaler sees pixels and only pixels. It never saw your prompt. It has no idea the scene was supposed to contain a shop sign, so it does the only thing an interpolator can do: it makes edges confident. A blurry six-pixel-tall word becomes a crisp six-pixel-tall word that spells something else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Path B — conditioned re-generation.&lt;/strong&gt; The low-resolution result goes back into the same model along with the original conditioning — prompt, reference images, whatever produced it the first time — and the model generates again at the higher resolution. Nothing is interpolated. The second pass knows the sign reads OPEN because the conditioning still says the sign reads OPEN.&lt;/p&gt;

&lt;p&gt;Both produce a file with the same pixel dimensions. Only one of them can recover information that was never in the low-resolution frames.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to tell which one you actually bought
&lt;/h2&gt;

&lt;p&gt;You don't need the vendor to tell you. Two tests, both cheap.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test 1: put legible text in the scene.&lt;/strong&gt; Generate at the low resolution, then at the high one. Look at the same word in both.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If it's an upscaler, the high-res glyphs are a &lt;em&gt;sharpened version of the same wrong shapes&lt;/em&gt;. The error is preserved and then rendered crisply. This is the tell — interpolation cannot invent a letter that wasn't there, so it commits to the smudge it was given.&lt;/li&gt;
&lt;li&gt;If it's a re-generation, the text is simply different. Often correct, sometimes wrong in a new way, but not a cleaned-up copy of the previous mistake.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Test 2: diff the motion.&lt;/strong&gt; An upscaler operates frame by frame, or over a short temporal window. Frame 40 of the high-res clip corresponds exactly to frame 40 of the low-res one. A re-generation is a fresh sample: same intent, same composition, but the micro-motion drifts — a hand lands a few milliseconds off, the hair moves differently.&lt;/p&gt;

&lt;p&gt;So: &lt;strong&gt;if your high-resolution render has frame-for-frame identical motion to your preview, you bought an upscaler.&lt;/strong&gt; That's not a criticism, it's just a different product, and it should be priced like one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part nobody puts in the docs
&lt;/h2&gt;

&lt;p&gt;Re-generation is not strictly better, and treating it as a free upgrade will break a pipeline.&lt;/p&gt;

&lt;p&gt;The trade-off is exact frame correspondence. If you are building anything that previews at low resolution and then commits at high resolution — an editor with a scrubbing timeline, a batch job that picks a thumbnail frame index, an approval workflow where a human signs off on the cheap render — an upscaler is the thing you want. Your frame 40 stays frame 40. With re-generation, the clip your reviewer approved is not the clip you ship. Same shot, different take.&lt;/p&gt;

&lt;p&gt;I've watched a team lose two days to this. They rendered previews, had a client pick an in-point by frame number, then rendered final at high resolution and cut on that number. The final motion had drifted maybe 80 ms. The edit landed mid-blink on every take.&lt;/p&gt;

&lt;p&gt;The rule I'd write down: &lt;strong&gt;preview-then-commit workflows want deterministic upscaling; one-shot delivery wants re-generation.&lt;/strong&gt; Decide which one you are before you pick the model, not after.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cost follows the pipeline, and per-second pricing hides it
&lt;/h2&gt;

&lt;p&gt;Most of these APIs bill per output second, with a different rate per resolution. That framing quietly conceals which pipeline you're on, because an upscale pass and a full second forward pass have wildly different compute costs but can be sold at similar-looking per-second rates.&lt;/p&gt;

&lt;p&gt;Worth doing the arithmetic yourself before committing to a length. On the model I've been testing most, the high-resolution path runs roughly 60% above the base rate per output second, which is consistent with a real second pass rather than a bolt-on upscaler — someone has written up &lt;a href="https://minimax-h3ai.video/blog/what-a-2k-h3-clip-actually-costs" rel="noopener noreferrer"&gt;what a 2K clip actually costs&lt;/a&gt; once you multiply that out across a realistic set of takes, including the failed ones. The failure rate matters more than the rate card, and almost nobody models it.&lt;/p&gt;

&lt;h2&gt;
  
  
  A concrete example of the difference
&lt;/h2&gt;

&lt;p&gt;MiniMax H3 is the clearest implementation of Path B I've found, mostly because it's documented as such rather than left ambiguous: the 768P result is fed back through the model along with the original context and generated again at 2K. The site I pulled the pricing from also puts &lt;a href="https://minimax-h3ai.video/#2k-output" rel="noopener noreferrer"&gt;the same frame down both paths&lt;/a&gt; side by side, which is the comparison every vendor should publish and almost none do.&lt;/p&gt;

&lt;p&gt;There's an architectural consequence worth noting for anyone planning to self-host: if the high-resolution stage is a separate hosted component, downloading the base weights gets you the low-resolution path and nothing else. Your local output will be correct and will not match the hosted result, and you'll spend a while assuming you configured something wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to ask a vendor
&lt;/h2&gt;

&lt;p&gt;Four questions. Any vendor running a real re-generation path can answer all four in one sentence; any vendor running an upscaler will get vague on the second one.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Is the high-resolution output conditioned on the original prompt, or only on the low-resolution frames?&lt;/li&gt;
&lt;li&gt;Is frame &lt;em&gt;n&lt;/em&gt; of the high-resolution render the same moment as frame &lt;em&gt;n&lt;/em&gt; of the low-resolution one?&lt;/li&gt;
&lt;li&gt;Is the high-resolution stage the same model or a separate one?&lt;/li&gt;
&lt;li&gt;If I self-host, do I get both stages?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The answers determine whether "2K" in your invoice means resolution or means detail. They're not the same thing, and only one of them survives contact with a shop sign.&lt;/p&gt;

</description>
      <category>api</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>The duration your video API accepts is not the duration it renders</title>
      <dc:creator>Ray Lin</dc:creator>
      <pubDate>Fri, 21 Aug 2026 15:38:33 +0000</pubDate>
      <link>https://dev.to/whereisthisplace/the-duration-your-video-api-accepts-is-not-the-duration-it-renders-4j86</link>
      <guid>https://dev.to/whereisthisplace/the-duration-your-video-api-accepts-is-not-the-duration-it-renders-4j86</guid>
      <description>&lt;p&gt;A sequence I cut to a music bed was three frames out at the first transition,&lt;br&gt;
nine at the second, and by the sixth segment nothing lined up with anything. I&lt;br&gt;
had asked every generation for ten seconds. Every generation had returned a&lt;br&gt;
file that was not ten seconds.&lt;/p&gt;

&lt;p&gt;Nothing in the API said so. The request took &lt;code&gt;duration: 10&lt;/code&gt;, returned &lt;code&gt;200&lt;/code&gt;,&lt;br&gt;
and produced an MP4 whose container duration was &lt;code&gt;8.708&lt;/code&gt;. No warning field, no&lt;br&gt;
note in the response body, and — the part that actually cost me the afternoon —&lt;br&gt;
no mention of it on the docs page I had read three times.&lt;/p&gt;

&lt;p&gt;This is a general property of latent video models rather than a bug in one&lt;br&gt;
provider, and once you know the shape of it you can handle it in about twenty&lt;br&gt;
lines. Here is the shape.&lt;/p&gt;
&lt;h2&gt;
  
  
  Seconds are the wrong unit
&lt;/h2&gt;

&lt;p&gt;A video diffusion model does not work on frames. It works on a compressed&lt;br&gt;
latent tensor, and the compression is temporal as well as spatial: a causal 3D&lt;br&gt;
autoencoder folds a run of input frames into a single latent frame.&lt;/p&gt;

&lt;p&gt;Because the encoder is causal, the first frame is kept whole and everything&lt;br&gt;
after it is compressed in groups. With a temporal stride of &lt;code&gt;s&lt;/code&gt;, a clip of &lt;code&gt;F&lt;/code&gt;&lt;br&gt;
frames becomes&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;latent_frames&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;F&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;which only divides evenly when &lt;code&gt;F ≡ 1 (mod s)&lt;/code&gt;. Frame counts that miss that&lt;br&gt;
condition get padded or truncated, so implementations pick the nearest legal&lt;br&gt;
count and render that instead.&lt;/p&gt;

&lt;p&gt;Stack a second constraint on top — many of these models generate in fixed&lt;br&gt;
blocks of latent frames rather than one at a time — and the set of renderable&lt;br&gt;
lengths collapses into a short arithmetic progression:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;F = head + block · n          n ∈ ℕ
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every legal duration is one of those &lt;code&gt;F&lt;/code&gt; values divided by the frame rate.&lt;br&gt;
Nothing between them is reachable. &lt;code&gt;duration: 10&lt;/code&gt; is not a request. It is a&lt;br&gt;
hint that gets snapped to a grid you were never shown.&lt;/p&gt;
&lt;h2&gt;
  
  
  What the snapping does to users
&lt;/h2&gt;

&lt;p&gt;Three separate problems, and only the first is obvious.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The output is not the length you promised.&lt;/strong&gt; Your UI said 10s, the file is&lt;br&gt;
8.708s, so your UI lied. Not by much, and not in a way anyone notices on one&lt;br&gt;
clip.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The error compounds.&lt;/strong&gt; Nobody makes one clip. They make six and cut them&lt;br&gt;
together. Six segments each 1.3 seconds short is eight seconds of drift, which&lt;br&gt;
is the difference between "cuts on the beat" and "re-render the sequence."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your cost estimate is wrong in the direction that generates tickets.&lt;/strong&gt; These&lt;br&gt;
APIs bill per second of output. Estimate from the requested duration, let the&lt;br&gt;
model render a longer legal block, and you have quoted one number and charged&lt;br&gt;
another. Users find that one on their own.&lt;/p&gt;
&lt;h2&gt;
  
  
  Quantise on the way in, not on the way out
&lt;/h2&gt;

&lt;p&gt;The fix is to stop treating duration as a free variable at the edge of your&lt;br&gt;
system. Resolve it to a legal value before anything is displayed, priced or&lt;br&gt;
persisted.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Grid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;fps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;block&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;head&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;min&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;max&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="cm"&gt;/** Legal frame counts are head + block·n, clamped to the provider's range. */&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;legalFrames&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Grid&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;head&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;block&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;max&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;min&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;quantise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;seconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Grid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;seconds&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fps&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;frames&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;legalFrames&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;best&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;best&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;best&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;requested&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;seconds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;frames&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;seconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;frames&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fps&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then three rules for the resolved value:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Show it instead of the requested one.&lt;/strong&gt; The number in the duration control&lt;br&gt;
should change to the number you are going to get, at the moment the user picks&lt;br&gt;
it. A slider that snaps is honest. A slider that accepts anything and rounds&lt;br&gt;
in private is not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Price it.&lt;/strong&gt; &lt;code&gt;frames / fps × rate&lt;/code&gt;, from the resolved frames. Never from the&lt;br&gt;
requested seconds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Store it on the job.&lt;/strong&gt; When somebody asks why their six clips do not add up,&lt;br&gt;
you want the answer in a column, not in a reconstruction.&lt;/p&gt;
&lt;h2&gt;
  
  
  The test that would have caught this on day one
&lt;/h2&gt;

&lt;p&gt;The bug was reachable from an integration test I had simply not thought to&lt;br&gt;
write:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;returns the duration it promised&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;requested&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;job&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;a still grey card&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;requested&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;meta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;ffprobe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;download&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBeCloseTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resolvedDuration&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note what it asserts. Not that the file matches the &lt;strong&gt;request&lt;/strong&gt; — that is a&lt;br&gt;
test you cannot pass and should not want to — but that it matches what the API&lt;br&gt;
&lt;strong&gt;told you it resolved to&lt;/strong&gt;. That is a contract you can hold a provider to, and&lt;br&gt;
if the provider returns no resolved value at all, the absence is itself the&lt;br&gt;
finding.&lt;/p&gt;

&lt;p&gt;Running the loop is also the cheapest way to discover the grid empirically.&lt;br&gt;
Seven requests, &lt;code&gt;ffprobe&lt;/code&gt; on each, and you have the progression whether or not&lt;br&gt;
anyone documented it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get the grid before you design around a number
&lt;/h2&gt;

&lt;p&gt;For anyone about to build on one of these APIs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Find the frame rate and the block size. They are often in the model card even
when they are missing from the API reference.&lt;/li&gt;
&lt;li&gt;Enumerate the legal durations once, at build time, and ship the list.&lt;/li&gt;
&lt;li&gt;Check whether &lt;em&gt;any&lt;/em&gt; legal duration is a whole number of seconds. There may be
exactly one. If your product ever cuts to music, that value is the only
sensible default you have.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last point is not hypothetical. On the model I work with daily, the block&lt;br&gt;
arithmetic is tight enough that across its entire published 4-to-15 second&lt;br&gt;
range precisely one setting comes out even — and it is not a number anyone&lt;br&gt;
would think to type. Someone&lt;br&gt;
&lt;a href="https://minimax-h3ai.video/blog/192-frames-is-the-only-whole-second" rel="noopener noreferrer"&gt;worked the progression out frame by frame and showed why only one lands on a whole second&lt;/a&gt;,&lt;br&gt;
which is ten minutes well spent if you are about to pick a default your users&lt;br&gt;
will inherit.&lt;/p&gt;

&lt;p&gt;The general lesson is smaller than the arithmetic: &lt;strong&gt;when a generative API&lt;br&gt;
accepts a continuous parameter the model can only satisfy discretely, the&lt;br&gt;
rounding is part of the contract.&lt;/strong&gt; Ask where the grid is before you let a&lt;br&gt;
number reach your UI.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I help run &lt;a href="https://minimax-h3ai.video" rel="noopener noreferrer"&gt;minimax-h3ai.video&lt;/a&gt;, an independent&lt;br&gt;
third-party interface for MiniMax H3. Not affiliated with MiniMax. Everything&lt;br&gt;
above is checked against the published docs and linked where it isn't.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>ai</category>
      <category>webdev</category>
      <category>typescript</category>
    </item>
    <item>
      <title>A GeoGuessr Decision Tree for Country-Level Photo Clues</title>
      <dc:creator>Ray Lin</dc:creator>
      <pubDate>Fri, 14 Aug 2026 12:04:31 +0000</pubDate>
      <link>https://dev.to/whereisthisplace/a-geoguessr-decision-tree-for-country-level-photo-clues-1i88</link>
      <guid>https://dev.to/whereisthisplace/a-geoguessr-decision-tree-for-country-level-photo-clues-1i88</guid>
      <description>&lt;p&gt;I maintain a photo-location tool, so I am biased toward software. That bias is useful only when a result can be checked. A country guess without the observations behind it is difficult to debug or trust.&lt;/p&gt;

&lt;p&gt;Competitive GeoGuessr players use a compact decision procedure: discard impossible countries, prefer standardized clues, and stop when the frame is underdetermined. The same workflow helps when you are checking a photo someone sent you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with information, not visual drama
&lt;/h2&gt;

&lt;p&gt;Look for driving side, road-marking conventions, bollards and roadside hardware, plate geometry, and script class. These clues are often constrained by standards or geography. Terrain, architecture, and vegetation are useful tie-breakers, but they cross borders easily.&lt;/p&gt;

&lt;p&gt;A tiny roadside post can carry more country-level information than a mountain dominating the frame. Record what is actually visible; do not infer letters, colors, or traffic direction hidden by blur.&lt;/p&gt;

&lt;h2&gt;
  
  
  An inspectable decision tree
&lt;/h2&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def country_hypotheses(frame):
    candidates = all_countries()
    if side := visible_driving_side(frame):
        candidates &amp;amp;= countries_using(side)
    if lines := classify_road_markings(frame):
        candidates &amp;amp;= regions_with(lines)
    if bollard := classify_bollard(frame):
        candidates &amp;amp;= countries_with_bollard_profile(bollard)
    if plate := classify_plate_geometry(frame):
        candidates &amp;amp;= countries_with_plate_pattern(plate)
    if script := classify_script(frame):
        candidates &amp;amp;= countries_using_script(script)
    return rank_with_soft_clues(candidates, frame)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The output should be a shortlist plus the observations that changed its ranking. A single country name is not an explanation and cannot be falsified.&lt;/p&gt;

&lt;h2&gt;
  
  
  Matching versus inference
&lt;/h2&gt;

&lt;p&gt;Reverse-image matching asks whether this frame, or a lookalike, appears in an index. It is strong for famous buildings and widely published scenes, but a generic street can produce a plausible image from another country. Scene inference reads road conventions, scripts, hardware, terrain, and built form. It works when the exact frame has never been indexed, but should become less specific as clue density falls.&lt;/p&gt;

&lt;p&gt;Use matching for leads and inference for a falsifiable shortlist. Verify candidates against an independent map, street image, or second visible clue. Two tools agreeing is not proof if both copied the same weak signal.&lt;/p&gt;

&lt;p&gt;Whichever tool you use, the property to insist on is that it names the clues behind each candidate. That is the design constraint behind the &lt;a href="https://whereisthisplace.online/tools/where-was-this-picture-taken" rel="noopener noreferrer"&gt;photo location checker&lt;/a&gt; I work on: it returns a ranked shortlist with the visual reasoning attached rather than a single pin, which is the only form of output you can actually argue with.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stop before false precision
&lt;/h2&gt;

&lt;p&gt;Use three independent anchors before calling a country likely. A script classification, a road-marking convention, and a roadside hardware profile are stronger together than three details from one sign. Stop at a region when there are no independent anchors. Indoor scenes without windows, heavily cropped screenshots, generated images, and generic suburbia can be structurally underdetermined. Showing that uncertainty is better engineering than inventing a precise pin.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to log
&lt;/h2&gt;

&lt;p&gt;Record provenance, metadata status, visible clues, output granularity, and why the candidate was accepted or rejected. Keep result screenshots and note the date because indexes and model versions change.&lt;/p&gt;

&lt;p&gt;Disclosure: I work on &lt;a href="https://whereisthisplace.online/" rel="noopener noreferrer"&gt;Where Is This Place&lt;/a&gt;, one of the tools in this space. Its outputs are candidates, not facts, and this article describes a verification workflow rather than a claim that any single tool is always correct.&lt;/p&gt;

</description>
      <category>machinelearning</category>
      <category>osint</category>
      <category>python</category>
      <category>beginners</category>
    </item>
    <item>
      <title>15 OSINT Tools Every Investigator Should Actually Open — In Order</title>
      <dc:creator>Ray Lin</dc:creator>
      <pubDate>Tue, 04 Aug 2026 14:40:20 +0000</pubDate>
      <link>https://dev.to/whereisthisplace/15-osint-tools-every-investigator-should-actually-open-in-order-2lfn</link>
      <guid>https://dev.to/whereisthisplace/15-osint-tools-every-investigator-should-actually-open-in-order-2lfn</guid>
      <description>&lt;p&gt;Installing more OSINT tools rarely fixes an investigation. Opening the right tool at the wrong stage can still waste an afternoon.&lt;/p&gt;

&lt;p&gt;The mistake I see most often is starting with the largest possible sweep. Someone has a username, so they run three username enumerators. Someone has a domain, so they open Shodan before checking the site itself. Someone has a photo, so they upload it to every reverse-image engine and treat zero results as a dead end.&lt;/p&gt;

&lt;p&gt;The useful unit is not a list of tools. It is an order of operations. Each stage should either answer the question cheaply or produce a better input for the next stage.&lt;/p&gt;

&lt;h2&gt;
  
  
  The order matters more than the list
&lt;/h2&gt;

&lt;p&gt;My default workflow has six stages:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Preserve the source and define the claim.&lt;/li&gt;
&lt;li&gt;Read what is still inside the file.&lt;/li&gt;
&lt;li&gt;Check whether the artifact has already been published.&lt;/li&gt;
&lt;li&gt;Read the visible evidence when matching fails.&lt;/li&gt;
&lt;li&gt;Pivot into usernames, emails, domains, and infrastructure.&lt;/li&gt;
&lt;li&gt;Preserve the chain of evidence.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The stages are deliberately asymmetric. A photo may stop at stage four. A domain investigation may begin at stage five. Maltego is useful only after you have enough entities to graph. Hunchly is useful before you think you need it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 0: preserve before searching
&lt;/h2&gt;

&lt;p&gt;Before opening a tool, write down the claim you are trying to support. “This looks like Lisbon” is not the same claim as “this image was captured on Rua da Conceição.” The second claim requires street-level evidence and a much stronger chain.&lt;/p&gt;

&lt;p&gt;Save the original file, record where it came from, hash it if the case matters, and work on a copy. Publicly reachable data is not automatically appropriate to publish. Decide who will review the conclusion before you collect a pile of screenshots no one else can audit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 1: ask the file first
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. ExifTool&lt;/strong&gt; should get the first turn for images and media files. If GPS survived, you have file evidence rather than an inference. Camera model, capture time, editing software, and embedded thumbnails can also explain how the file moved.&lt;/p&gt;

&lt;p&gt;Where it stops: screenshots never had camera GPS, and most social platforms strip metadata. “No EXIF” is not evidence that a photo is fake or untraceable. It only closes one lane.&lt;/p&gt;

&lt;p&gt;I also use a small offline parser for batch triage when I only need to know which JPEG or PNG files still carry coordinates. The important property is local execution: the source image should not leave the machine just to answer a metadata question.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 2: check whether it already exists
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;2. Google Lens&lt;/strong&gt;, &lt;strong&gt;3. TinEye&lt;/strong&gt;, and &lt;strong&gt;4. Yandex Images&lt;/strong&gt; overlap, but they do not answer the same question.&lt;/p&gt;

&lt;p&gt;Lens is strong on objects, text, and visually similar scenes. TinEye is better for exact copies, edits, and publication history. Yandex can surface regional matches the others miss. A useful reverse-search pass checks all three, changes the crop, and searches both the full frame and distinctive details.&lt;/p&gt;

&lt;p&gt;Where they lie: a zero-result screen feels final. It is not. It says the engine did not find a match in its index. It says nothing about whether the image is genuine, new, private, cropped, or simply poorly indexed.&lt;/p&gt;

&lt;p&gt;Operational note: every upload gives a third party the image. That may be acceptable for a public-domain case and unacceptable for source material.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 3: read the frame
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;5. Visual-clue geolocation&lt;/strong&gt; begins where reverse search ends. Instead of matching a copy, it works from what is visible: script and signage, road markings, traffic direction, plate shape, architecture, terrain, vegetation, utilities, weather, and shadows.&lt;/p&gt;

&lt;p&gt;This stage is slower because every clue must be separated into observation and inference. “The curb is painted black and white” is an observation. “This is probably Singapore” is an inference. Mixing them makes a confident story easy to write and hard to falsify.&lt;/p&gt;

&lt;p&gt;The practical rule is three independent anchors. A transit logo, a language fragment, and a road design can support each other. Three details all derived from one sign do not.&lt;/p&gt;

&lt;p&gt;When metadata is gone and the image was never indexed, I use &lt;a href="https://whereisthisplace.online/tools/find-locati0n-no-exif" rel="noopener noreferrer"&gt;a photo location finder that shows its reasoning&lt;/a&gt; as a candidate generator, then verify the suggested place independently. The clue list matters more than the pin.&lt;/p&gt;

&lt;p&gt;Where this stage stops: generic interiors, plain forests, heavy filters, night shots, and frames without readable text or infrastructure. “Not enough evidence” is a valid result.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 4: pivot from the surrounding identity
&lt;/h2&gt;

&lt;p&gt;If the case contains a username or email, the next five tools earn their turn.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Sherlock&lt;/strong&gt; is a fast username sweep. &lt;strong&gt;7. Maigret&lt;/strong&gt; goes wider. &lt;strong&gt;8. WhatsMyName&lt;/strong&gt; is useful when signal quality matters more than coverage. &lt;strong&gt;9. Holehe&lt;/strong&gt; checks whether an email is registered with supported services. &lt;strong&gt;10. Have I Been Pwned&lt;/strong&gt; gives breach exposure context.&lt;/p&gt;

&lt;p&gt;Treat every username hit as a lead, not a finding. A site returning HTTP 200 does not prove the account belongs to the target. Open the page, compare profile details, dates, avatars, writing style, and linked identities.&lt;/p&gt;

&lt;p&gt;This is also the stage most likely to expose the investigation. Username and email tools may query live services. Some services log requests, rate-limit them, or notify account holders.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 5: infrastructure only when infrastructure exists
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;11. theHarvester&lt;/strong&gt; collects public emails, subdomains, and hosts. &lt;strong&gt;12. Shodan&lt;/strong&gt; indexes exposed services. &lt;strong&gt;13. Censys&lt;/strong&gt; is strong for certificates and asset pivots.&lt;/p&gt;

&lt;p&gt;These tools are excellent when the case includes a company, domain, IP address, or certificate. They are theater when the only input is a street photo. Do not create an infrastructure stage just because the tools are familiar.&lt;/p&gt;

&lt;p&gt;Where they lie: large result sets look like progress. Most of the work is still entity resolution, scope checking, and eliminating stale infrastructure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 6: hold the case together
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;14. Maltego CE&lt;/strong&gt; becomes valuable when relationships no longer fit in a few browser tabs. Use it to represent entities and provenance, not to make weak connections look authoritative.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;15. Hunchly&lt;/strong&gt; captures and timestamps pages during research. That makes it the least glamorous tool on this list and one of the most important. Pages change. Accounts disappear. A conclusion that cannot be reconstructed later is a note, not evidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  A compact comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Best input&lt;/th&gt;
&lt;th&gt;What it returns&lt;/th&gt;
&lt;th&gt;Common failure&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ExifTool&lt;/td&gt;
&lt;td&gt;Original file&lt;/td&gt;
&lt;td&gt;Metadata and GPS&lt;/td&gt;
&lt;td&gt;Nothing after stripping&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google Lens&lt;/td&gt;
&lt;td&gt;Image or crop&lt;/td&gt;
&lt;td&gt;Similar objects and pages&lt;/td&gt;
&lt;td&gt;Visually similar wrong match&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TinEye&lt;/td&gt;
&lt;td&gt;Published image&lt;/td&gt;
&lt;td&gt;Copies and edits&lt;/td&gt;
&lt;td&gt;New or unindexed image&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Yandex Images&lt;/td&gt;
&lt;td&gt;Image or crop&lt;/td&gt;
&lt;td&gt;Regional visual matches&lt;/td&gt;
&lt;td&gt;Uneven regional coverage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Visual-clue geolocation&lt;/td&gt;
&lt;td&gt;Unindexed photo&lt;/td&gt;
&lt;td&gt;Ranked place candidates&lt;/td&gt;
&lt;td&gt;Thin evidence looks precise&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sherlock&lt;/td&gt;
&lt;td&gt;Username&lt;/td&gt;
&lt;td&gt;Candidate profiles&lt;/td&gt;
&lt;td&gt;HTTP false positives&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maigret&lt;/td&gt;
&lt;td&gt;Username&lt;/td&gt;
&lt;td&gt;Wider candidate set&lt;/td&gt;
&lt;td&gt;More noise&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WhatsMyName&lt;/td&gt;
&lt;td&gt;Username&lt;/td&gt;
&lt;td&gt;Curated service checks&lt;/td&gt;
&lt;td&gt;Narrower coverage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Holehe&lt;/td&gt;
&lt;td&gt;Email&lt;/td&gt;
&lt;td&gt;Registration signals&lt;/td&gt;
&lt;td&gt;Rate limits and live queries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HIBP&lt;/td&gt;
&lt;td&gt;Email&lt;/td&gt;
&lt;td&gt;Known breach exposure&lt;/td&gt;
&lt;td&gt;Triage, not attribution&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;theHarvester&lt;/td&gt;
&lt;td&gt;Domain&lt;/td&gt;
&lt;td&gt;Hosts, emails, subdomains&lt;/td&gt;
&lt;td&gt;Noisy and stale data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shodan&lt;/td&gt;
&lt;td&gt;IP or domain&lt;/td&gt;
&lt;td&gt;Internet-exposed services&lt;/td&gt;
&lt;td&gt;Not a people or image tool&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Censys&lt;/td&gt;
&lt;td&gt;Certificate or asset&lt;/td&gt;
&lt;td&gt;Infrastructure pivots&lt;/td&gt;
&lt;td&gt;Scope confusion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maltego CE&lt;/td&gt;
&lt;td&gt;Resolved entities&lt;/td&gt;
&lt;td&gt;Relationship graph&lt;/td&gt;
&lt;td&gt;Graphs amplify bad inputs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hunchly&lt;/td&gt;
&lt;td&gt;Browsing session&lt;/td&gt;
&lt;td&gt;Captured evidence trail&lt;/td&gt;
&lt;td&gt;It preserves; it does not find&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Three habits that keep the result defensible
&lt;/h2&gt;

&lt;p&gt;First, write observations before interpretations. If you cannot quote the raw clue without naming a place, you probably wrote an inference.&lt;/p&gt;

&lt;p&gt;Second, attempt falsification. Take the leading candidate and look for something that should be present but is not. A candidate that survives an attempt to break it is stronger than one you only tried to confirm.&lt;/p&gt;

&lt;p&gt;Third, record the tool and version. Search indexes, models, and datasets change. “Google found it” is not reproducible enough for an editor, incident review, or court filing.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Are free OSINT tools enough?
&lt;/h3&gt;

&lt;p&gt;Usually, if the workflow is narrow. Paid products buy coverage, speed, collaboration, or proprietary datasets. They do not remove the need to verify identity and provenance.&lt;/p&gt;

&lt;h3&gt;
  
  
  How accurate are AI geolocation tools?
&lt;/h3&gt;

&lt;p&gt;There is no useful global number. Accuracy depends on the dataset, geography, image quality, and whether the claim is country-, city-, or street-level. Ask what the number was measured on and treat every candidate as a hypothesis.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do OSINT tools alert the target?
&lt;/h3&gt;

&lt;p&gt;Some can. Passive archives and local metadata tools generally do not. Username sweeps, email-registration checks, and anything touching the target's infrastructure may appear in logs or trigger controls.&lt;/p&gt;

&lt;p&gt;The toolkit is the easy part. The difficult part is knowing which question each tool can answer, when it has earned its turn, and what evidence would prove your favorite candidate wrong.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: I work on the image-geolocation tool linked above. The other tools are included because they occupy different stages of the workflow; there are no affiliate links.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>osint</category>
      <category>investigation</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
