~/blog/moss-image-traditional-chinese-comic-cards

AI Workflow · part 28

[Dev Workflow] MOSS-Image Renders Traditional Chinese Correctly: 37 s to 2.2 s per Image on an RTX 5090, Then Comic Cards

❯ cat --toc

TL;DR

MOSS-Image is a 5B open-weights image model from OpenMOSS. Its prompt is JSON, and each text string gets its own box on the canvas. On the same Traditional Chinese news headline it was right in 4 of 4 images. Qwen-Image 2.1 was right in 0 of 4, writing 全 as 佺 every time. At the official 50 steps it takes 37 s per image on an RTX 5090. A community 4-step LoRA brings that to 2.2 s, once you fuse its q/k/v into the weights' single qkv layer. On a 2080 Ti 22G shared with a voice service, it runs at 58 s with the text encoder on CPU. Comic cards: 147 of 147 characters right. The weak spot is characters showing up in the wrong panel.

Hero card drawn by MOSS-Image itself: the headline MOSS-Image writes Traditional Chinese, then four crayon cards for 4/4 correct, the 4-step LoRA, the 2080 Ti and the 12-panel comic

Why I tested this

Every short video and social post I publish needs a thumbnail with a Traditional Chinese headline on it. My workflow so far has been two steps per image: an image model draws the background, and a web layout puts the text on top.

I wanted to drop the second step. Qwen-Image 2.1 is good at in-image text, so I gave it real news headlines. In all four test images it wrote the same character wrong. 「安全沙盒」 ("security sandbox") came out as 「安佺沙盒」, with an extra radical bolted onto 全.

OpenMOSS released MOSS-Image on October 16. It promises the two things I needed: accurate text inside the image, and control over where each piece of text goes. It also came with two problems. At its official settings it took 37 s per image on my RTX 5090, about 2.4x slower than Qwen-Image 2.1. And the always-on machine I wanted to run it on has a 2080 Ti 22G that already hosts my voice-narration service, with not enough free VRAM for both.

The first half of this post is a tutorial: what MOSS-Image is, the Traditional Chinese test, installing it on a 5090, getting from 37 s to 2.2 s, running it on the 2080 Ti, and making four-panel comic cards. The second half is the debugging log.

MOSS-Image is a 5B model whose prompt is a JSON layout

MOSS-Image has a 5B backbone and comes in Base and SFT variants. I use SFT. ComfyUI supported it on day 0, and Comfy-Org repackaged the weights on Hugging Face. License is Apache-2.0.

The prompt is JSON. summary describes the whole image. items lists the elements, such as a character, a scene or a piece of text. Each item has a bbox, and text items add a text field with the exact string to render. This is the portrait 1088x1920 thumbnail I tested with:

{
  "summary": {
    "meta": ["news thumbnail", "professional graphic design"],
    "canvas": "single",
    "caption": "Professional technology news thumbnail, dark navy studio background ..."
  },
  "items": [
    {"label": "character", "caption": "An adult East Asian female news anchor ...", "bbox": [-634, -490, 730, -44]},
    {"label": "text", "caption": "Large bold clear white headline in the upper right ...", "bbox": [-730, -11, 38, 500], "text": "輝達發表 AI 安全沙盒"},
    {"label": "text", "caption": "Small bright yellow rectangular news badge ...", "bbox": [307, 65, 518, 490], "text": "本週新聞"},
    {"label": "text", "caption": "Small white channel name along the bottom margin ...", "bbox": [787, -413, 922, 435], "text": "AI-Muninn"}
  ],
  "bbox_coord_space": "abs_centralized",
  "bbox_format": "yxyx"
}

Two things about bbox will trip you up. The origin is the center of the canvas and the unit is pixels, so on a 1088x1920 portrait x runs from -544 to 544 and y from -960 to 960. And the order is [y_min, x_min, y_max, x_max]. Y comes first.

MOSS bbox coordinates: origin at the canvas center, x from -544 to 544 and y from -960 to 960 on a 1088×1920 portrait canvas, written as y_min, x_min, y_max, x_max

The box is a suggestion, not a crop. Text mostly stays inside it. Characters spill out of theirs in nearly every image I generated. Use it for coarse layout, like headline top right and badge bottom right, not for pixel-exact placement.

On the same headline, MOSS got 4 of 4 right and Qwen-Image 2.1 got 0 of 4

I used the same news-thumbnail layout throughout: an anchor on the left, the headline 「輝達發表 AI 安全沙盒」 ("NVIDIA announces AI security sandbox") top right, a yellow 「本週新聞」 ("this week's news") badge, and the channel name AI-Muninn along the bottom. I ran it in portrait and landscape, with Chinese and English text sets, at seeds 42 and 123, and kept every image. Each model ran at its official recommended settings: MOSS 50 steps at CFG 3, Qwen-Image 2.1 40 steps at CFG 1.

The middle column is the one that matters for Traditional Chinese:

Model (RTX 5090)avg s/imagezh-TW headline + badge all correctall 3 text areas clean
MOSS-Image SFT int837.254/44/8
Qwen-Image 2.1 int815.120/42/8

Qwen-Image 2.1 made the same mistake every time. The 全 in 安全沙盒 gets a 亻 radical in front of it, and one image looks closer to 俭. In landscape at seed 42 it also turned 發 into a malformed character that does not exist. Rerolling the seed does not fix a mistake that repeats like that.

Same news-thumbnail layout, portrait, seed 42: MOSS on the left writes 安全沙盒 correctly, Qwen-Image 2.1 on the right writes 全 as 佺

MOSS has its own weak spot: the wide, flat channel-name box along the bottom of a landscape image. All 4 landscape images failed there. Two repeated "AI-Muninn" a second time, one wrote "AI—Mungiicinn", and one added three letter-shaped decorations. All 4 portrait channel names were clean.

Two MOSS landscape thumbnails: headline and badge correct, but the bottom channel name repeats AI-Muninn in one and becomes AI—Mungiicinn in the other

So in practice MOSS writes the headline, and I still overlay fixed small text like the channel name afterwards. The next two sections fix that.

Installing on an RTX 5090 takes one patch to the official plugin

My environment was ComfyUI 0.37.0 with torch 2.13.0+cu130, neither touched.

The plugin lives in the official repo under comfyui/moss-image/. Copy that folder to ComfyUI/custom_nodes/moss-image/. Its pyproject.toml has dependencies = [], so there is nothing to install. I used commit 52b87fc. After a restart you get six MOSS nodes.

Download three files from the Comfy-Org repo:

FolderFileSize
models/diffusion_models/moss_image_sft_int8_convrot.safetensors5.4 GB
models/text_encoders/qwen3.5_9b_int8_convrot.safetensors9.7 GB
models/vae/flux2-vae_small.safetensors250 MB

The first run fails at KSampler:

AttributeError: 'NoneType' object has no attribute 'dtype'

The official plugin defines q, k and v as three separate linear layers. The Comfy-Org int8 weights fuse them into a single qkv layer, so the plugin looks for proj.q and finds nothing. The fix is two edits inside the plugin folder. In loaders.py, detect the fused layout by checking for layers.0.proj.qkv.weight. In model.py, give _Block a fused branch that builds one 3x-wide linear layer and splits its output in three. Keep the separate branch too, because the 8-step Turbo weights I tried later still have separate q/k/v. The core change:

# model.py, _Block.forward
if self.fused_qkv:
    q, k, v = self.proj["qkv"](y).chunk(3, dim=-1)
else:
    q, k, v = self.proj["q"](y), self.proj["k"](y), self.proj["v"](y)

With that, the official example workflow runs, and all 8 comparison images above came out of it. The full diff is in the deep dive. The patch only touches the plugin folder, and it may become unnecessary once the official plugin or a native ComfyUI node catches up, so check the ComfyUI README in the official repo before you apply it.

A 4-step LoRA brings it from 37 s to 2.2 s per image

There are two community speedups. One is an 8-step Turbo checkpoint, toxicdog/MOSS-Image-SFT-Turbo-INT8-ConvRot, which replaces the main model and runs at 8 steps, CFG 1, Euler/simple. The other is a 4-step LoRA, akhaliq/MOSS-Image-SFT-4-steps (v2.0), which sits on top of the SFT int8 weights at strength 1 and runs at 4 steps, CFG 1, Euler/simple. Both are on Hugging Face. I used each one's model-card settings and the same prompts.

Read the first row. Warm averages exclude the first image:

Original 50 steps8-step Turbo4-step LoRA
Warm avg (s)37.313.862.22
First image, incl. load (s)n/a6.965.17
zh-TW target text all correctn/a3/33/3

The 4-step LoRA is 16.8x faster than the original. That is the combined effect of fewer steps, CFG 3 dropping to 1, and a different scheduler, not any single one of them.

The same layout three ways: original 50 steps at 37 s, 8-step Turbo at 3.9 s, 4-step LoRA at 2.2 s, all with the Chinese headline correct

Faces at 4 steps are softer than at 50. Text is just as sharp, and for a thumbnail that trade is fine. Both fast versions drew a blurry fake-English string in the upper-left background at seed 123, which made that image unusable. The landscape channel-name duplication did not show up this time, but I only tried one seed, so I am not calling it fixed.

Grep the log to confirm the LoRA actually loaded

If you load the 4-step LoRA with ComfyUI's built-in LoraLoaderModelOnly on the fused int8 weights, it silently skips the q/k/v parts of the LoRA and still produces images. A good-looking image proves nothing here. Check the log:

grep -c "lora key not loaded" comfyui.log

With the built-in loader I got 378 lines. That is 42 layers, times q, k and v, times 3 tensors each. A typical line:

lora key not loaded: diffusion_model.layers.0.proj.q.lora_down.weight

Any non-zero count means the LoRA is only partly applied, to the output projection and the FFN.

The fix is a custom loader node, MOSSImageFusedLoraLoader, which I added to the plugin's nodes.py. It fuses the LoRA's q/k/v into one qkv adapter. Use it in place of LoraLoaderModelOnly. A full load shows three signals, and all three must hold:

  1. lora key not loaded appears 0 times.
  2. The loader's stat line reads fused_blocks=42 targets=168 tensors=504 applied=168.
  3. The log prints 168 patches attached before each sampling run.

As an on/off control I ran the same seed and JSON at 4 steps with the LoRA disconnected. Face and text came out badly distorted. With it connected, both were clear.

If your weights already have separate q/k/v, like the 8-step Turbo file, the built-in loader works. Run the same grep anyway.

Same seed, same JSON, 4 steps: LoRA off on the left turns the face and text to mush, LoRA on at the right is clean

On a 2080 Ti 22G, move the text encoder to CPU and MOSS shares the card with the voice service

My always-on AI Lab machine has a modded 2080 Ti 22G that already hosts a ComfyUI instance (about 8.4 GB), my voice-narration service (about 5.4 GB) and a second image-generation ComfyUI. The voice service refuses jobs when less than 2048 MiB is free.

On the 5090, MOSS used 15.5 GiB of card memory. Most of that is the 9.7 GB Qwen3.5-9B text encoder. So the encoder goes to CPU. In the workflow, change device on MOSSImageCLIPLoader from default to cpu. Everything else is the 5090 4-step LoRA workflow, fused loader included:

"102": {"class_type": "MOSSImageCLIPLoader",
        "inputs": {"clip_name": "qwen3.5_9b_int8_convrot.safetensors", "device": "cpu"}}

The last column is why this works for me:

per image (warm)CPU text encodevoice service
5090, all on GPU2.2 sn/an/a
2080 Ti, text encoder on CPU58.5 sabout 26.8 savailable throughout, min free 4,695 MiB

58.5 s is 26x slower than the 5090, and about half of it is CPU text encoding. But it runs on a machine that is already on, without waking the 5090. Target zh-TW text was correct in 4 of 4 images, and the seed 123 background-text flaw showed up here too, same as on the 5090. When I started, the card had 6,010 MiB in use. If it is busier than that, wait.

Two more things on this card. At CFG 1 the negative prompt is never used, but the empty-string negative CLIPTextEncode still runs on CPU and burns about 27 s. Replace it with ConditioningZeroOut fed from the positive encoder's output. The output is pixel-identical, and the first image dropped from 89.7 s to 62.4 s. And the smaller W4A8 text encoder is slower on CPU, not faster: 108 s per image. Both are in the deep dive.

Columns: RTX 5090, 2080 Ti with the int8 text encoder on CPU, 2080 Ti with W4A8 on CPU; same seeds for Chinese portrait, English portrait and Chinese landscape

Four-panel comic cards got 147 of 147 characters right, with characters in the wrong panels

The thing I actually wanted out of all this was comic-style social cards. Four 1024x1280 (4:5) cards, one panel each, for a carousel post. The story: my AI assistant Yui is moving data and gets stuck waiting on the disk, and Claude, drawn as an orange octopus with round glasses, comes to the rescue.

On the 5090 with the 4-step LoRA, each card took about 1.5 s warm. I generated each of the four panels with three seeds, 12 images total. Across titles and dialogue that is 147 Chinese characters, and all 147 were correct.

Each card's JSON has four parts:

  • summary.caption: pencil, brown ink lines, light watercolor, cream paper, what the characters look like, and "top 18% reserved for handwritten title, no color blocks".
  • One scene item.
  • One title text item, boxed at 8 to 92% of the width and 4 to 15.5% of the height.
  • One or two dialogue text items, each described as "hand-drawn rounded speech bubble, tail pointing at X".

The model draws the speech bubbles and their tails itself. Nothing is added in post. That hand-drawn feel is better than overlaid text, which is why I went with this version.

The alternative is a text-free base image plus a PIL overlay in the 標楷體 (Kaiu) font. The text is always right and the lines are easy to edit, but the font looks too formal for a comic. If your lines change often, pick that.

First-round four-panel comic card: top row lettered by MOSS itself, bottom row drawn by MOSS with the text overlaid in Kaiu

The real limitation is characters showing up in panels they should not be in. My first version put this in the global summary.caption:

Claude when present is a tiny orange octopus with round spectacles.

The octopus appeared in panels 1, 2 and 4, three out of four, which spoils the panel-3 reveal where the rescue arrives. Three changes fixed it:

  1. The global description only lists characters present in every panel.
  2. Each panel's scene names who is in it.
  3. Each panel says "exactly one or two speech bubbles in this panel, each character appears once".

In a second round I used the same trick on the version where MOSS writes the text itself: each panel's prompt describes only the characters in that panel, with no global cast list, and every character's look is repeated word for word wherever it appears. Across 88 images (four new stories plus six art styles), the octopus stopped showing up early in the picks. The watercolor set still needed one repair pass, and a bear shopkeeper wandered into the windows of the flat-vector set. The 36 picks still had 2 wrong characters (种 for 種, and 位位 for 位子), and the watercolor style often gave lines to the wrong character.

One four-panel story drawn in six styles: American retro, pixel RPG, ink wash, claymation, 90s cel animation, crayon; all 24 panels have the text right

One more trap: writing an uppercase "NO TEXT" in the prompt makes the model print "NO TEXT" as the title.

Character consistency is "recognizably the same", no better. Hairstyle, headphones and jacket color hold across the four panels. Head-to-body ratio, hair length and the number of boxes Yui carries drift, and so does the number of octopus tentacles. I used no reference image and no character LoRA.

The comic version

Every title and speech bubble in these 12 panels was lettered by MOSS with the 4-step LoRA, no overlay. The signs stay in Chinese on purpose: the joke is the miswritten character. Each panel took four candidates to find one with every letter right, and the English set needed one title swap after "Liftoff" came out as "Lifetoff" four times in a row. The hero image at the top is MOSS too; 3 of 8 English candidates had every word right.

Just One Thumbnail: the author asks the red Qwen-Image robot to letter 安全沙盒; it says it is the best writer here

Feeling Confident: close-up of the red robot saying Chinese? Easy!

Delivered: the red robot holds up a sign reading 安佺沙盒, with an extra stroke, and says Perfect!

Review: the author points at the wrong character and asks Best writer here?; the robot sweats: Bonus stroke, free

Next Artist: the blue MOSS robot writes 安全沙盒 correctly while a wall clock shows 37 s

Speed Rocket: the author straps a 4 steps rocket to MOSS, who says Boost installed!

Blast Off: the rocket flies away on its own and the author shouts Hey—!

Bolted On This Time: the octopus bolts the rocket on; MOSS finishes before the author ends the sentence; stopwatch 2.2 s

The Old GPU Wants In: MOSS and a headphone-wearing bird squeeze onto an old graphics card stage with a huge dictionary

Help Downstairs: an old man wearing a CPU chip hat looks words up; the timer reads 58 s

The Pocket Edition: the author offers a Compressed book; the CPU grandpa sweats and the timer reads 108 s

Comic Cards Done: the octopus waves from every panel of the finished comic and says I'm the tour guide!

Deep dive: LoRA keys, fusing q/k/v, W4A8 on CPU, the voice service and ConditioningZeroOut

You can skip this section. Everything you need to run the workflow is above; this is the evidence and the debugging detail behind it.

378 lines of "lora key not loaded", and the images still render

The 4-step LoRA has 252 adapters: q, k, v, o and FFN in every layer. The fused int8 weights only have proj.qkv, so the 126 q/k/v adapters find no target. Each one reports its alpha, down and up tensors, so 126 x 3 = 378 lines, 126 each for q, k and v.

The o and FFN adapters apply normally, which is why a partially applied LoRA still produces a clean-looking image. I did not run a blind test between full and partial application.

The on/off control in numbers, at seed 42, same JSON, 4 steps, CFG 1: mean absolute pixel difference 35.7/255, RMS 70.2/255. With the LoRA off, face and text are distorted.

Fusing q/k/v: concatenate the down matrices along the rank, build a block-diagonal up matrix

LoRA fusion: stack the three q, k, v down matrices into rank 384 and lay the three up matrices out block-diagonally so the product fits the fused qkv weight; applied as-is, 378 lora keys fail to load and images still render

The fused forward pass computes qkv = W_qkv · x and splits the result in three. So the fused LoRA delta has to be the three deltas stacked:

ΔW_qkv = cat(ΔWq, ΔWk, ΔWv; dim=0)

Each delta is ΔW = (alpha/r) · B · A, where A is the down matrix and B is the up matrix. The three A matrices differ, so you cannot share one A across q, k and v. The correct fusion:

A_fused = cat(Aq, Ak, Av; dim=0)                          # stack the ranks: 128 each, 384 total
B_fused = block_diag((αq/rq)·Bq, (αk/rk)·Bk, (αv/rv)·Bv)  # fold each group's scale into B
alpha_fused = rank_fused = 384                             # so the outer scale is 1

The loader's core code. It runs on CPU in fp32 and does not rewrite the int8 model file:

downs = [state[s + ".lora_down.weight"].float() for s in sources]   # sources = q, k, v
ups = [state[s + ".lora_up.weight"].float() for s in sources]
scales = [float(state.get(s + ".alpha", a.shape[0])) / a.shape[0]
          for s, a in zip(sources, downs)]
down = torch.cat(downs, dim=0)
up = torch.block_diag(*[b * scale for b, scale in zip(ups, scales)])
state[target + ".lora_down.weight"] = down
state[target + ".lora_up.weight"] = up
state[target + ".alpha"] = torch.tensor(float(down.shape[0]))

The loader errors out instead of silently under-applying. It compares each layer against a test vector before and after fusion, checks that no adapter tensor is left unmapped, and requires the patch count ModelPatcher accepts to equal the number of converted targets. The actual log:

MOSS_TURBO raw adapter roots=252 unmapped=126
MOSS_TURBO VERIFIED fused_blocks=42 targets=168 tensors=504 applied=168 strength=1 concat_max_abs_error=0

168 is 42 fused qkv layers plus 126 o/FFN targets. On random matrices with different ranks and alphas, the max error was 3.8e-6. The same loader on the 2080 Ti gave 3.7e-9.

One caveat. This goes through ComfyUI's ModelPatcher on int8 weights, so the precision differs from the LoRA author's diffusers path, which uses a bf16 base and keeps the LoRA unmerged in fp32.

The plugin's fused-qkv patch, full diff against OpenMOSS commit 52b87fc

--- upstream/loaders.py
+++ trial/loaders.py
@@ -83,6 +83,7 @@
             "text_dim": 4096,
             "freq_dim": 256,
             "rope_base": 10000.0,
+            "fused_qkv": "layers.0.proj.qkv.weight" in state,
         }
     )
--- upstream/model.py
+++ trial/model.py
-    def __init__(self, dim, ffn_dim, heads, device, dtype, operations):
+    def __init__(self, dim, ffn_dim, heads, device, dtype, operations, fused_qkv=False):
         super().__init__()
         self.heads = heads
+        self.fused_qkv = fused_qkv
@@
-                name: operations.Linear(dim, dim, bias=False, device=device, dtype=dtype)
-                for name in ("q", "k", "v", "o")
+                name: operations.Linear(dim, 3 * dim if name == "qkv" else dim, bias=False, device=device, dtype=dtype)
+                for name in (("qkv", "o") if fused_qkv else ("q", "k", "v", "o"))
@@
-        dtype = self.proj["q"].weight.dtype
+        dtype = self.proj["qkv" if self.fused_qkv else "q"].weight.dtype
         y = y.to(dtype)
-        q = self.q_norm(self.proj["q"](y).unflatten(-1, (self.heads, -1))).flatten(2)
-        k = self.k_norm(self.proj["k"](y).unflatten(-1, (self.heads, -1))).flatten(2)
-        x = x + self.proj["o"](_attention(q, k, self.proj["v"](y), rope, self.heads)) * gate_a[:, None]
+        if self.fused_qkv:
+            q, k, v = self.proj["qkv"](y).chunk(3, dim=-1)
+        else:
+            q, k, v = self.proj["q"](y), self.proj["k"](y), self.proj["v"](y)
+        q = self.q_norm(q.unflatten(-1, (self.heads, -1))).flatten(2)
+        k = self.k_norm(k.unflatten(-1, (self.heads, -1))).flatten(2)
+        x = x + self.proj["o"](_attention(q, k, v, rope, self.heads)) * gate_a[:, None]

The model's main __init__ in model.py also has to accept fused_qkv=False and pass it to each _Block. The fused chunk order is q, then k, then v, and the LoRA fusion has to stack in the same order.

After the patch, 42 harmless "unexpected" lines remain for layers.N.comfy_attention.config. Their value is comfy_kitchen_int8, an attention-backend hint the plugin does not read.

The W4A8 text encoder is slower on CPU

Comfy-Org also ships a W4A8 GPTQ text encoder, 7.3 GB against 9.7 GB for int8. Same 2080 Ti machine, same 4 prompts, encoder on CPU:

Text encoderpositive encode (warm avg)per image (warm avg)first imageload (log)
int8 ConvRot26.8 s58.5 s89.7 s8,061 MB
W4A8 GPTQ76.9 s108.4 s175.8 s5,568 MB

Encoding is 2.87x slower and the whole image 1.85x slower. There is no W4A8-specific CPU kernel. The generic comfy-kitchen implementation dequantizes before it computes, so 4-bit only saves memory. The file is also mixed precision, not 4-bit throughout. Text accuracy was the same, 4 of 4. On CPU, use int8.

My first attempt on the 2080 Ti squeezed the voice service down to 477 MiB free

Before I tried MOSS, the card's baseline was about 14,170 MiB used: ComfyUI 8,442, voice 5,412, the image ComfyUI 312. The voice service reported 7,831 MiB free.

I first ran a 512x512 Qwen-Image 2.1 workflow with its text encoder still on the GPU. It finished in 6.46 s, and the voice service's health check went to available=false, free_mib=477, used_mib=21524. Its threshold is 2048 MiB. The process did not die. It refused jobs.

I rolled the service config back per a rule I had written in advance, restarted, and stopped there. Then I reran the Qwen workflow with CLIPLoader set to device=cpu at 256x256. It took 57.04 s, the card peaked at 18.7 GiB, and the voice service stayed available throughout with a minimum of 3,515 MiB free. That is where the text-encoder-on-CPU setup for MOSS came from.

It also hit my automated news pipeline: early that morning it skipped the entire batch because the narration service reported itself unavailable. The pipeline now waits up to 20 minutes for voice, re-checking every 30 s, and never preempts a running job.

For the real MOSS run on the 2080 Ti I polled voice health once a second: 267 checks during the int8 group and 503 during the W4A8 group. Every one returned available=true, and the minimum free memory was 4,695 MiB.

ConditioningZeroOut removes the negative encode at no cost

At CFG 1, KSampler only uses the positive conditioning, but the negative CLIPTextEncode with an empty string still executes. On the 2080 Ti's CPU that is 26.8 s, part of the 89.7 s first image.

Swap the negative encoder for ConditioningZeroOut, feed it the positive CLIPTextEncode output, and connect KSampler's negative input to it.

To verify, I rendered the same seed and JSON both ways and compared pixel by pixel. The images were pixel-identical (mean absolute difference: 0). The first image went from 89.7 s to 62.4 s.

This is only free at CFG 1. Above CFG 1, a zeroed negative and an empty-string encoding are different inputs, and the results change.

Still unresolved

  • The fast versions add fake English to the background at seed 123. The sample is small, so I do not know how often it happens. Do not publish their output unreviewed.
  • Whether the fast versions really fix the landscape channel-name box: one seed is not enough to say.
  • The 4-step LoRA's model card suggests 6 to 8 steps above 1024². I kept 4, and soft faces at 1088x1920 are the cost.
  • Comic character consistency relies on text description alone, and head-to-body ratio and prop counts drift. I have not tried reference images or a character LoRA.
  • I used the official OpenMOSS plugin plus my own patch. I have not compared its speed against the native ComfyUI nodes that came later.

Also in this series: One RTX 2080 Ti Narrates Every Daily AI-News Short: VoxCPM2 GGUF Q4_K_M at Under 2 s per Line

FAQ

Does MOSS-Image render Traditional Chinese text accurately?
On the same news-thumbnail prompt, in portrait and landscape with two seeds each, MOSS-Image got the Traditional Chinese headline and badge right in 4 of 4 images. Qwen-Image 2.1 got 0 of 4, writing 全 as 佺 in every one. The weak spot in MOSS is a wide, flat text box along the bottom of a landscape image: all 4 landscape channel names came out with extra or repeated characters, so I still overlay fixed small text afterwards.
How do I know the MOSS-Image 4-step LoRA actually applied?
Check the ComfyUI log, not the image. The built-in LoraLoaderModelOnly on the Comfy-Org int8 weights prints 378 'lora key not loaded' lines, because the LoRA has separate q/k/v keys and the weights have one fused qkv layer. Images still render. Fuse the LoRA's q/k/v into the qkv shape; a full load prints 0 of those lines.
Can an RTX 2080 Ti run MOSS-Image?
Yes. On a 2080 Ti 22G that also hosts a voice-narration service, putting the Qwen3.5-9B text encoder on CPU and using the 4-step LoRA gives about 58 s per image, and the voice service stayed available the whole time. The same workflow takes 2.2 s on an RTX 5090.
Is a W4A8 text encoder faster on CPU than int8?
No, it is slower. With the text encoder on CPU, int8 took 58.5 s per image and the Comfy-Org W4A8 GPTQ file took 108.4 s. Text encoding alone was 2.87x slower. The file is smaller, but there is no CPU kernel for W4A8, so it gets dequantized before compute and you save memory, not time.
In a multi-panel comic, why does a character appear in panels where it should not?
Usually because it is described in the global caption. My first version had 'Claude when present is a tiny orange octopus with round spectacles' in the global summary, and the octopus showed up in panels 1, 2 and 4. Keeping the global caption to characters present in every panel, naming who is present in each panel's scene, and saying each character appears once fixed it.

Read next

Don't miss the next one

Subscribe, and you won't.

One-click unsubscribe anytime.