<?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</title>
    <description>The most recent home feed on DEV Community.</description>
    <link>https://dev.to</link>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/"/>
    <language>en</language>
    <item>
      <title>Splitting "jumped" into jump/ed with three regexes, and the 3 words out of 36 it got wrong</title>
      <dc:creator>Sam</dc:creator>
      <pubDate>Fri, 13 Nov 2026 01:33:34 +0000</pubDate>
      <link>https://dev.to/samtools/splitting-jumped-into-jumped-with-three-regexes-and-the-3-words-out-of-36-it-got-wrong-3209</link>
      <guid>https://dev.to/samtools/splitting-jumped-into-jumped-with-three-regexes-and-the-3-words-out-of-36-it-got-wrong-3209</guid>
      <description>&lt;p&gt;Speech-language pathologists measure a child's MLU, mean length of utterance, by counting morphemes, not words. "Doggie jumped" is two words but three morphemes: &lt;em&gt;doggie&lt;/em&gt;, &lt;em&gt;jump&lt;/em&gt;, &lt;em&gt;-ed&lt;/em&gt;. On a 50-utterance sample that counting is slow, so the MLU calculator I maintain has an "auto-mark" button: it rewrites &lt;code&gt;jumped&lt;/code&gt; as &lt;code&gt;jump/ed&lt;/code&gt;, and every slash adds one morpheme to the count. The clinician then fixes whatever it got wrong.&lt;/p&gt;

&lt;p&gt;This post is about how little code that takes, and where it breaks.&lt;/p&gt;

&lt;h2&gt;
  
  
  The whole marker
&lt;/h2&gt;

&lt;p&gt;Three suffix rules, three exception lists, and two guards for capitalised words:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;ing&lt;/span&gt;$&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="no"&gt;KEEP_ING&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;indexOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s1"&gt;'/'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&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="n"&gt;lw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;ed&lt;/span&gt;$&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="no"&gt;KEEP_ED&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;indexOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s1"&gt;'/'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&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="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="no"&gt;KEEP_S&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;indexOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!/&lt;/span&gt;&lt;span class="n"&gt;ss&lt;/span&gt;$&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;us&lt;/span&gt;$&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;is&lt;/span&gt;$&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;ous&lt;/span&gt;$&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lw&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="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&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="s1"&gt;'/'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&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="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The length checks keep &lt;code&gt;bed&lt;/code&gt;, &lt;code&gt;red&lt;/code&gt; and &lt;code&gt;is&lt;/code&gt; away from the rules before the lists are even consulted. The exception lists hold the words that only look inflected: &lt;code&gt;KEEP_ING&lt;/code&gt; has &lt;em&gt;thing, ceiling, morning, nothing&lt;/em&gt;; &lt;code&gt;KEEP_ED&lt;/code&gt; has &lt;em&gt;need, bread, hundred, instead&lt;/em&gt;; &lt;code&gt;KEEP_S&lt;/code&gt; has &lt;em&gt;always, pants, scissors, news, lots&lt;/em&gt;. Words that start with a capital in the middle of a line are treated as names and left alone, and anything that already contains an apostrophe is skipped, because contractions and possessives are counted by a separate rule (&lt;code&gt;Mommy's&lt;/code&gt; and &lt;code&gt;can't&lt;/code&gt; are 2 each).&lt;/p&gt;

&lt;p&gt;Irregular forms need nothing at all. &lt;em&gt;went&lt;/em&gt;, &lt;em&gt;feet&lt;/em&gt; and &lt;em&gt;children&lt;/em&gt; have no suffix to strip, so they stay at 1, which is how most counting guides want them. Same for &lt;em&gt;gonna&lt;/em&gt;, &lt;em&gt;birthday&lt;/em&gt; and &lt;em&gt;doggie&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing it on 36 words
&lt;/h2&gt;

&lt;p&gt;I ran the real function from the page in Node on 36 words a 3-year-old might say, and compared each count with the convention the tool itself follows (its own exception lists):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;jumped     -&amp;gt; jump/ed      2
sleeping   -&amp;gt; sleep/ing    2
hiking     -&amp;gt; hik/ing      2
dishes     -&amp;gt; dishe/s      2
ladies     -&amp;gt; ladie/s      2
pants      -&amp;gt; pants        1
scissors   -&amp;gt; scissors     1
pajamas    -&amp;gt; pajama/s     2   &amp;lt;- should be 1
jeans      -&amp;gt; jean/s       2   &amp;lt;- should be 1
does       -&amp;gt; doe/s        2   &amp;lt;- depends on your manual
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;33 of 36 came out as intended. The three misses are instructive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;pajamas and jeans.&lt;/strong&gt; The tool already counts &lt;em&gt;pants&lt;/em&gt; and &lt;em&gt;scissors&lt;/em&gt; as one morpheme, because there is no singular "pant" a toddler is contrasting it with. &lt;em&gt;pajamas&lt;/em&gt; and &lt;em&gt;jeans&lt;/em&gt; are the same kind of word, they just weren't in the list. The rule is right; the list is incomplete. That's the general failure mode of this design: every miss is a missing entry, and you only find them by feeding it real transcripts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;does.&lt;/strong&gt; Here the code isn't wrong so much as opinionated. &lt;em&gt;goes&lt;/em&gt; comes out as &lt;code&gt;goe/s&lt;/code&gt; and &lt;em&gt;does&lt;/em&gt; as &lt;code&gt;doe/s&lt;/code&gt;, two morphemes each. Some manuals count &lt;em&gt;does&lt;/em&gt; as a single irregular form, the way &lt;em&gt;went&lt;/em&gt; is. If you follow one of those, you fix it by hand. A tool can't settle that for you, and pretending it can would be worse than leaving the slash visible.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cosmetic bug that doesn't matter
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;dishes&lt;/code&gt; becomes &lt;code&gt;dishe/s&lt;/code&gt;, &lt;code&gt;ladies&lt;/code&gt; becomes &lt;code&gt;ladie/s&lt;/code&gt;. Linguistically the cut is &lt;em&gt;dish/es&lt;/em&gt;, and &lt;em&gt;ladies&lt;/em&gt; is &lt;em&gt;lady&lt;/em&gt; plus a plural ending, but MLU only cares how many pieces there are, not where the cut falls, and both versions give 2. I left it, because the clinician sees the marked text and &lt;code&gt;dishe/s&lt;/code&gt; is obviously "dish plus plural" to a human reader. Getting the boundary right would mean a small stemmer for &lt;em&gt;-es&lt;/em&gt;, &lt;em&gt;-ies&lt;/em&gt; and doubled consonants (&lt;em&gt;running&lt;/em&gt;, &lt;em&gt;stopped&lt;/em&gt;), and none of that changes a single count.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not a real stemmer or an NLP library
&lt;/h2&gt;

&lt;p&gt;A Porter stemmer would give &lt;em&gt;hike&lt;/em&gt; from &lt;em&gt;hiking&lt;/em&gt;, which looks nicer, but it also strips derivational endings like &lt;em&gt;-ness&lt;/em&gt; and &lt;em&gt;-ful&lt;/em&gt;, which usually don't count as separate morphemes in this kind of sample. A part-of-speech tagger would tell &lt;em&gt;bored&lt;/em&gt; (adjective) from &lt;em&gt;jumped&lt;/em&gt; (verb), but child speech is full of fragments that taggers handle badly, and the whole page runs offline in the browser with no model to download. Three regexes plus lists that grow from real misses turned out to be the more honest tool: when it is wrong, you can see exactly why.&lt;/p&gt;

&lt;p&gt;If you want to try it on your own sample, it's the &lt;a href="https://slpagecalc.com/mlu-calculator/" rel="noopener noreferrer"&gt;MLU calculator&lt;/a&gt; on SLP Age Calc. Paste one utterance per line, press auto-mark, and check the slashes before you trust the number.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>regex</category>
      <category>webdev</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Load Testing Our Checkout Pipeline Without Taking Down Production</title>
      <dc:creator>Taocarts跨境</dc:creator>
      <pubDate>Fri, 13 Nov 2026 01:32:18 +0000</pubDate>
      <link>https://dev.to/taocarts/load-testing-our-checkout-pipeline-without-taking-down-production-1324</link>
      <guid>https://dev.to/taocarts/load-testing-our-checkout-pipeline-without-taking-down-production-1324</guid>
      <description>&lt;p&gt;--&amp;gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Load Testing Our Checkout Pipeline Without Taking Down Production
&lt;/h1&gt;

&lt;p&gt;Every cross-border commerce team learns the same lesson during peak season: the checkout path is the one place you cannot afford to fall over, and it is also the place you are most afraid to stress-test. At Taocarts, a platform that mirrors Chinese marketplaces (Taobao, 1688) into overseas storefronts and consolidates purchasing and shipping, we spent a peak season watching our checkout bend before we finally built a safe way to break it on purpose.&lt;/p&gt;

&lt;h2&gt;
  
  
  The naive approach fails quietly
&lt;/h2&gt;

&lt;p&gt;Our first instinct was to point a load tool at the production checkout URL and crank the concurrency. It "worked" once, then we agreed never to do it again — a misconfigured ramp brought the live storefront down for nine minutes during a real promo. Testing against production is too risky; not testing at all is worse.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we actually do now
&lt;/h2&gt;

&lt;p&gt;We run load tests against a full staging environment that mirrors production's topology: same Laravel API, same database engine, same queue workers, but isolated tenant data and a separate payment gateway in sandbox mode. The goal is never to "test the real thing", it is to test the same code and the same shape of traffic.&lt;/p&gt;

&lt;p&gt;The traffic shape matters more than the volume. A real checkout burst is not uniform. It is a sharp ramp at a promo start, a long plateau, then a second spike when the "last chance" reminder fires. We replay recorded traffic patterns from previous peaks, scaled up, rather than a synthetic flat flood.&lt;/p&gt;

&lt;h2&gt;
  
  
  The parts that actually break
&lt;/h2&gt;

&lt;p&gt;Under load, the checkout path fails in predictable places. Database connections exhaust first because every request opens a short-lived read. We moved read-heavy lookups (product display, exchange-rate snapshots) behind a cache with a short TTL, so a spike in browsing does not translate one-to-one into database hits.&lt;/p&gt;

&lt;p&gt;The second failure is the payment confirmation step. When the gateway slows, requests pile up waiting for a callback ACK. We capped in-flight payment confirmations per worker and pushed the slow part onto a queue, so a slow gateway degrades gracefully instead of cascading.&lt;/p&gt;

&lt;p&gt;The third is email and notification fan-out. Sending a confirmation per checkout synchronously is fine at 10 requests per second and fatal at 400. We made all notifications fire-and-forget through the queue.&lt;/p&gt;

&lt;h2&gt;
  
  
  How we know it worked
&lt;/h2&gt;

&lt;p&gt;We do not trust a single passing run. Each test produces a report: p95 and p99 latency at target concurrency, error rate, and where the saturation point was. If p99 climbs past our threshold before we reach target load, the test "failed" by design and we go fix the bottleneck. The point is to find the wall in staging, not in front of real buyers.&lt;/p&gt;

&lt;h2&gt;
  
  
  The payoff
&lt;/h2&gt;

&lt;p&gt;After we standardized this, our last two promo peaks passed without a checkout incident. More importantly, when something does saturate, we now have a number for where, instead of a 3 a.m. guessing game.&lt;/p&gt;

&lt;p&gt;Load testing checkout is not glamorous, and it will never be the demo slide. But the checkout is where revenue is won or lost in milliseconds — testing it safely, against a mirror of production, is one of the highest-leverage things a commerce backend team can do.&lt;/p&gt;

</description>
      <category>programming</category>
    </item>
    <item>
      <title>Can You Upscale a 480p Hit the Road Dude Clip to 1080p? Lanczos vs Bicubic vs Unsharp, Scored Against a Ground Truth</title>
      <dc:creator>Peiguan</dc:creator>
      <pubDate>Fri, 13 Nov 2026 01:31:44 +0000</pubDate>
      <link>https://dev.to/lpgit/can-you-upscale-a-480p-hit-the-road-dude-clip-to-1080p-lanczos-vs-bicubic-vs-unsharp-scored-12k5</link>
      <guid>https://dev.to/lpgit/can-you-upscale-a-480p-hit-the-road-dude-clip-to-1080p-lanczos-vs-bicubic-vs-unsharp-scored-12k5</guid>
      <description>&lt;p&gt;A question that comes up about Hit the Road Dude AI videos, the State Property pickup-window meme where the driver repeats "State?" and tells the visitor to hit the road, goes roughly: "My clip is 480p. Can I upscale it to 1080p so it doesn't look soft on a TikTok or Shorts upload?" Chatbots usually answer "yes, with ffmpeg or an AI upscaler" without saying what that buys you.&lt;/p&gt;

&lt;p&gt;I build &lt;a href="https://hittheroaddude.app/" rel="noopener noreferrer"&gt;hittheroaddude.app&lt;/a&gt;, an independent studio that makes a 15-second, vertical 9:16 remake from one photo at 480p or 720p, with a newly generated voice (&lt;a href="https://hittheroaddude.app/faq/" rel="noopener noreferrer"&gt;FAQ&lt;/a&gt;). It doesn't sell a 1080p option, so anyone who wants 1080p is upscaling outside the site. This post doesn't claim any upscaler makes a generated clip look better. It shows how to &lt;em&gt;measure&lt;/em&gt; what the common ffmpeg options do, using a method that has an honest answer: build a ground truth first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why you can't just compare two upscales
&lt;/h2&gt;

&lt;p&gt;If you only have the 480p file, there is no "correct" 1080p to compare against, and two upscales can only be compared by eye. The fix is to go backwards. Take a detailed 1080x1920 frame, shrink it to 480 px wide, upscale that back to 1080 px with each method, and score each result against the original frame. Whatever the method gets wrong shows up as a lower score. It is a synthetic test, not a claim about generated video, but the &lt;em&gt;ranking&lt;/em&gt; tells you which filter loses the least when detail has been thrown away.&lt;/p&gt;

&lt;p&gt;The script below makes two test frames: a "graphic" one with thin lines, small text and hard edges, and a "photo" one built from smooth blurred colour plus grain. It scores PSNR (average pixel error, in dB, higher is better) and SSIM (structural similarity from ffmpeg's &lt;code&gt;ssim&lt;/code&gt; filter, 1.0 is identical).&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="c1"&gt;#!/usr/bin/env python3&lt;/span&gt;
&lt;span class="s2"&gt;"""Score ffmpeg upscalers against a known ground truth.

A real 480p clip has no 1080p original to compare with, so build one: render a
detailed 1080x1920 frame, shrink it to 480x854 (the "480p" you would have), upscale it
back with each method, and score the result against the original frame.

usage: upscale_compare.py [--keep DIR]
needs: ffmpeg on PATH, Python 3, Pillow, numpy
"""&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;argparse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tempfile&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="no"&gt;PIL&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nc"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;ImageDraw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;ImageFilter&lt;/span&gt;

&lt;span class="no"&gt;W&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;H&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;SW&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;SH&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1080&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1920&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;480&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;854&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;ffmpeg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"ffmpeg"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"-v"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;capture_output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;returncode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stderr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;make_ground_truth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;rng&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;default_rng&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"graphic"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="c1"&gt;# hard edges, thin lines, small text: stress test&lt;/span&gt;
        &lt;span class="n"&gt;im&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"RGB"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;W&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;H&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;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;110&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ImageDraw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Draw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;im&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;W&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;line&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;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&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="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;H&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;span class="n"&gt;fill&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;230&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;230&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;230&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="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;H&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;160&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rectangle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;W&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;70&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;outline&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&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="n"&gt;width&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"STATE? THIS IS PRIVATE PROPERTY. HIT THE ROAD, DUDE."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fill&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ellipse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;800&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;780&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1280&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;outline&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;im&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                          &lt;span class="c1"&gt;# soft "photo-like" content: blurred noise + gradients + a bit of grain&lt;/span&gt;
        &lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;H&lt;/span&gt; &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;W&lt;/span&gt; &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;im&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromarray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;astype&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"uint8"&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;span class="nf"&gt;resize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;W&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;H&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="no"&gt;BICUBIC&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;im&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;im&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ImageFilter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;GaussianBlur&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;asarray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;im&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;astype&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;float32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;normal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&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="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;H&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;W&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nc"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromarray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;astype&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"uint8"&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;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="no"&gt;METHODS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s2"&gt;"bilinear"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"scale={W}:{H}:flags=bilinear"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"bicubic"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"scale={W}:{H}:flags=bicubic"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"lanczos"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"scale={W}:{H}:flags=lanczos"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"lanczos+unsharp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"scale={W}:{H}:flags=lanczos,unsharp=5:5:0.8:5:5:0.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"lanczos+unsharp(strong)"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"scale={W}:{H}:flags=lanczos,unsharp=5:5:1.8:5:5:0.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;ssim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"ffmpeg"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"-v"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"info"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"-i"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"-i"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"-lavfi"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"ssim"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"-f"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"null"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"-"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                       &lt;span class="n"&gt;capture_output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="s2"&gt;"All:([0-9.]+)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stderr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;psnr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;asarray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;convert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"RGB"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;float64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;asarray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;convert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"RGB"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;float64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;mse&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="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&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="nf"&gt;mean&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="mf"&gt;99.0&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;mse&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log10&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;mse&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&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;span class="n"&gt;ap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;argparse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ArgumentParser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"--keep"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;help&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"keep the intermediate PNGs in this directory"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse_args&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;work&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keep&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;tempfile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mkdtemp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"upscale_"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;makedirs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;work&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exist_ok&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"{'content':8} {'method':24} {'PSNR dB':&amp;gt;8} {'SSIM':&amp;gt;7}"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"graphic"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"photo"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;gt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"{work}/{kind}_gt.png"&lt;/span&gt;
        &lt;span class="n"&gt;small&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"{work}/{kind}_480.png"&lt;/span&gt;
        &lt;span class="nf"&gt;make_ground_truth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;ffmpeg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"-i"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"-vf"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"scale={SW}:{SH}:flags=area"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;small&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;       &lt;span class="c1"&gt;# the "480p" you start from&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;graph&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="no"&gt;METHODS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&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;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"{work}/{kind}_{re.sub(r'[^a-z]+', '_', name)}.png"&lt;/span&gt;
            &lt;span class="nf"&gt;ffmpeg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"-i"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;small&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"-vf"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;W&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="no"&gt;W&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;H&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="no"&gt;H&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"{kind:8} {name:24} {psnr(out, gt):8.2f} {ssim(out, gt):7.4f}"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="nf"&gt;main&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;h2&gt;
  
  
  What it printed
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;content  method                    PSNR dB    SSIM
graphic  bilinear                    20.12  0.7773
graphic  bicubic                     20.69  0.7866
graphic  lanczos                     20.82  0.7769
graphic  lanczos+unsharp             21.04  0.7610
graphic  lanczos+unsharp(strong)     20.64  0.7241
photo    bilinear                    36.26  0.9583
photo    bicubic                     36.59  0.9598
photo    lanczos                     36.57  0.9598
photo    lanczos+unsharp             36.35  0.9583
photo    lanczos+unsharp(strong)     36.01  0.9559
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Checked on the box with ffmpeg 7.1.5, Python 3.13, Pillow 13 and NumPy 2.5. The test frames are seeded, so you should get the same numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the numbers say
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;On smooth, photo-like content the methods are almost tied.&lt;/strong&gt; Everything lands between 36.0 and 36.6 dB and 0.956 to 0.960 SSIM. Bicubic and lanczos are best by a hair, and the sharpening filter makes the score slightly &lt;em&gt;worse&lt;/em&gt;, because it adds edges the original frame never had.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On hard edges the ranking is messier.&lt;/strong&gt; Lanczos plus a light unsharp has the best PSNR (21.04 dB) but the worst SSIM of the non-aggressive options (0.761 against 0.787 for plain bicubic). The two metrics disagree, which is a sign that none of these filters recovers detail. They differ in how they distribute the error.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Strong sharpening loses on both metrics&lt;/strong&gt; (20.64 dB, 0.724 SSIM). If a clip looks "crunchy" after upscaling, that is the cause.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;None of them rescue the lost detail.&lt;/strong&gt; A 480p file doesn't contain 1080p information, so upscaling changes the pixel grid, not what is in the picture. What it can do is make a platform more likely to keep a higher bitrate for your upload, which is a different benefit and one I haven't measured.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I'd actually do
&lt;/h2&gt;

&lt;p&gt;Use &lt;code&gt;scale=1080:-2:flags=bicubic&lt;/code&gt; or &lt;code&gt;lanczos&lt;/code&gt; and skip the sharpening, or add the light unsharp only if you like how it looks on your own clip and check it at phone size. Upscale once, from the original download, as the last step before export, so you don't compress twice. Keep the vertical 9:16 shape: for a 480-wide source that is &lt;code&gt;scale=1080:1920&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg -i clip.mp4 -vf &lt;span class="s2"&gt;"scale=1080:1920:flags=lanczos"&lt;/span&gt; -c:v libx264 -crf 18 -preset slow -c:a copy clip_1080.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That command is not tested on a real generated clip, since I'm not publishing any generated results here. It is the same &lt;code&gt;scale&lt;/code&gt; filter the script ran on PNGs. If you want to test on your own clip, extract a frame, downscale it, upscale it, and run the metrics from this script on the pair.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it fits
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://hittheroaddude.app/guides/480p-or-720p/" rel="noopener noreferrer"&gt;480p or 720p guide&lt;/a&gt; covers the choice the studio actually offers. If you're deciding whether the sharper 720p is worth it before going anywhere near an upscaler, start there. Then upload only a photo of yourself, an adult who agreed, or your own pet, and label the result as an AI remake.&lt;/p&gt;

&lt;p&gt;For reference, checked on November 13, 2026 at 09:02 (UTC+8): one clip at hittheroaddude.app is $9.99 at 480p (6 credits) or $12.99 at 720p (14 credits), with credit packs for signed-in accounts at $27.99 for 30 credits, $71.99 for 100 and $129.99 for 200 (&lt;a href="https://hittheroaddude.app/pricing/" rel="noopener noreferrer"&gt;pricing&lt;/a&gt;).&lt;/p&gt;

</description>
      <category>ffmpeg</category>
      <category>python</category>
      <category>video</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>CVE-2026-126190: CVE-2026-126190: Polynomial-Time Denial of Service in Soup Sieve CSS Descendant Combinator</title>
      <dc:creator>CVE Reports</dc:creator>
      <pubDate>Fri, 13 Nov 2026 01:31:20 +0000</pubDate>
      <link>https://dev.to/cverports/cve-2026-126190-cve-2026-126190-polynomial-time-denial-of-service-in-soup-sieve-css-descendant-10a5</link>
      <guid>https://dev.to/cverports/cve-2026-126190-cve-2026-126190-polynomial-time-denial-of-service-in-soup-sieve-css-descendant-10a5</guid>
      <description>&lt;h1&gt;
  
  
  CVE-2026-126190: Polynomial-Time Denial of Service in Soup Sieve CSS Descendant Combinator
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Vulnerability ID:&lt;/strong&gt; CVE-2026-126190&lt;br&gt;
&lt;strong&gt;CVSS Score:&lt;/strong&gt; 7.5&lt;br&gt;
&lt;strong&gt;Published:&lt;/strong&gt; 2026-11-13&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;CVE-2026-126190 (GHSA-mmhr-3c5m-f64f) is an inefficient algorithmic complexity vulnerability (CWE-407) in Soup Sieve, the default CSS selector library integrated into Beautiful Soup 4. In versions prior to 4.0, evaluating CSS selectors that contain descendant combinators (whitespace between compound selectors) executes an unmemoized upward traversal of the document tree. When evaluated against deeply nested HTML or XML documents, the time complexity scales polynomially at O(n^(k+1)), where n is the tree depth and k is the number of descendant combinators. Remote attackers can supply compact, deeply nested markup (1 KB to 4 KB) to trigger prolonged CPU starvation and denial of service in Python applications.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;Soup Sieve versions prior to 4.0 fail to memoize ancestor evaluations when matching CSS descendant combinators, leading to O(n^(k+1)) polynomial time complexity and remote denial of service via small, deeply nested HTML or XML payloads.&lt;/p&gt;




&lt;h3&gt;
  
  
  ⚠️ Exploit Status: POC
&lt;/h3&gt;

&lt;h2&gt;
  
  
  Technical Details
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Vulnerability ID&lt;/strong&gt;: CVE-2026-126190 / GHSA-mmhr-3c5m-f64f&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Weakness Class&lt;/strong&gt;: CWE-407: Inefficient Algorithmic Complexity&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CVSS v3.1 Score&lt;/strong&gt;: 7.5 HIGH (AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Affected Component&lt;/strong&gt;: soupsieve (&amp;lt; 4.0.0) / CSSMatch.match_past_relations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time Complexity&lt;/strong&gt;: O(n^(k+1)) unpatched vs. O(n) patched&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Primary Impact&lt;/strong&gt;: Denial of Service via CPU Exhaustion&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exploit Status&lt;/strong&gt;: Public PoC Available&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Patched Version&lt;/strong&gt;: soupsieve 4.0&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Affected Systems
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Soup Sieve (soupsieve PyPI package)&lt;/li&gt;
&lt;li&gt;Beautiful Soup 4 (bs4) applications utilizing .select() or .select_one()&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;soupsieve&lt;/strong&gt;: &amp;lt; 4.0.0 (Fixed in: &lt;code&gt;4.0&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Code Analysis
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Commit: &lt;a href="https://github.com/facelessuser/soupsieve/commit/ff1a5ce7e5addc31626d7b6053511fecdb4f6320" rel="noopener noreferrer"&gt;ff1a5ce&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Cache whether an element is or has a matching ancestor when matching the descendant combinator (a b) to avoid polynomial time complexity.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gu"&gt;diff --git a/soupsieve/css_match.py b/soupsieve/css_match.py&lt;/span&gt;
index ced22901..f2bcdabc 100644
&lt;span class="gd"&gt;--- a/soupsieve/css_match.py&lt;/span&gt;
&lt;span class="gi"&gt;+++ b/soupsieve/css_match.py&lt;/span&gt;
&lt;span class="gu"&gt;@@ -614,6 +614,7 @@ def __init__(&lt;/span&gt;
         self.enable_cache = not bool(self.flags &amp;amp; util.NOCACHE)
         self.nth_cache: dict[Hashable, list[int]] = {}
         self.sib_cache: dict[Hashable, int] = {}
&lt;span class="gi"&gt;+        self.anc_cache: dict[tuple[int, int], bool] = {}&lt;/span&gt;
         self.dir_cache: dict[int, int] = {}
         self.index_cache: dict[int, dict[int, int]] = {}

&lt;span class="gu"&gt;@@ -648,6 +649,7 @@ def reset(self) -&amp;gt; None:  # pragma: no cover&lt;/span&gt;

         self.nth_cache.clear()
         self.sib_cache.clear()
&lt;span class="gi"&gt;+        self.anc_cache.clear()&lt;/span&gt;
         self.dir_cache.clear()
         self.index_cache.clear()

&lt;span class="gu"&gt;@@ -989,6 +991,31 @@ def match_general_sibling(self, el: bs4.Tag, relation: ct.SelectorList) -&amp;gt; Match&lt;/span&gt;

         return found

&lt;span class="gi"&gt;+    def match_ancestor(self, el: bs4.Tag, relation: ct.SelectorList) -&amp;gt; MatchGenerator:&lt;/span&gt;
&lt;span class="gi"&gt;+        """&lt;/span&gt;
&lt;span class="gi"&gt;+        Match descendant combinator.&lt;/span&gt;
&lt;span class="gi"&gt;+&lt;/span&gt;
&lt;span class="gi"&gt;+        A selector with several descendant combinators tries every ancestor for each one, which is&lt;/span&gt;
&lt;span class="gi"&gt;+        a polynomial of the depth with a degree of the number of combinators. Every element visited&lt;/span&gt;
&lt;span class="gi"&gt;+        on the walk shares the result, so cache whether each one is or has a matching ancestor.&lt;/span&gt;
&lt;span class="gi"&gt;+        """&lt;/span&gt;
&lt;span class="gi"&gt;+&lt;/span&gt;
&lt;span class="gi"&gt;+        found = False&lt;/span&gt;
&lt;span class="gi"&gt;+        visited: list[tuple[int, int]] = []&lt;/span&gt;
&lt;span class="gi"&gt;+        parent: bs4.Tag | None = el&lt;/span&gt;
&lt;span class="gi"&gt;+        while not found and parent and (parent := self.get_parent(parent)):&lt;/span&gt;
&lt;span class="gi"&gt;+            key = (id(parent), id(relation))&lt;/span&gt;
&lt;span class="gi"&gt;+            if key in self.anc_cache:&lt;/span&gt;
&lt;span class="gi"&gt;+                found = self.anc_cache[key]&lt;/span&gt;
&lt;span class="gi"&gt;+                break&lt;/span&gt;
&lt;span class="gi"&gt;+            visited.append(key)&lt;/span&gt;
&lt;span class="gi"&gt;+            found = yield parent, relation&lt;/span&gt;
&lt;span class="gi"&gt;+&lt;/span&gt;
&lt;span class="gi"&gt;+        for key in visited:&lt;/span&gt;
&lt;span class="gi"&gt;+            self.anc_cache[key] = found&lt;/span&gt;
&lt;span class="gi"&gt;+&lt;/span&gt;
&lt;span class="gi"&gt;+        return found&lt;/span&gt;
&lt;span class="gi"&gt;+&lt;/span&gt;
     def match_past_relations(self, el: bs4.Tag, relation: ct.SelectorList) -&amp;gt; MatchGenerator:
         """Match past relationship."""

&lt;span class="gu"&gt;@@ -998,9 +1025,12 @@ def match_past_relations(self, el: bs4.Tag, relation: ct.SelectorList) -&amp;gt; MatchG&lt;/span&gt;
             return found

         if relation[0].rel_type == REL_PARENT:
&lt;span class="gd"&gt;-            parent: bs4.Tag | None = el&lt;/span&gt;
&lt;span class="gd"&gt;-            while not found and parent and (parent := self.get_parent(parent)):&lt;/span&gt;
&lt;span class="gd"&gt;-                found = parent is not None and (yield parent, relation)&lt;/span&gt;
&lt;span class="gi"&gt;+            if self.enable_cache:&lt;/span&gt;
&lt;span class="gi"&gt;+                found = yield from self.match_ancestor(el, relation)&lt;/span&gt;
&lt;span class="gi"&gt;+            else:&lt;/span&gt;
&lt;span class="gi"&gt;+                parent: bs4.Tag | None = el&lt;/span&gt;
&lt;span class="gi"&gt;+                while not found and parent and (parent := self.get_parent(parent)):&lt;/span&gt;
&lt;span class="gi"&gt;+                    found = parent is not None and (yield parent, relation)&lt;/span&gt;
         elif relation[0].rel_type == REL_CLOSE_PARENT:
             parent = self.get_parent(el)
             found = parent is not None and (yield parent, relation)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Exploit Details
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/facelessuser/soupsieve/security/advisories/GHSA-mmhr-3c5m-f64f" rel="noopener noreferrer"&gt;GitHub Security Advisory&lt;/a&gt;: Official advisory containing benchmark data and proof-of-concept demonstrating O(n^(k+1)) polynomial slowdown using nested div tags.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Mitigation Strategies
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Upgrade the soupsieve Python package to version 4.0 or later to enable ancestor evaluation caching.&lt;/li&gt;
&lt;li&gt;Audit codebase to ensure the soupsieve.NOCACHE flag is never enabled when evaluating selectors on untrusted HTML or XML input.&lt;/li&gt;
&lt;li&gt;Refactor CSS selectors to use direct child combinators (&amp;gt;) instead of whitespace descendant combinators where document structure permits.&lt;/li&gt;
&lt;li&gt;Enforce maximum DOM nesting depth limits and execution timeouts around HTML parsing routines.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Remediation Steps:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Run &lt;code&gt;pip install --upgrade 'soupsieve&amp;gt;=4.0'&lt;/code&gt; in all affected Python environments and container images.&lt;/li&gt;
&lt;li&gt;Verify the installed version using &lt;code&gt;python -c 'import soupsieve; print(soupsieve.__version__)'&lt;/code&gt; to confirm it is 4.0 or newer.&lt;/li&gt;
&lt;li&gt;Search application source code for references to &lt;code&gt;sv.NOCACHE&lt;/code&gt; or &lt;code&gt;util.NOCACHE&lt;/code&gt; and remove them from untrusted parsing workflows.&lt;/li&gt;
&lt;li&gt;If immediate patching is not possible, rewrite multi-stage descendant selectors (e.g., &lt;code&gt;section div div&lt;/code&gt;) to use child combinators (&lt;code&gt;section &amp;gt; div &amp;gt; div&lt;/code&gt;) or single-stage queries.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/facelessuser/soupsieve/security/advisories/GHSA-mmhr-3c5m-f64f" rel="noopener noreferrer"&gt;GitHub Security Advisory GHSA-mmhr-3c5m-f64f&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://nvd.nist.gov/vuln/detail/CVE-2026-126190" rel="noopener noreferrer"&gt;NVD Detail for CVE-2026-126190&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.cve.org/CVERecord?id=CVE-2026-126190" rel="noopener noreferrer"&gt;CVE.org Record for CVE-2026-126190&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/facelessuser/soupsieve/pull/351" rel="noopener noreferrer"&gt;Soup Sieve Pull Request #351&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/facelessuser/soupsieve/commit/ff1a5ce7e5addc31626d7b6053511fecdb4f6320" rel="noopener noreferrer"&gt;Soup Sieve Fix Commit ff1a5ce7e5addc31626d7b6053511fecdb4f6320&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/facelessuser/soupsieve/releases/tag/4.0" rel="noopener noreferrer"&gt;Soup Sieve 4.0 Release Notes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/facelessuser/soupsieve" rel="noopener noreferrer"&gt;Soup Sieve GitHub Repository&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;&lt;a href="https://cvereports.com/reports/CVE-2026-126190" rel="noopener noreferrer"&gt;Read the full report for CVE-2026-126190 on our website&lt;/a&gt; for more details including interactive diagrams and full exploit analysis.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>cve</category>
      <category>cybersecurity</category>
    </item>
    <item>
      <title>Encrypting data in localStorage does not protect your JavaScript (or your users)</title>
      <dc:creator>VEILVM</dc:creator>
      <pubDate>Fri, 13 Nov 2026 01:30:27 +0000</pubDate>
      <link>https://dev.to/veilvm/encrypting-data-in-localstorage-does-not-protect-your-javascript-or-your-users-40fk</link>
      <guid>https://dev.to/veilvm/encrypting-data-in-localstorage-does-not-protect-your-javascript-or-your-users-40fk</guid>
      <description>&lt;p&gt;A pattern shows up in a lot of front-end code reviews: some piece of data — a token, a cached profile, a license flag, sometimes even a "secret" configuration — is written to &lt;code&gt;localStorage&lt;/code&gt;, and when someone objects, the answer is "we'll encrypt it." The team imports a library, picks a password or a key baked into the bundle, and calls the problem solved.&lt;/p&gt;

&lt;p&gt;It is not solved. In the browser, encryption without a key the attacker does not have is a delay, and often a short one. This article separates three different goals that get bundled under "encrypt localStorage," because they need different designs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three problems people think they are solving
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Hiding application logic.&lt;/strong&gt; The JavaScript that implements a feature should not be easy to copy. &lt;code&gt;localStorage&lt;/code&gt; is irrelevant to this. Your logic is already in the JS files the browser downloaded.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stopping a casual user from editing a value.&lt;/strong&gt; For example, flipping &lt;code&gt;plan: "free"&lt;/code&gt; to &lt;code&gt;plan: "pro"&lt;/code&gt; in DevTools. Encryption can make that edit slightly harder if the value is also integrity-protected, but only if the key is not sitting next to the ciphertext, and only if the server still enforces the plan.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Protecting data at rest from another program on the same machine.&lt;/strong&gt; This is a real threat model (shared computers, malware, other extensions), and it is the one Web Crypto can actually help with — if, and only if, the key is not shipped in your bundle.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you do not know which of these you mean, you will pick a library, wrap &lt;code&gt;localStorage.setItem&lt;/code&gt;, and ship a harder-to-read JSON blob that still decrypts in the console in one call.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the key lives is the whole story
&lt;/h2&gt;

&lt;p&gt;A typical "encrypted localStorage" helper looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="no"&gt;KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"s3cret-please-ignore"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// or CryptoJS.enc.Utf8.parse("...")&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;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&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="n"&gt;cipher&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;encrypt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cipher&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;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&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="n"&gt;cipher&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&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="nb"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;decrypt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cipher&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;KEY&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;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The ciphertext in Application → Local Storage looks impressive. The key is in the same origin's JavaScript, which the same person who opened DevTools can also read. From there the decrypt function is a copy-paste away.&lt;/p&gt;

&lt;p&gt;Variations that fail the same way:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Key derived from a hard-coded salt and a hard-coded passphrase.&lt;/li&gt;
&lt;li&gt;Key fetched from your own front-end config file or a "secret" environment variable that was inlined at build time (&lt;code&gt;VITE_STORAGE_KEY&lt;/code&gt; and friends).&lt;/li&gt;
&lt;li&gt;Key "hidden" by obfuscating the file that contains it. Obfuscation raises the cost of &lt;em&gt;finding&lt;/em&gt; the key; it does not create a key the user does not have. Once the app runs, the key is in memory.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your app can decrypt the value without the user entering a secret, so can anyone who controls that browser origin.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Web Crypto is actually good for
&lt;/h2&gt;

&lt;p&gt;The Web Crypto API (&lt;code&gt;crypto.subtle&lt;/code&gt;) is a solid tool. Use it when the key is not a constant in your repo.&lt;/p&gt;

&lt;p&gt;A model that makes sense: the user has a passphrase (or a platform authenticator), you derive a key with &lt;code&gt;PBKDF2&lt;/code&gt; or &lt;code&gt;Argon2&lt;/code&gt; (the latter via a library; Web Crypto provides PBKDF2), and you encrypt with AES-GCM so you get confidentiality &lt;em&gt;and&lt;/em&gt; integrity.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;enc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextEncoder&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;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;keyFromPassphrase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;passphrase&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;salt&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="n"&gt;base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;subtle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;importKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;"raw"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;enc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;passphrase&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"PBKDF2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"deriveKey"&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;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;subtle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deriveKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"PBKDF2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;salt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;iterations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;310_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"SHA-256"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"AES-GCM"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;256&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"encrypt"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"decrypt"&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;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This protects data at rest against someone who has the disk or the &lt;code&gt;localStorage&lt;/code&gt; dump but &lt;em&gt;not&lt;/em&gt; the passphrase. It does not protect data from the user who knows the passphrase, from XSS on your origin (which can prompt or wait for the key to be used), or from your own application logic being read.&lt;/p&gt;

&lt;p&gt;XSS is the uncomfortable part. Anything your page can decrypt, a injected script on the same origin can decrypt too. &lt;code&gt;localStorage&lt;/code&gt; plus encryption is not a substitute for a strict CSP, HTTP-only cookies for session tokens, or not storing tokens in JavaScript-accessible storage in the first place.&lt;/p&gt;

&lt;h2&gt;
  
  
  License flags and "pro unlocked" values
&lt;/h2&gt;

&lt;p&gt;This is the case that looks like a code-protection problem and is actually an authorization problem.&lt;/p&gt;

&lt;p&gt;If the client reads &lt;code&gt;localStorage.plan === "pro"&lt;/code&gt; and shows the paid UI, an encrypted value does not help. The user patches the decrypt result, or the branch that reads it, or they skip the check. The server has to be the source of truth for entitlements, and the client should treat a local cache as a cache.&lt;/p&gt;

&lt;p&gt;What &lt;em&gt;is&lt;/em&gt; a code-protection problem is the function that &lt;em&gt;validates&lt;/em&gt; a license offline — a signature check, a domain lock, a short-lived offline grant. That function is source you may want to protect, because the failure mode is "patch this one &lt;code&gt;if&lt;/code&gt;." Encrypting the string it writes to &lt;code&gt;localStorage&lt;/code&gt; does not protect that function. Protecting the function (and still verifying on the server when a network is available) does.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://vm.ldvmp.com/?utm_source=devto&amp;amp;utm_medium=post&amp;amp;utm_campaign=localstorage-encryption-is-not-protection" rel="noopener noreferrer"&gt;VEILVM&lt;/a&gt; is a browser-based JSVMP workspace for that narrower job: you extract the sensitive function and compile it to a custom bytecode with a small interpreter, so a beautifier does not immediately reveal the check. It raises cost; it does not replace server-side authorization, and it does not make a baked-in storage key safe.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical policy
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Session tokens: prefer HTTP-only, Secure, SameSite cookies. If you must keep a token in the browser, know that any script on your origin can read it; encryption with a bundled key adds almost nothing.&lt;/li&gt;
&lt;li&gt;Cached user data: encrypt only when you have a user-held secret or a platform key, and document the threat you are addressing (at-rest on a shared machine, not "users cannot see their data").&lt;/li&gt;
&lt;li&gt;Entitlements and prices: the server decides; the client may cache a signed blob it can verify, not a boolean it can flip.&lt;/li&gt;
&lt;li&gt;Proprietary logic: protect the code, do not bury it in &lt;code&gt;localStorage&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want to try VM protection on a small license or verification function, VEILVM runs in the browser with no sign-up: 3 free runs a day, up to 5 KB per file (resets at 00:00 UTC). A free account raises the input limit to 30 KB. Pro is $9/month or $79/year and adds an API for CI. Standard requests are processed in memory and are not written to logs. Start at &lt;a href="https://vm.ldvmp.com/?utm_source=devto&amp;amp;utm_medium=post&amp;amp;utm_campaign=localstorage-encryption-is-not-protection" rel="noopener noreferrer"&gt;https://vm.ldvmp.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>security</category>
      <category>webdev</category>
      <category>privacy</category>
    </item>
    <item>
      <title>Ranking 58 electric cars on sourced facts only, and what the rules turned up</title>
      <dc:creator>Ejaz Arain</dc:creator>
      <pubDate>Fri, 13 Nov 2026 01:29:19 +0000</pubDate>
      <link>https://dev.to/ejazarain/ranking-58-electric-cars-on-sourced-facts-only-and-what-the-rules-turned-up-1k3p</link>
      <guid>https://dev.to/ejazarain/ranking-58-electric-cars-on-sourced-facts-only-and-what-the-rules-turned-up-1k3p</guid>
      <description>&lt;p&gt;Most car rankings start from a test drive or a reviewer's opinion. For the Q4 2026 edition of the TrustList rankings we tried something stricter: order electric and plug-in hybrid cars only on facts that can be sourced and dated. These are crash-test results from Euro NCAP, IIHS or NHTSA, plus what each maker states on its own pages for that market.&lt;/p&gt;

&lt;p&gt;This post covers the rules we used, and what they did to the results.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rules
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Fixed, public weights.&lt;/strong&gt; Safety 25, range 20, charging 15, software updates 15, battery warranty 10, driver assistance 10, app 5.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A rank number needs evidence.&lt;/strong&gt; A car is ranked if it has a crash-test result or three sourced facts. Anything else is listed, not ranked.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Like with like.&lt;/strong&gt; Ranges are compared only on the same test cycle. A range with no named cycle is held but not compared.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Facts expire.&lt;/strong&gt; A fact read more than a year ago is not counted until it is read again.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Price is shown, never scored.&lt;/strong&gt; Nobody pays for a place.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each fact on the page carries the date it was read. For this edition that was 17 or 18 October 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the rules did
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Coverage.&lt;/strong&gt; The UK electric ranking lists 58 cars and ranks 50. The UK plug-in hybrid ranking lists 13 and ranks 5. The Europe electric ranking lists 15 and ranks 12.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Safety barely separates the leaders.&lt;/strong&gt; All of the UK top 24 have a Euro NCAP result. Twenty-one have five stars and three have four (Kia EV3, Kia EV4, Cupra Raval). The heaviest weight, then, decides almost nothing at the top.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Missing fields decide a lot.&lt;/strong&gt; For 13 of the UK top 24 we hold nothing about software updates from the maker's UK pages. For 11 we hold nothing on battery warranty. Not one of the five ranked plug-in hybrids has software updates on record, and in Europe 10 of the 12 ranked cars have none. These are gaps in what the makers publish, not proof that a feature is missing. A sourced-facts model can only credit what is written down.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule 3 matters.&lt;/strong&gt; The longest WLTP range in the UK top 24 is the Volkswagen ID.7, at 697 km. It ranks 21st, because we hold nothing on its charging, software updates or driver assistance. The Cupra Born (624 km) and Cupra Raval (442 km) quote ranges with no named test cycle, so those figures are not compared with the WLTP ones.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Units are not always comparable.&lt;/strong&gt; Some makers publish a 10 to 80 percent charging time. Others give only a peak power, such as "up to 260 kW DC" for the Hyundai IONIQ 5. Among the cars that publish the time, the UK range runs from 18 minutes (Kia EV6, Genesis GV60) to 32 minutes (Toyota bZ4X).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule 2 lets a car rank without a crash test.&lt;/strong&gt; The Toyota Prius Plug-in Hybrid is fifth among UK plug-in hybrids on three sourced facts: 85 km of WLTP electric range, a three-year battery warranty and its app. In Europe, the Leapmotor B03X, the Leapmotor T03 and the Rimac Nevera rank the same way.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Price stays out of the score.&lt;/strong&gt; The UK number one, the Polestar 3, starts at £76,540. The Kia EV3 is seventh from £31,555. The Cupra Raval, 24th, is the cheapest of the top 24 at £25,495 with the Electric Car Grant applied.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we would change
&lt;/h2&gt;

&lt;p&gt;The biggest weakness of the model is visible above. A maker that publishes less ranks lower, whatever the car is like. We think that is the right incentive for a buyer reading makers' pages. But it means the ranking measures disclosure as much as the car. The next edition is due in January 2027, and every gap listed here can be closed by a dated page from the maker.&lt;/p&gt;

&lt;p&gt;The rankings, with each fact and its read date:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://trustlist.uk/best/electric-cars/united-kingdom" rel="noopener noreferrer"&gt;https://trustlist.uk/best/electric-cars/united-kingdom&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://trustlist.uk/best/plug-in-hybrid-cars/united-kingdom" rel="noopener noreferrer"&gt;https://trustlist.uk/best/plug-in-hybrid-cars/united-kingdom&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://trustlist.uk/best/electric-cars/europe" rel="noopener noreferrer"&gt;https://trustlist.uk/best/electric-cars/europe&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Method: &lt;a href="https://trustlist.uk/trust" rel="noopener noreferrer"&gt;https://trustlist.uk/trust&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>datascience</category>
      <category>opendata</category>
      <category>electricvehicles</category>
      <category>data</category>
    </item>
    <item>
      <title>Play Google Com Console: Complete Guide for Developers</title>
      <dc:creator>Play Console Store</dc:creator>
      <pubDate>Fri, 13 Nov 2026 01:28:45 +0000</pubDate>
      <link>https://dev.to/playconsolestore1/play-google-com-console-complete-guide-for-developers-149c</link>
      <guid>https://dev.to/playconsolestore1/play-google-com-console-complete-guide-for-developers-149c</guid>
      <description>&lt;p&gt;When developers search for the "play google com console," they're looking for one thing: the front door to Google's Android publishing platform. The exact address is &lt;strong&gt;play.google.com/console&lt;/strong&gt; — that's the Google Play Console, the web dashboard where Android developers register, upload apps, manage releases, and track performance. But getting there and getting &lt;em&gt;in&lt;/em&gt; isn't always as simple as typing a URL: wrong bookmarks, signed-in personal accounts, expired sessions, and regional redirects trip up developers every day.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In short:&lt;/strong&gt; The Play Google Com console lives at play.google.com/console — sign in with the Google account that owns your developer account, keep 2-Step Verification on, and use the desktop web console for full publishing work. This guide covers the official URLs, sign-in, account switching, the mobile app, and fixes for the access errors that lock developers out.&lt;/p&gt;

&lt;h2&gt;
  
  
  What are the official Play Console URLs?
&lt;/h2&gt;

&lt;p&gt;Bookmark these — they're the only addresses you should ever use to reach the console:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Main dashboard:&lt;/strong&gt; &lt;code&gt;play.google.com/console&lt;/code&gt; — your developer home after sign-in, showing all apps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sign-up page:&lt;/strong&gt; &lt;code&gt;play.google.com/console/signup&lt;/code&gt; — where new developers register and pay the $25 fee.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Developers homepage:&lt;/strong&gt; &lt;code&gt;play.google.com/console/about&lt;/code&gt; — overview of features (useful to share with clients).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Help center:&lt;/strong&gt; &lt;a href="https://support.google.com/googleplay/android-developer" rel="noopener noreferrer"&gt;support.google.com/googleplay/android-developer&lt;/a&gt; — official documentation for every feature.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Security warning:&lt;/strong&gt; only ever sign in at &lt;code&gt;play.google.com&lt;/code&gt; or &lt;code&gt;accounts.google.com&lt;/code&gt;. Phishing sites mimic the console login with lookalike domains (play-google-console.com, googleplayconsole.net). Check the address bar before entering credentials — Google will never ask you to sign in to the console from any other domain.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;th&gt;Address&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Main dashboard&lt;/td&gt;
&lt;td&gt;play.google.com/console&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sign-up page&lt;/td&gt;
&lt;td&gt;play.google.com/console/signup&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Developers homepage&lt;/td&gt;
&lt;td&gt;play.google.com/console/about&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  How do I sign in to the Play Console?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Use the right Google account
&lt;/h3&gt;

&lt;p&gt;The console doesn't have its own username/password system — it uses &lt;strong&gt;Google account sign-in&lt;/strong&gt;. The Google account you sign in with becomes (or is linked to) your developer identity, so choose deliberately:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Signing in with your personal Gmail creates a &lt;em&gt;personal&lt;/em&gt; developer account tied to that address.&lt;/li&gt;
&lt;li&gt;Agencies should use a dedicated work Google account, not an employee's personal Gmail — when people leave, access gets messy.&lt;/li&gt;
&lt;li&gt;One Google account can own one developer account but be &lt;em&gt;invited&lt;/em&gt; to many others as a user.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 2: Complete the sign-in flow
&lt;/h3&gt;

&lt;p&gt;Go to play.google.com/console. If you're not signed in, Google redirects you to accounts.google.com. Enter your email, password, and complete 2-Step Verification if enabled (it should be — developer accounts are high-value targets). After authentication, you land on the &lt;strong&gt;All apps&lt;/strong&gt; dashboard.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Handle the first-visit setup
&lt;/h3&gt;

&lt;p&gt;First-time visitors without a developer account see the registration flow: choose personal or organization account type, complete identity verification, pay the $25 registration fee, and accept the Developer Distribution Agreement. Verification typically takes a few hours to a couple of days. Until it's complete, most console sections show as locked or pending.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do I switch between multiple developer accounts?
&lt;/h2&gt;

&lt;p&gt;Freelancers and agencies often juggle several developer accounts — their own plus clients'. The console handles this through Google's account switcher plus an invitation system:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Invited users:&lt;/strong&gt; clients invite you via &lt;strong&gt;Setup → Users and permissions → Invite new users&lt;/strong&gt;. You accept with your own Google account and switch between accounts from the profile avatar menu. This is the correct way to manage client apps — never ask clients for their Google password.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chrome profiles:&lt;/strong&gt; run each developer identity in a separate Chrome profile. This eliminates the classic mistake of uploading a client's app while signed in as yourself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Account switcher:&lt;/strong&gt; click your avatar (top right) to switch Google accounts without signing out. The console remembers the last developer account per Google identity.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Common pitfall:&lt;/strong&gt; being signed in to five Google accounts in one browser and not knowing which one owns the developer account. If the console shows "no developer account found," you're almost certainly signed in with the wrong Google identity — check the avatar email before assuming your account is gone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Can I access the console on my phone?
&lt;/h2&gt;

&lt;p&gt;Google offers the &lt;strong&gt;Play Console app&lt;/strong&gt; (Android and iOS) as a companion to the web dashboard. It's genuinely useful for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Checking download and revenue statistics on the go&lt;/li&gt;
&lt;li&gt;Reading and replying to user reviews&lt;/li&gt;
&lt;li&gt;Receiving push alerts for policy violations, crashes spikes, and review milestones&lt;/li&gt;
&lt;li&gt;Monitoring staged rollout progress&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What it &lt;strong&gt;can't&lt;/strong&gt; do: create developer accounts, upload app bundles, complete policy declarations, or manage users and permissions. For those, you need the desktop web console. Think of the mobile app as mission-control telemetry, not the cockpit.&lt;/p&gt;

&lt;p&gt;To install it, search "Google Play Console" in the Play Store or App Store and sign in with the same Google account that owns your developer account. If you manage multiple developer accounts as an invited user, they all appear in the app's account switcher.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do I fix common console access problems?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  "This account isn't a developer account"
&lt;/h3&gt;

&lt;p&gt;You signed in with a Google account that has no developer registration. Either register at play.google.com/console/signup or switch to the Google account that owns the developer account (check the avatar email).&lt;/p&gt;

&lt;h3&gt;
  
  
  Redirect loops and blank pages
&lt;/h3&gt;

&lt;p&gt;Usually caused by corrupted cookies or overzealous extensions. Fix: open an incognito window and sign in fresh. If that works, clear cookies for google.com in your main browser and disable ad-blockers on play.google.com — several popular blockers break the console's JavaScript.&lt;/p&gt;

&lt;h3&gt;
  
  
  Session expired / signed out unexpectedly
&lt;/h3&gt;

&lt;p&gt;The console times out idle sessions for security. If it happens constantly (not just after idleness), check whether a VPN or corporate proxy is rotating your IP — Google treats rapid IP changes as suspicious and kills sessions. Whitelist google.com in the VPN or use a stable connection for console work.&lt;/p&gt;

&lt;h3&gt;
  
  
  "Access denied" on a client's account
&lt;/h3&gt;

&lt;p&gt;Your invitation was revoked, expired, or never accepted. Ask the account owner to re-invite you under Setup → Users and permissions. Invitations expire after a set period, and owners sometimes remove freelancers after projects end without notice.&lt;/p&gt;

&lt;h3&gt;
  
  
  Regional redirect issues
&lt;/h3&gt;

&lt;p&gt;In some regions, play.google.com redirects through a country-specific domain or loads slowly. The console itself is global — your developer account works identically everywhere — but if pages fail to load, try a direct connection without VPN first, then re-enable it.&lt;/p&gt;

&lt;p&gt;| Problem | Likely cause | Fix |&lt;br&gt;
|---|---|&lt;br&gt;
| "This account isn't a developer account" | Signed in with a Google account that has no developer registration | Register at play.google.com/console/signup, or switch to the Google account that owns the developer account |&lt;br&gt;
| Redirect loops and blank pages | Corrupted cookies or overzealous extensions | Use an incognito window; if that works, clear cookies for google.com and disable ad-blockers on play.google.com |&lt;br&gt;
| Session expired / signed out unexpectedly | VPN or corporate proxy rotating your IP | Whitelist google.com in the VPN, or use a stable connection for console work |&lt;br&gt;
| "Access denied" on a client's account | Invitation revoked, expired, or never accepted | Ask the account owner to re-invite you under Setup → Users and permissions |&lt;br&gt;
| Regional redirect issues | play.google.com redirects through a country-specific domain or loads slowly | Try a direct connection without VPN first, then re-enable it |&lt;/p&gt;

&lt;h2&gt;
  
  
  What are the console access best practices for teams?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Never share one Google account&lt;/strong&gt; across a team. Invite each member with their own Google identity and assign least-privilege roles (Release manager, Support, Finance — not Admin for everyone).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enable 2-Step Verification&lt;/strong&gt; on every Google account with console access. Developer accounts are prime phishing targets because they control app signing keys and revenue.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use security keys&lt;/strong&gt; (hardware tokens) for account owners — they're phishing-resistant in a way SMS codes aren't.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit access quarterly:&lt;/strong&gt; Setup → Users and permissions shows every invited user and their role. Remove ex-employees and finished contractors promptly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep a private access log&lt;/strong&gt; of which Google account owns each developer account. When someone leaves, this document is the difference between a smooth handover and a locked-out business.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The bottom line
&lt;/h2&gt;

&lt;p&gt;The Play Google Com console lives at one address — &lt;strong&gt;play.google.com/console&lt;/strong&gt; — but using it well means mastering sign-in discipline: the right Google account, clean browser sessions, proper team invitations instead of shared passwords, and the mobile app for on-the-go monitoring. Most "access problems" are really account-confusion problems, and they're solved in the avatar menu, not in a support ticket. If you'd like expert help getting your console access right from day one, &lt;a href="https://playconsolestore.com/" rel="noopener noreferrer"&gt;Play Console Store&lt;/a&gt; helps Android developers with Play Console setup, account access configuration, and team permission structuring.&lt;/p&gt;




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

&lt;p&gt;&lt;strong&gt;What is the correct URL for the Play Google Com console?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The official URL is play.google.com/console. New developers register at play.google.com/console/signup. Bookmark it, and only sign in at play.google.com or accounts.google.com — any other domain asking for your console credentials is a phishing attempt. If you land anywhere else, close the tab and type the address yourself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why does play.google.com/console say I don't have a developer account?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You're signed in with the wrong Google account. The console ties developer registration to a specific Google identity — click your avatar (top right) to check which email is signed in, then switch to the one you registered with or complete registration for the current one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I access the Play Console from my phone?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes, via the Play Console app for Android and iOS. It covers statistics, reviews, alerts, and rollout monitoring, but full publishing workflows (uploads, policy declarations, user management) require the desktop web console at play.google.com/console. Install it by searching "Google Play Console" in the Play Store or App Store, then sign in with the same Google account.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I manage multiple developer accounts in the console?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Have each client invite your Google account under Setup → Users and permissions, then switch between accounts from the avatar menu. For clean separation, use a separate Chrome profile per client so you never upload to the wrong account. Never ask a client for their Google password — the invitation flow exists precisely so you never need it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The console keeps signing me out — how do I fix it?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;First try an incognito window to rule out cookie or extension issues. If the problem persists, check for VPN IP rotation, since Google kills sessions on rapid IP changes. Then clear google.com cookies and disable ad-blockers on play.google.com. If none of that helps, try a direct connection without the VPN first, then re-enable it.&lt;/p&gt;




&lt;h2&gt;
  
  
  About the author
&lt;/h2&gt;

&lt;p&gt;This guide was prepared by the publishing team at &lt;a href="https://playconsolestore.com/" rel="noopener noreferrer"&gt;Play Console Store&lt;/a&gt; — an independent marketplace for Google Play publishing services (release management, closed testing coordination, policy compliance, and app publishing assistance). Play Console Store is not affiliated with or endorsed by Google LLC.&lt;/p&gt;

</description>
      <category>android</category>
      <category>googleplay</category>
      <category>testing</category>
      <category>mobile</category>
    </item>
    <item>
      <title>Seedream 5.5: native transparent backgrounds are the feature to care about</title>
      <dc:creator>Jerry Simth</dc:creator>
      <pubDate>Fri, 13 Nov 2026 01:27:42 +0000</pubDate>
      <link>https://dev.to/jerrysimth/seedream-55-native-transparent-backgrounds-are-the-feature-to-care-about-5hfh</link>
      <guid>https://dev.to/jerrysimth/seedream-55-native-transparent-backgrounds-are-the-feature-to-care-about-5hfh</guid>
      <description>&lt;p&gt;ByteDance released Seedream 5.5 on November 9, 2026, and most of the coverage leads with photorealism — fair enough, it is a visible step up from the 5.0 line. But the more practical news for anyone who actually ships images is quieter: &lt;strong&gt;5.5 is the first Seedream that cuts subjects out natively.&lt;/strong&gt; Turn on the transparent background toggle and the model returns a PNG with a real alpha channel. No remove.bg step, no masking in Photoshop, no paying a second tool to finish the job.&lt;/p&gt;

&lt;p&gt;I run a small studio site that hosts 33 image models side by side, so new releases go straight into the rotation. Here's what 5.5 is actually good for, and where it isn't the right pick.&lt;/p&gt;

&lt;h2&gt;
  
  
  The transparent background, in practice
&lt;/h2&gt;

&lt;p&gt;The workflow is exactly as boring as it should be: write your prompt, flip the toggle, get a cutout. What it's for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Product shots for listings and ads&lt;/strong&gt; — a perfume bottle, a sneaker, a gadget, isolated and ready to drop onto any background.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stickers, icons and UI assets&lt;/strong&gt; — the kind of small graphics that used to mean a generation plus a cleanup pass.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mockups and compositing&lt;/strong&gt; — generate the subject once, reuse it across ten layouts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1lu6v5m1w8i62c4ajn56.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1lu6v5m1w8i62c4ajn56.png" alt="Cute robot mascot sticker with a transparent background — generated with Seedream 5.5" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;A sticker-style cutout straight from the generator — real Seedream 5.5 output, PNG with an alpha channel, no cleanup pass.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2o8jzipym5nzub9977au.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2o8jzipym5nzub9977au.webp" alt="Amber glass perfume bottle on wet dark stone in morning light — generated with Seedream 5.5" width="800" height="1000"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;A product shot of the kind you'd normally shoot in a studio — real Seedream 5.5 output.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Two honest caveats. First, cutout quality still depends on the subject: hard edges (packaging, bottles, electronics) come out clean; hair, fur and wispy edges are where any automatic cutout earns its keep, so test your edge cases before you build a workflow around them. Second, semi-transparent materials — glass, smoke, sheer fabric — are genuinely hard to represent in an alpha channel, and no model has that fully solved. Transparency is a feature, not magic.&lt;/p&gt;

&lt;p&gt;The toggle itself costs nothing extra: the price is the same as a normal generation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Multi-round editing that holds up
&lt;/h2&gt;

&lt;p&gt;The other real change is edit stability. 5.5 is built to survive round after round on the same subject — restyle a product shot, swap the background, change the lighting, then change it back. The studio takes up to 10 reference images, so you can hold a product, a style reference and a background in the same edit. Edits start at 5 credits, plus 2 per extra reference.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Faotybqd8zbxzgydcioc4.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Faotybqd8zbxzgydcioc4.webp" alt="The same perfume bottle moved onto a marble vanity by a background-swap edit — edited with Seedream 5.5" width="800" height="1076"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The same bottle from above, moved onto a marble vanity by a background-swap edit — one round, same subject held.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This is the difference between a model you prompt once and a model you art-direct. The first kind gives you a lottery ticket; the second gives you a working file.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it costs
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Text to image: &lt;strong&gt;4 credits at 1K and 1.5K, 6 credits at 2K&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Edits: &lt;strong&gt;from 5 credits&lt;/strong&gt; (+2 per extra reference, up to 10)&lt;/li&gt;
&lt;li&gt;15 aspect ratios, from 1:3 skyscraper to 21:9 ultrawide; JPEG and PNG output&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's cheaper per image than Seedream 5 Pro, which is part of the point — 5.5 is the new generation, not a premium tier above it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where 5 Pro still wins
&lt;/h2&gt;

&lt;p&gt;Honesty requires the other side: Seedream 5 Pro still renders the family's sharpest headline typography at 2K. If your job is a poster with a big display headline and small badge text, 5 Pro is the sharper pick. 5.5 wins on price, framing range, transparency and edit depth; 5 Pro wins on lettering. Pick by the job, not by the version number.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;The model runs on &lt;a href="https://pizyn.com/models/seedream-5-5/" rel="noopener noreferrer"&gt;Seedream 5.5&lt;/a&gt; in the studio — 20 free credits at signup, no card, drafts from 4 credits. If transparent backgrounds are part of your workflow, the toggle is the first thing worth testing.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>tutorial</category>
      <category>design</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>Testing Client Timeout Behavior</title>
      <dc:creator>Jeff</dc:creator>
      <pubDate>Fri, 13 Nov 2026 01:26:35 +0000</pubDate>
      <link>https://dev.to/jeff_pdc/testing-client-timeout-behavior-413</link>
      <guid>https://dev.to/jeff_pdc/testing-client-timeout-behavior-413</guid>
      <description>&lt;p&gt;Quick answer: A timeout is the client deciding to stop waiting, not a response from the server, so test it with fixed mock latency that lands just before and just after the threshold. Assert that the call aborts with the right error, the loading state resets, timers and in-flight work are cleaned up, retries follow a backoff schedule, and a response that arrives after the abort is ignored rather than applied twice. If you only test the fast, successful call, the slow path is still untested.&lt;/p&gt;

&lt;h2&gt;
  
  
  A timeout is a client decision, not a server error
&lt;/h2&gt;

&lt;p&gt;When the threshold passes, the server may still be processing the request, which is why a blind retry can create a duplicate side effect. Three distinct decisions get conflated under the word timeout. A connect timeout means the TCP connection never established. A read or request timeout means the connection opened but the full response did not arrive in time. An idle timeout means the response started and then stalled, such as headers arriving with no body. Each needs its own case because the cleanup and the error the caller sees are different. Default values in an HTTP library are generic and rarely match your real latency distribution, so leaving them untouched either makes users stare at a spinner or kills slow but successful calls.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pin the latency to the threshold
&lt;/h2&gt;

&lt;p&gt;Use a local mock with a fixed, known delay rather than a real network, and set the delay to the threshold value and to one millisecond on either side. The boundary case is the one that reveals off-by-one configuration: a response that resolves at the limit should still succeed, while one a moment later should abort. Keep the delays large and stable so a busy machine cannot blur them, and run the near and far cases in the same test so the threshold is judged against both. A desktop API workspace can set mock latency directly and drive conditional overrides, so the timing is chosen in advance and the same run reproduces every time. Powerduck works this way, keeping the timeout test local and repeatable instead of dependent on live network conditions.&lt;/p&gt;

&lt;h2&gt;
  
  
  The timeout types worth covering
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Timeout type&lt;/th&gt;
&lt;th&gt;Trigger&lt;/th&gt;
&lt;th&gt;Mock setup&lt;/th&gt;
&lt;th&gt;Expected client behavior&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Connect&lt;/td&gt;
&lt;td&gt;TCP connection never opens&lt;/td&gt;
&lt;td&gt;Endpoint that refuses or drops the connection&lt;/td&gt;
&lt;td&gt;Fail fast, retry with backoff&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Read/request&lt;/td&gt;
&lt;td&gt;Connected, full response too slow&lt;/td&gt;
&lt;td&gt;Fixed latency just over the threshold&lt;/td&gt;
&lt;td&gt;Abort, show error, reset loading&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Idle/stream&lt;/td&gt;
&lt;td&gt;Response stalls after headers&lt;/td&gt;
&lt;td&gt;Send headers, then hold the body&lt;/td&gt;
&lt;td&gt;Abort the partial read, clean up&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Late response&lt;/td&gt;
&lt;td&gt;Result arrives just after abort&lt;/td&gt;
&lt;td&gt;Latency just past the threshold&lt;/td&gt;
&lt;td&gt;Ignore the late result, no double apply&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What to assert after the abort
&lt;/h2&gt;

&lt;p&gt;Check the error type and message rather than only that a promise rejected, and confirm the loading flag returns to idle so the UI is not stuck. Verify the timer that drove the timeout is cleared and the in-flight request is cancelled rather than left running. For retries, assert the count and spacing against a backoff schedule and an overall deadline, and treat a non-idempotent POST carefully because an automatic retry can repeat a charge or a create. The late-response row matters most: when the answer arrives after the abort, the client must not apply it to state that already moved on, and must not process the same result twice. A 200 that lands late is still a response the client should treat as stale.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest limits
&lt;/h2&gt;

&lt;p&gt;A local mock runs in the desktop main process and reproduces waiting time, but it does not model the full shape of a real failure. It cannot reproduce a half-open TCP connection, a network reset, packet loss, or a server that keeps processing after the client leaves, and application-level fixed delays do not exercise the operating system's real socket behavior. The mock proves the named timeout paths and the client's handling of them; it does not replace genuine network tests or a check of what the server does when the caller disconnects. Importing a schema does not deploy a backend, and a passing mock run is not evidence about production connectivity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What is the difference between a connect and a read timeout?&lt;/strong&gt; A connect timeout means the connection never opened. A read timeout means it opened but the response did not finish in time, so the cleanup and error differ.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why test a response that arrives after the timeout?&lt;/strong&gt; The server may still answer. The client must ignore that late result instead of applying it to stale state or handling the same response twice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Should a timed-out POST be retried automatically?&lt;/strong&gt; Only if the operation is idempotent or carries an idempotency key. Otherwise the retry can repeat a side effect that the first request already performed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How should I choose the threshold?&lt;/strong&gt; Base it on the real latency distribution rather than a local round trip, set separate connect and read values, and confirm the choice against live measurements.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bottom line
&lt;/h2&gt;

&lt;p&gt;Treat timeout as client behavior and pin it with fixed mock latency around the exact threshold, covering the connect, read, idle, and late-response cases. Assert on the error, loading reset, cleanup, backoff-driven retries, and the late response that gets ignored, then verify real socket behavior and server-side handling separately. For more on this approach, see the &lt;a href="https://www.powerduck.com/blog/api-testing-guide-hub/" rel="noopener noreferrer"&gt;API testing guide hub&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>api</category>
      <category>testing</category>
      <category>debugging</category>
      <category>programming</category>
    </item>
    <item>
      <title>Book cover sizes that actually break at upload</title>
      <dc:creator>尚毅段</dc:creator>
      <pubDate>Fri, 13 Nov 2026 01:26:29 +0000</pubDate>
      <link>https://dev.to/succday/book-cover-sizes-that-actually-break-at-upload-1n99</link>
      <guid>https://dev.to/succday/book-cover-sizes-that-actually-break-at-upload-1n99</guid>
      <description>&lt;p&gt;A cover that looks finished can still fail at the KDP upload step. When it does, the error is usually not about the artwork. It is a number that came from a different version of the book.&lt;/p&gt;

&lt;p&gt;I maintain the sizing calculator at BookCoverGen, and the same handful of mismatches show up in support mail. Here they are in the order they cost people the most time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The only arithmetic that matters
&lt;/h2&gt;

&lt;p&gt;A print cover is one flat file:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;width = panel + panel + spine + 0.25 in&lt;/li&gt;
&lt;li&gt;height = trim height + 0.25 in&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The 0.25 in is 0.125 in of bleed on each outside edge. The panels come from the trim size. The spine comes from page count times paper thickness.&lt;/p&gt;

&lt;p&gt;For 6 x 9 in at 300 pages on black-and-white white paper, KDP's per-page figure is 0.002252 in, so the spine is 0.676 in and the full wrap is 12.93 x 9.25 in. Every case below is that calculation going wrong in a different place.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Your page count changed after the cover was built
&lt;/h2&gt;

&lt;p&gt;This one gets caught in the previewer, not at your desk.&lt;/p&gt;

&lt;p&gt;Re-paginate a manuscript and the spine moves. 6 x 9 at 250 pages on white paper is 0.563 in. At 300 pages it is 0.676 in. That is 0.113 in, close enough to an eighth of an inch, and it lands exactly where the author name sits on the spine. On a fold, an eighth of an inch is the difference between centered and cut.&lt;/p&gt;

&lt;p&gt;Build the cover after the final proof, not after the first draft. If the interior changes, re-run the calculation before the next upload.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The paper changed, the cover did not
&lt;/h2&gt;

&lt;p&gt;KDP's spine math is per paper type:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;black-and-white white paper: 0.002252 in per page&lt;/li&gt;
&lt;li&gt;black-and-white cream paper: 0.002500 in per page&lt;/li&gt;
&lt;li&gt;premium color white paper: 0.002347 in per page&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At 300 pages the white-paper spine is 0.676 in. The same 300 pages on cream is 0.750 in. The file has to grow by 0.074 in, and the front and back panels shift outward with it. Switch paper at the pricing step and keep the old cover, and the spine type is off the fold.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. The file has no bleed, or bleed on two edges
&lt;/h2&gt;

&lt;p&gt;A print file is trimmed after it is printed. The artwork has to run past the trim line on the top, bottom and outside edges, which adds 0.25 in to both dimensions.&lt;/p&gt;

&lt;p&gt;Two common versions of this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the file is built at trim size (12 x 9 in for 6 x 9 at 300 pages, not 12.93 x 9.25 in);&lt;/li&gt;
&lt;li&gt;the designer adds bleed at the outer edges of the front and back panels but not across the spine wrap.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both look fine on screen, because the missing strip is white and the background is white. The previewer is the first place the trim sliver shows up, and a background that stops at trim size will show a hairline at the cut.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. The ebook cover was exported from the print panel
&lt;/h2&gt;

&lt;p&gt;Ebooks have no spine, no bleed and no back cover. KDP asks for a front cover of 1,600 x 2,560 px at a 1.6 aspect ratio, JPEG or TIFF, under 50 MB.&lt;/p&gt;

&lt;p&gt;The print panel for 6 x 9 in at 300 DPI is 1,800 x 2,700 px. The ratio is 1.5, not 1.6, so it does not simply scale down. What happens next depends on the store: the image gets cropped, padded, or rendered smaller in the thumbnail grid than a properly sized file. Export the ebook cover at the pixel size, separately from the print export.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Spine text on a book that will not print it
&lt;/h2&gt;

&lt;p&gt;KDP prints spine text only above 79 pages. Below that the spine stays blank, so artwork should run through it and type should not be there at all. Type placed on a 40-page file does not appear in print; if the page count later grows past 79, the old file still has type set for a spine width it no longer has.&lt;/p&gt;

&lt;p&gt;When there is spine text, it needs 0.0625 in clear on each side of the fold. Thin books have thin margins. Check the template before the title goes on the spine.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. More than one page, fonts not embedded, images below 300 DPI
&lt;/h2&gt;

&lt;p&gt;These are the boring checks that produce failed preflight:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the wrap is one page: back cover, spine, front cover, never three separate files;&lt;/li&gt;
&lt;li&gt;fonts embedded, not linked;&lt;/li&gt;
&lt;li&gt;images at 300 DPI at final size;&lt;/li&gt;
&lt;li&gt;one flattened layer, no live transparency.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The barcode area on the back cover has to stay clear. The template marks where it goes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before you upload, check these six
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Final page count, after the last proof.&lt;/li&gt;
&lt;li&gt;The paper type you will actually print on.&lt;/li&gt;
&lt;li&gt;Full wrap = panels + spine + 0.25 in.&lt;/li&gt;
&lt;li&gt;Bleed 0.125 in on the top, bottom and outside edges.&lt;/li&gt;
&lt;li&gt;Spine text only over 79 pages, 0.0625 in clear on each side.&lt;/li&gt;
&lt;li&gt;Ebook cover exported at 1,600 x 2,560 px, separate from the print wrap.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;KDP publishes the current figures and changes them. The calculator on our site runs the same arithmetic with your page count and exports a layout guide at 1 unit = 1 inch, but the printer's template is the final word. When a file fails at upload, the error text is usually accurate about which of these six it is.&lt;/p&gt;

</description>
      <category>books</category>
      <category>design</category>
      <category>selfpublishing</category>
      <category>print</category>
    </item>
    <item>
      <title>Turning the Page: Replacing single-spa with CollageJS</title>
      <dc:creator>José Pablo Ramírez Vargas</dc:creator>
      <pubDate>Fri, 13 Nov 2026 01:25:48 +0000</pubDate>
      <link>https://dev.to/webjose/turning-the-page-replacing-single-spa-with-collagejs-335h</link>
      <guid>https://dev.to/webjose/turning-the-page-replacing-single-spa-with-collagejs-335h</guid>
      <description>&lt;p&gt;It's been a while since I don't write about the micro-frontends topic, mostly due to other responsibilities and then other interests.&lt;/p&gt;

&lt;p&gt;However, today I'm back with this very interesting topic: Micro-frontends.&lt;/p&gt;

&lt;p&gt;As many people know already, I created only short 4 years ago the only plug-in in existence for Vite and the excellent &lt;code&gt;single-spa&lt;/code&gt; routing library. The plug-in, named &lt;code&gt;vite-plugin-single-spa&lt;/code&gt; was highly successful, serving up to 25,000+ downloads from &lt;a href="http://npmjs.com/package/vite-plugin-single-spa" rel="noopener noreferrer"&gt;npmjs.com&lt;/a&gt; weekly with less than 10 bugs reported.&lt;/p&gt;

&lt;p&gt;This plug-in enabled many of us to opt-in to move modern projects to &lt;code&gt;single-spa&lt;/code&gt; because we were unwilling to give up Vite. In short: The plug-in was a complete success.&lt;/p&gt;

&lt;p&gt;Nothing is forever, however. For people that don't know, &lt;code&gt;single-spa&lt;/code&gt; creator passed away in November 2025, and no core team was left behind to perpetuate and evolve the &lt;code&gt;single-spa&lt;/code&gt; environment.&lt;/p&gt;

&lt;p&gt;We, &lt;code&gt;single-spa&lt;/code&gt; users were now left adrift with a library that most likely won't be maintained ever again. It is time to switch to a different technology.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deciding the Switch
&lt;/h2&gt;

&lt;p&gt;For me, the decision was simple because my selection of &lt;code&gt;single-spa&lt;/code&gt; back in 2022 was also simple: Simple router with minimal restrictions, easy micro-frontend interface (the lifecycle functions) and everything else was opt-in, such as the use of &lt;code&gt;import-map-overrides&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Now we face extinction, and moving to the previously-studied-and-discarded micro-frontends solution was not an option: All their problems and dislikes back then are still there; the alternatives haven't changed. I still didn't want the complex Piral requiring a CDN and online accounts, or Module Federation being only a micro-frontend solution if we are talking same-framework projects (my projects aren't same-framework projects), etc.&lt;/p&gt;

&lt;p&gt;In light of this, I decided to contribute an evolution of &lt;code&gt;single-spa&lt;/code&gt;: &lt;a href="https://collagejs.dev" rel="noopener noreferrer"&gt;CollageJS&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  CollageJS: Inception
&lt;/h2&gt;

&lt;p&gt;After working with the amazing &lt;a href="https://svelte.dev" rel="noopener noreferrer"&gt;Svelte&lt;/a&gt; framework, single-spa and my Vite plug-in, I've come to realize I had a lot of margin for improvement. This improvement is &lt;strong&gt;CollageJS&lt;/strong&gt;' main objective.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CollageJS&lt;/strong&gt; has taken the core concept of the single-spa &lt;code&gt;parcel&lt;/code&gt; and has evolved it to make it the only thing needed. In &lt;strong&gt;CollageJS&lt;/strong&gt;, this concept is named &lt;a href="https://collagejs.dev/docs/introduction" rel="noopener noreferrer"&gt;Core Piece&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Core pieces are the micro-frontends and they can be as big or as small as we want them to be. These pieces are defined in TypeScript as an interface:&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;CorePiece&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;TProps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;TMeta&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;mount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;MountFn&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;TProps&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;MountFn&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;TProps&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&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;span class="n"&gt;relocate&lt;/span&gt;&lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="nc"&gt;RelocateFn&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;RelocateFn&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;span class="n"&gt;update&lt;/span&gt;&lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="nc"&gt;UpdateFn&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;TProps&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;UpdateFn&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;TProps&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&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;span class="n"&gt;meta&lt;/span&gt;&lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="nc"&gt;TMeta&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;remountable&lt;/span&gt;&lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="kt"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;relocatable&lt;/span&gt;&lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="kt"&gt;boolean&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;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The only required method is &lt;code&gt;mount&lt;/code&gt;. This keeps the creation of pieces (micro-frontends) super easy.&lt;/p&gt;

&lt;h3&gt;
  
  
  Improvements Over single-spa
&lt;/h3&gt;

&lt;p&gt;Because &lt;code&gt;single-spa&lt;/code&gt; had struggled over the years to keep up, it is no surprise to know that there were many small and not-so-small bugs and issues at its core. I have made sure &lt;strong&gt;CollageJS&lt;/strong&gt; overcomes them. Just to name a few improvements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Full TypeScript support&lt;/li&gt;
&lt;li&gt;Multiple copies of pieces can coexist at the same time&lt;/li&gt;
&lt;li&gt;All framework adapters support update of properties and a framework-specific component to mount core pieces&lt;/li&gt;
&lt;li&gt;No name collisions between adapter component properties and the properties of the micro-fronted being mounted (the core piece)&lt;/li&gt;
&lt;li&gt;Idempotent &lt;code&gt;unmount&lt;/code&gt; lifecycle&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you would like to see a more complete list of improvements, head to the &lt;a href="https://collagejs.dev/guides/migration-from-single-spa" rel="noopener noreferrer"&gt;migration guide&lt;/a&gt;, at the bottom.&lt;/p&gt;

&lt;h2&gt;
  
  
  Improved Developer Experience
&lt;/h2&gt;

&lt;p&gt;Have you ever worked with import maps? If you have, you understand that their use is quite convenient but makes us developers repeat module names around, especially because most of the time, we need to externalize the modules whenever we bundle our projects.&lt;/p&gt;

&lt;p&gt;With the new &lt;a href="https://collagejs.dev/docs/vite-overview" rel="noopener noreferrer"&gt;set of Vite plug-ins&lt;/a&gt; for &lt;strong&gt;CollageJS&lt;/strong&gt;, our developer experience is improved. In root projects (the project that loads other projects and provides the import map):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Import maps are auto-externalized&lt;/li&gt;
&lt;li&gt;We get the import map overriding tool set up&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In core piece projects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Import maps are auto-externalized while in dev mode automatically&lt;/li&gt;
&lt;li&gt;CSS is mounted for us&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We also get a brand new import map-overriding solution with a modern user interface and more advanced features:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdyi9txaplvvu4shyxlnz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdyi9txaplvvu4shyxlnz.png" alt="@collagejs/imo user interface" width="800" height="429"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Migrating Projects
&lt;/h2&gt;

&lt;p&gt;Migrating projects from &lt;code&gt;single-spa&lt;/code&gt; to &lt;strong&gt;CollageJS&lt;/strong&gt; is incredibly fast. Generally speaking, most projects take only about 10 minutes of work. This means that migrating an entire system, even if comprised of dozens of projects, can migrate in one day with barely any visible difference.&lt;/p&gt;

&lt;p&gt;If you're in the &lt;code&gt;single-spa&lt;/code&gt; boat and want out, just follow &lt;a href="https://collagejs.dev/guides/migration-from-single-spa" rel="noopener noreferrer"&gt;the migration guide&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test Drive CollageJS
&lt;/h2&gt;

&lt;p&gt;If you're feeling curious about how this works, it can be tested in minutes by cloning the template repository, either with &lt;a href="https://github.com/new?template_name=root-template&amp;amp;template_owner=collagejs" rel="noopener noreferrer"&gt;GitHub's template funcionality&lt;/a&gt;, or with the degit command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx degit https://github.com/collagejs/root-template.git
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Feel free to experience micro-frontends with full Vite v8 and TypeScript support. Happy coding!&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>typescript</category>
      <category>beginners</category>
      <category>opensource</category>
    </item>
    <item>
      <title>The follow-up I lost in a Slack thread (and the one-page tracker that would have caught it)</title>
      <dc:creator>lululapeng</dc:creator>
      <pubDate>Fri, 13 Nov 2026 01:25:36 +0000</pubDate>
      <link>https://dev.to/lululapeng/the-follow-up-i-lost-in-a-slack-thread-and-the-one-page-tracker-that-would-have-caught-it-4247</link>
      <guid>https://dev.to/lululapeng/the-follow-up-i-lost-in-a-slack-thread-and-the-one-page-tracker-that-would-have-caught-it-4247</guid>
      <description>&lt;p&gt;I once let a warm “let’s start next month” message die in a Slack thread I forgot to star.&lt;/p&gt;

&lt;p&gt;The client was real. The budget was real. I had quoted, they had said yes-in-spirit, and then a product launch ate both of our weeks. When I finally remembered to nudge them, the thread was buried under 200 unread messages and they had already hired someone who followed up on a Tuesday.&lt;/p&gt;

&lt;p&gt;The fix was not “check Slack more.” It was a &lt;strong&gt;single place&lt;/strong&gt; that answered three questions every Friday in under ten minutes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Who is open?&lt;/strong&gt; — lead, discovery, proposal sent, waiting, active, won, lost
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When did I last touch them?&lt;/strong&gt; — a date, not a vibe
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What is the next visible action?&lt;/strong&gt; — one verb: send proposal, nudge, send invoice, schedule kickoff, mark lost&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I keep it boring on purpose: a Clients list, a Pipeline board (Lead → Discovery → Proposal → Won / Active / Lost), and an Invoices table (Draft → Sent → Due → Overdue → Paid). Notion works. A Markdown table works. A spreadsheet works. The tool matters less than looking at it once a week before you open chat.&lt;/p&gt;

&lt;p&gt;You can steal the shape today: make three columns (name / last touch / next action), dump every half-alive lead into it, and set one 15-minute Friday review. No kit required.&lt;/p&gt;

&lt;p&gt;Want a free taste of the paperwork stack first? Five one-pagers (rate card, kickoff questions, change-order, three client emails, tax habits) sit in a pay-what-you-want sampler:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://lulupearl0.gumroad.com/l/freelance-starter-sampler" rel="noopener noreferrer"&gt;https://lulupearl0.gumroad.com/l/freelance-starter-sampler&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Need a clean deposit/final invoice PDF with no signup?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://eclectic-cactus-07703e.netlify.app/" rel="noopener noreferrer"&gt;https://eclectic-cactus-07703e.netlify.app/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you want the Notion-ready Markdown tables + a CSV import starter + a weekly dashboard checklist in one ZIP, I bundled it as a $19 Freelance Client Tracker (no monthly SaaS):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://lulupearl0.gumroad.com/l/freelance-client-tracker" rel="noopener noreferrer"&gt;https://lulupearl0.gumroad.com/l/freelance-client-tracker&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Building in public: store has views, &lt;strong&gt;revenue is still $0&lt;/strong&gt;. Soft promo stays honest. If you use a simpler tracker ritual that actually sticks, I would love to hear it.&lt;/p&gt;

</description>
      <category>beginners</category>
      <category>career</category>
      <category>productivity</category>
      <category>freelancing</category>
    </item>
  </channel>
</rss>
