<?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: Muhamad Revanza Rafif Hamdani</title>
    <description>The latest articles on DEV Community by Muhamad Revanza Rafif Hamdani (@revanzart).</description>
    <link>https://dev.to/revanzart</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4151595%2F31b958fc-6e04-437c-bc2b-8d265f1dd23b.jpg</url>
      <title>DEV Community: Muhamad Revanza Rafif Hamdani</title>
      <link>https://dev.to/revanzart</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/revanzart"/>
    <language>en</language>
    <item>
      <title>One Pillow Script, Two Sizes: Cover Decisions Behind 39 Packaged Products</title>
      <dc:creator>Muhamad Revanza Rafif Hamdani</dc:creator>
      <pubDate>Sun, 25 Oct 2026 12:42:18 +0000</pubDate>
      <link>https://dev.to/revanzart/one-pillow-script-two-sizes-cover-decisions-behind-39-packaged-products-36me</link>
      <guid>https://dev.to/revanzart/one-pillow-script-two-sizes-cover-decisions-behind-39-packaged-products-36me</guid>
      <description>&lt;p&gt;I still have the first cover I rendered with Pillow. It was a 1280×720 JPEG of a product that looked fine in my image viewer and like a blurry mess on Gumroad. The problem was not the art. I had rendered the text layer at screen resolution and let Pillow downsample the whole composition, so the title antialiased into mush. That one bad cover sat on product number one for two days while I rewrote the script that now packages all 39.&lt;/p&gt;

&lt;p&gt;Here is what I learned about covers, sizes, and the decisions behind them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two sizes, one script, and why both exist
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;pack_product.py&lt;/code&gt; renders exactly two images per product: a cover at 1280×720 and a thumbnail at 600×600. That is not arbitrary. The 1280×720 cover is the wide format that marketplaces and product pages expect. The 600×600 square is what gets cropped to a circle on social cards and looks right in grid views where a wide banner becomes a thin sliver.&lt;/p&gt;

&lt;p&gt;The decision that fixed my blurry mess: render text at final resolution. Never compose at a smaller size and resize up, and never render sharp text and then let the cover get scaled down by the platform. Pillow draws text at whatever point size you give it on whatever canvas you give it, so I set the canvas to 1280×720 from the start and pick font sizes that hold up at 600×600 when the thumbnail version resamples the same composition.&lt;/p&gt;

&lt;h2&gt;
  
  
  What goes on the cover
&lt;/h2&gt;

&lt;p&gt;Every cover gets three things: the product title, a short subtitle, and the count of files inside. That last one comes straight from the command line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python pack_product.py my-product &lt;span class="s2"&gt;"My Product"&lt;/span&gt; &lt;span class="s2"&gt;"A short subtitle"&lt;/span&gt; 9
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;9&lt;/code&gt; is the file count, and putting it on the cover was one of the better decisions I made. Buyers on asset marketplaces want to know what they are getting before they click. A cover that says "9 files" answers the first question without anyone opening the listing. It also forced me to actually know the count for every product, which caught two packs where I had miscounted during zipping.&lt;/p&gt;

&lt;p&gt;The script bundles the zip and renders both images into a &lt;code&gt;_system&lt;/code&gt; folder next to where you run it. I made that configurable with &lt;code&gt;--dir&lt;/code&gt; or a &lt;code&gt;PACK_ROOT&lt;/code&gt; environment variable after I filled up a working directory with 39 covers and could not find anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  The constraint that shaped everything: 1 vCPU and 2 GB of RAM
&lt;/h2&gt;

&lt;p&gt;None of this ran on a beefy machine. The whole pipeline, covers included, ran on a single-core box with 2 GB of RAM and no GPU. That changes how you write a Pillow script.&lt;/p&gt;

&lt;p&gt;The rules I ended up with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One image job at a time. No parallel rendering on a 1-core box, it just thrashes.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;gc.collect()&lt;/code&gt; after each asset, because Pillow image objects hold buffers longer than you expect.&lt;/li&gt;
&lt;li&gt;Measure peak RSS with &lt;code&gt;resource.getrusage(RUSAGE_SELF).ru_maxrss&lt;/code&gt;, not average memory. Average tells you nothing about whether the process dies at asset 30 of 39.&lt;/li&gt;
&lt;li&gt;Render at the resolution you need. A 600×600 thumbnail does not need a 4000 px source in memory.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The cover renderer follows all of these. It loads the background, draws the text layers, saves both sizes, and releases everything before the next product starts. That is why 39 products packaged on a machine that would choke on a parallel batch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the covers fit in the wider pipeline
&lt;/h2&gt;

&lt;p&gt;Covers are step four. Before any product gets packaged, the assets already went through &lt;code&gt;make_metadata.py&lt;/code&gt;, which writes the exact upload CSVs each marketplace demands: Adobe's 5-column header, Vecteezy's 4-column, Dreamstime's 15-column. All three reject a wrong header instantly, so the script writes the header they demand and nothing else. And &lt;code&gt;upload_tracker.py&lt;/code&gt; keeps a ledger of what has been posted, because running a batch twice is the fastest way to get an account flagged.&lt;/p&gt;

&lt;p&gt;The point is that the cover is not a design decision in isolation. It is the last step of a chain, and if any earlier step is wrong, the prettiest cover in the world sits on top of a rejected upload. That is why the kit is four small scripts and not one monolith. Two of them, &lt;code&gt;make_metadata.py&lt;/code&gt; and &lt;code&gt;upload_tracker.py&lt;/code&gt;, use only the Python standard library. The other two need exactly two third-party packages: Pillow for covers, reportlab for PDFs.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would tell someone starting today
&lt;/h2&gt;

&lt;p&gt;Do not hand-make 39 covers in a design tool. Do not make 39 different cover designs either. One script, one layout, two sizes, variable text. Consistency across a catalogue reads as professionalism even when the layout is simple, and it means packaging product number 40 takes the same thirty seconds as product number 1.&lt;/p&gt;

&lt;p&gt;The total output of this pipeline was 39 products, 856 MB, backed by about 19,000 lines of pipeline code and 300+ pages of research. The four scripts are the packaging layer I kept reaching for, so I cleaned them up and documented them as the pipeline starter kit. If you want the same scripts, including the cover renderer with the two sizes and the RAM habits baked in, they are here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://revanzart-shop.revanzart.workers.dev/go/pipeline-starter-kit?src=devto" rel="noopener noreferrer"&gt;Get pipeline starter kit&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Building digital products? &lt;a href="https://revanzart-shop.revanzart.workers.dev/guides" rel="noopener noreferrer"&gt;Free guides &amp;amp; kits for digital product sellers&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>sideprojects</category>
      <category>ai</category>
      <category>automation</category>
      <category>marketing</category>
    </item>
    <item>
      <title>Two Dependencies Total: Pillow And Reportlab Are All Your Product Needs</title>
      <dc:creator>Muhamad Revanza Rafif Hamdani</dc:creator>
      <pubDate>Sun, 25 Oct 2026 12:42:07 +0000</pubDate>
      <link>https://dev.to/revanzart/two-dependencies-total-pillow-and-reportlab-are-all-your-product-needs-4n50</link>
      <guid>https://dev.to/revanzart/two-dependencies-total-pillow-and-reportlab-are-all-your-product-needs-4n50</guid>
      <description>&lt;p&gt;Last February I lost a whole evening to a CSV header. I had 40 finished JPEGs ready for Adobe Stock, generated the metadata file, uploaded it, and got an instant rejection. Not a warning, not a partial accept. Rejected. The problem was one column: Adobe's upload CSV wants exactly five columns in a specific order, and I had six. A single extra field, probably a trailing keyword I thought was helpful, and the whole batch was dead on arrival.&lt;/p&gt;

&lt;p&gt;That night I rewrote the metadata step as a script that writes the header the platform demands and nothing else. It became the first of four tools that now package every product I sell. This post is about why those tools need exactly two third-party dependencies, and why I think that number matters more than people realize.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four things that actually stall you
&lt;/h2&gt;

&lt;p&gt;Making the art is the fun part. What stalls solo makers is everything after it. In my case it was four specific things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Metadata. Adobe wants a 5-column CSV, Vecteezy wants 4 columns, Dreamstime wants 15. Get the header wrong and the upload is rejected before a human ever sees your work.&lt;/li&gt;
&lt;li&gt;Double-posting. Run your batch script twice, as I did once, and you can post the same asset twice. That is one of the fastest ways to get an account flagged. I now keep a ledger script that remembers what is done, so a re-run is impossible.&lt;/li&gt;
&lt;li&gt;Turning a document into a product. You have the content, but a raw Markdown file does not look like something anyone would pay for.&lt;/li&gt;
&lt;li&gt;Packaging. A zip with a real cover image sells. A bare file does not.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;My solution for all four is four standalone Python scripts. No framework, no cloud account, no database server.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why only two dependencies
&lt;/h2&gt;

&lt;p&gt;Here is the full list of third-party packages across all four scripts: Pillow and reportlab. That is it.&lt;/p&gt;

&lt;p&gt;Pillow renders the product covers (1280 by 720) and thumbnails (600 by 600). reportlab turns Markdown into a clean styled PDF with headings, tables, lists, and links. The other two scripts, the metadata builder and the upload tracker, use the Python standard library alone. If you only need those two, you install nothing at all.&lt;/p&gt;

&lt;p&gt;I did not set out with a minimalism goal. It fell out of running this on a small server. The whole pipeline runs on 1 vCPU and 2 GB RAM with no GPU. Every dependency you add is memory you do not have, a version conflict you will eventually debug at midnight, and a supply chain surface you have to think about. Pillow and reportlab are both mature, both do exactly one job well, and both fit comfortably in 2 GB when you use them one file at a time.&lt;/p&gt;

&lt;p&gt;The practical rule I landed on: if the standard library can do it with under about fifty lines of code, write the fifty lines. CSV writing, file tracking, zip creation, all of that is built in. Only reach for a package when the job genuinely needs it, like image rendering or PDF layout.&lt;/p&gt;

&lt;h2&gt;
  
  
  The small server habits that make it work
&lt;/h2&gt;

&lt;p&gt;Two dependencies do not help if your process eats the box. These habits are what let the pipeline run on that 1-core machine:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One process at a time. Never fan out image jobs in parallel on a single core. It does not go faster, it just swaps.&lt;/li&gt;
&lt;li&gt;Call &lt;code&gt;gc.collect()&lt;/code&gt; after each asset. Freeing memory between items keeps peak usage flat instead of climbing.&lt;/li&gt;
&lt;li&gt;Measure peak RSS, not average. &lt;code&gt;resource.getrusage(RUSAGE_SELF).ru_maxrss&lt;/code&gt; gives you the real ceiling. Average memory lies.&lt;/li&gt;
&lt;li&gt;Run heavy render steps as one file per subprocess, so memory is actually returned to the OS when the file finishes.&lt;/li&gt;
&lt;li&gt;Render at the resolution you need. Inspecting a preview at 60 dpi instead of 300 is the difference between fitting in RAM and not.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For scale reference: these scripts packaged 39 finished products, 856 MB of output, as part of a parent project with around 19,000 lines of pipeline code. The rules behind them came from 300+ pages of research into what the stock platforms actually accept and reject.&lt;/p&gt;

&lt;h2&gt;
  
  
  The folder structure that keeps a catalogue sane
&lt;/h2&gt;

&lt;p&gt;One structural decision saved me more time than any script: a fixed layout for every project.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;project/
  assets/            raw output, one file per asset
  output/            ready-to-submit files plus metadata.csv
  packs/             finished product zips plus covers
  research/          notes and sources
  scripts/           the tools
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The one I got wrong early: keep &lt;code&gt;research/&lt;/code&gt; forever. I deleted scrape logs and notes once to clean up, then needed them two months later for a follow-up product and had to redo the work. Your research folder is raw material for future products. It is not clutter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the tooling lives now
&lt;/h2&gt;

&lt;p&gt;Those four scripts, &lt;code&gt;make_metadata.py&lt;/code&gt;, &lt;code&gt;upload_tracker.py&lt;/code&gt;, &lt;code&gt;md2pdf.py&lt;/code&gt;, and &lt;code&gt;pack_product.py&lt;/code&gt;, are the packaging layer of everything I sell. They are collected, documented, and shipped as part of the ai sticker factory playbook, which is the honest writeup of this whole system: the prompt side, the platform rules, the RAM constraints, and the packaging scripts, all validated on that same 1 vCPU, 2 GB box. If you want the same pipeline without rediscovering the CSV header rules the hard way, that is where it is.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://revanzart-shop.revanzart.workers.dev/go/ai-sticker-factory-playbook?src=devto" rel="noopener noreferrer"&gt;Get ai sticker factory playbook&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Building digital products? &lt;a href="https://revanzart-shop.revanzart.workers.dev/guides" rel="noopener noreferrer"&gt;Free guides &amp;amp; kits for digital product sellers&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>sideprojects</category>
      <category>ai</category>
      <category>automation</category>
      <category>marketing</category>
    </item>
    <item>
      <title>The 1280 720 Lesson: Why a Zip Isn't Shippable Until It Has a Cover"**</title>
      <dc:creator>Muhamad Revanza Rafif Hamdani</dc:creator>
      <pubDate>Sat, 24 Oct 2026 23:12:43 +0000</pubDate>
      <link>https://dev.to/revanzart/the-1280-720-lesson-why-a-zip-isnt-shippable-until-it-has-a-cover-4noc</link>
      <guid>https://dev.to/revanzart/the-1280-720-lesson-why-a-zip-isnt-shippable-until-it-has-a-cover-4noc</guid>
      <description>&lt;h1&gt;
  
  
  The 1280×720 Lesson: Why a Zip Isn't Shippable Until It Has a Cover
&lt;/h1&gt;

&lt;p&gt;I had 39 products zipped and ready. Then I opened one of the zips on a marketplace preview page and watched it render as a grey file icon next to thumbnails that actually looked like products. Nothing was broken in the files themselves. The packaging was broken. A zip with no cover is not a product. It is a download link with extra steps.&lt;/p&gt;

&lt;p&gt;That moment changed how I build. Here is what I learned, and the rules that came out of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cover is the product, on screen
&lt;/h2&gt;

&lt;p&gt;Buyers never see your files. They see the thumbnail first, then the cover, and only sometimes the file list. My pipeline had scripts for metadata, tracking, and PDF generation, but packaging was an afterthought. That was backwards.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;pack_product.py&lt;/code&gt; now does the whole job in one command: it zips the files, renders a &lt;strong&gt;1280×720&lt;/strong&gt; cover, and a &lt;strong&gt;600×600&lt;/strong&gt; thumbnail. The cover gets the product title and a short subtitle. The thumbnail is the thing that survives being squeezed into a grid of 20 other listings.&lt;/p&gt;

&lt;p&gt;You can run it like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python pack_product.py my-product &lt;span class="s2"&gt;"My Product"&lt;/span&gt; &lt;span class="s2"&gt;"A short subtitle"&lt;/span&gt; 9
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It writes the zip, the cover, and the thumb into a &lt;code&gt;_system&lt;/code&gt; folder next to where you run it. Output location is configurable with &lt;code&gt;--dir&lt;/code&gt; or the &lt;code&gt;PACK_ROOT&lt;/code&gt; environment variable, so you do not edit the script per project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bad metadata dies before your file does
&lt;/h2&gt;

&lt;p&gt;The second thing I learned is that platforms reject you before they ever look at the asset. A wrong CSV header is an instant bounce. Adobe wants a 5 column file. Vecteezy wants 4 columns. Dreamstime wants 15. One script, &lt;code&gt;make_metadata.py&lt;/code&gt;, writes the exact header each channel demands and nothing else.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python make_metadata.py ./my-assets &lt;span class="nt"&gt;--platform&lt;/span&gt; adobe &lt;span class="nt"&gt;--out&lt;/span&gt; metadata.csv &lt;span class="nt"&gt;--ext&lt;/span&gt; .jpg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is a real temptation to hand edit these CSVs. Do not. Build them from the folder. If you pass &lt;code&gt;--titles titles.json&lt;/code&gt; you get real titles and keywords. Without it, the filename is title cased and used as a first pass title, which is fine for checking the pipeline runs, but replace it before you submit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running the batch twice is how accounts get flagged
&lt;/h2&gt;

&lt;p&gt;The fastest way to get a seller account flagged is to upload the same 40 files twice because you lost track of what was already posted. &lt;code&gt;upload_tracker.py&lt;/code&gt; is a ledger that makes that impossible:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python upload_tracker.py pending items.txt
python upload_tracker.py &lt;span class="k"&gt;done&lt;/span&gt; &lt;span class="s2"&gt;"asset-001.jpg"&lt;/span&gt;
python upload_tracker.py status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run &lt;code&gt;pending&lt;/code&gt; before every session. Run &lt;code&gt;done&lt;/code&gt; only after the upload confirms. The tracker remembers, so a re run never double posts. This sounds trivial until you are 200 files into a catalogue and genuinely cannot remember which batch went out on Tuesday.&lt;/p&gt;

&lt;h2&gt;
  
  
  The doc to product step
&lt;/h2&gt;

&lt;p&gt;Some of what I sell is documentation: guides, notes, reference sheets. Those need to look like products, not like a text file someone renamed. &lt;code&gt;md2pdf.py&lt;/code&gt; turns a Markdown file into a styled PDF with headings, tables, lists, and links:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python md2pdf.py content.md product.pdf &lt;span class="s2"&gt;"My Product Title"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One command, and the file goes from draft to shippable. It uses reportlab, which is one of only two third party packages in the whole kit. The other is Pillow, for the covers. The metadata and tracker scripts use the standard library alone, so if that is all you need, you skip the install entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  It all ran on one small server
&lt;/h2&gt;

&lt;p&gt;Every number in this pipeline comes from a single &lt;strong&gt;1 vCPU, 2 GB RAM, no GPU&lt;/strong&gt; machine. No fan out, no parallel image jobs. One process at a time. &lt;code&gt;gc.collect()&lt;/code&gt; after each asset so memory goes back. Peak RSS measured with &lt;code&gt;resource.getrusage&lt;/code&gt;, not average, because the average hides the spike that kills you. Heavy render steps run one file per subprocess so the OS reclaims memory. Previews render at 60 dpi, not 300, because I am inspecting them, not printing them.&lt;/p&gt;

&lt;p&gt;The parent project behind this shipped &lt;strong&gt;39 products, 856 MB of output&lt;/strong&gt;, from roughly &lt;strong&gt;19,000 lines&lt;/strong&gt; of pipeline code and over &lt;strong&gt;300 pages&lt;/strong&gt; of research. That volume is why the rules exist. The tracker exists because of a near miss. The cover rule exists because of the grey file icon.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep the research folder
&lt;/h2&gt;

&lt;p&gt;One structural rule worth its own paragraph. This is the folder layout the kit assumes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;project/
  assets/     raw output, one file per asset
  output/     ready-to-submit files + metadata.csv
  packs/      finished zips + covers
  research/   notes and sources
  scripts/    the tools
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep &lt;code&gt;research/&lt;/code&gt; forever. Scrape logs and notes are the raw material for the next product. I deleted mine once to clean up and I will not make that mistake again.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this kit actually is
&lt;/h2&gt;

&lt;p&gt;These four scripts are the packaging layer I wish I had on day one. They are not a tutorial that ends at "here is how you would". They are the working tools, running the same rules that got 39 products out the door.&lt;/p&gt;

&lt;p&gt;If you want the full version of this, the reasoning behind each rule, the platform headers, the RAM habits, and the build flow from prompt to finished product, that is what the &lt;strong&gt;ai sticker factory playbook&lt;/strong&gt; collects. The scripts are the how. The playbook is the why, plus the mistakes that produced the rules.&lt;/p&gt;

&lt;p&gt;Start with one product. Give it a 1280×720 cover and a 600×600 thumbnail. Then zip it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://revanzart-shop.revanzart.workers.dev/go/ai-sticker-factory-playbook?src=devto" rel="noopener noreferrer"&gt;Get ai sticker factory playbook&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Building digital products? &lt;a href="https://revanzart-shop.revanzart.workers.dev/guides" rel="noopener noreferrer"&gt;Free guides &amp;amp; kits for digital product sellers&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>sideprojects</category>
      <category>ai</category>
      <category>automation</category>
      <category>marketing</category>
    </item>
    <item>
      <title>Two Lines in Requirements.txt: Pillow, Reportlab, and Nothing Else</title>
      <dc:creator>Muhamad Revanza Rafif Hamdani</dc:creator>
      <pubDate>Sat, 24 Oct 2026 23:12:26 +0000</pubDate>
      <link>https://dev.to/revanzart/two-lines-in-requirementstxt-pillow-reportlab-and-nothing-else-e94</link>
      <guid>https://dev.to/revanzart/two-lines-in-requirementstxt-pillow-reportlab-and-nothing-else-e94</guid>
      <description>&lt;h1&gt;
  
  
  Two Lines in Requirements.txt: Pillow, Reportlab, and Nothing Else
&lt;/h1&gt;

&lt;p&gt;The rejection email was four words long: "Invalid CSV header."&lt;/p&gt;

&lt;p&gt;I had just spent three hours naming 60 vector files and writing titles. All of it was sitting in a spreadsheet I built by hand, and Vecteezy's uploader refused the whole batch because one column was in the wrong place. No partial credit. No "fix it and retry." Just reject.&lt;/p&gt;

&lt;p&gt;That was the moment I stopped treating packaging as something I would do "later, carefully." It turns out "carefully" is not a schedule. So I wrote the scripts. They shipped 39 products out the door, and the entire dependency list is this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pillow
reportlab
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two lines. The rest is the Python standard library. Here is what actually works and why.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrong headers get rejected instantly, so generate them
&lt;/h2&gt;

&lt;p&gt;Adobe, Vecteezy and Dreamstime each want a different CSV. Adobe takes 5 columns, Vecteezy takes 4, and Dreamstime takes 15. Get one header text wrong and the whole upload dies before a human ever looks at your art.&lt;/p&gt;

&lt;p&gt;So &lt;code&gt;make_metadata.py&lt;/code&gt; does one job: it walks a folder of files and writes the exact header each platform demands, nothing more. Three runs, three files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python make_metadata.py ./my-assets &lt;span class="nt"&gt;--platform&lt;/span&gt; adobe      &lt;span class="nt"&gt;--out&lt;/span&gt; metadata.csv   &lt;span class="nt"&gt;--ext&lt;/span&gt; .jpg
python make_metadata.py ./my-assets &lt;span class="nt"&gt;--platform&lt;/span&gt; vecteezy   &lt;span class="nt"&gt;--out&lt;/span&gt; vecteezy.csv   &lt;span class="nt"&gt;--ext&lt;/span&gt; .jpg
python make_metadata.py ./my-assets &lt;span class="nt"&gt;--platform&lt;/span&gt; dreamstime &lt;span class="nt"&gt;--out&lt;/span&gt; dreamstime.csv &lt;span class="nt"&gt;--ext&lt;/span&gt; .jpg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without extra input, the filename is title-cased into the title. That is fine for a first pass, but replace it before you submit. The script accepts &lt;code&gt;--titles titles.json&lt;/code&gt; when you have real titles and keywords ready.&lt;/p&gt;

&lt;p&gt;This script needs no install. Standard library only. If metadata is the thing you keep putting off, you can fix it today with zero packages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Double posting is how accounts get flagged
&lt;/h2&gt;

&lt;p&gt;The second failure mode is sneakier. You run a batch halfway, something interrupts you, you run it again, and suddenly 40 of your 60 assets are live twice. Running a batch twice is the fastest way to get an account flagged.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;upload_tracker.py&lt;/code&gt; is a plain ledger. It remembers what you have marked done, so a re-run only shows what is left:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python upload_tracker.py pending items.txt   &lt;span class="c"&gt;# what is NOT done yet&lt;/span&gt;
python upload_tracker.py &lt;span class="k"&gt;done&lt;/span&gt; &lt;span class="s2"&gt;"asset-001.jpg"&lt;/span&gt;
python upload_tracker.py status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No database, no cloud account, no sync service. A file plus a script. It has never failed me because there is nothing in it to fail.&lt;/p&gt;

&lt;h2&gt;
  
  
  A product needs a cover, and a PDF needs a renderer
&lt;/h2&gt;

&lt;p&gt;A zip with a real cover sells. A bare file does not. That is the only reason Pillow is in the requirements. &lt;code&gt;pack_product.py&lt;/code&gt; zips the files and renders a 1280x720 cover plus a 600x600 thumbnail:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python pack_product.py my-product &lt;span class="s2"&gt;"My Product"&lt;/span&gt; &lt;span class="s2"&gt;"A short subtitle"&lt;/span&gt; 9
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The other package, reportlab, exists because a PDF with real headings, tables, lists and links makes a Markdown doc look like a product instead of a draft:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python md2pdf.py content.md product.pdf &lt;span class="s2"&gt;"My Product Title"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those are the only two third-party packages. Everything else, the CSV generation, the tracking, the zip, runs on Python alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  The whole pipeline ran on 1 vCPU and 2 GB of RAM
&lt;/h2&gt;

&lt;p&gt;This is the part people find hard to believe. The pipeline that produced 39 products and 856 MB of output ran on 1 vCPU, 2 GB of RAM and no GPU. It held together because of a few unglamorous habits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One process at a time. Never fan out image jobs in parallel on one core.&lt;/li&gt;
&lt;li&gt;Call &lt;code&gt;gc.collect()&lt;/code&gt; after each asset to free memory between items.&lt;/li&gt;
&lt;li&gt;Measure peak RSS, not average. &lt;code&gt;resource.getrusage(RUSAGE_SELF).ru_maxrss&lt;/code&gt; tells you the real ceiling.&lt;/li&gt;
&lt;li&gt;Run heavy render steps as one file per subprocess, so memory goes back to the OS.&lt;/li&gt;
&lt;li&gt;Render at the resolution you need. A 60 dpi preview beats a 300 dpi one when you are just checking.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The parent project has roughly 19,000 lines of pipeline code and 300+ pages of research behind the rules these scripts enforce. The scripts themselves are small. The point is that small is enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  The folder layout that keeps it from turning into a junk drawer
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;project/
  assets/            raw output (one file per asset)
  output/            ready-to-submit files + metadata.csv
  packs/             finished product zips + covers
  research/          your notes and sources
  scripts/           these tools
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep &lt;code&gt;research/&lt;/code&gt; forever. Scrape logs and notes are the raw material for the next product. Deleting them to "clean up" is a mistake I made once and will not repeat.&lt;/p&gt;

&lt;p&gt;These four scripts sit at the packaging layer. Above them, the full pipeline runs on a free stack: Hermes Agent schedules jobs, retries failures and makes decisions, with 9Router handling local LLM access. The packaging layer is where the rules from 300+ pages of research get enforced, and it is the part you can copy today.&lt;/p&gt;

&lt;p&gt;None of this is clever. The requirements file is two lines. The scripts are boring on purpose. That is exactly why they shipped 39 products instead of sitting in a backlog with the metadata CSVs I never got around to fixing.&lt;/p&gt;

&lt;p&gt;If you are building vector packs, stickers or templates and the packaging keeps stalling you, this is the tool built from that work, the same scripts and the playbook behind them.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://revanzart-shop.revanzart.workers.dev/go/vector-prompt-pack?src=devto" rel="noopener noreferrer"&gt;Get vector prompt pack&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Building digital products? &lt;a href="https://revanzart-shop.revanzart.workers.dev/guides" rel="noopener noreferrer"&gt;Free guides &amp;amp; kits for digital product sellers&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>sideprojects</category>
      <category>ai</category>
      <category>automation</category>
      <category>marketing</category>
    </item>
    <item>
      <title>Requirements.txt as Discipline: Stdlib-Only Scripts Need No Install</title>
      <dc:creator>Muhamad Revanza Rafif Hamdani</dc:creator>
      <pubDate>Sat, 24 Oct 2026 17:36:52 +0000</pubDate>
      <link>https://dev.to/revanzart/requirementstxt-as-discipline-stdlib-only-scripts-need-no-install-37cn</link>
      <guid>https://dev.to/revanzart/requirementstxt-as-discipline-stdlib-only-scripts-need-no-install-37cn</guid>
      <description>&lt;h1&gt;
  
  
  Requirements.txt as Discipline: Stdlib-Only Scripts Need No Install
&lt;/h1&gt;

&lt;p&gt;The second time I ran my upload batch, I got the email I had been dreading. A channel had flagged my account for double-posting. Nothing had gone wrong with the art. Nothing had crashed. My pipeline had simply done its job twice, and now I was sitting there writing an apology to a stock platform over a batch of files I had already posted. That day I changed three things: I split my pipeline into small standalone scripts, I made two of them depend on nothing but the Python standard library, and I wrote a tracker so a re-run could never double-post again. After that, I packaged 39 finished products, 856 MB of output, with a toolset that runs on a single-core, 2 GB server.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem with a fat environment
&lt;/h2&gt;

&lt;p&gt;Every side project starts with a frameworks wish list. Then reality shows up: a two year old script that only works because of a pinned dependency from a version you can't remember, a server with 2 GB of RAM where even installing packages takes forever, and an eventual move to a new machine that breaks everything quietly. When your pipeline codes packaging and metadata, what really hurts is brittleness.&lt;/p&gt;

&lt;p&gt;So I built the pipeline as four separate scripts. Two of them, &lt;code&gt;make_metadata.py&lt;/code&gt; and &lt;code&gt;upload_tracker.py&lt;/code&gt;, use only the standard library. &lt;code&gt;make_metadata.py&lt;/code&gt; builds the exact Adobe (5-col), Vecteezy (4-col) and Dreamstime (15-col) upload CSVs from a folder of files. &lt;code&gt;upload_tracker.py&lt;/code&gt; is a ledger that remembers what you have already posted, so a re-run never double-posts. No install. No venv. &lt;code&gt;python make_metadata.py ./my-assets --platform adobe --out metadata.csv --ext .jpg&lt;/code&gt; and the header the platform demands comes out, nothing else. The other two, &lt;code&gt;md2pdf.py&lt;/code&gt; and &lt;code&gt;pack_product.py&lt;/code&gt;, do need packages: Pillow for the covers and reportlab for PDF output. That is the entire dependency list. Two packages, and only two, and they are isolated in the two scripts that actually need to render pixels or pages.&lt;/p&gt;

&lt;p&gt;That split is the whole point. A CSV generator and a ledger are pure text and logic. They have no business pulling in an image library, and when they do not, you can run them anywhere Python exists. Python 3.9+, a venv, &lt;code&gt;pip install -r requirements.txt&lt;/code&gt;. If you only use the two stdlib scripts, you skip the install entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Platform rules are the spec
&lt;/h2&gt;

&lt;p&gt;The thing that makes metadata generation a good fit for a dependency free script is that the platforms publish their rules. All three channels reject a wrong CSV header instantly, which means the header is not a suggestion. &lt;code&gt;make_metadata.py&lt;/code&gt; writes the header they demand. Not a superset, not a comment line someone thought was helpful, just the header. When I say the 300+ pages of research behind this pipeline matter, this is what I mean: the rules the scripts enforce are the same rules that got my files rejected before I read them properly.&lt;/p&gt;

&lt;p&gt;The tracker follows the same logic. The fastest way to get an account flagged is running your batch twice, so &lt;code&gt;upload_tracker.py pending items.txt&lt;/code&gt; shows what is not done yet, &lt;code&gt;upload_tracker.py done "asset-001.jpg"&lt;/code&gt; records one, and &lt;code&gt;status&lt;/code&gt; tells you where you stand. Nothing clever. A ledger file and some string handling. That is exactly why it has survived every environment change since.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern generalizes
&lt;/h2&gt;

&lt;p&gt;Once I saw how well the split worked, I applied it to the slow parts. On a 1 vCPU, 2 GB box with no GPU, the pipeline only works if you respect those limits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One process at a time. Never fan out image jobs in parallel on a single-core box.&lt;/li&gt;
&lt;li&gt;Call &lt;code&gt;gc.collect()&lt;/code&gt; after each asset so memory actually comes back.&lt;/li&gt;
&lt;li&gt;Measure peak RSS with &lt;code&gt;resource.getrusage(RUSAGE_SELF).ru_maxrss&lt;/code&gt;, not average. The ceiling is what kills you.&lt;/li&gt;
&lt;li&gt;Run heavy render steps one file per subprocess, so memory returns to the OS.&lt;/li&gt;
&lt;li&gt;Inspect previews at 60 dpi instead of rendering at 300.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And keep a fixed folder layout, because a catalogue turns into a junk drawer without one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;project/
  assets/     raw output (one file per asset)
  output/     ready-to-submit files + metadata.csv
  packs/      finished product zips + covers
  research/   your notes and sources
  scripts/    these tools
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep &lt;code&gt;research/&lt;/code&gt; forever. Deleting scrape logs to clean up was the mistake this project made once and won't repeat. That folder is the raw material for future products.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the kit is for
&lt;/h2&gt;

&lt;p&gt;The four scripts are the packaging layer of a prompt engine that turned one input into 39 finished products. The parent project is roughly 19,000 lines of pipeline code. The packaging tools themselves are the part you can pick up today: build the CSVs, track what you have posted, turn a Markdown doc into a product PDF, zip it with a 1280x720 cover and a 600x600 thumbnail. &lt;code&gt;make_metadata.py&lt;/code&gt; and &lt;code&gt;upload_tracker.py&lt;/code&gt; run on the standard library alone. &lt;code&gt;pack_product.py&lt;/code&gt; writes into a &lt;code&gt;_system&lt;/code&gt; folder and takes &lt;code&gt;--dir&lt;/code&gt; or &lt;code&gt;PACK_ROOT&lt;/code&gt; if you want it elsewhere. No framework, no cloud account.&lt;/p&gt;

&lt;p&gt;I did not write this as a tutorial that ends at here is how you would do it. I wrote it because running your batch twice and getting flagged is a pain I would rather you avoid. The scripts did that for me.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://revanzart-shop.revanzart.workers.dev/go/vector-prompt-pack?src=devto" rel="noopener noreferrer"&gt;Get vector prompt pack&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Building digital products? &lt;a href="https://revanzart-shop.revanzart.workers.dev/guides" rel="noopener noreferrer"&gt;Free guides &amp;amp; kits for digital product sellers&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>sideprojects</category>
      <category>ai</category>
      <category>automation</category>
      <category>marketing</category>
    </item>
    <item>
      <title>Two Packages, One 2 GB Server: Requirements.txt as Product Discipline</title>
      <dc:creator>Muhamad Revanza Rafif Hamdani</dc:creator>
      <pubDate>Sat, 24 Oct 2026 17:36:11 +0000</pubDate>
      <link>https://dev.to/revanzart/two-packages-one-2-gb-server-requirementstxt-as-product-discipline-19c1</link>
      <guid>https://dev.to/revanzart/two-packages-one-2-gb-server-requirementstxt-as-product-discipline-19c1</guid>
      <description>&lt;h1&gt;
  
  
  Two Packages, One 2 GB Server: Requirements.txt as Product Discipline
&lt;/h1&gt;

&lt;p&gt;The first batch I ever ran twice returned 39 rejected CSVs in under a minute. Not because my art was bad. Because I had fat-fingered a header and then re-run the upload script, and every channel treated the second pass as a fresh submission. One mistake, a whole evening gone. That day I learned that the most expensive things in a product pipeline are not render times. They are the small, dumb, repeatable errors that a script should have made impossible.&lt;/p&gt;

&lt;p&gt;This post is about that lesson, and about something that surprised me while cleaning up the kit built from that mess: the strongest proof of discipline in a pipeline is often a tiny &lt;code&gt;requirements.txt&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The requirements file is a promise to yourself
&lt;/h2&gt;

&lt;p&gt;The pipeline starter kit has four scripts. &lt;code&gt;make_metadata.py&lt;/code&gt; builds upload CSVs. &lt;code&gt;upload_tracker.py&lt;/code&gt; is the double-post ledger. &lt;code&gt;md2pdf.py&lt;/code&gt; turns Markdown into a PDF. &lt;code&gt;pack_product.py&lt;/code&gt; zips files and renders a cover at 1280x720 plus a thumbnail at 600x600.&lt;/p&gt;

&lt;p&gt;Two of those need third-party packages: &lt;strong&gt;Pillow&lt;/strong&gt; for covers, &lt;strong&gt;reportlab&lt;/strong&gt; for PDFs. The other two need nothing at all, just the standard library.&lt;/p&gt;

&lt;p&gt;So the install is two lines in &lt;code&gt;requirements.txt&lt;/code&gt;. Not fourteen. Not a framework, not an SDK, not a cloud client. When your dependency list is that short, every package you add has to justify itself against the 2 GB machine it has to run on. That is not minimalism for its own sake. It is a budget, and the same instinct that says "no" to a new package is the instinct that says "no" to running your batch twice.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rules a 2 GB box will teach you
&lt;/h2&gt;

&lt;p&gt;This entire pipeline ran on 1 vCPU, 2 GB RAM, no GPU, and it packaged 39 products totaling 856 MB of output. That is not a flex. It is a constraint that forced the code into better habits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One process at a time. Never fan out image jobs in parallel on a single core.&lt;/li&gt;
&lt;li&gt;Free memory between items with &lt;code&gt;gc.collect()&lt;/code&gt; after each asset.&lt;/li&gt;
&lt;li&gt;Measure peak RSS, not average, using &lt;code&gt;resource.getrusage(RUSAGE_SELF).ru_maxrss&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Process one file per subprocess for heavy render steps, so memory goes back to the OS.&lt;/li&gt;
&lt;li&gt;Render at the resolution you need. A 60 dpi preview will tell you what a 300 dpi render at the wrong size will not.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every one of those habits is also a reliability habit. A pipeline that never runs out of memory is a pipeline that finishes the batch, and a pipeline that finishes the batch once is a pipeline you do not re-run by accident.&lt;/p&gt;

&lt;h2&gt;
  
  
  The platform rules that eat whole evenings
&lt;/h2&gt;

&lt;p&gt;The four scripts exist because of four specific failure points, and each one has a hard rule attached.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Metadata.&lt;/strong&gt; Adobe wants a 5-column CSV. Vecteezy wants 4 columns. Dreamstime wants 15. Get the header wrong and the upload is rejected instantly. &lt;code&gt;make_metadata.py&lt;/code&gt; writes the header the channel demands and nothing else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Double posting.&lt;/strong&gt; The fastest way to get an account flagged is submitting the same batch twice. &lt;code&gt;upload_tracker.py&lt;/code&gt; is a ledger. &lt;code&gt;pending items.txt&lt;/code&gt; shows what you have not posted. &lt;code&gt;done "asset-001.jpg"&lt;/code&gt; marks it. A re-run physically cannot double-post because the script checks the ledger first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Docs into products.&lt;/strong&gt; You already have the content. &lt;code&gt;md2pdf.py content.md product.pdf "My Product Title"&lt;/code&gt; makes it look like a product in one command.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Packaging.&lt;/strong&gt; A zip with a real cover sells. A bare file does not. &lt;code&gt;pack_product.py my-product "My Product" "A short subtitle" 9&lt;/code&gt; writes the zip, the cover, and the thumbnail into a &lt;code&gt;_system&lt;/code&gt; folder, or wherever &lt;code&gt;--dir&lt;/code&gt; or &lt;code&gt;PACK_ROOT&lt;/code&gt; points.&lt;/p&gt;

&lt;p&gt;Note how small each rule is. None of them needs a service. None needs a database. They are all just scripts that refuse to guess.&lt;/p&gt;

&lt;h2&gt;
  
  
  The folder layout is part of the requirements
&lt;/h2&gt;

&lt;p&gt;A catalogue becomes a junk drawer within a week unless you fix the structure first. The kit assumes one shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;project/
  assets/      raw output, one file per asset
  output/      ready-to-submit files plus metadata.csv
  packs/       finished zips and covers
  research/    your notes and sources
  scripts/     these tools
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The one rule I will defend hardest is keeping &lt;code&gt;research/&lt;/code&gt;. Those scrape logs and notes are raw material for the next product. Deleting them to "clean up" is a mistake this project made once and is not repeating.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is actually inside the kit
&lt;/h2&gt;

&lt;p&gt;Four standalone Python scripts. No framework. No cloud account. Python 3.9+.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; venv .venv
&lt;span class="nb"&gt;source&lt;/span&gt; .venv/bin/activate
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you only use the metadata and tracker scripts, you can skip the install entirely. They need nothing but Python.&lt;/p&gt;

&lt;p&gt;Opt into real labels with &lt;code&gt;--titles titles.json&lt;/code&gt; when you have them. Without it, the filename is title-cased and used as the title, which is fine for a first pass and wrong for a submission.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest part
&lt;/h2&gt;

&lt;p&gt;These scripts came out of a parent project with roughly 19,000 lines of pipeline code and more than 300 pages of research behind the rules they enforce. I cut it down because the rules did not need 19,000 lines. They needed four scripts, a short requirements file, and the discipline to not add to either.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://revanzart-shop.revanzart.workers.dev/go/pipeline-starter-kit?src=devto" rel="noopener noreferrer"&gt;Get pipeline starter kit&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Building digital products? &lt;a href="https://revanzart-shop.revanzart.workers.dev/guides" rel="noopener noreferrer"&gt;Free guides &amp;amp; kits for digital product sellers&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>sideprojects</category>
      <category>ai</category>
      <category>automation</category>
      <category>marketing</category>
    </item>
    <item>
      <title>Running a 39-Product Asset Pipeline on a 2 GB Single-Core VPS</title>
      <dc:creator>Muhamad Revanza Rafif Hamdani</dc:creator>
      <pubDate>Sat, 24 Oct 2026 17:14:34 +0000</pubDate>
      <link>https://dev.to/revanzart/running-a-39-product-asset-pipeline-on-a-2-gb-single-core-vps-1k85</link>
      <guid>https://dev.to/revanzart/running-a-39-product-asset-pipeline-on-a-2-gb-single-core-vps-1k85</guid>
      <description>&lt;h1&gt;
  
  
  Running a 39-Product Asset Pipeline on a 2 GB Single-Core VPS
&lt;/h1&gt;

&lt;p&gt;The morning I knew the pipeline was broken, I was staring at a Dreamstime upload screen telling me my CSV header was invalid. Row one, column one. Nothing else. I had 39 finished products, 856 MB of output, around 19,000 lines of pipeline code behind them, and no idea which of the three channel formats I had mangled. That rejection cost me a morning and taught me the rule this whole build runs on: the platform format is the contract, and everything else is decoration.&lt;/p&gt;

&lt;p&gt;Here is what actually works when you have one core, 2 GB of RAM, no GPU, and no patience for a second failed batch.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four jobs that actually stall you
&lt;/h2&gt;

&lt;p&gt;Making the art is the fun part. The four things that stop a solo maker are metadata, double posting, doc to product conversion, and packaging. Each one has a narrow, dumb solution.&lt;/p&gt;

&lt;p&gt;For metadata, all three channels reject a wrong CSV header instantly. Adobe wants 5 columns, Vecteezy wants 4, Dreamstime wants 15. A small script that writes exactly that header, derived from a folder of files, removes an entire class of rejection.&lt;/p&gt;

&lt;p&gt;For double posting, the fastest way to get an account flagged is running your batch twice. A plain ledger file that records what you already submitted, and refuses to hand you the same item again, makes that impossible. It also gives you a &lt;code&gt;pending&lt;/code&gt; view so you always know what is left.&lt;/p&gt;

&lt;p&gt;For turning a doc into a product, the content already exists. It just needs to look like something you would pay for. One command that turns Markdown into a clean PDF with headings, tables, lists, and links gets you there.&lt;/p&gt;

&lt;p&gt;For packaging, a zip with a real cover sells. A bare file does not. So the pack step zips the files and renders a 1280x720 cover and a 600x600 thumbnail in the same pass.&lt;/p&gt;

&lt;h2&gt;
  
  
  One process, more RAM than you think
&lt;/h2&gt;

&lt;p&gt;The RAM problem on a 2 GB box is not average usage. It is the peak. A single image job can look harmless and then spike when you least expect it.&lt;/p&gt;

&lt;p&gt;Four habits made the whole pipeline fit on 1 vCPU:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Run one process at a time. Never fan out image jobs in parallel on a single core. Parallelism on one core is just slower work with a higher memory ceiling.&lt;/li&gt;
&lt;li&gt;Call &lt;code&gt;gc.collect()&lt;/code&gt; after every asset. It is one line and it keeps your footprint flat across a 39 product run.&lt;/li&gt;
&lt;li&gt;Measure peak RSS, not average. &lt;code&gt;resource.getrusage(RUSAGE_SELF).ru_maxrss&lt;/code&gt; gives you the real number, and the real number is what kills a small box.&lt;/li&gt;
&lt;li&gt;Process one file per subprocess for heavy render steps. The OS gets the memory back when the process exits, which is more reliable than trusting your own cleanup code.&lt;/li&gt;
&lt;li&gt;Render at the resolution you need. A preview inspected at 60 dpi costs a fraction of the same preview at 300.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this is clever. It is the difference between a box that finishes the batch and one that gets killed mid-run with half a product folder on disk.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two packages, no framework, no cloud account
&lt;/h2&gt;

&lt;p&gt;The pipeline needs exactly two third party libraries. Pillow for the covers and thumbnails. Reportlab for the PDF output. The metadata builder and the upload tracker use the standard library alone, so they run on a bare Python with no install at all.&lt;/p&gt;

&lt;p&gt;That matters more than it sounds. Every dependency you add is another thing that can fail on a fresh server at 2 AM, and another reason the pipeline needs a babysitter. Python 3.9, a virtual environment, &lt;code&gt;pip install pillow reportlab&lt;/code&gt;, and you are done.&lt;/p&gt;

&lt;p&gt;The folder layout is the other half of the discipline. Without one, a catalogue turns into a junk drawer within a month:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;project/
  assets/      raw output, one file per asset
  output/      ready to submit files plus metadata.csv
  packs/       finished product zips and covers
  research/    notes and sources
  scripts/     the tools
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep &lt;code&gt;research/&lt;/code&gt; forever. The scrape logs and notes are the raw material for future products. Deleting them to clean up is a mistake this project made once and will not repeat.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rules behind the scripts
&lt;/h2&gt;

&lt;p&gt;Every header, column count, and required field in the kit traces back to something concrete. Three channel formats. A fixed set of required metadata per platform. A cover size of 1280x720 and a thumbnail of 600x600. The kit enforces those because a wrong one costs you a rejection and a re-upload, and a re-upload batch is exactly how accounts get flagged.&lt;/p&gt;

&lt;p&gt;That is also why the tracker sits in front of the upload step. The pipeline does not trust memory, yours or mine. It trusts a ledger file.&lt;/p&gt;

&lt;p&gt;The 39 products, the 856 MB of output, and the 300 plus pages of research behind these rules were all validated on that same 1 vCPU, 2 GB, no GPU box. The numbers are not a benchmark. They are what this rig actually produced.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this came from
&lt;/h2&gt;

&lt;p&gt;These four scripts are the packaging layer, not a framework. If you want the exact tools that turned one prompt engine into 39 finished products, they are packaged as the compliance metadata kit: the metadata builder for Adobe, Vecteezy, and Dreamstime, the upload tracker, the Markdown to PDF converter, and the product packer. Standalone Python, two dependencies, the same rules enforced.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://revanzart-shop.revanzart.workers.dev/go/compliance-metadata-kit?src=devto" rel="noopener noreferrer"&gt;Get compliance metadata kit&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Building digital products? &lt;a href="https://revanzart-shop.revanzart.workers.dev/guides" rel="noopener noreferrer"&gt;Free guides &amp;amp; kits for digital product sellers&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>sideprojects</category>
      <category>ai</category>
      <category>automation</category>
      <category>marketing</category>
    </item>
    <item>
      <title>No GPU, No Cloud: Four Python Scripts That Package Stock Assets for Three Platforms</title>
      <dc:creator>Muhamad Revanza Rafif Hamdani</dc:creator>
      <pubDate>Sat, 24 Oct 2026 17:13:10 +0000</pubDate>
      <link>https://dev.to/revanzart/no-gpu-no-cloud-four-python-scripts-that-package-stock-assets-for-three-platforms-4g3a</link>
      <guid>https://dev.to/revanzart/no-gpu-no-cloud-four-python-scripts-that-package-stock-assets-for-three-platforms-4g3a</guid>
      <description>&lt;h1&gt;
  
  
  No GPU, No Cloud: Four Python Scripts That Package Stock Assets for Three Platforms
&lt;/h1&gt;

&lt;p&gt;I still remember the moment the rejection landed. I had spent a weekend generating a batch of vector assets, opened the Adobe Stock contributor portal, and watched the upload fail on the CSV uploaded to the system. Wrong column count. Seven hours of production, blocked by a header row. That was the day I stopped trusting "I'll just build the CSV by hand" and wrote the first of the four scripts that now package everything I sell.&lt;/p&gt;

&lt;p&gt;Here is what actually ships my work, and why each piece exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four scripts and the two dependencies
&lt;/h2&gt;

&lt;p&gt;The whole kit is four standalone Python files. No framework, no cloud account, no GPU.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;make_metadata.py&lt;/code&gt; builds the exact Adobe (5 column), Vecteezy (4 column) and Dreamstime (15 column) upload CSVs from a folder of files.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;upload_tracker.py&lt;/code&gt; is a ledger that remembers what you already posted, so a re-run never double-posts.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;md2pdf.py&lt;/code&gt; turns a Markdown file into a styled PDF with headings, tables, lists and links.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pack_product.py&lt;/code&gt; bundles a product: it zips the files and renders a cover at 1280x720 plus a thumbnail at 600x600.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Only two third-party packages are involved: &lt;strong&gt;Pillow&lt;/strong&gt; for the covers in &lt;code&gt;pack_product.py&lt;/code&gt;, and &lt;strong&gt;reportlab&lt;/strong&gt; for the PDF output in &lt;code&gt;md2pdf.py&lt;/code&gt;. The metadata and tracker scripts use the standard library alone, so you can run them without installing anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why metadata is the real bottleneck
&lt;/h2&gt;

&lt;p&gt;All three platforms reject a wrong CSV header instantly. Not on review, not a day later. Instantly, at upload. Adobe wants five columns, Vecteezy wants four, Dreamstime wants fifteen, and none of them agree on the order.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;make_metadata.py&lt;/code&gt; writes the header each platform demands and nothing else:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python make_metadata.py ./my-assets &lt;span class="nt"&gt;--platform&lt;/span&gt; adobe &lt;span class="nt"&gt;--out&lt;/span&gt; metadata.csv &lt;span class="nt"&gt;--ext&lt;/span&gt; .jpg
python make_metadata.py ./my-assets &lt;span class="nt"&gt;--platform&lt;/span&gt; vecteezy &lt;span class="nt"&gt;--out&lt;/span&gt; vecteezy.csv &lt;span class="nt"&gt;--ext&lt;/span&gt; .jpg
python make_metadata.py ./my-assets &lt;span class="nt"&gt;--platform&lt;/span&gt; dreamstime &lt;span class="nt"&gt;--out&lt;/span&gt; dreamstime.csv &lt;span class="nt"&gt;--ext&lt;/span&gt; .jpg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can pass &lt;code&gt;--titles titles.json&lt;/code&gt; to supply real titles and keywords. Without it, the filename gets title-cased and used as the title, which is fine for a first pass. Replace it before you submit for real.&lt;/p&gt;

&lt;h2&gt;
  
  
  The double-post trap
&lt;/h2&gt;

&lt;p&gt;The fastest way to get a contributor account flagged is running your batch twice. It happens to almost everyone: you upload, something looks off, you re-run the script, and now the same asset exists twice.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;upload_tracker.py&lt;/code&gt; makes that impossible:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python upload_tracker.py pending items.txt   &lt;span class="c"&gt;# shows what is NOT done yet&lt;/span&gt;
python upload_tracker.py &lt;span class="k"&gt;done&lt;/span&gt; &lt;span class="s2"&gt;"asset-001.jpg"&lt;/span&gt;
python upload_tracker.py status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A ledger beats memory. On a 39 product run, nobody remembers which sticker got posted on Tuesday.&lt;/p&gt;

&lt;h2&gt;
  
  
  Packaging on 1 vCPU and 2 GB of RAM
&lt;/h2&gt;

&lt;p&gt;This entire pipeline ran on a single core, 2 GB of RAM, with no GPU. That is not a brag, it is a constraint I had to design around. A few habits made it work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One process at a time.&lt;/strong&gt; Never fan out image jobs in parallel on a one core box.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Free memory between items.&lt;/strong&gt; Call &lt;code&gt;gc.collect()&lt;/code&gt; after each asset.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measure peak RSS, not average.&lt;/strong&gt; &lt;code&gt;resource.getrusage(RUSAGE_SELF).ru_maxrss&lt;/code&gt; tells you the real ceiling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Process one file per subprocess&lt;/strong&gt; for heavy render steps, so memory goes back to the OS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Render at the resolution you need.&lt;/strong&gt; Checking a preview at 60 dpi beats rendering it at 300.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Packaging itself is one command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python pack_product.py my-product &lt;span class="s2"&gt;"My Product"&lt;/span&gt; &lt;span class="s2"&gt;"A short subtitle"&lt;/span&gt; 9
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That writes &lt;code&gt;my-product.zip&lt;/code&gt;, &lt;code&gt;my-product-cover.jpg&lt;/code&gt; and &lt;code&gt;my-product-thumb.jpg&lt;/code&gt; into a &lt;code&gt;_system&lt;/code&gt; folder. Point it elsewhere with &lt;code&gt;--dir&lt;/code&gt; or the &lt;code&gt;PACK_ROOT&lt;/code&gt; environment variable, no need to edit the script.&lt;/p&gt;

&lt;p&gt;A zip with a real cover sells. A bare file does not.&lt;/p&gt;

&lt;h2&gt;
  
  
  The folder layout that keeps a catalogue from rotting
&lt;/h2&gt;

&lt;p&gt;Structure is what separates a product line from a junk drawer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;project/
  assets/            raw output (one file per asset)
  output/            ready-to-submit files + metadata.csv
  packs/             finished product zips + covers
  research/          your notes and sources
  scripts/           these tools
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep &lt;code&gt;research/&lt;/code&gt; forever. Your scrape logs and notes are the raw material for the next product. Deleting them to clean up is a mistake this project made once and will not repeat.&lt;/p&gt;

&lt;h2&gt;
  
  
  The numbers behind this
&lt;/h2&gt;

&lt;p&gt;These scripts are the packaging layer of a pipeline that produced 39 finished products, 856 MB of output, backed by roughly 19,000 lines of pipeline code in the parent project and 300 plus pages of research behind the rules the scripts enforce. All of it validated on 1 vCPU, 2 GB of RAM and no GPU.&lt;/p&gt;

&lt;p&gt;Above the four scripts sits a free stack: an orchestrator that schedules jobs, retries failures and makes decisions, plus a local LLM proxy for model access, both at zero cost. No cloud bill, no rented GPU.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to get the full system
&lt;/h2&gt;

&lt;p&gt;The scripts are the tool. The rules they enforce, the folder layout, the RAM discipline, the platform header specs and the order of operations, are the playbook. I packaged all of it as the ai sticker factory playbook, because the four scripts alone will still leave you guessing what to feed them and in what order.&lt;/p&gt;

&lt;p&gt;Build the pipeline once. Then let it run while you make more art.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://revanzart-shop.revanzart.workers.dev/go/ai-sticker-factory-playbook?src=devto" rel="noopener noreferrer"&gt;Get ai sticker factory playbook&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Building digital products? &lt;a href="https://revanzart-shop.revanzart.workers.dev/guides" rel="noopener noreferrer"&gt;Free guides &amp;amp; kits for digital product sellers&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>sideprojects</category>
      <category>ai</category>
      <category>automation</category>
      <category>marketing</category>
    </item>
    <item>
      <title>Two Packages, One Server: Metadata CSVs, Covers and PDFs for 39 Products</title>
      <dc:creator>Muhamad Revanza Rafif Hamdani</dc:creator>
      <pubDate>Sat, 24 Oct 2026 12:42:01 +0000</pubDate>
      <link>https://dev.to/revanzart/two-packages-one-server-metadata-csvs-covers-and-pdfs-for-39-products-5acn</link>
      <guid>https://dev.to/revanzart/two-packages-one-server-metadata-csvs-covers-and-pdfs-for-39-products-5acn</guid>
      <description>&lt;h1&gt;
  
  
  Two Packages, One Server: Metadata CSVs, Covers and PDFs for 39 Products
&lt;/h1&gt;

&lt;p&gt;The first time a stock platform rejected one of my upload batches, it took me four hours to understand why. One column was out of order in the CSV. That's it. I had generated 39 products, zipped them, written the docs, and the batch died on a header row.&lt;/p&gt;

&lt;p&gt;That rejection changed how I build. I stopped treating metadata and packaging as the boring afterthought and wrote scripts that enforce the rules instead of leaving them to my attention span at 1 a.m. Here is what actually matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three platforms, three headers, no mercy
&lt;/h2&gt;

&lt;p&gt;Adobe wants five columns. Vecteezy wants four. Dreamstime wants fifteen. Get one wrong and the whole file bounces before a human ever sees your art. I built one script, &lt;code&gt;make_metadata.py&lt;/code&gt;, that writes the exact header each platform demands and nothing else. You point it at a folder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python make_metadata.py ./my-assets &lt;span class="nt"&gt;--platform&lt;/span&gt; adobe &lt;span class="nt"&gt;--out&lt;/span&gt; metadata.csv &lt;span class="nt"&gt;--ext&lt;/span&gt; .jpg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It title-cases the filename and uses that as a first pass. Fine for a dry run, not fine for submission. Pass &lt;code&gt;--titles titles.json&lt;/code&gt; to supply real titles and keywords before you upload. The script does not guess, it writes the columns.&lt;/p&gt;

&lt;p&gt;If you only use this one and the tracker, you need no install at all. Standard library only.&lt;/p&gt;

&lt;h2&gt;
  
  
  A ledger beats your memory
&lt;/h2&gt;

&lt;p&gt;The fastest way to get an account flagged is running a batch twice. I got close once. It was 861 files, a re-run after a crash, and only a gut feeling that stopped me. After that I wrote &lt;code&gt;upload_tracker.py&lt;/code&gt;. It remembers what you posted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python upload_tracker.py pending items.txt   &lt;span class="c"&gt;# what is NOT done yet&lt;/span&gt;
python upload_tracker.py &lt;span class="k"&gt;done&lt;/span&gt; &lt;span class="s2"&gt;"asset-001.jpg"&lt;/span&gt;
python upload_tracker.py status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A re-run of the same list cannot double-post, because the ledger knows. This is the least glamorous tool in the kit and the one I would pay for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Docs that look like products
&lt;/h2&gt;

&lt;p&gt;You have the content. You need it to look like something worth buying. &lt;code&gt;md2pdf.py&lt;/code&gt; turns a Markdown file into a clean styled PDF with headings, tables, lists and links:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python md2pdf.py content.md product.pdf &lt;span class="s2"&gt;"My Product Title"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Packaging: a zip alone does not sell
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;pack_product.py&lt;/code&gt; zips the files and renders two images: a cover at 1280×720 and a thumbnail at 600×600.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python pack_product.py my-product &lt;span class="s2"&gt;"My Product"&lt;/span&gt; &lt;span class="s2"&gt;"A short subtitle"&lt;/span&gt; 9
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It writes the zip, cover and thumb into a &lt;code&gt;_system&lt;/code&gt; folder next to where you run it. Point it elsewhere with &lt;code&gt;--dir&lt;/code&gt; or the &lt;code&gt;PACK_ROOT&lt;/code&gt; environment variable. No editing the script.&lt;/p&gt;

&lt;p&gt;That is the entire dependency story: two third-party packages across four scripts. Pillow for the covers, reportlab for the PDFs. Nothing else.&lt;/p&gt;

&lt;h2&gt;
  
  
  It ran on 1 vCPU and 2 GB of RAM
&lt;/h2&gt;

&lt;p&gt;The full pipeline produced 39 products, 856 MB of output, from a single-core server with no GPU. It works because of five habits, not hardware:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One process at a time.&lt;/strong&gt; Never fan out image jobs in parallel on a one-core box.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Free memory between items.&lt;/strong&gt; &lt;code&gt;gc.collect()&lt;/code&gt; after each asset.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measure peak RSS, not average.&lt;/strong&gt; &lt;code&gt;resource.getrusage(RUSAGE_SELF).ru_maxrss&lt;/code&gt; tells you the real ceiling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One file per subprocess&lt;/strong&gt; for heavy render steps, so memory returns to the OS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Render at the resolution you need.&lt;/strong&gt; Inspecting a preview at 60 dpi beats rendering it at 300.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The folder layout matters too. Keep &lt;code&gt;assets/&lt;/code&gt;, &lt;code&gt;output/&lt;/code&gt;, &lt;code&gt;packs/&lt;/code&gt;, &lt;code&gt;research/&lt;/code&gt; and &lt;code&gt;scripts/&lt;/code&gt; separate. And keep &lt;code&gt;research/&lt;/code&gt; forever. I deleted scrape logs once to "clean up." They were the raw material for the next three products. I won't repeat that.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this came from
&lt;/h2&gt;

&lt;p&gt;These four scripts sit under a larger pipeline: roughly 19,000 lines of code in the parent project, with 300+ pages of research behind the rules the scripts enforce. The tools are the packaging layer of everything I sell.&lt;/p&gt;

&lt;p&gt;I packaged them as the &lt;strong&gt;compliance metadata kit&lt;/strong&gt;: the four working scripts, the folder structure, and the RAM notes above. Not a tutorial that ends at "here's how you'd do it." The same tools that shipped 39 products on a 2 GB server.&lt;/p&gt;

&lt;p&gt;If you have ever lost a day to a rejected CSV column, start there.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://revanzart-shop.revanzart.workers.dev/go/compliance-metadata-kit?src=devto" rel="noopener noreferrer"&gt;Get compliance metadata kit&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Building digital products? &lt;a href="https://revanzart-shop.revanzart.workers.dev/guides" rel="noopener noreferrer"&gt;Free guides &amp;amp; kits for digital product sellers&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>sideprojects</category>
      <category>ai</category>
      <category>automation</category>
      <category>marketing</category>
    </item>
    <item>
      <title>The Stdlib Ledger That Stops a Re-Run From Double-Posting Uploads</title>
      <dc:creator>Muhamad Revanza Rafif Hamdani</dc:creator>
      <pubDate>Sat, 24 Oct 2026 12:41:51 +0000</pubDate>
      <link>https://dev.to/revanzart/the-stdlib-ledger-that-stops-a-re-run-from-double-posting-uploads-4o1c</link>
      <guid>https://dev.to/revanzart/the-stdlib-ledger-that-stops-a-re-run-from-double-posting-uploads-4o1c</guid>
      <description>&lt;h1&gt;
  
  
  The Stdlib Ledger That Stops a Re-Run From Double-Posting Uploads
&lt;/h1&gt;

&lt;p&gt;I nearly lost an upload account to a rerun. My batch script had already pushed 200 files to a stock site. I wanted to check one thing, so I ran the script again. It did not check anything. It started uploading from the top. I caught it around item 40 by watching the progress bar scroll past filenames I recognized. The platform had not flagged me yet, but that is luck, not a system.&lt;/p&gt;

&lt;p&gt;The fix was not a smarter uploader. It was a ledger. &lt;code&gt;upload_tracker.py&lt;/code&gt; is about 40 lines of standard library Python, and it does one thing: it remembers what you have already posted. That alone removed the most dangerous failure mode in the whole pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a rerun is the fastest way to get flagged
&lt;/h2&gt;

&lt;p&gt;Stock platforms treat repeated identical submissions as spam. A second upload of the same file is not just wasted time, it is a signal. Do it across a few hundred files and you move from "new seller" to "pattern to review."&lt;/p&gt;

&lt;p&gt;The usual workaround is a naming convention plus eyeballing a folder. That breaks the moment you batch-generate files, because regenerated filenames shift. The ledger moves the source of truth out of the filesystem and into a state file that only the tracker writes.&lt;/p&gt;

&lt;p&gt;The commands are deliberately dumb:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python upload_tracker.py pending items.txt   &lt;span class="c"&gt;# shows what is NOT done yet&lt;/span&gt;
python upload_tracker.py &lt;span class="k"&gt;done&lt;/span&gt; &lt;span class="s2"&gt;"asset-001.jpg"&lt;/span&gt;
python upload_tracker.py status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No database, no cloud, no install. &lt;code&gt;upload_tracker.py&lt;/code&gt; uses the standard library alone. The ledger is local state, and local state on a 1 vCPU, 2 GB box is exactly what you want.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule the ledger enforces: &lt;code&gt;pending&lt;/code&gt; is the only input
&lt;/h2&gt;

&lt;p&gt;Once the tracker exists, the workflow changes shape. You do not run a batch from your asset folder. You run it from &lt;code&gt;pending&lt;/code&gt;. The batch script asks the ledger what is unfinished, and the ledger returns only items with no &lt;code&gt;done&lt;/code&gt; record.&lt;/p&gt;

&lt;p&gt;That gives you three guarantees for free:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Rerun safety.&lt;/strong&gt; If a job dies halfway, rerun it. The completed half is skipped.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resumability.&lt;/strong&gt; You can stop at any point and pick up tomorrow.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auditability.&lt;/strong&gt; &lt;code&gt;status&lt;/code&gt; tells you exactly what went out, without reading platform dashboards.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I have not double-posted since. Not once. The ledger costs nothing to maintain because it has no dependencies, and the failure it prevents is the one that cannot be undone.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same rule, applied to CSVs
&lt;/h2&gt;

&lt;p&gt;The double-post problem has a quieter twin: rejected batches. All three channels reject a wrong CSV header instantly, and a rejected batch wastes a review cycle. &lt;code&gt;make_metadata.py&lt;/code&gt; writes the exact header each platform demands:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Adobe&lt;/strong&gt;: 5 columns&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vecteezy&lt;/strong&gt;: 4 columns&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dreamstime&lt;/strong&gt;: 15 columns&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One command per platform:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python make_metadata.py ./my-assets &lt;span class="nt"&gt;--platform&lt;/span&gt; adobe &lt;span class="nt"&gt;--out&lt;/span&gt; metadata.csv &lt;span class="nt"&gt;--ext&lt;/span&gt; .jpg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It also uses only the standard library. It reads a folder, writes the header the platform demands, and nothing else. If you pass &lt;code&gt;--titles titles.json&lt;/code&gt; it uses your real titles and keywords. Without it, filenames are title-cased as a first pass. That is fine for testing, but replace it before you submit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two packages, and only two
&lt;/h2&gt;

&lt;p&gt;The rest of the kit stays lean by design. Only &lt;code&gt;pack_product.py&lt;/code&gt; needs Pillow for covers and thumbnails, and only &lt;code&gt;md2pdf.py&lt;/code&gt; needs reportlab for PDF output. If you use just the metadata and tracker scripts, you can skip &lt;code&gt;pip install&lt;/code&gt; entirely. On a machine this small, fewer dependencies means fewer failure modes.&lt;/p&gt;

&lt;p&gt;The packaging step follows the same pattern. &lt;code&gt;pack_product.py&lt;/code&gt; bundles a zip with a 1280x720 cover and a 600x600 thumbnail, because a zip with a real cover sells and a bare file does not. Output goes to a &lt;code&gt;_system&lt;/code&gt; folder, and you can redirect it with &lt;code&gt;--dir&lt;/code&gt; or the &lt;code&gt;PACK_ROOT&lt;/code&gt; environment variable, with no script edits.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this looked like in production
&lt;/h2&gt;

&lt;p&gt;This is not a thought experiment. The parent project produced 39 finished products, 856 MB of output, backed by roughly 19,000 lines of pipeline code and more than 300 pages of research, all on 1 vCPU, 2 GB RAM, no GPU. The habits that made that possible:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One process at a time.&lt;/strong&gt; Never fan out image jobs on a 1-core box.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Free memory between items.&lt;/strong&gt; Call &lt;code&gt;gc.collect()&lt;/code&gt; after each asset.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measure peak RSS, not average.&lt;/strong&gt; &lt;code&gt;resource.getrusage(RUSAGE_SELF).ru_maxrss&lt;/code&gt; tells you the real ceiling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Render at the resolution you need.&lt;/strong&gt; A 60 dpi preview beats a 300 dpi render for inspection.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The ledger sits above all of this. It is the cheapest component in the stack and the one that taught me the most: in an automated pipeline, the state file matters more than the code that runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I packaged from this
&lt;/h2&gt;

&lt;p&gt;I pulled the four working scripts into a starter kit: &lt;code&gt;make_metadata.py&lt;/code&gt;, &lt;code&gt;upload_tracker.py&lt;/code&gt;, &lt;code&gt;md2pdf.py&lt;/code&gt; and &lt;code&gt;pack_product.py&lt;/code&gt;, with the folder layout that scales (&lt;code&gt;assets/&lt;/code&gt;, &lt;code&gt;output/&lt;/code&gt;, &lt;code&gt;packs/&lt;/code&gt;, &lt;code&gt;research/&lt;/code&gt;, &lt;code&gt;scripts/&lt;/code&gt;) and the RAM notes above. Keep &lt;code&gt;research/&lt;/code&gt; forever, by the way. I deleted mine once to "clean up" and will not repeat that mistake.&lt;/p&gt;

&lt;p&gt;The tracker is the piece that makes the kit safe to run more than once. Everything else is packaging.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://revanzart-shop.revanzart.workers.dev/go/compliance-metadata-kit?src=devto" rel="noopener noreferrer"&gt;Get compliance metadata kit&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Building digital products? &lt;a href="https://revanzart-shop.revanzart.workers.dev/guides" rel="noopener noreferrer"&gt;Free guides &amp;amp; kits for digital product sellers&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>sideprojects</category>
      <category>ai</category>
      <category>automation</category>
      <category>marketing</category>
    </item>
    <item>
      <title>Building Upload Tracker Ledger With Python Stdlib Before Your First Upload</title>
      <dc:creator>Muhamad Revanza Rafif Hamdani</dc:creator>
      <pubDate>Fri, 23 Oct 2026 17:13:58 +0000</pubDate>
      <link>https://dev.to/revanzart/building-upload-tracker-ledger-with-python-stdlib-before-your-first-upload-5bmi</link>
      <guid>https://dev.to/revanzart/building-upload-tracker-ledger-with-python-stdlib-before-your-first-upload-5bmi</guid>
      <description>

&lt;p&gt;&lt;em&gt;Building digital products? &lt;a href="https://revanzart-shop.revanzart.workers.dev/guides" rel="noopener noreferrer"&gt;Free guides &amp;amp; kits for digital product sellers&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>sideprojects</category>
      <category>ai</category>
      <category>automation</category>
      <category>marketing</category>
    </item>
    <item>
      <title>Stop Double-Posting Stock Assets With CSV Ledger That Remembers Every Upload</title>
      <dc:creator>Muhamad Revanza Rafif Hamdani</dc:creator>
      <pubDate>Fri, 23 Oct 2026 17:13:43 +0000</pubDate>
      <link>https://dev.to/revanzart/stop-double-posting-stock-assets-with-csv-ledger-that-remembers-every-upload-14hj</link>
      <guid>https://dev.to/revanzart/stop-double-posting-stock-assets-with-csv-ledger-that-remembers-every-upload-14hj</guid>
      <description>

&lt;p&gt;&lt;em&gt;Building digital products? &lt;a href="https://revanzart-shop.revanzart.workers.dev/guides" rel="noopener noreferrer"&gt;Free guides &amp;amp; kits for digital product sellers&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>sideprojects</category>
      <category>ai</category>
      <category>automation</category>
      <category>marketing</category>
    </item>
  </channel>
</rss>
