<?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: Ilan Kim</title>
    <description>The latest articles on DEV Community by Ilan Kim (@ilan_kim).</description>
    <link>https://dev.to/ilan_kim</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%2F4120289%2Faf6b8a4b-75f0-46f4-8d4e-7e1d5e1b540a.JPG</url>
      <title>DEV Community: Ilan Kim</title>
      <link>https://dev.to/ilan_kim</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ilan_kim"/>
    <language>en</language>
    <item>
      <title>NovelAI V5 upscaling measured: Enhance at 35 and 45 Anlas vs ComfyUI Anima i2i, and how much the style shifts (ΔE)</title>
      <dc:creator>Ilan Kim</dc:creator>
      <pubDate>Thu, 01 Oct 2026 04:11:06 +0000</pubDate>
      <link>https://dev.to/ilan_kim/novelai-v5-upscaling-measured-enhance-at-35-and-45-anlas-vs-comfyui-anima-i2i-and-how-much-the-2kl0</link>
      <guid>https://dev.to/ilan_kim/novelai-v5-upscaling-measured-enhance-at-35-and-45-anlas-vs-comfyui-anima-i2i-and-how-much-the-2kl0</guid>
      <description>&lt;p&gt;NovelAI's Enhance changed V5 images more than a local Anima i2i pass in ComfyUI did: the mean color difference from the original (ΔE2000) was between 10 and 15 for Enhance, and between 8.7 and 10.4 for Anima i2i even at denoise 0.55. If you only want a bigger image, the standalone Upscale at 1 Anlas stayed closest to the original.&lt;/p&gt;

&lt;p&gt;This is for anyone who has hesitated over which route to take when enlarging a V5 image. On 2026-10-01 I prepared three originals with &lt;code&gt;nai-diffusion-5-full&lt;/code&gt;, ran them through NAI Enhance 1.5×, Enhance Max and the standalone Upscale, and then ran the same originals through 15 ComfyUI jobs on an RTX 5090. A few terms if NovelAI is new to you: Anlas is NovelAI's generation credit, Enhance is the web UI option that enlarges an image and redraws it, and Anima is the model I run locally in ComfyUI. I usually generate in NovelAI first and move to Anima when I get stuck.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enhance 1.5× cost 35 Anlas, Max cost 45 Anlas
&lt;/h2&gt;

&lt;p&gt;Even on Opus, NovelAI's top subscription tier, Enhance and Upscale are not free. One of the conditions for free generation is that no other image is used as a base, so feeding in an existing picture takes Anlas off your balance (subscription page checked 2026-10-01).&lt;/p&gt;

&lt;p&gt;I ran each action in the web UI and checked the balance after every one. Generating the originals cost 0 Anlas, as in &lt;a href="https://dev.to/ilan_kim/novelai-v5-on-opus-usage-limits-the-2026-09-21-subscription-anlas-reset-and-the-apinovelainet-567l"&gt;the earlier post&lt;/a&gt;. Here is what each action deducted:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;th&gt;Output size (portrait original)&lt;/th&gt;
&lt;th&gt;Anlas deducted&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Original generation 832×1216, 28 steps&lt;/td&gt;
&lt;td&gt;832×1216&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Standalone Upscale&lt;/td&gt;
&lt;td&gt;1664×2432&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Enhance 1.5×&lt;/td&gt;
&lt;td&gt;1280×1856&lt;/td&gt;
&lt;td&gt;35&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Enhance Max&lt;/td&gt;
&lt;td&gt;1467×2144&lt;/td&gt;
&lt;td&gt;45&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The two portrait images and the one landscape image all cost the same. That is higher than the 30 and 38 written in a community tool. The button in the Enhance window also showed 35 and 45, so look at that number before you click.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enhance output records a strength 0.5 i2i request
&lt;/h2&gt;

&lt;p&gt;Enhance output PNGs carry the request that produced them. NovelAI hides generation data inside its PNGs (the "stealth metadata" covered further down), and opening it shows the request as it was sent. Both levels were an &lt;code&gt;Img2ImgRequest&lt;/code&gt; with strength 0.5, noise 0 and 28 steps. The two levels differed like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Enhance 1.5×&lt;/th&gt;
&lt;th&gt;Enhance Max&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;i2i request resolution&lt;/td&gt;
&lt;td&gt;1280×1856&lt;/td&gt;
&lt;td&gt;832×1216 (original size)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Final output&lt;/td&gt;
&lt;td&gt;1280×1856&lt;/td&gt;
&lt;td&gt;1467×2144&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;End of prompt&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;-2::upscaled, blurry::,&lt;/code&gt; appended automatically&lt;/td&gt;
&lt;td&gt;nothing appended&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Seed&lt;/td&gt;
&lt;td&gt;new random value every time&lt;/td&gt;
&lt;td&gt;new random value every time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Character prompts&lt;/td&gt;
&lt;td&gt;carried over from the original&lt;/td&gt;
&lt;td&gt;carried over from the original&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;The i2i request size was the enlarged size for 1.5× and the original size for Max.&lt;/strong&gt; Max also had &lt;code&gt;upscaled_enhance: true&lt;/code&gt; recorded separately, so Max appears to enlarge the image after the i2i pass.&lt;/p&gt;

&lt;p&gt;Metadata alone cannot settle the processing order inside the server, so I left that as unverified. Magnitude 2 in the Enhance window is the same value as Strength 0.5.&lt;/p&gt;

&lt;p&gt;The seed changes every time you click. Feeding in the same original again gives a different result, so save a result you like right away.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enhance changed the image more than Anima i2i at 0.55
&lt;/h2&gt;

&lt;p&gt;The reference is the original enlarged to the output size with Lanczos. ΔE2000 looks at color difference, and grayscale SSIM looks at how much of the lines and shapes survive. The values are the range across the three originals:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Scale&lt;/th&gt;
&lt;th&gt;Mean ΔE&lt;/th&gt;
&lt;th&gt;SSIM&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;NAI standalone Upscale&lt;/td&gt;
&lt;td&gt;2×&lt;/td&gt;
&lt;td&gt;0.85~1.04&lt;/td&gt;
&lt;td&gt;0.966~0.981&lt;/td&gt;
&lt;td&gt;1 Anlas&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ESRGAN (&lt;code&gt;RealESRGAN_x4plus_anime_6B&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;2×&lt;/td&gt;
&lt;td&gt;2.07~2.59&lt;/td&gt;
&lt;td&gt;0.904~0.931&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Anima i2i denoise 0.25&lt;/td&gt;
&lt;td&gt;1.5×&lt;/td&gt;
&lt;td&gt;4.33~5.72&lt;/td&gt;
&lt;td&gt;0.717~0.790&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Anima i2i denoise 0.40&lt;/td&gt;
&lt;td&gt;1.5×&lt;/td&gt;
&lt;td&gt;6.29~7.90&lt;/td&gt;
&lt;td&gt;0.615~0.700&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Anima i2i denoise 0.55&lt;/td&gt;
&lt;td&gt;1.5×&lt;/td&gt;
&lt;td&gt;8.69~10.44&lt;/td&gt;
&lt;td&gt;0.555~0.631&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Anima i2i denoise 0.40&lt;/td&gt;
&lt;td&gt;2×&lt;/td&gt;
&lt;td&gt;5.38~6.95&lt;/td&gt;
&lt;td&gt;0.693~0.765&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NAI Enhance 1.5×&lt;/td&gt;
&lt;td&gt;1.5×&lt;/td&gt;
&lt;td&gt;10.63~15.43&lt;/td&gt;
&lt;td&gt;0.481~0.606&lt;/td&gt;
&lt;td&gt;35 Anlas&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NAI Enhance Max&lt;/td&gt;
&lt;td&gt;about 1.76×&lt;/td&gt;
&lt;td&gt;10.19~14.29&lt;/td&gt;
&lt;td&gt;0.498~0.638&lt;/td&gt;
&lt;td&gt;45 Anlas&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;By the numbers, Enhance changed the image the most. Apart from the background image, saturation changed by 0.03 or less. SSIM fell at the same time, so the lines and shapes changed too. The exception is Enhance 1.5× on the background image, where saturation rose by 0.10 and the blue channel by 19.&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%2F5wclasak0tgtk1le5o19.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%2F5wclasak0tgtk1le5o19.webp" alt="Face crops of the original and the NAI Upscale, Enhance 1.5×, Enhance Max, Anima i2i 0.40 and 0.55 results, cut at the same position and placed side by side" width="800" height="457"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Cropping the face shows the same order. Upscale is hard to tell apart from the original. Enhance 1.5× redrew the bangs and the eye highlights. Max changed even the eye shape and the irises. Anima i2i 0.40 kept the outlines closer to the original.&lt;/p&gt;

&lt;p&gt;The usual community answer is that moving to a local model changes the style because the checkpoint is different. I had taken that on trust and assumed Enhance would be the safe option. Yet Enhance, which uses the same V5 model, changed the image just as much. Even with the same model, an i2i pass changes this much.&lt;/p&gt;

&lt;h2&gt;
  
  
  In the two-character image, Enhance kept the eye color and Anima changed it
&lt;/h2&gt;

&lt;p&gt;The difference was clearest on the two-character original, which I generated with the prompt split across two character prompt slots. The blue-haired character with glasses has blue eyes in the original. Enhance kept the blue eyes at both levels. Anima i2i turned them green from 0.40 on.&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%2Fbwvp0veez9fp9t18a6lr.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%2Fbwvp0veez9fp9t18a6lr.webp" alt="Face crops of the blue-haired character from the two-character original compared across six methods; only the two Anima i2i panels have green eyes" width="800" height="393"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Green is the other character's eye color. The glasses frames also went from black to gold. On the Anima side I used core nodes only, so both characters' attributes were written on a single line.&lt;/p&gt;

&lt;p&gt;That line contains only &lt;code&gt;green eyes&lt;/code&gt;, not &lt;code&gt;blue eyes&lt;/code&gt;. The NAI character slots did not have &lt;code&gt;blue eyes&lt;/code&gt; either. This one image cannot tell whether the cause is the single-line prompt or the difference between models.&lt;/p&gt;

&lt;p&gt;The Enhance request carried both character slots unchanged. The glasses frames, though, became thick and square, and Max also changed the shape of the mouth. In this one image, the eye color survived and the details were redrawn.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sign lettering was rewritten by every i2i pass
&lt;/h2&gt;

&lt;p&gt;On the rainy neon street, no i2i pass kept the lettering. Enhance changed the layout and colors of the signs wholesale. Anima i2i 0.40 kept part of the large sign's shape, and at 0.55 that changed as well.&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%2Fkhbewwhgf3l0k7mj9uqn.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%2Fkhbewwhgf3l0k7mj9uqn.webp" alt="Crops of the neon-sign street original and the six methods' results at the same position; every i2i result has different sign lettering" width="800" height="352"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Most of the sign lettering in the original is meaningless characters anyway. It is still a problem if you liked the sign design, and in that case you should use only Upscale or ESRGAN.&lt;/p&gt;

&lt;h2&gt;
  
  
  Local ComfyUI post-processing on an RTX 5090: 2 to 38 seconds per image, 0 Anlas
&lt;/h2&gt;

&lt;p&gt;The local setup is ComfyUI 0.33.1 on Windows 11, with torch 2.11.0+cu128 and an RTX 5090 32GB. I used core nodes only, with no custom nodes, and queued the 15 jobs one after another through the HTTP API (&lt;code&gt;POST /prompt&lt;/code&gt;). Run times per job type were:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Job&lt;/th&gt;
&lt;th&gt;Pipeline&lt;/th&gt;
&lt;th&gt;Run time&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ESRGAN 2×&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;UpscaleModelLoader&lt;/code&gt; 4×, then Lanczos to 2× size&lt;/td&gt;
&lt;td&gt;1.63~1.66 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Anima i2i 1.5×&lt;/td&gt;
&lt;td&gt;Lanczos 1.5×, then &lt;code&gt;VAEEncode&lt;/code&gt; and &lt;code&gt;KSampler&lt;/code&gt; 30 steps&lt;/td&gt;
&lt;td&gt;17.77~19.17 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Anima i2i 2×&lt;/td&gt;
&lt;td&gt;Lanczos 2×, then the same i2i&lt;/td&gt;
&lt;td&gt;37.73~38.28 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Anima ran at cfg 4.5 with &lt;code&gt;er_sde&lt;/code&gt; and &lt;code&gt;simple&lt;/code&gt;. The seed was 20261001, with no artist tags. The first run took 21 seconds because of model loading, so I left it out. The times are for this one GPU.&lt;/p&gt;

&lt;p&gt;Anima is the only local model I kept. Its weights are non-commercial, but the images it generates can be used commercially; the Anima README says so. NoobAI-XL, which is on the same PC, prohibits commercial use of its outputs, so I left it out (checked 2026-10-01).&lt;/p&gt;

&lt;h2&gt;
  
  
  Stealth metadata disappears on a ComfyUI save but survives webp
&lt;/h2&gt;

&lt;p&gt;NovelAI PNGs hide the prompt in the alpha channel, with the settings stored alongside it in the least significant bits. This is what NovelAI's stealth metadata is. I reimplemented the logic of the official reader and read the file at each stage:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stage&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;NAI original PNG, Upscale and Enhance output&lt;/td&gt;
&lt;td&gt;survives&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;macOS &lt;code&gt;sips&lt;/code&gt; PNG re-save&lt;/td&gt;
&lt;td&gt;survives (text chunks are lost)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lossless webp, lossy q90 webp (Pillow, sharp)&lt;/td&gt;
&lt;td&gt;survives&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ComfyUI &lt;code&gt;SaveImage&lt;/code&gt; PNG&lt;/td&gt;
&lt;td&gt;lost (saved as RGB, no alpha)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JPEG conversion&lt;/td&gt;
&lt;td&gt;lost&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Half-size resize (one original)&lt;/td&gt;
&lt;td&gt;lost (&lt;code&gt;magic not found&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;If you only convert to webp and upload, the full prompt goes along with it.&lt;/strong&gt; It read back intact even from a q90 lossy webp.&lt;/p&gt;

&lt;p&gt;The original PNG also has the prompt in its &lt;code&gt;Comment&lt;/code&gt; and &lt;code&gt;Description&lt;/code&gt; text chunks. To hide it, strip both the alpha channel and the text chunks, then read the final file again before uploading to confirm.&lt;/p&gt;

&lt;p&gt;One pass through ComfyUI and the NAI metadata is gone. In its place, &lt;code&gt;SaveImage&lt;/code&gt; writes the workflow as a &lt;code&gt;prompt&lt;/code&gt; chunk. If you need the NAI generation info, keep the original PNG separately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choose by how much of the original you want to keep
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;If you only want to enlarge the image as it is, the NAI standalone Upscale stays closest. If you have a local GPU, ESRGAN also stays around ΔE 2.&lt;/li&gt;
&lt;li&gt;If there are several characters and you don't mind the details being redrawn, look at Enhance first. In this round's single two-character image, it kept the eye color. The result is different every time, so save it right away.&lt;/li&gt;
&lt;li&gt;To save Anlas, start Anima i2i at denoise 0.25 to 0.40. With several characters, check the result with a crop.&lt;/li&gt;
&lt;li&gt;For backgrounds where signs or lettering matter, avoid i2i and use only Upscale or ESRGAN.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;All the pictures in this post were made with AI. Images: AI-generated (NovelAI Diffusion V5, ComfyUI Anima; cover image OpenAI Images).&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;NAI Enhance (35 or 45 Anlas) redrew V5 images more than local Anima i2i at denoise 0.55, while the 1 Anlas standalone Upscale and ESRGAN stayed closest to the original. Every i2i pass rewrote the sign lettering, and a webp conversion keeps the prompt in the stealth metadata, so strip it before you post.&lt;/p&gt;

&lt;h2&gt;
  
  
  Unverified
&lt;/h2&gt;

&lt;p&gt;The internal processing order of Enhance: I could not confirm it. The metadata only records the request size and the &lt;code&gt;upscaled_enhance&lt;/code&gt; value, and it does not show at which stage the server enlarges the image.&lt;/p&gt;

&lt;p&gt;Seed-to-seed variance of Enhance results: I did not measure it. There is only one result per setting, so I don't know how much ΔE moves across repeated runs.&lt;/p&gt;

&lt;p&gt;Enhance strengths other than Magnitude 2: I did not run them. I used only the default of 2 and did not check the price or the amount of change at other strengths.&lt;/p&gt;

&lt;p&gt;Processing time on the NAI side: I did not measure it. A person clicked one image at a time, so all I have is a rough sense of the time, and queue waits were mixed in, so I kept it out of the table.&lt;/p&gt;

&lt;p&gt;The Curated model and other local models: out of scope. The originals used only &lt;code&gt;nai-diffusion-5-full&lt;/code&gt;, and locally I ran only Anima.&lt;/p&gt;

&lt;h2&gt;
  
  
  Changelog
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Version&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1.0&lt;/td&gt;
&lt;td&gt;2026-10-01 first version (9 NAI Enhance 1.5×, Max and Upscale runs plus 15 ComfyUI jobs, measuring Anlas, ΔE, SSIM and metadata)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;AI disclosure: translated and edited with AI assistance (Claude) from my Korean original. Measurements were run with Claude Code under my supervision; I reviewed the result.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>novelai</category>
      <category>comfyui</category>
      <category>stablediffusion</category>
      <category>ai</category>
    </item>
    <item>
      <title>Crack cash payouts: the 8.8% is other-income tax, not a fee — what you receive, May filing, business registration</title>
      <dc:creator>Ilan Kim</dc:creator>
      <pubDate>Wed, 30 Sep 2026 08:44:45 +0000</pubDate>
      <link>https://dev.to/ilan_kim/crack-cash-payouts-the-88-is-other-income-tax-not-a-fee-what-you-receive-may-filing-5gd5</link>
      <guid>https://dev.to/ilan_kim/crack-cash-payouts-the-88-is-other-income-tax-not-a-fee-what-you-receive-may-filing-5gd5</guid>
      <description>&lt;p&gt;When you convert Crack cash to money, the 8.8% that disappears is not a fee: it is income tax withheld on "other income" (기타소득) under Korea's Income Tax Act, so converting KRW 1,000,000 leaves KRW 912,000 in your bank account. If you have little other income, you can get most of it back by filing Korea's comprehensive income tax return in May.&lt;/p&gt;

&lt;p&gt;Crack (크랙) is a Korean AI character-chat platform run by Wrtn (뤼튼); creators earn "cash" there when users spend paid Crackers on their works. I wrote this for creators who are building up cash on Crack. I've worked on payment and settlement code as a backend developer. On 2026-09-30 I saved the original text of Crack's announcement and cash guide, then recomputed the 8.8% from the articles of the Income Tax Act. I also read the Crack cash screen on my own account and the tax calculation in Crack's public web code. This is not tax advice. Your actual tax depends on your own income mix and the facts of your situation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 8.8% is withholding tax on other income
&lt;/h2&gt;

&lt;p&gt;Other income (기타소득) is the Korean tax category for irregular income such as lecture fees, and the payer withholds tax before paying you. The 8.8% is exactly what the other-income withholding formula produces. &lt;strong&gt;60% of the payment is deducted as deemed necessary expenses, and the remaining 40% is taxed at 20%.&lt;/strong&gt; Local income tax of 10% of that is added on top.&lt;/p&gt;

&lt;p&gt;Here is the calculation for a KRW 1,000,000 conversion:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Conversion           KRW 1,000,000
Other-income amount = 1,000,000 × (1 − 0.6) = 400,000   Income Tax Act Enforcement Decree Art. 87(1-2)
Income tax          = 400,000 × 20%          =  80,000   Income Tax Act Art. 129(1)6(d)
Local income tax    = 80,000 × 10%           =   8,000   Local Tax Act Art. 103-13(1)
Total KRW 88,000 = 8.8% of the payment, deposit KRW 912,000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Korean, those citations are 소득세법 시행령 §87 1의2, 소득세법 §129①6라 and 지방세법 §103의13①.&lt;/p&gt;

&lt;p&gt;Crack's own documents point the same way. The screen where you register your conversion details carries a personal-data collection notice. As its legal basis it cites Income Tax Act Articles 21, 127, 145 and 164. Article 21 defines other income, and Article 145 sets out how other income is withheld. Article 144, the withholding provision for business income, is not on the list.&lt;/p&gt;

&lt;p&gt;The Crack Originals notice is more direct. On payouts to original authors, it says they are "transferred to your account after 8.8% other-income tax withholding" ("기타소득세 8.8% 원천징수 후 계좌로 이체"). The 3.3% that Korean freelancers usually see deducted is business-income withholding. Crack cash is handled differently.&lt;/p&gt;

&lt;h2&gt;
  
  
  Converting KRW 1,000,000 puts KRW 912,000 in your account
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What lands in your account is the conversion amount minus 8.8%.&lt;/strong&gt; The rate at which cash accrues depends on your work's rating and whether it is registered for Crack Only. Your paid Cracker usage is multiplied by that rate, and the resulting cash arrives on the 1st of the following month.&lt;/p&gt;

&lt;p&gt;As an example, I took a work with 100 million paid Crackers used in a month. I picked a large number to reach the range where the deduction actually bites. I assumed all accrued cash is converted at once:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tier&lt;/th&gt;
&lt;th&gt;Accrual rate&lt;/th&gt;
&lt;th&gt;Cash earned (KRW)&lt;/th&gt;
&lt;th&gt;8.8% withheld (KRW)&lt;/th&gt;
&lt;th&gt;Deposit (KRW)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Regular creator, Safety&lt;/td&gt;
&lt;td&gt;1.0%&lt;/td&gt;
&lt;td&gt;1,000,000&lt;/td&gt;
&lt;td&gt;88,000&lt;/td&gt;
&lt;td&gt;912,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Official creator, Unsafety (from 9/1)&lt;/td&gt;
&lt;td&gt;1.0%&lt;/td&gt;
&lt;td&gt;1,000,000&lt;/td&gt;
&lt;td&gt;88,000&lt;/td&gt;
&lt;td&gt;912,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Official creator, Safety&lt;/td&gt;
&lt;td&gt;1.5%&lt;/td&gt;
&lt;td&gt;1,500,000&lt;/td&gt;
&lt;td&gt;132,000&lt;/td&gt;
&lt;td&gt;1,368,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Official creator, Unsafety + Crack Only&lt;/td&gt;
&lt;td&gt;2.0%&lt;/td&gt;
&lt;td&gt;2,000,000&lt;/td&gt;
&lt;td&gt;176,000&lt;/td&gt;
&lt;td&gt;1,824,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Official creator, Safety + Crack Only&lt;/td&gt;
&lt;td&gt;2.5%&lt;/td&gt;
&lt;td&gt;2,500,000&lt;/td&gt;
&lt;td&gt;220,000&lt;/td&gt;
&lt;td&gt;2,280,000&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Crack Only started on 2026-08-12. Only official creators' works can apply (per the notice as of 2026-09-30). In exchange for publishing a work exclusively on Crack for 18 months, the rate goes up by 1 percentage point. Each series has a cumulative payout cap of KRW 1 billion, counted afresh from September 1.&lt;/p&gt;

&lt;p&gt;The conversion screen calculates the same way. In the public web code, the amount you enter is multiplied by a &lt;code&gt;taxRate&lt;/code&gt; supplied by the server. Fractions of a won are dropped. The screen shows the requested payout amount (지급 요청 금액) and the tax deduction (세금 공제). Below those come the expected payout amount (예상 지급 금액) and expected payout date (지급 예상일). The guide adds that the tax calculation on the screen is an example.&lt;/p&gt;

&lt;p&gt;My account holds 0 cash, so I couldn't bring up that screen myself. When you tap 현금으로 바꾸기 (Convert to cash), you're first told to register your conversion details (전환 정보). The guide says Crack takes some time to verify the registered details.&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%2F3wltwj89dip50qb6z2z1.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%2F3wltwj89dip50qb6z2z1.webp" alt="The prompt shown after tapping 현금으로 바꾸기 (Convert to cash): conversion details only need to be registered once." width="800" height="573"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Cash-out starts at KRW 10,000 and pays on the 10th and 25th of each month
&lt;/h2&gt;

&lt;p&gt;Crack announced a revamp of its payout policy on 2026-07-29. &lt;strong&gt;From September 1, cash can be converted to money.&lt;/strong&gt; The name also changed from Wrtn cash to Crack cash. Exchanging cash for goods in the old cash store ended that day.&lt;/p&gt;

&lt;p&gt;The conversion terms are in the Crack cash guide (revision of 2026-09-03):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The minimum request is KRW 10,000, with no cap per request.&lt;/li&gt;
&lt;li&gt;There is no conversion fee.&lt;/li&gt;
&lt;li&gt;Payouts go out on the 10th and 25th of each month (the next business day if that date is a holiday), and you must apply at least 5 business days before a payout date.&lt;/li&gt;
&lt;li&gt;You register your name, resident registration number and a bank account in your own name, and submit copies of your ID and bankbook.&lt;/li&gt;
&lt;li&gt;Minors aged 14 or older also submit a guardian consent form, a family relation certificate and the guardian's ID. Their limits are KRW 2,000,000 per request and KRW 10,000,000 per month.&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/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5ir0op4b4uzyhndmxzvu.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%2F5ir0op4b4uzyhndmxzvu.webp" alt="The Crack cash screen, with the balance shown in won and the conversion-details and Convert to cash buttons." width="800" height="481"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The balance is shown in won, not in cash units. In its Super Week notice, Crack said one Cracker is worth KRW 1. But I found no sentence stating that 1 cash equals KRW 1. The amounts in this post assume 1 cash = KRW 1.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conversions of KRW 125,000 or less are not taxed under the statute
&lt;/h2&gt;

&lt;p&gt;Under Income Tax Act Art. 84, no tax is imposed when the other-income amount per payment is KRW 50,000 or less. Working backward with the 60% expense deduction, the boundary is a KRW 125,000 conversion. &lt;strong&gt;Up to KRW 125,000 the tax is KRW 0; at KRW 200,000, KRW 17,600 is deducted.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;However, Crack's documents don't say whether it treats one conversion as one payment under that article. The web code has no branch by amount either; it applies the same rate. The guide only says tax "may be" deducted.&lt;/p&gt;

&lt;p&gt;I don't recommend splitting conversions into small pieces to stay under the boundary. Without knowing how Crack counts payments, all you'd get is more requests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cash saved over several months is reported in one lump on the payment date
&lt;/h2&gt;

&lt;p&gt;Withholding happens on the day money is actually paid out. If you convert several months of cash at once, it is not split by the month it accrued. The guide says it is reported in a lump sum based on the payout date. It also tells you to keep that in mind when filing your comprehensive income tax return.&lt;/p&gt;

&lt;p&gt;So on the payment statement Crack files with the National Tax Service, the amount is grouped under a single payout date. &lt;strong&gt;The figures to check in May are your conversion history, not your accrual history.&lt;/strong&gt; If you note the amount and the deduction for every conversion, reconciling later is easy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Crack's 8.8% and May income tax: at KRW 3,000,000 of other-income amount or less, you can choose separate taxation
&lt;/h2&gt;

&lt;p&gt;Korea's comprehensive income tax return (종합소득세) for the previous year is filed in May. As long as Crack cash is treated as other income, which path you take depends on your total other-income amount for the year. &lt;strong&gt;If your other-income amount is KRW 3,000,000 or less, you can choose separate taxation.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That total also includes other withheld other income, such as lecture fees. Other income that the statute always taxes separately or always combines is left out of it (Income Tax Act Art. 14(3)8(a)).&lt;/p&gt;

&lt;p&gt;If Crack is your only such income and the 60% expense deduction applies, the boundary is KRW 7,500,000 of conversions. If your actual expenses exceed 60% and you deduct those instead, the boundary goes up (proviso to Enforcement Decree Art. 87(1-2)).&lt;/p&gt;

&lt;p&gt;If you choose separate taxation (분리과세), the withholding is final. If you have no other comprehensive income, you don't need to file in May (Art. 73(1)8). In return, you don't get back the tax already withheld.&lt;/p&gt;

&lt;p&gt;If you choose to combine (합산), it is added to your other income and recalculated at the basic progressive rates. I ran the numbers for someone with no income besides Crack. I included only the KRW 1,500,000 basic personal deduction and the KRW 70,000 standard tax credit:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Annual conversions (KRW)&lt;/th&gt;
&lt;th&gt;Other-income amount (KRW)&lt;/th&gt;
&lt;th&gt;National tax withheld (KRW)&lt;/th&gt;
&lt;th&gt;Separate taxation&lt;/th&gt;
&lt;th&gt;Tax determined if combined (KRW)&lt;/th&gt;
&lt;th&gt;National tax refunded (KRW)&lt;/th&gt;
&lt;th&gt;Refund incl. local tax (KRW)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1,000,000&lt;/td&gt;
&lt;td&gt;400,000&lt;/td&gt;
&lt;td&gt;80,000&lt;/td&gt;
&lt;td&gt;Available&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;80,000&lt;/td&gt;
&lt;td&gt;88,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7,500,000&lt;/td&gt;
&lt;td&gt;3,000,000&lt;/td&gt;
&lt;td&gt;600,000&lt;/td&gt;
&lt;td&gt;Available&lt;/td&gt;
&lt;td&gt;20,000&lt;/td&gt;
&lt;td&gt;580,000&lt;/td&gt;
&lt;td&gt;638,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20,000,000&lt;/td&gt;
&lt;td&gt;8,000,000&lt;/td&gt;
&lt;td&gt;1,600,000&lt;/td&gt;
&lt;td&gt;Not available, must combine&lt;/td&gt;
&lt;td&gt;320,000&lt;/td&gt;
&lt;td&gt;1,280,000&lt;/td&gt;
&lt;td&gt;1,408,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;50,000,000&lt;/td&gt;
&lt;td&gt;20,000,000&lt;/td&gt;
&lt;td&gt;4,000,000&lt;/td&gt;
&lt;td&gt;Not available, must combine&lt;/td&gt;
&lt;td&gt;1,445,000&lt;/td&gt;
&lt;td&gt;2,555,000&lt;/td&gt;
&lt;td&gt;2,810,500&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Once that total exceeds KRW 3,000,000, there is no choice. Combining is mandatory and you file by May 31. Even someone who converted KRW 50,000,000 in a year gets a refund if Crack is their only income. Withholding is 20% of the income amount. The tax on a KRW 18,500,000 tax base at the basic rates comes out lower than that.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you have a salary, combining costs you more from the 24% marginal bracket up
&lt;/h2&gt;

&lt;p&gt;If you have employment income, the other-income amount sits on top of it. Compare the 20% withholding with your marginal rate. I calculated for an employee who converted KRW 10,000,000 in a year, which is an other-income amount of KRW 4,000,000:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Existing tax base (KRW)&lt;/th&gt;
&lt;th&gt;Marginal rate&lt;/th&gt;
&lt;th&gt;National tax difference if combined (KRW)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;10,000,000&lt;/td&gt;
&lt;td&gt;6%&lt;/td&gt;
&lt;td&gt;Refund 560,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;30,000,000&lt;/td&gt;
&lt;td&gt;15%&lt;/td&gt;
&lt;td&gt;Refund 200,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;70,000,000&lt;/td&gt;
&lt;td&gt;24%&lt;/td&gt;
&lt;td&gt;Additional payment 160,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;120,000,000&lt;/td&gt;
&lt;td&gt;35%&lt;/td&gt;
&lt;td&gt;Additional payment 600,000&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If the added amount stays inside the 6% or 15% bracket, combining works in your favor. &lt;strong&gt;From the 24% bracket up, combining means paying more.&lt;/strong&gt; If the added amount crosses a bracket boundary, compare the tax with and without combining. When combining costs more and that eligible total is KRW 3,000,000 or less, ending it with separate taxation is the better choice.&lt;/p&gt;

&lt;p&gt;The KRW 4,000,000 in this example is over KRW 3,000,000, so combining is mandatory. This is a simple comparison that leaves out any change in deductions on the employment side.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creators paid out every month should check the business registration test
&lt;/h2&gt;

&lt;p&gt;Right now Crack handles cash conversions as other income. But the platform doesn't decide the income category. Income Tax Act Art. 19(1)21 does. &lt;strong&gt;Income from activity carried on continuously and repeatedly for profit is business income.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Korea's National Tax Service published a tax guide for YouTubers and other new types of business, &lt;a href="https://www.nts.go.kr/comm/nttFileDownload.do?fileKey=56d32ae3eff35468305a07d1085cb387" rel="noopener noreferrer"&gt;유튜버 등 신종업종 사업자 세무안내&lt;/a&gt; (PDF, Korean). It says that even without a matching industry code, doing the activity continuously and repeatedly creates an obligation to register as a business. The test is scale and frequency, not whether you have registered. A creator who gets paid out every month may well meet it.&lt;/p&gt;

&lt;p&gt;If it becomes business income, a few things change:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Necessary expenses are no longer a fixed 60%; they come from your books or the official expense rates.&lt;/li&gt;
&lt;li&gt;If you create alone, it may qualify as VAT-exempt personal services. The basis is VAT Act Art. 26(1)15 and Enforcement Decree Art. 42.&lt;/li&gt;
&lt;li&gt;Under the National Health Insurance dependent rules, business income is counted separately.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Health insurance dependents: KRW 20,000,000 a year; dependent tax deduction: KRW 1,000,000 of income amount
&lt;/h3&gt;

&lt;p&gt;If you're listed as a dependent on a family member's National Health Insurance (피부양자) or claimed as a dependent on their tax return (부양가족), this part may matter more. &lt;strong&gt;To stay a health insurance dependent, your total income must be KRW 20,000,000 a year or less.&lt;/strong&gt; Business income is looked at separately. With no business registration and KRW 5,000,000 a year or less, it is treated as zero.&lt;/p&gt;

&lt;p&gt;I couldn't confirm whether other income is counted as gross receipts or as income amount here. If it's counted as income amount, the boundary is KRW 50,000,000 of conversions.&lt;/p&gt;

&lt;p&gt;The basic deduction for a dependent requires a total income amount of KRW 1,000,000 or less. With Crack income only, that is KRW 2,500,000 of conversions. Whether amounts taxed separately count toward it is left as unverified.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding the withholding record for Crack payouts on Hometax
&lt;/h2&gt;

&lt;p&gt;Hometax (홈택스) is the National Tax Service's e-filing site. Type 원천징수영수증 (withholding receipt) into its integrated search (통합검색) and 14 menus come up. &lt;strong&gt;Only one of them is for the person receiving income:&lt;/strong&gt; 지급명세서(원천징수 내역)등 조회 (payment statement / withholding history lookup), under 나의 소득·연말정산 (my income and year-end settlement). The rest are for companies making payments.&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%2Fp1desagx13t9el6ffvsk.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%2Fp1desagx13t9el6ffvsk.webp" alt="Hometax integrated search results: the only menu for income earners is the payment statement lookup under 나의 소득·연말정산." width="800" height="272"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The May return is filed under 세금신고 (tax filing) → 종합소득세 신고 (comprehensive income tax return). If you missed the deadline and only want your refund, there is a separate 종합소득세 기한후 환급신고 (late refund filing for comprehensive income tax) menu. I couldn't confirm when Crack payouts show up on the screen after you log in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist before you convert your cash
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Prepare a copy of your ID and a copy of a bankbook in your own name. Minors need three additional guardian documents.&lt;/li&gt;
&lt;li&gt;Verifying conversion details takes time, so register them well ahead of the 5-business-day cutoff before a payout date.&lt;/li&gt;
&lt;li&gt;Every time you convert, save the tax deduction and deposit amounts shown on the screen.&lt;/li&gt;
&lt;li&gt;Before May of the following year, check the Crack payouts in Hometax's payment statement lookup.&lt;/li&gt;
&lt;li&gt;If your eligible other-income amount for the year (Crack included, always-separate items excluded) is KRW 3,000,000 or less, choose whichever of separate taxation or combining works in your favor. If it's more, file a combined return.&lt;/li&gt;
&lt;li&gt;If payouts keep coming every month, first ask your tax office or a tax accountant whether it counts as business income.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;The 8.8% Crack deducts on cash-out is other-income withholding, not a fee, and if your other income is small, filing in May can get most of it back. Record every conversion, check the payout on Hometax before May, and if you're paid out every month, ask a tax office or accountant whether it is business income.&lt;/p&gt;

&lt;h2&gt;
  
  
  Unverified
&lt;/h2&gt;

&lt;p&gt;This post is not tax advice. I couldn't confirm the items below from original documents or screens, so they are stated with conditions. The conclusion can change depending on your facts.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cash-to-won conversion: there is no official statement. There is only a balance shown in won and a notice saying one Cracker is worth KRW 1.&lt;/li&gt;
&lt;li&gt;Which item of other income applies: Crack hasn't said. Whether it's Art. 21(1)15 or item 19, the deemed expense is 60% either way, so the 8.8% is the same.&lt;/li&gt;
&lt;li&gt;Whether the minimum taxable threshold applies per conversion: not confirmed. The web code applies the same rate regardless of the amount.&lt;/li&gt;
&lt;li&gt;Whether it counts as business income: this is a question of fact. I found no industry code or official ruling for AI character creation.&lt;/li&gt;
&lt;li&gt;Whether it qualifies as VAT-exempt personal services, and whether the penalty for not registering applies: not confirmed.&lt;/li&gt;
&lt;li&gt;Expense rates for tax year 2026: they are published in April 2027. So I did not calculate the tax for the business-income case.&lt;/li&gt;
&lt;li&gt;How other income is counted for health insurance dependents: I couldn't confirm whether the basis is gross receipts or income amount.&lt;/li&gt;
&lt;li&gt;Whether separately taxed amounts count toward the dependent tax deduction: I couldn't settle it from the statute alone.&lt;/li&gt;
&lt;li&gt;When payouts appear on Hometax: unknown, since I didn't see the screen after logging in.&lt;/li&gt;
&lt;li&gt;The actual rate on the conversion screen: I couldn't bring the screen up because my account balance is 0. I don't know whether the server's &lt;code&gt;taxRate&lt;/code&gt; is 0.088.&lt;/li&gt;
&lt;li&gt;How long the first payout takes to arrive: I haven't seen a case from the September payouts.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Changelog
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Version&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1.0&lt;/td&gt;
&lt;td&gt;2026-09-30 First English version, adapted from the Korean original (Crack announcement and cash guide text, Income Tax Act articles cross-checked, conversion math reproduced, Crack cash screen checked)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Cover image: AI-generated (OpenAI Images)&lt;/p&gt;

&lt;p&gt;&lt;em&gt;AI disclosure: translated and edited with AI assistance (Claude) from my Korean original. Measurements were run with Claude Code under my supervision; I reviewed the result.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>tax</category>
      <category>korea</category>
      <category>creators</category>
      <category>ai</category>
    </item>
    <item>
      <title>Parsing VRM in Flutter without a package: vrm_dart fails on VRM 1.0 (LateInitializationError), so read the GLB chunks yourself</title>
      <dc:creator>Ilan Kim</dc:creator>
      <pubDate>Tue, 22 Sep 2026 08:20:59 +0000</pubDate>
      <link>https://dev.to/ilan_kim/parsing-vrm-in-flutter-without-a-package-vrmdart-fails-on-vrm-10-lateinitializationerror-so-e8l</link>
      <guid>https://dev.to/ilan_kim/parsing-vrm-in-flutter-without-a-package-vrmdart-fails-on-vrm-10-lateinitializationerror-so-e8l</guid>
      <description>&lt;p&gt;You do not need a package to read VRM metadata in Flutter. A VRM file is a glTF 2.0 GLB: a 12-byte header, then a JSON chunk carrying the model name, author, license, expressions, mesh list and the index of the thumbnail, then a BIN chunk carrying the thumbnail bytes. The one Dart library that exists, gatari/vrm_dart, throws &lt;code&gt;LateInitializationError&lt;/code&gt; on every VRM 1.0 file and, on VRM 0.x, returns a null thumbnail unless the image happens to be named "Thumbnail". The parser I ended up with is 101 lines of &lt;code&gt;dart:convert&lt;/code&gt; and &lt;code&gt;dart:typed_data&lt;/code&gt; and reads all of it on both versions.&lt;/p&gt;

&lt;p&gt;This is a full rewrite of a post I wrote in December 2024. It is for anyone who needs to show a VRM model's name, author and thumbnail inside a Flutter app. On 2026-09-22, on macOS Apple Silicon with Flutter 3.47.2 and Dart 3.13.2, I ran vrm_dart and a hand-written parser against five sample files, then dropped the parser into a Flutter package and ran &lt;code&gt;flutter test&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Feed a VRM 1.0 file to vrm_dart exactly the way the 2024 post did. &lt;code&gt;parse()&lt;/code&gt; returns, and the first access to the metadata throws:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;== Seed-san.vrm (10917800 B)
parse(): ok
vrmMeta access: LateError: LateInitializationError: Field 'vrmMeta' has not been initialized.
thumbnailByteData: null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The 2024 situation was this. The project had a Unity viewport and a Flutter UI, and the model list screen lived on the Flutter side, so the Flutter side had to read VRM metadata on its own. Unity has UniVRM and Unreal has VRM4U; Flutter had nothing. What I found was gatari/vrm_dart, a repository never published to pub. I copied two files into the project, and these three lines got me the thumbnail and part of the metadata:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;vrmByteData&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;File&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="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;readAsBytes&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;vrmParser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;VrmFileParser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ByteData&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sublistView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Uint8List&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fromList&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vrmByteData&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
&lt;span class="n"&gt;vrmParser&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The post ended with the regret that BlendShapeGroup and the mesh list were out of reach. Since then the ground has moved. VRoid Studio exports VRM 1.0 from v1.20.0, released 2023-03 (checked 2026-09-22), so 1.0 files are now common. vrm_dart has had no commits since 2021-10-13, and a pub.dev search for vrm still returns zero VRM packages.&lt;/p&gt;

&lt;p&gt;VRM 0.x files trip it up in two more ways. A file with no images at all, like the add-on test fixture &lt;code&gt;minimal.vrm&lt;/code&gt;, dies inside &lt;code&gt;parse()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;== minimal.vrm (4660 B)
parse(): _TypeError: type 'Null' is not a subtype of type 'List&amp;lt;dynamic&amp;gt;' in type cast
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And a 0.x file that does have a thumbnail still comes back with &lt;code&gt;thumbnailByteData&lt;/code&gt; null when the image is not named "Thumbnail", even though the metadata's &lt;code&gt;texture&lt;/code&gt; field points straight at it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the measurements show
&lt;/h2&gt;

&lt;p&gt;vrm_dart only ever looks at the extension named &lt;code&gt;VRM&lt;/code&gt;. VRM 1.0 stores everything under &lt;code&gt;VRMC_vrm&lt;/code&gt;, with a different metadata schema and different names for the main fields: &lt;code&gt;title&lt;/code&gt; became &lt;code&gt;name&lt;/code&gt;, &lt;code&gt;author&lt;/code&gt; became &lt;code&gt;authors&lt;/code&gt;, &lt;code&gt;licenseName&lt;/code&gt; became &lt;code&gt;licenseUrl&lt;/code&gt;, and the thumbnail moved from &lt;code&gt;texture&lt;/code&gt; to &lt;code&gt;thumbnailImage&lt;/code&gt;. With no branch for that, the &lt;code&gt;late&lt;/code&gt; field &lt;code&gt;vrmMeta&lt;/code&gt; is never assigned, and the first read throws.&lt;/p&gt;

&lt;p&gt;Start with the file structure. A VRM is a GLB, the binary container for glTF 2.0. The first 12 bytes are the header: the magic &lt;code&gt;glTF&lt;/code&gt;, version 2 and the total length, each a little-endian uint32. Chunks follow. Each chunk is a length and a type, 4 bytes each, then the payload. The standard chunk types are &lt;code&gt;JSON&lt;/code&gt; and &lt;code&gt;BIN&lt;/code&gt;; unknown types are skipped. All five sample files had chunk lengths that were multiples of 4. &lt;code&gt;minimal.vrm&lt;/code&gt; has a single JSON chunk and no BIN chunk at all.&lt;/p&gt;

&lt;p&gt;The VRM data lives under &lt;code&gt;extensions&lt;/code&gt; in the JSON chunk, and that is where 0.x and 1.0 diverge:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;VRM 0.x&lt;/th&gt;
&lt;th&gt;VRM 1.0&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Extension name&lt;/td&gt;
&lt;td&gt;&lt;code&gt;VRM&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;VRMC_vrm&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Model name&lt;/td&gt;
&lt;td&gt;&lt;code&gt;meta.title&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;meta.name&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Author&lt;/td&gt;
&lt;td&gt;&lt;code&gt;meta.author&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;meta.authors&lt;/code&gt; (array)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;License&lt;/td&gt;
&lt;td&gt;&lt;code&gt;meta.licenseName&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;meta.licenseUrl&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Thumbnail&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;meta.texture&lt;/code&gt; = index into &lt;code&gt;textures[]&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;meta.thumbnailImage&lt;/code&gt; = index into &lt;code&gt;images[]&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Expressions&lt;/td&gt;
&lt;td&gt;&lt;code&gt;blendShapeMaster.blendShapeGroups[]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;expressions.preset&lt;/code&gt; / &lt;code&gt;expressions.custom&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Meshes&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;meshes[]&lt;/code&gt; (plain glTF)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;meshes[]&lt;/code&gt; (plain glTF)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The thumbnail path is the sharpest difference. The 0.x schema defines &lt;code&gt;texture&lt;/code&gt; as "Thumbnail of VRM model", and because it is a texture index you go through &lt;code&gt;textures[i].source&lt;/code&gt; before you reach &lt;code&gt;images[]&lt;/code&gt;. The 1.0 schema defines &lt;code&gt;thumbnailImage&lt;/code&gt; as "The index to the thumbnail image of the model in gltf.images" (checked 2026-09-22). In 1.0, &lt;code&gt;name&lt;/code&gt;, &lt;code&gt;authors&lt;/code&gt; and &lt;code&gt;licenseUrl&lt;/code&gt; are required.&lt;/p&gt;

&lt;p&gt;vrm_dart takes neither path. It searches the &lt;code&gt;images&lt;/code&gt; array for an entry named "Thumbnail", so any other name is a miss, and it never reads &lt;code&gt;thumbnailImage&lt;/code&gt; at all. The third problem is casting: the generated code casts &lt;code&gt;images&lt;/code&gt; and &lt;code&gt;bufferViews&lt;/code&gt; to non-null lists, which is where &lt;code&gt;_TypeError&lt;/code&gt; comes from on a file that has neither key.&lt;/p&gt;

&lt;p&gt;I compared the two parsers on five samples. For 0.x, the CC0 add-on test fixtures &lt;code&gt;minimal.vrm&lt;/code&gt; and &lt;code&gt;triangle.vrm&lt;/code&gt;. There is no public 0.x sample with a thumbnail, so I built two with Blender 5.2.2 LTS and VRM add-on 4.7.1, attaching a 64×64 PNG to &lt;code&gt;triangle.vrm&lt;/code&gt;: one with the image named "Thumbnail", one named "swas_thumb". For 1.0, VirtualCast's &lt;code&gt;Seed-san.vrm&lt;/code&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%2F65xm13s7cwb2rabyhwqp.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%2F65xm13s7cwb2rabyhwqp.webp" alt="vrm_dart versus the direct parser across five VRM samples. vrm_dart reads everything only on the 0.x file whose image is named Thumbnail"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Sample&lt;/th&gt;
&lt;th&gt;Version&lt;/th&gt;
&lt;th&gt;vrm_dart meta&lt;/th&gt;
&lt;th&gt;vrm_dart thumbnail&lt;/th&gt;
&lt;th&gt;Direct parser&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;minimal.vrm&lt;/td&gt;
&lt;td&gt;0.x&lt;/td&gt;
&lt;td&gt;&lt;code&gt;_TypeError&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;_TypeError&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;meta, 17 expressions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;triangle.vrm&lt;/td&gt;
&lt;td&gt;0.x&lt;/td&gt;
&lt;td&gt;&lt;code&gt;_TypeError&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;_TypeError&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;meta, 17 expressions, 1 mesh&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;triangle-thumb-Thumbnail.vrm&lt;/td&gt;
&lt;td&gt;0.x&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;td&gt;317 B&lt;/td&gt;
&lt;td&gt;meta, thumbnail, 17 expressions, 1 mesh&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;triangle-thumb-swas_thumb.vrm&lt;/td&gt;
&lt;td&gt;0.x&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;td&gt;null&lt;/td&gt;
&lt;td&gt;meta, thumbnail, 17 expressions, 1 mesh&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Seed-san.vrm&lt;/td&gt;
&lt;td&gt;1.0&lt;/td&gt;
&lt;td&gt;&lt;code&gt;LateInitializationError&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;null&lt;/td&gt;
&lt;td&gt;meta, thumbnail, 18 expressions, 5 meshes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;vrm_dart reads exactly one row completely: the 0.x file whose image is named "Thumbnail". That is presumably the kind of file the 2024 post happened to succeed on. The direct parser reads all five. Seed-san's thumbnail came out as a 512×512 PNG of 192,765 bytes, and its meshes are hair, hair_tail, head, robo_arm and wear.&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%2Fai4ynhwg737oz7wb8xcd.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%2Fai4ynhwg737oz7wb8xcd.webp" alt="Console output from reading Seed-san.vrm with the direct parser: VRMC_vrm metadata, thumbnail, 18 expressions and 5 meshes"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Expression names changed between versions too. The 1.0 spec's expressions document lists the renames (checked 2026-09-22), and the 17 names in the 0.x samples and 18 in the 1.0 sample match this table:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;VRM 0.x&lt;/th&gt;
&lt;th&gt;VRM 1.0&lt;/th&gt;
&lt;th&gt;Note&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;joy&lt;/td&gt;
&lt;td&gt;happy&lt;/td&gt;
&lt;td&gt;renamed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sorrow&lt;/td&gt;
&lt;td&gt;sad&lt;/td&gt;
&lt;td&gt;renamed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fun&lt;/td&gt;
&lt;td&gt;relaxed&lt;/td&gt;
&lt;td&gt;renamed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;a, i, u, e, o&lt;/td&gt;
&lt;td&gt;aa, ih, ou, ee, oh&lt;/td&gt;
&lt;td&gt;lip sync&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;blink_l, blink_r&lt;/td&gt;
&lt;td&gt;blinkLeft, blinkRight&lt;/td&gt;
&lt;td&gt;spelling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;lookup, lookdown, lookleft, lookright&lt;/td&gt;
&lt;td&gt;lookUp, lookDown, lookLeft, lookRight&lt;/td&gt;
&lt;td&gt;spelling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;surprised&lt;/td&gt;
&lt;td&gt;new in 1.0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

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

&lt;ol&gt;
&lt;li&gt;Read the GLB header and chunks. &lt;code&gt;ByteData&lt;/code&gt; from &lt;code&gt;dart:typed_data&lt;/code&gt; is enough, and the JSON chunk goes through &lt;code&gt;utf8.decode&lt;/code&gt; then &lt;code&gt;jsonDecode&lt;/code&gt;:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;   &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ByteData&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sublistView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bytes&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;d&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getUint32&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="n"&gt;Endian&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;little&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mh"&gt;0x46546C67&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;FormatException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'not a GLB'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
   &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getUint32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Endian&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;little&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
   &lt;span class="kt"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;dynamic&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;Uint8List&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;bin&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
   &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="n"&gt;off&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
   &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;off&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
     &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;len&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getUint32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;off&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Endian&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;little&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
     &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getUint32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;off&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;Endian&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;little&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
     &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Uint8List&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sublistView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;off&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;off&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;len&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;type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mh"&gt;0x4E4F534A&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jsonDecode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;utf8&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// 'JSON'&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;type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mh"&gt;0x004E4942&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;bin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 'BIN'&lt;/span&gt;
     &lt;span class="n"&gt;off&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;off&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;len&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;&amp;amp;&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="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;root&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;FormatException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'no JSON chunk'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Tell the versions apart from &lt;code&gt;extensions&lt;/code&gt;. &lt;code&gt;VRMC_vrm&lt;/code&gt; present means 1.0, &lt;code&gt;VRM&lt;/code&gt; means 0.x, and neither means it is a plain glTF, not a VRM:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;   &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;ext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'extensions'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kt"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;dynamic&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;dynamic&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;{};&lt;/span&gt;
   &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;is10&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ext&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;containsKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'VRMC_vrm'&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="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;is10&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;ext&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;containsKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'VRM'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
     &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;FormatException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'glTF but neither VRM (0.x) nor VRMC_vrm (1.0) extension'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
   &lt;span class="p"&gt;}&lt;/span&gt;
   &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;is10&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;ext&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'VRMC_vrm'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ext&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'VRM'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kt"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;dynamic&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;
   &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;meta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'meta'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kt"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;dynamic&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Read the metadata with the version's field names: &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;author&lt;/code&gt; and &lt;code&gt;licenseName&lt;/code&gt; for 0.x; &lt;code&gt;name&lt;/code&gt;, &lt;code&gt;authors&lt;/code&gt; and &lt;code&gt;licenseUrl&lt;/code&gt; for 1.0. If a 1.0 file's &lt;code&gt;creditNotation&lt;/code&gt; is &lt;code&gt;required&lt;/code&gt;, the specification obliges whoever uses the model to display its credit. It does not say where or in what form, so that part is your app's decision.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Resolve the thumbnail along the two separate paths. The image bytes are a slice of the BIN chunk given by &lt;code&gt;byteOffset&lt;/code&gt; and &lt;code&gt;byteLength&lt;/code&gt; of &lt;code&gt;bufferViews[i]&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;   &lt;span class="n"&gt;Uint8List&lt;/span&gt; &lt;span class="nf"&gt;bufferView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
     &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;bv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'bufferViews'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kt"&gt;List&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="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'byteOffset'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kt"&gt;int&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;Uint8List&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sublistView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bin&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'byteLength'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
   &lt;span class="p"&gt;}&lt;/span&gt;
   &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;imageIndex&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;is10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
     &lt;span class="n"&gt;imageIndex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'thumbnailImage'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kt"&gt;int&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="k"&gt;else&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;meta&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'texture'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'texture'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
     &lt;span class="n"&gt;imageIndex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'textures'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'texture'&lt;/span&gt;&lt;span class="p"&gt;]][&lt;/span&gt;&lt;span class="s"&gt;'source'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
   &lt;span class="p"&gt;}&lt;/span&gt;
   &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;image&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;imageIndex&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'images'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="n"&gt;imageIndex&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
   &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;thumbnail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'bufferView'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;bufferView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'bufferView'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Walk expressions and meshes with the right keys. On 0.x it is &lt;code&gt;presetName&lt;/code&gt; and &lt;code&gt;name&lt;/code&gt; inside &lt;code&gt;blendShapeMaster.blendShapeGroups&lt;/code&gt;; on 1.0 it is the keys of &lt;code&gt;expressions.preset&lt;/code&gt; and &lt;code&gt;expressions.custom&lt;/code&gt;. Meshes are plain glTF: &lt;code&gt;name&lt;/code&gt; and &lt;code&gt;primitives.length&lt;/code&gt; from &lt;code&gt;meshes[]&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Keep &lt;code&gt;dart:io&lt;/code&gt; out of the parser when it goes into a Flutter package. Take a single &lt;code&gt;Uint8List&lt;/code&gt;, so a file path, &lt;code&gt;rootBundle&lt;/code&gt; and &lt;code&gt;file_picker&lt;/code&gt; bytes all go through the same function. I put it into a &lt;code&gt;flutter create --template=package&lt;/code&gt; project this way; all 4 tests passed and the dependency count added is zero.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Treat image-less files as normal. &lt;code&gt;images&lt;/code&gt; and &lt;code&gt;bufferViews&lt;/code&gt; may be absent, and the BIN chunk may be absent too. The code above lets all of that fall through as &lt;code&gt;null&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The Blender side of this is written up separately in &lt;a href="https://dev.to/ilan_kim/blender-5x-and-the-vrm-add-on-52-lts-needs-440-53-alpha-blocks-even-471-4l74"&gt;Blender 5.x and the VRM add-on: 5.2 LTS needs 4.4.0+, 5.3 alpha blocks even 4.7.1&lt;/a&gt;. The 0.x thumbnail samples in this post were built with that post's setup.&lt;/p&gt;

&lt;p&gt;Images: both are the measurement logs (&lt;code&gt;evidence/parse-glb.log&lt;/code&gt; and &lt;code&gt;evidence/vrm-dart-legacy.log&lt;/code&gt;) redrawn as a table and a console view. The 1.0 sample is Seed-san by VirtualCast, Inc., under the VRM Public License 1.0.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;Skip the package search: the VRM metadata you want is one JSON chunk behind a 12-byte header, and the only real work is branching on &lt;code&gt;VRM&lt;/code&gt; versus &lt;code&gt;VRMC_vrm&lt;/code&gt;. If you must keep vrm_dart, know that it only works on 0.x files whose thumbnail image is literally named "Thumbnail".&lt;/p&gt;

&lt;h2&gt;
  
  
  Unverified
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Whether 0.x files exported by VRoid Studio or UniVRM actually name the thumbnail image "Thumbnail": the file the 2024 post read no longer exists, and this post's 0.x thumbnail samples were made with the Blender add-on.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;dart:io&lt;/code&gt;-free parser on mobile and web Flutter, fed through &lt;code&gt;rootBundle&lt;/code&gt; or &lt;code&gt;file_picker&lt;/code&gt;: I only ran &lt;code&gt;flutter test&lt;/code&gt; on the host VM.&lt;/li&gt;
&lt;li&gt;Malformed files whose JSON chunk is not padded to 4 bytes: all five samples were multiples of 4. The code rounds the next chunk offset up to a multiple of 4, but I did not test such a file.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Changelog
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Version&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1.0&lt;/td&gt;
&lt;td&gt;2024-12-03 original Korean post (partial VRM 0.x meta and thumbnail via vrm_dart)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2.0&lt;/td&gt;
&lt;td&gt;2026-09-22 full rewrite (vrm_dart reproduced on Dart 3.13.2, zero-dependency direct parser measured on 5 samples across 0.x and 1.0, 4 flutter tests)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2.2&lt;/td&gt;
&lt;td&gt;2026-09-30 added the AI disclosure line (no content change)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;AI disclosure: translated and edited with AI assistance (Claude) from my Korean original. Measurements were run with Claude Code under my supervision; I reviewed the result.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>dart</category>
      <category>vrm</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Blender 5.x and the VRM add-on: 5.2 LTS needs 4.4.0+, 5.3 alpha blocks even 4.7.1 (blender_version_max is exclusive)</title>
      <dc:creator>Ilan Kim</dc:creator>
      <pubDate>Tue, 22 Sep 2026 06:45:52 +0000</pubDate>
      <link>https://dev.to/ilan_kim/blender-5x-and-the-vrm-add-on-52-lts-needs-440-53-alpha-blocks-even-471-4l74</link>
      <guid>https://dev.to/ilan_kim/blender-5x-and-the-vrm-add-on-52-lts-needs-440-53-alpha-blocks-even-471-4l74</guid>
      <description>&lt;p&gt;On Blender 5.2 LTS, VRM Addon for Blender only enables from 4.4.0 up, and on 5.3 alpha even the latest 4.7.1 refuses to enable. The cause is &lt;code&gt;blender_version_max&lt;/code&gt; in the extension manifest, which is an exclusive bound: it names the first Blender version the extension does &lt;strong&gt;not&lt;/strong&gt; support, not the last one it does. For 4.3.0 and newer, every combination the manifest gate blocks still enables through the legacy add-on zip, which has no version gate at all. The 3.9.0 line is the exception: it fails on both paths, because Blender 5.0 removed an API it imports.&lt;/p&gt;

&lt;p&gt;This is for anyone who upgraded to Blender 5.x and found the VRM add-on missing from the panel or refusing to switch on. On 2026-09-22, on macOS Apple Silicon, I installed four add-on versions into four Blender versions two different ways and ran all 32 cells headless. The Blender builds were 5.0.1, 5.1.2, 5.2.2 LTS and 5.3.0 alpha; the add-on versions were 3.9.0, 4.3.0, 4.4.0 and 4.7.1.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Drop the 4.3.0 extension zip into Blender 5.2.2 LTS. The install reports success, and then the add-on never turns on. The console has the reason:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ERROR vrm: This Blender version (5.2.2) must be less than the maximum version (5.2.0)
Package was expected to be installed but not found: vrm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On 5.3.0 alpha the latest 4.7.1 hits the same wall, with only the version in parentheses changing to &lt;code&gt;(5.3.0)&lt;/code&gt;. Inside the app it is quieter than that: the Extensions panel shows no error, because the VRM entry is not listed at all.&lt;/p&gt;

&lt;p&gt;Since Blender 4.2, add-ons ship as extensions, and an extension zip carries a &lt;code&gt;blender_manifest.toml&lt;/code&gt; at its root declaring &lt;code&gt;blender_version_min&lt;/code&gt; and &lt;code&gt;blender_version_max&lt;/code&gt;. When those values do not match the running Blender, the files unpack fine and activation is refused.&lt;/p&gt;

&lt;p&gt;VRM Addon for Blender publishes two zips per release:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Extension: &lt;code&gt;VRM_Addon_for_Blender-Extension-4_7_1.zip&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Legacy add-on: &lt;code&gt;VRM_Addon_for_Blender-4_7_1.zip&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The two behave differently on 5.x, and that difference is the point of this post.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the measurements show
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;blender_version_max&lt;/code&gt; is not the last supported version. It is the first unsupported one. The manifest example in the Blender manual says so outright (checked 2026-09-22):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="c"&gt;# Optional: Blender version that the extension does not support, earlier versions are supported.&lt;/span&gt;
&lt;span class="py"&gt;blender_version_max&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"5.1.0"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The wording of the runtime error, &lt;code&gt;must be less than&lt;/code&gt;, means the same thing.&lt;/p&gt;

&lt;p&gt;So 4.3.0 declaring &lt;code&gt;blender_version_max = "5.2.0"&lt;/code&gt; is a statement that it supports up to 5.1.x. From 5.2.0 onward it is blocked whatever the patch number. The latest 4.7.1 declares &lt;code&gt;5.3.0&lt;/code&gt;, which covers 5.2 LTS and stops short of 5.3 alpha.&lt;/p&gt;

&lt;p&gt;I downloaded the manifest from all 71 release tags to find the boundaries. &lt;code&gt;blender_version_min&lt;/code&gt; is &lt;code&gt;4.2.0&lt;/code&gt; on every tag. Only &lt;code&gt;blender_version_max&lt;/code&gt; ever moved, and it moved four times:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;code&gt;blender_version_max&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;Add-on range&lt;/th&gt;
&lt;th&gt;First tag&lt;/th&gt;
&lt;th&gt;Blender it enables on&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;5.0.0&lt;/td&gt;
&lt;td&gt;v3.9.0 – v3.15.0&lt;/td&gt;
&lt;td&gt;2025-07-19&lt;/td&gt;
&lt;td&gt;4.2 – 4.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5.1.0&lt;/td&gt;
&lt;td&gt;v3.16.0 – v3.21.1&lt;/td&gt;
&lt;td&gt;2025-11-08&lt;/td&gt;
&lt;td&gt;4.2 – 5.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5.2.0&lt;/td&gt;
&lt;td&gt;v3.22.0 – v4.3.3&lt;/td&gt;
&lt;td&gt;2026-03-16&lt;/td&gt;
&lt;td&gt;4.2 – 5.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5.3.0&lt;/td&gt;
&lt;td&gt;v4.4.0 – v4.7.1&lt;/td&gt;
&lt;td&gt;2026-07-11&lt;/td&gt;
&lt;td&gt;4.2 – 5.2 LTS&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Sampling only the x.y.0 tags gets the boundaries wrong. Looking at v4.0.0 alone hid the entire 5.1.0 group of 22 tags. The "5.2 LTS needs 4.4.0 or newer" conclusion comes out of this table.&lt;/p&gt;

&lt;p&gt;Then I checked what actually enables, across 32 cells. Each cell got a fresh &lt;code&gt;BLENDER_USER_RESOURCES&lt;/code&gt; directory so it could not inherit state from a previous cell or from my own Blender config. O means the add-on enabled &lt;strong&gt;and&lt;/strong&gt; the roundtrip passed, where the roundtrip is importing a VRM file, exporting it, and importing the export again, for both a VRM 0.x and a VRM 1.0 sample.&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%2Ffbb3qcp6jcx9eyuu6tye.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%2Ffbb3qcp6jcx9eyuu6tye.webp" alt="Compatibility matrix for extension installs versus legacy add-on installs. The same zip diverges on 5.2.2 and 5.3.0 alpha" width="799" height="547"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Extension install:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Add-on \ Blender&lt;/th&gt;
&lt;th&gt;5.0.1&lt;/th&gt;
&lt;th&gt;5.1.2&lt;/th&gt;
&lt;th&gt;5.2.2 LTS&lt;/th&gt;
&lt;th&gt;5.3.0 alpha&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;3.9.0&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4.3.0&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4.4.0&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4.7.1&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Legacy add-on install:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Add-on \ Blender&lt;/th&gt;
&lt;th&gt;5.0.1&lt;/th&gt;
&lt;th&gt;5.1.2&lt;/th&gt;
&lt;th&gt;5.2.2 LTS&lt;/th&gt;
&lt;th&gt;5.3.0 alpha&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;3.9.0&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4.3.0&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4.4.0&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4.7.1&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The legacy path has no version gate. 4.3.0 enabled on 5.2.2 and on 5.3.0 alpha and passed the roundtrip in both. That means the direct cause of the extension being refused on the spot is the manifest gate. It does not mean those combinations are free of other API incompatibilities — this run only went as far as activation plus a two-file roundtrip.&lt;/p&gt;

&lt;p&gt;Of the four versions I tested, only the 3.9.0 line fails to activate at all. Install it the legacy way and it still raises during module load:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ImportError: cannot import name 'ShaderNodeTexPointDensity' from 'bpy.types' (unknown location)
This add-on is not compatible with Blender version 5.0 or later. Your current version is 5.2.2.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Blender 5.0 removed &lt;code&gt;bpy.types.ShaderNodeTexPointDensity&lt;/code&gt;, 3.9.0 imports that type at module load, and the add-on carries its own guard that prints the second line. There is no install path that makes this combination work.&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%2Fmlq1ng3uwnefb92xbtl0.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%2Fmlq1ng3uwnefb92xbtl0.webp" alt="Console output from putting the 4.3.0 extension zip into Blender 5.2.2 LTS, showing must be less than the maximum version (5.2.0)" width="799" height="547"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The silence in the Extensions panel has its own explanation. The panel queries the &lt;code&gt;extensions.blender.org&lt;/code&gt; API filtered by Blender version, and the filtering happens server side. Ask with &lt;code&gt;blender_version=5.2.2&lt;/code&gt; and you get vrm 4.7.1 back; ask with &lt;code&gt;5.3.0&lt;/code&gt; and there is no vrm entry in the response at all (checked 2026-09-22). The panel is not hiding an error, it is being handed an empty result.&lt;/p&gt;

&lt;p&gt;The roundtrip passed in all 20 cells that enabled. Two samples were used: &lt;code&gt;minimal.vrm&lt;/code&gt; for VRM 0.x, which is CC0, and VirtualCast's &lt;code&gt;Seed-san.vrm&lt;/code&gt; for VRM 1.0, licensed under the VRM Public License 1.0. Exporting Seed-san grows it from 10,917,800 bytes to somewhere between 16,549,208 and 16,551,476 bytes — the exact figure varies slightly per combination, and all of them land near 152% of the original. I did not investigate why it grows. Whether it is related to the texture duplication issue #1114 is a separate question I have not answered.&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;Check your Blender version first. On 5.2 LTS you need add-on 4.4.0 or newer. On 5.0 and 5.1 the latest 4.7.1 enables fine. If you are pinned to an older add-on, read the right-hand column of the boundary table above.&lt;/li&gt;
&lt;li&gt;If VRM is missing from the Extensions panel, grab the extension zip from the GitHub releases directly — &lt;code&gt;VRM_Addon_for_Blender-Extension-&amp;lt;version&amp;gt;.zip&lt;/code&gt; — and use Install from Disk. The panel only ever offers the single latest version.&lt;/li&gt;
&lt;li&gt;If the extension is refused with &lt;code&gt;must be less than the maximum version&lt;/code&gt;, the legacy zip is a way through. Install &lt;code&gt;VRM_Addon_for_Blender-&amp;lt;version&amp;gt;.zip&lt;/code&gt; from the same release as an add-on rather than an extension. It is also the only way to run 4.7.1 on 5.3 alpha. Be aware that this puts you outside the range the maintainer declares support for; if something breaks, moving the add-on to a version inside the supported range is the first thing to try.&lt;/li&gt;
&lt;li&gt;The top-level folder name inside the legacy zip changes between versions. 3.9.0 unpacks to &lt;code&gt;VRM_Addon_for_Blender-3_9_0&lt;/code&gt;, and the 4.x releases unpack to &lt;code&gt;VRM_Addon_for_Blender-release&lt;/code&gt;. If you are enabling it from a script, discover the module with &lt;code&gt;addon_utils.modules()&lt;/code&gt; rather than hardcoding the name.&lt;/li&gt;
&lt;li&gt;Give up on the 3.9.0 line under 5.x. &lt;code&gt;ShaderNodeTexPointDensity&lt;/code&gt; is gone and no install path gets around it.&lt;/li&gt;
&lt;li&gt;If you took the legacy route, export a VRM and import it back. That roundtrip is as far as this measurement went.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Reading VRM files from a Flutter app rather than Blender is written up separately in &lt;a href="https://dev.to/ilan_kim/parsing-vrm-in-flutter-without-a-package-vrmdart-fails-on-vrm-10-lateinitializationerror-so-e8l"&gt;Parsing VRM in Flutter without a package&lt;/a&gt;. The VRM 0.x thumbnail samples in that post were built with the 5.2.2 LTS and 4.7.1 combination from this one.&lt;/p&gt;

&lt;p&gt;Images: both are the measurement logs (&lt;code&gt;evidence/matrix-install.log&lt;/code&gt;) redrawn as a table and a console view.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;Read &lt;code&gt;blender_version_max&lt;/code&gt; as the first version that is excluded, and pick the add-on from the boundary table rather than from the panel. The legacy zip bypasses the gate, but bypassing a declared support range is a workaround, not a fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  Unverified
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Whether combinations enabled via the legacy bypass are fully functional: only the roundtrip was checked. Spring bones and the expression editing panels were not.&lt;/li&gt;
&lt;li&gt;Windows and Linux behaviour: this ran on a single macOS Apple Silicon machine. The manifest gate is platform independent, but I could not confirm that 3.9.0 raises the same message elsewhere.&lt;/li&gt;
&lt;li&gt;When the add-on will support Blender 5.3 after its stable release: these numbers are from 5.3.0 alpha. If &lt;code&gt;blender_version_max&lt;/code&gt; moves to &lt;code&gt;5.4.0&lt;/code&gt;, the 5.3 column changes.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Changelog
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Version&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1.0&lt;/td&gt;
&lt;td&gt;2026-09-22 first version (Blender 5.0.1 – 5.3.0 alpha x add-on 3.9.0 – 4.7.1, 32 cells across extension and legacy installs)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1.2&lt;/td&gt;
&lt;td&gt;2026-09-22 added the cross-link to the Flutter VRM parsing post&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1.3&lt;/td&gt;
&lt;td&gt;2026-09-30 added the AI disclosure line (no content change)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;AI disclosure: translated and edited with AI assistance (Claude) from my Korean original. Measurements were run with Claude Code under my supervision; I reviewed the result.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>blender</category>
      <category>python</category>
      <category>vrm</category>
      <category>tooling</category>
    </item>
    <item>
      <title>Calling the NovelAI V5 API directly: nai-diffusion-5-full request body, params_version 4/3/1/0 identical, missing v4_prompt is a 500</title>
      <dc:creator>Ilan Kim</dc:creator>
      <pubDate>Mon, 14 Sep 2026 01:09:22 +0000</pubDate>
      <link>https://dev.to/ilan_kim/calling-the-novelai-v5-api-directly-nai-diffusion-5-full-request-body-paramsversion-4310-133a</link>
      <guid>https://dev.to/ilan_kim/calling-the-novelai-v5-api-directly-nai-diffusion-5-full-request-body-paramsversion-4310-133a</guid>
      <description>&lt;p&gt;&lt;code&gt;nai-diffusion-5-full&lt;/code&gt; takes the same body skeleton V4 took, posted to &lt;code&gt;https://image.novelai.net/ai/generate-image&lt;/code&gt;, and &lt;code&gt;params_version&lt;/code&gt; — the one field every third-party tool fills in differently — returns a pixel-for-pixel identical image whether you send 4, 3, 1, or 0 with this body skeleton. JSON and multipart bodies gave the same result too. What does break it: drop either &lt;code&gt;v4_prompt&lt;/code&gt; or &lt;code&gt;v4_negative_prompt&lt;/code&gt; and you get &lt;code&gt;500 Internal Server Error&lt;/code&gt; rather than a 400, set &lt;code&gt;Content-Type&lt;/code&gt; by hand on a &lt;code&gt;FormData&lt;/code&gt; body and you get a &lt;code&gt;missing multipart boundary&lt;/code&gt; 400, and post to the old host and you get a &lt;code&gt;model must be a valid enum value&lt;/code&gt; 400.&lt;/p&gt;

&lt;p&gt;This is for people calling NovelAI V5 from their own scripts rather than through a ComfyUI node or a GUI like NAIA (a community-built desktop front end for NovelAI). On 2026-09-14, on macOS 26.5.2 with Node v22.22.3 and an Opus account (&lt;code&gt;tier: 3&lt;/code&gt;), I generated six images inside the free band and compared request formats, error responses, and balance changes; I also read the strings out of the official web client bundle the same day. Two terms if NovelAI is new to you: Anlas is NovelAI's generation credit, and Opus is the top subscription tier, which generates one image at a time at the common sizes and up to 28 steps without spending any, as long as no base image is involved and the allowance has not run out. The policy and the per-size cost table are in &lt;a href="https://dev.to/ilan_kim/novelai-v5-on-opus-usage-limits-the-2026-09-21-subscription-anlas-reset-and-the-apinovelainet-567l"&gt;the earlier post&lt;/a&gt;, the measurements through a ComfyUI node are in &lt;a href="https://dev.to/ilan_kim/running-novelai-v5-in-comfyui-nai-diffusion-5-full-works-and-the-error-fetching-anlas-400-is-a-18gl"&gt;the one after it&lt;/a&gt;, and this post covers only the request contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;If you look at third-party tools' request bodies to work out how to call V5 yourself, the &lt;code&gt;params_version&lt;/code&gt; value is different in every one of them. ComfyUI_NAIDGenerator, a ComfyUI node for NovelAI, sends &lt;code&gt;1&lt;/code&gt;. The README of ComfyUI_RS_NAI_API_Request, another ComfyUI node, says "V5 requests use params_version: 4 while older models keep params_version: 3". The changelog of NAIWeaver, a standalone NovelAI client, says "send params_version: 4" (all checked 2026-09-14). The official OpenAPI document has no &lt;code&gt;required&lt;/code&gt; array and no &lt;code&gt;nai-diffusion-5&lt;/code&gt; string, so it cannot settle which one is right.&lt;/p&gt;

&lt;p&gt;From there, nudging the body around gets you three responses. All three are verbatim from this round of testing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST https://api.novelai.net/ai/generate-image  (old host)
400 {"statusCode":400,"message":"model must be a valid enum value"}

POST https://image.novelai.net/ai/generate-image  (v4_prompt or v4_negative_prompt omitted)
500 {"statusCode":500,"message":"Internal Server Error"}

POST https://image.novelai.net/ai/generate-image  (FormData + Content-Type: multipart/form-data set by hand)
400 {"statusCode":400,"message":"missing multipart boundary"}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first one has different wording from the "update to the image URL" 400 the balance lookup returns. The old host accepts the generation request itself but does not know the V5 model ID, so from the message alone it looks like you typed the model ID wrong.&lt;/p&gt;

&lt;p&gt;The second is the confusing one. It is a 500 rather than a 400-class validation error, so it reads like a server outage, but the cause is your request body. The third one appears when you hand Node's &lt;code&gt;fetch&lt;/code&gt; a &lt;code&gt;FormData&lt;/code&gt; body and set the header yourself, because the boundary &lt;code&gt;undici&lt;/code&gt; would have attached automatically disappears.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the measurements show
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;params_version&lt;/code&gt; does not change the result with this body skeleton. Same prompt, seed &lt;code&gt;1234567890&lt;/code&gt;, 832×1216, 28 steps, one image per value:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;code&gt;params_version&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;Response&lt;/th&gt;
&lt;th&gt;Response time&lt;/th&gt;
&lt;th&gt;PNG &lt;code&gt;Source&lt;/code&gt;
&lt;/th&gt;
&lt;th&gt;Anlas&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;4 (official web client)&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;2.5 s&lt;/td&gt;
&lt;td&gt;&lt;code&gt;NovelAI Diffusion V5 0ADF9AB7&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;9,930 → 9,930&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3 (the value said to be for older models)&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;1.8 s&lt;/td&gt;
&lt;td&gt;same&lt;/td&gt;
&lt;td&gt;no change&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1 (ComfyUI_NAIDGenerator)&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;1.9 s&lt;/td&gt;
&lt;td&gt;same&lt;/td&gt;
&lt;td&gt;no change&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0 (a value no tool uses)&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;1.8 s&lt;/td&gt;
&lt;td&gt;same&lt;/td&gt;
&lt;td&gt;no change&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Comparing the four images pixel by pixel with Pillow gives a mean absolute difference of 0 and 0% of pixels changed, so they are completely identical. The &lt;code&gt;Comment&lt;/code&gt; metadata of the returned PNGs has no &lt;code&gt;params_version&lt;/code&gt; field either. If the value were read and acted on, 0 should at least have been rejected, so with this skeleton it has to be treated as ignored. Whether that also holds for other field combinations — character prompts, inpainting and so on — I did not check.&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%2Ftwdmardaaceuag9wtf94.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%2Ftwdmardaaceuag9wtf94.webp" alt="A contact sheet placing the four images received with params_version 4, 3, 1, and 0 side by side; all four are identical down to the pixel" width="800" height="288"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So where does &lt;code&gt;4&lt;/code&gt; come from? In the novelai.net bundle I pulled on 2026-09-14 (&lt;code&gt;_app-b7172cc1a6a0b340.js&lt;/code&gt;), the default parameters in the &lt;code&gt;nai-diffusion-5-full&lt;/code&gt; branch start with &lt;code&gt;params_version:4&lt;/code&gt;, and there is migration code that raises stored settings to 4 if &lt;code&gt;params_version&amp;lt;4&lt;/code&gt;. The string &lt;code&gt;v5_prompt&lt;/code&gt; appears 0 times; &lt;code&gt;v4_prompt&lt;/code&gt; appears twice.&lt;/p&gt;

&lt;p&gt;The transport format makes no difference to the result either. Sending the &lt;code&gt;params_version: 1&lt;/code&gt; body as JSON and sending it as a JSON blob in the &lt;code&gt;request&lt;/code&gt; part of a &lt;code&gt;FormData&lt;/code&gt; both returned 200 with identical pixels. The debug toggle text in that same bundle describes the default as "Image generation requests will be sent as multipart form data." and the checked state as "JSON (pre-multipart-form format)", so multipart is the official client's default and JSON survives as the legacy format.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Transport&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;Content-Type&lt;/code&gt; handling&lt;/th&gt;
&lt;th&gt;Response&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;JSON&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;application/json&lt;/code&gt; set explicitly&lt;/td&gt;
&lt;td&gt;200, 917,294-byte zip&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;multipart&lt;/td&gt;
&lt;td&gt;header not set (automatic boundary)&lt;/td&gt;
&lt;td&gt;200, pixels identical to JSON&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;multipart&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;multipart/form-data&lt;/code&gt; set by hand&lt;/td&gt;
&lt;td&gt;400 &lt;code&gt;missing multipart boundary&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;You cannot leave out &lt;code&gt;v4_prompt&lt;/code&gt; or &lt;code&gt;v4_negative_prompt&lt;/code&gt;. With everything else left alone, a body with both keys removed, a body with only &lt;code&gt;v4_prompt&lt;/code&gt; removed, and a body with only &lt;code&gt;v4_negative_prompt&lt;/code&gt; removed all came back with the same 500, and none of them moved the balance. Together with the absence of &lt;code&gt;v5_prompt&lt;/code&gt; in the bundle, I take this to mean V5 still requires the V4 structure as it is.&lt;/p&gt;

&lt;p&gt;Authentication errors all arrive lumped into a single 401. Leave the &lt;code&gt;Authorization&lt;/code&gt; header out entirely, or put a space inside the token value, and the response is the same &lt;code&gt;401 {"statusCode":401,"message":"Unauthorized"}&lt;/code&gt; either way. Since the response cannot tell you whether the token string is mangled or the header is missing, don't print the value: check its length and whether it is unchanged by &lt;code&gt;trim()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For the free band, this adds the landscape size to the table in the earlier post. 1216×832 (pixel product 1,011,712) at 28 steps, one image, was also 0 Anlas, and across six generations &lt;code&gt;usage.percent&lt;/code&gt; did not move from 87. The battery reading is an integer, which means per-image consumption below 1% is invisible in it.&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%2Fe7zljdpfdowslshd6836.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%2Fe7zljdpfdowslshd6836.webp" alt="A V5 Full sample received at the 1216×832 landscape size; one image at 28 steps, so 0 Anlas" width="799" height="547"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I did not find an API that quotes the price up front this time. &lt;code&gt;POST /ai/generate-image/request-price&lt;/code&gt;, which appears in the web bundle, returned &lt;code&gt;404 page not found&lt;/code&gt; on &lt;code&gt;image.novelai.net&lt;/code&gt; and &lt;code&gt;404 Cannot POST /ai/generate-image/request-price&lt;/code&gt; on &lt;code&gt;api.novelai.net&lt;/code&gt;. The web UI does show an estimated Anlas cost on the generate button, but I could not get the same number out of this path, and whether another path exists I could not confirm.&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;Use one host, &lt;code&gt;https://image.novelai.net&lt;/code&gt;. Generation (&lt;code&gt;/ai/generate-image&lt;/code&gt;) and balance lookup (&lt;code&gt;/user/subscription&lt;/code&gt;) are both on it, while &lt;code&gt;api.novelai.net&lt;/code&gt; rejects generation with &lt;code&gt;model must be a valid enum value&lt;/code&gt; and lookups with "update to the image URL".&lt;/li&gt;
&lt;li&gt;Use the body below as it stands. This JSON is the full body that returned 200 in the &lt;code&gt;params_version: 4&lt;/code&gt; row of the table above, and the prompt goes in the same string in three places: top-level &lt;code&gt;input&lt;/code&gt;, &lt;code&gt;parameters.prompt&lt;/code&gt;, and &lt;code&gt;v4_prompt.caption.base_caption&lt;/code&gt;. Use &lt;code&gt;4&lt;/code&gt; for &lt;code&gt;params_version&lt;/code&gt; to match the official web client, but don't read any meaning into the value:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"input"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1girl, solo, silver hair, school uniform, looking at viewer, upper body, simple background, masterpiece, best quality"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"nai-diffusion-5-full"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"generate"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"parameters"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"params_version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"width"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;832&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"height"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1216&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"scale"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"sampler"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"k_euler_ancestral"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"steps"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"seed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1234567890&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"n_samples"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"ucPreset"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"qualityToggle"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"sm"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"sm_dyn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"dynamic_thresholding"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"controlnet_strength"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"legacy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"add_original_image"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"cfg_rescale"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"noise_schedule"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"karras"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"legacy_v3_extend"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"uncond_scale"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"negative_prompt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"prompt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1girl, solo, silver hair, school uniform, looking at viewer, upper body, simple background, masterpiece, best quality"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"reference_image_multiple"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"reference_information_extracted_multiple"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"reference_strength_multiple"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"extra_noise_seed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1234567890&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"v4_prompt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"use_coords"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"use_order"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"caption"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"base_caption"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1girl, solo, silver hair, school uniform, looking at viewer, upper body, simple background, masterpiece, best quality"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"char_captions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"v4_negative_prompt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"use_coords"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"use_order"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"caption"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"base_caption"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"char_captions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save that JSON as &lt;code&gt;body.json&lt;/code&gt;, put the file below in the same folder, and it runs as is. The key file is a text file with a &lt;code&gt;pst-&lt;/code&gt; token on the line after a line labeled "novelai", and the token stays in memory only. Running it with &lt;code&gt;node nai-v5-direct.mjs &amp;lt;key file&amp;gt;&lt;/code&gt; got me a 200 and an &lt;code&gt;image_0.png&lt;/code&gt; whose pixels match the table above:&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="c1"&gt;// node nai-v5-direct.mjs &amp;lt;key file&amp;gt; — the key file has a pst- token on the line after the "novelai" label&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;readFile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;writeFile&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:fs/promises&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;readFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\r?\n&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findIndex&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="sr"&gt;/novelai/i&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="nx"&gt;l&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&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="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;readFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;body.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://image.novelai.net/ai/generate-image&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;token&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&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="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content-type&lt;/span&gt;&lt;span class="dl"&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="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;writeFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;out.zip&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arrayBuffer&lt;/span&gt;&lt;span class="p"&gt;()));&lt;/span&gt; &lt;span class="c1"&gt;// image_0.png is inside&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;r&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;For transport, set &lt;code&gt;Content-Type: application/json&lt;/code&gt; explicitly if you send JSON, and do not set a &lt;code&gt;Content-Type&lt;/code&gt; header yourself if you use &lt;code&gt;FormData&lt;/code&gt;. Node 22's &lt;code&gt;fetch&lt;/code&gt; builds the header with the boundary on its own for a &lt;code&gt;FormData&lt;/code&gt; body.&lt;/li&gt;
&lt;li&gt;The response is a &lt;code&gt;binary/octet-stream&lt;/code&gt; zip with &lt;code&gt;image_0.png&lt;/code&gt; inside. Check the model from the &lt;code&gt;Source&lt;/code&gt; value in the PNG's &lt;code&gt;tEXt&lt;/code&gt; chunk (&lt;code&gt;NovelAI Diffusion V5 0ADF9AB7&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;To stay at 0 Anlas, keep all of it: Opus, one image (&lt;code&gt;n_samples: 1&lt;/code&gt;), no base image, a pixel product of 1,048,576 or less, and 28 steps or fewer — then read &lt;code&gt;/user/subscription&lt;/code&gt; before and after the generation and compare the sum &lt;code&gt;trainingStepsLeft.fixedTrainingStepsLeft + trainingStepsLeft.purchasedTrainingSteps&lt;/code&gt;. &lt;code&gt;trainingStepsLeft&lt;/code&gt; is an object rather than an integer, so you cannot subtract it directly. &lt;code&gt;usage.percent&lt;/code&gt; is too coarse to use for per-image checks.&lt;/li&gt;
&lt;li&gt;Respect the API document's "all generation requests must be initiated by a human action" and its 429 note ("API clients should not attempt to retry") by sending human-initiated requests one at a time with a gap between them. This round of measurements ran with 6 seconds between requests and a concurrency of 1.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Image: the sample outputs in this post are AI images generated with NovelAI Diffusion V5.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;With this body skeleton, &lt;code&gt;params_version&lt;/code&gt; is ignored — 4, 3, 1, and 0 all return the same pixels — and JSON and multipart bodies produced the same image. The parts that actually matter are the host, &lt;code&gt;image.novelai.net&lt;/code&gt;, and keeping both &lt;code&gt;v4_prompt&lt;/code&gt; and &lt;code&gt;v4_negative_prompt&lt;/code&gt; in the body, since dropping either one returns a 500 rather than a validation 400.&lt;/p&gt;

&lt;h2&gt;
  
  
  Unverified
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;How a 429 behaves: the API document explicitly forbids retrying, so I didn't provoke one on purpose. 429 isn't even documented for the image endpoints, so I don't know the actual message.&lt;/li&gt;
&lt;li&gt;What happens when &lt;code&gt;director_reference_*&lt;/code&gt; fields are sent to V5: V5 doesn't support Precise Reference, but the schema doesn't block these fields. Whether that is a 400 or "ignored and billed", I didn't run, because of the billing risk.&lt;/li&gt;
&lt;li&gt;The actual charge for a 29-step or 2-image request: that is the paid band, so I didn't generate it. The price lookup path I tried was a 404, so the only figure I have is the 45 Anlas for the large size from the earlier post.&lt;/li&gt;
&lt;li&gt;The absolute capacity of the Opus battery (allowance): &lt;code&gt;usage.percent&lt;/code&gt; stayed fixed at 87 across six images, so I couldn't work backwards to a per-image draw.&lt;/li&gt;
&lt;li&gt;How &lt;code&gt;params_version&lt;/code&gt; behaves with other body skeletons: I didn't run it with character-prompt, inpainting, or img2img bodies. Large values such as 9 also fell outside the generation budget.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Changelog
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Version&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1.0&lt;/td&gt;
&lt;td&gt;2026-09-14 first version (macOS 26.5.2 · Node v22.22.3 · Opus, 6 free-tier generations)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1.1&lt;/td&gt;
&lt;td&gt;2026-09-30 added the AI disclosure line (no content change)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;AI disclosure: translated and edited with AI assistance (Claude) from my Korean original. Measurements were run with Claude Code under my supervision; I reviewed the result.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>novelai</category>
      <category>api</category>
      <category>node</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Running NovelAI V5 in ComfyUI: nai-diffusion-5-full works, and the Error fetching Anlas 400 is a moved endpoint</title>
      <dc:creator>Ilan Kim</dc:creator>
      <pubDate>Sun, 13 Sep 2026 07:59:16 +0000</pubDate>
      <link>https://dev.to/ilan_kim/running-novelai-v5-in-comfyui-nai-diffusion-5-full-works-and-the-error-fetching-anlas-400-is-a-18gl</link>
      <guid>https://dev.to/ilan_kim/running-novelai-v5-in-comfyui-nai-diffusion-5-full-works-and-the-error-fetching-anlas-400-is-a-18gl</guid>
      <description>&lt;p&gt;&lt;code&gt;nai-diffusion-5-full&lt;/code&gt; and &lt;code&gt;nai-diffusion-5-curated&lt;/code&gt; work as they are through the ComfyUI_NAIDGenerator node, even though the README says the IDs haven't been checked against the live API. The &lt;code&gt;Error fetching Anlas&lt;/code&gt; 400 that shows up next to them has nothing to do with the models: the node still asks &lt;code&gt;https://api.novelai.net/user/data&lt;/code&gt; for your balance, and NovelAI moved its user endpoints to &lt;code&gt;image.novelai.net&lt;/code&gt;, so two spots in &lt;code&gt;nodes.py&lt;/code&gt; fix it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Overview
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;Source&lt;/code&gt; metadata in the PNGs the node brought back read &lt;code&gt;NovelAI Diffusion V5 0ADF9AB7&lt;/code&gt; and &lt;code&gt;NovelAI Diffusion V5 DB276663&lt;/code&gt; respectively, and on an Opus account one image at Normal size (832×1216) and 28 steps cost 0 Anlas. Two terms if NovelAI is new to you: Anlas is NovelAI's generation credit, and Opus is the top subscription tier, which generates the common sizes without spending any.&lt;/p&gt;

&lt;p&gt;I wrote this for people who read the README line "have not been independently confirmed against the live API yet" and hesitated, and for anyone hitting &lt;code&gt;Error fetching Anlas&lt;/code&gt; the way issue #51 describes. I installed ComfyUI v0.35.0 fresh on macOS and pinned the node to its 2026-08-21 commit, &lt;code&gt;52da75a&lt;/code&gt; (package version 1.0.9).&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Wire the Anlas Tracker node into Preview Any and run it, and the preview text reads &lt;code&gt;Error fetching Anlas&lt;/code&gt; while the console prints one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[NovelAI] Failed to fetch Anlas balance: 400 Client Error: Bad Request for url: https://api.novelai.net/user/data
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The node added the two V5 model IDs in PR #50 on 2026-08-21, but the README still carries the caveat that they were never compared against the live API. Issue #51, opened the next day, reports a generation 400 on a trial account together with &lt;code&gt;Error fetching Anlas&lt;/code&gt;, and as of 2026-09-13 it has no replies.&lt;/p&gt;

&lt;p&gt;Meanwhile NovelAI moved its user endpoints to &lt;code&gt;image.novelai.net&lt;/code&gt; around the V5 launch. The 400 from the old host is exactly what I saw in &lt;a href="https://swiftlyaside.tistory.com/7" rel="noopener noreferrer"&gt;my earlier Korean post&lt;/a&gt;, so I checked whether the node was failing for the same reason.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cause
&lt;/h2&gt;

&lt;p&gt;It is because &lt;code&gt;_get_user_data&lt;/code&gt; in &lt;code&gt;nodes.py&lt;/code&gt; calls &lt;code&gt;/user/data&lt;/code&gt; with &lt;code&gt;USER_API_BASE_URL = "https://api.novelai.net"&lt;/code&gt;. Attach a token and call both hosts directly, and their answers diverge:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Call&lt;/th&gt;
&lt;th&gt;Response&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET https://api.novelai.net/user/data&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;400 &lt;code&gt;{"statusCode":400,"message":"Please refresh NovelAI.net. If using a third-party tool, update to the image URL."}&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET https://image.novelai.net/user/data&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;200, includes &lt;code&gt;subscription.trainingStepsLeft&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Without a token both hosts return 401, so reproducing this needs a valid one. Swapping the host isn't the end of it either. On the new host &lt;code&gt;trainingStepsLeft&lt;/code&gt; is not an integer but a shape like &lt;code&gt;{"fixedTrainingStepsLeft": 9898, "purchasedTrainingSteps": 32}&lt;/code&gt;, so the Tracker text prints the dictionary verbatim and the Generate node's before-and-after subtraction &lt;code&gt;start_anlas - final_anlas&lt;/code&gt; dies with &lt;code&gt;TypeError: unsupported operand type(s) for -: 'dict' and 'dict'&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The model IDs themselves were fine. Here is what a ModelOption → Generate → Save Image workflow returned over &lt;code&gt;POST /prompt&lt;/code&gt;, with the same prompt, seed, and 28 steps as the earlier post:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;th&gt;PNG &lt;code&gt;Source&lt;/code&gt;
&lt;/th&gt;
&lt;th&gt;Subscription Anlas&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nai-diffusion-5-full&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;832×1216&lt;/td&gt;
&lt;td&gt;200, 4.1 s&lt;/td&gt;
&lt;td&gt;&lt;code&gt;NovelAI Diffusion V5 0ADF9AB7&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;9,898 → 9,898&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nai-diffusion-5-curated&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;512×768&lt;/td&gt;
&lt;td&gt;200, 1.6 s&lt;/td&gt;
&lt;td&gt;&lt;code&gt;NovelAI Diffusion V5 DB276663&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;9,898 → 9,898&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&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%2F9va5zh9grilhxjypz8j5.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%2F9va5zh9grilhxjypz8j5.webp" alt="A V5 Full normal-size output received through the ComfyUI_NAIDGenerator node" width="800" height="1169"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There is one trap. The PNG that Save Image writes keeps only the &lt;code&gt;prompt&lt;/code&gt; chunk, and the &lt;code&gt;Source&lt;/code&gt; field NovelAI set is gone. The node saves the original separately under a running number such as &lt;code&gt;output/NAI_autosave_00001_.png&lt;/code&gt;, so read the metadata from the file whose number matches the run you just made.&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%2Fb79v9mkwwtjr1ohccqak.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%2Fb79v9mkwwtjr1ohccqak.webp" alt="The ComfyUI graph after the patch: the Anlas Tracker node feeding Preview Any, which shows 9930 Anlas" width="800" height="322"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;Fix &lt;code&gt;_get_user_data&lt;/code&gt; in &lt;code&gt;nodes.py&lt;/code&gt; in two places. Point the host at &lt;code&gt;image.novelai.net&lt;/code&gt;, and normalize the dictionary-shaped &lt;code&gt;trainingStepsLeft&lt;/code&gt; into an integer total:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;-    USER_API_BASE_URL = "https://api.novelai.net"
&lt;/span&gt;&lt;span class="gi"&gt;+    USER_API_BASE_URL = "https://image.novelai.net"
&lt;/span&gt;@@
     response.raise_for_status()
&lt;span class="gd"&gt;-    return response.json()
&lt;/span&gt;&lt;span class="gi"&gt;+    data = response.json()
+    steps = data.get("subscription", {}).get("trainingStepsLeft")
+    if isinstance(steps, dict):
+        data["subscription"]["trainingStepsLeft"] = int(steps.get("fixedTrainingStepsLeft", 0) or 0) + int(steps.get("purchasedTrainingSteps", 0) or 0)
+    return data
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Restart the server and run the Anlas Tracker again, and it shows &lt;code&gt;9930 Anlas&lt;/code&gt;. That is the subscription 9,898 plus the purchased 32, the same number the web UI shows as your total, and when I generated one more V5 image at Normal size after the patch, the console printed &lt;code&gt;Generation cost: 0 Anlas&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;For V5, picking &lt;code&gt;nai-diffusion-5-full&lt;/code&gt; in ModelOption is all there is to it. On Opus, leave &lt;code&gt;limit_opus_free&lt;/code&gt; on and Normal size at 28 steps or fewer stays in the free band.&lt;/li&gt;
&lt;li&gt;I sent the same patch upstream as &lt;a href="https://github.com/bedovyy/ComfyUI_NAIDGenerator/pull/52" rel="noopener noreferrer"&gt;PR #52&lt;/a&gt;, along with a correction to the unconfirmed line in the README. I also posted the reproduction log on issue #51 with a link to this article.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Image: the sample output was generated with NovelAI Diffusion V5.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;Both V5 model IDs work through the node as shipped, and the &lt;code&gt;Error fetching Anlas&lt;/code&gt; 400 is the balance lookup still pointing at &lt;code&gt;api.novelai.net&lt;/code&gt;. Fixing it takes the new host plus one normalization, because &lt;code&gt;trainingStepsLeft&lt;/code&gt; now arrives as a dictionary rather than an integer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Unverified
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The generation 400 on a trial account: I don't have a trial account, so I couldn't reproduce the generation failure from issue #51. Whether it shares a cause with the balance lookup failure, I don't know.&lt;/li&gt;
&lt;li&gt;Regression on V4.5 and older models: I judged from the code alone that the patch touches only the balance lookup function and leaves the generation path alone, and I did not re-run a V4.5 generation.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Changelog
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Version&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1.0&lt;/td&gt;
&lt;td&gt;2026-09-13 first written (ComfyUI 0.35.0 + ComfyUI_NAIDGenerator 52da75a, measured on an Opus account)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1.1&lt;/td&gt;
&lt;td&gt;2026-09-30 added the AI disclosure line (no content change)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;AI disclosure: translated and edited with AI assistance (Claude) from my Korean original. Measurements were run with Claude Code under my supervision; I reviewed the result.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>comfyui</category>
      <category>novelai</category>
      <category>python</category>
      <category>api</category>
    </item>
    <item>
      <title>NovelAI V5 on Opus: usage limits, the 2026-09-21 subscription Anlas reset, and the api.novelai.net to image.novelai.net move</title>
      <dc:creator>Ilan Kim</dc:creator>
      <pubDate>Fri, 11 Sep 2026 17:28:39 +0000</pubDate>
      <link>https://dev.to/ilan_kim/novelai-v5-on-opus-usage-limits-the-2026-09-21-subscription-anlas-reset-and-the-apinovelainet-567l</link>
      <guid>https://dev.to/ilan_kim/novelai-v5-on-opus-usage-limits-the-2026-09-21-subscription-anlas-reset-and-the-apinovelainet-567l</guid>
      <description>&lt;p&gt;On an Opus account (NovelAI's top subscription tier), NovelAI Diffusion V5 still costs 0 Anlas (NovelAI's generation credits) up to Normal size (832×1216) at 28 steps and one image per request; Large (1024×1536) is where 45 Anlas starts coming out of your balance. From September 21, 2026, your unused subscription Anlas resets to 0 when your subscription period ends, and the balance endpoint has moved from &lt;code&gt;api.novelai.net&lt;/code&gt; to &lt;code&gt;image.novelai.net&lt;/code&gt;. The reset is tied to the end of your period, not to the effective date: on September 22, the day after it took effect, this account's balance was still untouched.&lt;/p&gt;

&lt;h2&gt;
  
  
  Overview
&lt;/h2&gt;

&lt;p&gt;V5 shipped on 2026-08-21, and with it came a "battery" usage limit on Opus subscriptions. The announcement doesn't say how much a single generation draws or where you can see it, so I ran the numbers myself against the API. The reset itself is still a policy statement here: this account's paid period runs to September 29, so the day-after query only shows that nothing is wiped on the effective date. I measured on 2026-09-11 and 2026-09-13, and queried once more on 2026-09-22.&lt;/p&gt;

&lt;p&gt;I wrote this for two groups: people deciding whether to keep or cancel Opus, and people driving the NovelAI API directly from tools like ComfyUI nodes. In that order: the per-size cost table, the new &lt;code&gt;usage&lt;/code&gt; field, and the endpoint that moved.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Querying your balance at the old address now returns a 400. The response body is a single line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET https://api.novelai.net/user/subscription → 400
{"statusCode":400,"message":"Please refresh NovelAI.net. If using a third-party tool, update to the image URL."}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On top of that there's a policy change. The official blog post (2026-08-20) says a rechargeable battery-style usage limit applies to V5 only, refilling automatically over roughly a week, and that starting 31 days after the V5 release, unused subscription Anlas is reset to 0 when a subscription period ends. V5 launched on August 21, which puts the effective date at September 21.&lt;/p&gt;

&lt;p&gt;Image models older than V5 and the text models stay unlimited. But Anlas was visibly draining on an Opus account while generating with V5, so I needed to find out under exactly which conditions it gets charged.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I measured
&lt;/h2&gt;

&lt;p&gt;On an Opus account (&lt;code&gt;tier: 3&lt;/code&gt;) I generated one image at a time with the same prompt, seed, and 28 steps, calling &lt;code&gt;GET https://image.novelai.net/user/subscription&lt;/code&gt; before and after each generation and diffing &lt;code&gt;trainingStepsLeft&lt;/code&gt;. Balance queries have to go to that host now to get a normal response.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;th&gt;Anlas charged&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nai-diffusion-5-full&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;512×768 (Small)&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nai-diffusion-5-full&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;832×1216 (Normal)&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nai-diffusion-5-full&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1024×1536 (Large)&lt;/td&gt;
&lt;td&gt;45&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nai-diffusion-4-5-full&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;832×1216 (Normal)&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nai-diffusion-5-curated&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;512×768 (Small)&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Within what I measured, everything up to Normal size (832×1216) at 28 steps and one image per request was 0 Anlas, and subscription Anlas started coming out at Large (1024×1536). The ComfyUI node (&lt;code&gt;ComfyUI_NAIDGenerator&lt;/code&gt;) implements the Opus free tier as up to 1,048,576 pixels (1024×1024), up to 28 steps, and one image — I didn't measure 29 steps, multi-image batches, or the exact pixel boundary this time. Large size is where subscription Anlas gets charged, and that subscription Anlas (&lt;code&gt;fixedTrainingStepsLeft&lt;/code&gt;) is what disappears when your subscription period ends after September 21. If you only cancel renewal and still have paid time left, the clock is the end of that paid period.&lt;/p&gt;

&lt;p&gt;I queried the same account again once the effective date had passed. On 2026-09-22 the subscription Anlas read 9,898, the same figure as the September 13 sample, and &lt;code&gt;expiresAt&lt;/code&gt; was still 1790674321, which is 2026-09-29 18:32 KST. So at least on this account nothing was wiped on the effective date; it is simply still inside its paid period. That is one sample, but it rules out a blanket reset on the day itself. I'll query again after that renewal and report what the reset does.&lt;/p&gt;

&lt;p&gt;The test account held 9,961 subscription Anlas and 32 purchased Anlas (&lt;code&gt;purchasedTrainingSteps&lt;/code&gt;) as separate line items, and the 45 for the Large generation came out of subscription Anlas only (9,961 → 9,916). If the total the web UI shows doesn't match, check whether it's the sum of the two. The reset targets the subscription side; whether purchased Anlas is exempt is listed under Unverified below.&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%2Fz82upg1ih911rbhzodbw.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%2Fz82upg1ih911rbhzodbw.webp" alt="A V5 Full normal-size output generated for 0 Anlas on an Opus account" width="800" height="1169"&gt;&lt;/a&gt;&lt;br&gt;
The battery sits at the end of the subscription response, in a &lt;code&gt;usage&lt;/code&gt; field. It didn't move once across four V5 generations. When I queried again 46 hours later, &lt;code&gt;percent&lt;/code&gt; had dropped to 98 — after a handful of additional V5 generations from the web UI on this account in between (count not recorded). So the battery is a meter that really does go down, and &lt;code&gt;timeUntilNextPercent&lt;/code&gt; stayed at 7,888 even at 98%, so it isn't a live countdown. A third sample nine days later was back at 100%, so it does recharge, but September 19 — the date NovelAI's official account gave for the usage limit renewal, seen 2026-09-14 — falls inside that gap, so I can't tell natural recharge from the renewal. &lt;code&gt;timeUntilNextPercent&lt;/code&gt; read 7,888 in all three samples. That's as far as the evidence goes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"usage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"percent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"isNegative"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"timeUntilNextPercent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;7888&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I checked the V5 model IDs while I was there. Both &lt;code&gt;nai-diffusion-5-full&lt;/code&gt; and &lt;code&gt;nai-diffusion-5-curated&lt;/code&gt; returned 200 with an image, and the PNG &lt;code&gt;Source&lt;/code&gt; metadata read &lt;code&gt;NovelAI Diffusion V5 0ADF9AB7&lt;/code&gt; and &lt;code&gt;NovelAI Diffusion V5 DB276663&lt;/code&gt; respectively. Sending an ID that doesn't exist returns &lt;code&gt;Validation error: model nai-diffusion-5-nonexistent doesn't exist&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For reference, a Japanese write-up (note.com, 2026-08-21) put V5 generation cost at 11 Anlas for Small, 26 for Normal, and 39 for Large, while my measurement for Large was 45. That write-up doesn't state step count or account tier (the web UI default is 23 steps; my runs were 28), so it isn't directly comparable. Logged out, the web UI showed 26 Anlas for the default settings (V5 Curated, 23 steps, Normal size), which is the non-subscriber rate with no free tier.&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%2Fyjviiq2qaepgbb4xwtvd.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%2Fyjviiq2qaepgbb4xwtvd.webp" alt="NovelAI web UI while logged out. The generate button on the V5 Curated defaults shows 26 Anlas." width="648" height="1056"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;Keeping Opus doesn't exempt you from the V5 battery usage limit. That said, within what I measured, the Anlas charging rules are the same as V4.5 — up to Normal size, 28 steps or fewer, one image at a time, it's 0 Anlas. Battery capacity and recharge rate are still unknown. A second sample 46 hours later showed 98%, but I didn't record how many images were generated in between, so how much use it takes to get blocked stays unverified.&lt;/li&gt;
&lt;li&gt;If you plan to cancel, order matters. After September 21, whatever subscription Anlas is left disappears when your subscription period ends, so it's better to burn the subscription side down before that period runs out — with Large generations or multi-image batches.&lt;/li&gt;
&lt;li&gt;In third-party tools, point the balance query at &lt;code&gt;image.novelai.net&lt;/code&gt;. If generation works but the balance won't load, that's almost always this.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Image: the sample output was generated with NovelAI Diffusion V5.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;On Opus, V5 is still free up to Normal size at 28 steps, and if you plan to cancel, spend down your subscription Anlas before your paid period ends once the reset policy takes effect on September 21, 2026. If a tool can generate but can't read your balance, the old host is the most likely cause, so check which balance endpoint it calls.&lt;/p&gt;

&lt;h2&gt;
  
  
  Unverified
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;How the subscription Anlas reset actually behaves: the effective date has passed, but this account still has paid time left, so there was nothing to observe. I'll re-query after its renewal on 2026-09-29.&lt;/li&gt;
&lt;li&gt;Battery capacity and recharge rate: three samples show it going down and coming back up, but the generations in between weren't counted and the September 19 renewal falls inside the gap, so I couldn't derive a drain or recharge rate.&lt;/li&gt;
&lt;li&gt;The reset notice text and battery display in the web UI: my browser session was logged out, so I couldn't capture the subscriber view.&lt;/li&gt;
&lt;li&gt;Whether purchased Anlas is exempt from the reset: inferred from the official post mentioning subscription Anlas only.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Changelog
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Version&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1.0&lt;/td&gt;
&lt;td&gt;2026-09-11 first written (measured on an Opus account via API)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1.1&lt;/td&gt;
&lt;td&gt;2026-09-13 second battery sample added (re-queried 46 hours later)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1.2&lt;/td&gt;
&lt;td&gt;2026-09-22 re-queried the day after the effective date (subscription Anlas unchanged at 9,898, battery back at 100%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1.3&lt;/td&gt;
&lt;td&gt;2026-09-30 added the AI disclosure line (no content change)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;AI disclosure: translated and edited with AI assistance (Claude) from my Korean original. Measurements were run with Claude Code under my supervision; I reviewed the result.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>novelai</category>
      <category>ai</category>
      <category>api</category>
      <category>generativeai</category>
    </item>
  </channel>
</rss>
