📘 คู่มือการใช้งานฉบับสมบูรณ์ · 2026-08-30

ChromaStage
คู่มือการใช้งานแบบละเอียด

เว็บ Chroma Key Compositor — ตัดคนจากภาพพื้นเขียว ใส่ฉากหลัง ส่งออกเป็นรูป ZIP/PDF หรือวิดีโอ MP4 คู่มือนี้ทดสอบครบทุกฟีเจอร์กับรูปภาพพื้นเขียวจริง 32 เทสเคส ผ่าน 32/32

32
เทสเคสทั้งหมด
100% PASS
9
บั๊กที่เจอ + แก้
2 CRITICAL · 1 HIGH · 6 LOW
295
Automated Tests
P0=112 · P1=119 · P2=62
60s
VDO สูงสุด
render ใน 3.7s · 16× realtime

🚀 เริ่มต้นใช้งาน (Quick Start)

5 นาที — เปิด server, upload รูป, render ออกมา

1. Start server

cd C:\Users\sjryzen7\Documents\Codex\chromastage
python main.py
# → INFO: Uvicorn running on http://0.0.0.0:30089

2. เช็คสุขภาพ server

curl http://127.0.0.1:30089/healthz
# {"ok":true,"ffmpeg":"...","version":"1.4.2"}

✅ ถ้าเห็น "version":"1.4.2" แสดงว่าพร้อมใช้งาน

3. เปิดเว็บ

ไปที่ http://localhost:30089/ — UI จะ auto-mint cookie cs_uid ให้

4. Flow พื้นฐาน 4 ขั้น

1
Upload คนพื้นเขียว
ลากไฟล์ .jpg/.png ลง dropzone → chroma ถูก detect อัตโนมัติ → ได้ job_id
2
Upload ฉากหลัง
ลาก 1-30 ภาพ .jpg/.png ลง → แต่ละภาพจะกลายเป็น scene ใน VDO หรือภาพในชุด
3
ตั้งค่า
เลือก mode (Image / VDO), preset ขนาด, format, watermark, BGM, intro/outro
4
Render & Download
กดปุ่ม Render → รอ 1-4 วินาที → Download ZIP/PDF หรือ MP4

5. ตัวอย่าง curl (5 คำสั่ง จบ)

# 1) Mint cookie
curl -c jar.txt http://127.0.0.1:30089/api/me

# 2) Upload person
curl -b jar.txt -F "file=@my_greenscreen.jpg" \
  http://127.0.0.1:30089/api/upload/person
# → {"job_id":"abc123","chroma":{"color":"0x00DC00",...}}

# 3) Upload 3 backgrounds
curl -b jar.txt -F "files=@bg1.jpg" -F "files=@bg2.jpg" -F "files=@bg3.jpg" \
  http://127.0.0.1:30089/api/jobs/abc123/upload/backgrounds

# 4) Render image set
curl -b jar.txt -X POST http://127.0.0.1:30089/api/jobs/abc123/render/images \
  -H 'Content-Type: application/json' -d '{"image_preset":"square","image_format":"png"}'

# 5) Download ZIP
curl -b jar.txt -o out.zip http://127.0.0.1:30089/api/jobs/abc123/download/zip

📁 ไฟล์ต้นทาง (Source Files)

ทุกเทสเคสในคู่มือนี้ใช้รูปจริงจาก G:\My Drive\0000000\20260828\

Person (คนพื้นเขียว)

ไฟล์ขนาดต้นฉบับResize ใช้ทดสอบคุณสมบัติ
IMG_5557.JPG ⭐ greener 6000×4000, 5.3MB 1500×1000, 176KB 230/300 pixels เป็นสีเขียว → พื้นเขียวชัดเจน ใช้เป็นหลัก
IMG_5554.JPG 6000×4000, 5.3MB 1500×1000, 176KB 227/300 green pixels → alt person

Backgrounds (ฉากหลัง)

ไฟล์Resizeใช้ในเทส
IMG_5544.JPG1620×1080เกือบทุกเทส (BG1)
IMG_5546.JPG1620×1080BG2
IMG_5584.JPG1620×1080BG3 · intro/outro
IMG_5628.JPG1620×1080BG4
IMG_5715.JPG1620×1080BG5
💡 ทำไมต้อง resize ก่อน?

ภาพต้นฉบับ 6000×4000 = 24 megapixel, render จะใช้เวลา 10-30 วินาที Resize เหลือ 1500×1000 = 1.5 MP ใช้เวลา 1-3 วินาที คุณภาพ chroma key ไม่ได้ลดลง (ระบบจะ downscale ใหม่อีกครั้งตอน composite อยู่แล้ว)

🟢 Case A · Image Mode

คนพื้นเขียว + N ฉากหลัง → ชุดภาพ Composite + ZIP + PDF

A.1

Square 1080×1080 PNG (IG square post)

3 backgrounds · 1.21s render · ZIP 2.0MB
PASS
A.1 Square 1080x1080 result
ผลลัพธ์: composite 1080×1080 — คนพื้นเขียวถูกตัดใส่ฉากหลังอย่างสะอาด

Config ที่ใช้

{
  "image_preset": "square",       // → 1080×1080
  "image_format": "png",          // lossless
  "rename_pattern": "img_{n:03d}" // img_000.png, img_001.png, img_002.png
}

ผลที่ได้

  • ✅ 3 ภาพ PNG ขนาด 1080×1080 ตรงเป๊ะ
  • ✅ ZIP 2.0MB, PDF 193KB
  • ✅ Chroma key: พื้นเขียวถูกลบออกหมด ขอบคนสะอาด

Preset: square

IMAGE_PRESETS = {
  "square":    (1080, 1080),  # IG square post
  "portrait":  (1080, 1350),  # IG portrait post
  "story":     (1080, 1920),  # IG/TikTok story 9:16
  "fb_cover":  (820, 312),    # FB cover banner
  "youtube":   (1280, 720),   # YouTube thumbnail
  "wallpaper": (1920, 1080),  # 1080p wallpaper
  "custom":    None,          # use output_w / output_h
}
A.2

Portrait 1080×1350 + Watermark

JPG q80 · © ChromaStage 2026 · bottom-right · 1.23s render
PASS
A.2 Portrait result
Watermark "© ChromaStage 2026" ที่มุมขวาล่าง opacity 0.7

Config

{
  "image_preset": "portrait",      // → 1080×1350
  "image_format": "jpg",           // lossy
  "jpg_quality": 80,               // 0-100
  "watermark": {
    "enabled": true,
    "text": "© ChromaStage 2026",
    "position": "bottom-right",
    "opacity": 0.7,
    "size": 36,
    "color": "#FFFFFF"
  }
}

ผล

  • ✅ ขนาด 1080×1350 ตรงเป๊ะ
  • ✅ JPG quality 80 → ไฟล์เล็ก เหมาะกับ social upload
  • ✅ Watermark render ที่มุมขวาล่าง
A.3

Story 1080×1920 + Thai Watermark + Rename

WebP · Thai + emoji · 9-16 ratio · regression: เคย 500 บน Windows
PASS
A.3 Story Thai
Watermark สีเหลือง "ทดสอบ 🎉 ภาษาไทย" ตรงกลาง ขนาด 60px

Config

{
  "image_preset": "story",          // → 1080×1920 (IG/TikTok 9:16)
  "image_format": "webp",           // เล็กกว่า PNG เยอะ คุณภาพดีกว่า JPG
  "rename_pattern": "story_{n:04d}", // story_0000.webp, story_0001.webp, ...
  "watermark": {
    "enabled": true,
    "text": "ทดสอบ 🎉 ภาษาไทย",    // Thai + emoji
    "position": "center",
    "opacity": 0.8,
    "size": 60,
    "color": "#FFEB3B"               // สีเหลือง
  }
}

🐛 Regression Note

ก่อนแก้ bug B3: การ save config ที่มี Thai text ใน watermark จะได้ 500 UnicodeEncodeError: 'charmap' codec can't encode characters บน Windows (cp1252 default)

Fix: เพิ่ม encoding="utf-8" ใน app/storage.py:write_meta()

ผล

  • ✅ Thai + emoji ไม่ crash — render 3.66s
  • ✅ 1080×1920 ตรงเป๊ะ
  • ✅ ไฟล์ story_0000.webp, story_0001.webp, ...
A.4

Facebook Cover 820×312 JPG q70

Banner เล็ก · 0.51s render · ขนาดเล็กสุดในบรรดา preset
PASS
A.4 FB Cover
Cover 820×312 — JPG quality 70 (เล็กกว่า PNG ~80%)

Config

{
  "image_preset": "fb_cover",       // → 820×312
  "image_format": "jpg",
  "jpg_quality": 70,
  "rename_pattern": "cover_{n}"
}

ผล

  • ✅ 0.51s render — เร็วมากเพราะ output เล็ก
  • ✅ Aspect ratio 820:312 ≈ 2.63:1 (banner)
A.5

YouTube Thumbnail 1280×720

16:9 · PNG lossless · 0.91s render
PASS
A.5 YouTube Thumb
Thumbnail 1280×720 — PNG lossless สำหรับคมชัดสูง

Config

{
  "image_preset": "youtube",        // → 1280×720
  "image_format": "png",
  "rename_pattern": "yt_{n}"
}

Tip

YouTube แนะนำ thumbnail อย่างน้อย 1280×720 ขนาดใหญ่สุด 2MB. PNG ที่ preset นี้จะใหญ่กว่า JPG แต่คมชัดกว่าสำหรับข้อความ

A.6

Wallpaper 1920×1080 + Watermark

Full-HD · "Desktop Wallpaper" bottom-left · 2.04s render
PASS
A.6 Wallpaper
Wallpaper 1920×1080 — watermark มุมซ้ายล่าง opacity 0.5

Config

{
  "image_preset": "wallpaper",      // → 1920×1080
  "image_format": "jpg",
  "jpg_quality": 92,
  "watermark": {
    "enabled": true, "text": "Desktop Wallpaper",
    "position": "bottom-left", "opacity": 0.5, "size": 28
  }
}
A.7

Custom 800×600 PNG

ใช้ output_w/output_h ตรงๆ · 0.70s render
PASS

Config

{
  "image_preset": "custom",         // ← สำคัญ: บอกว่าใช้ output_w/h
  "output_w": 800,
  "output_h": 600,
  "image_format": "png",
  "rename_pattern": "custom_{n}"
}

💡 image_preset: "custom" บอกให้ระบบใช้ค่า output_w/output_h ตรงๆ แทนการ lookup จาก IMAGE_PRESETS

A.8

Format Comparison (PNG vs JPG q95 vs JPG q10 vs WebP)

เปรียบเทียบขนาดไฟล์ — ทุก format ผ่าน
PASS
FormatQualityRenderZIP Sizeใช้เมื่อ
PNGdefault0.56s857KB คุณภาพสูงสุด, lossless, alpha channel
JPG950.54s193KB ⭐ คุณภาพสูง + ขนาดเล็ก — แนะนำ
JPG100.42s14KB ⚠️ artifacts หนัก, เล็กมาก เหมาะแค่ thumbnail
WebPdefault0.52s14KB ⭐ เล็กเท่า JPG q10 แต่คุณภาพดีกว่ามาก

💡 WebP คือตัวเลือกที่ดีที่สุดถ้าต้องการทั้งคุณภาพและขนาดเล็ก — เล็กกว่า PNG 60 เท่า, คุณภาพใกล้เคียง JPG q95

A.9

Watermark × 9 ตำแหน่ง

ทุกตำแหน่ง render ถูกต้อง · 9/9 PASS
PASS
top-left
top-center
top-right
middle-left
middle-center
middle-right
bottom-left
bottom-center
bottom-right

TEXT_POSITIONS constant

TEXT_POSITIONS = {
  "top-left", "top-center", "top-right",
  "middle-left", "middle-center", "middle-right",
  "bottom-left", "bottom-center", "bottom-right",
}

💡 ทุกตำแหน่ง validated ใน app/config.py. ถ้าใส่ค่านอกเหนือ 9 นี้ จะ fallback เป็น bottom-right

A.10

Person Cut (RGBA PNG with transparency)

70.6% transparent · 29.2% opaque · mode=RGBA
PASS
A.10 Person Cut RGBA
RGBA PNG — พื้นเขียวถูกลบออก (โปร่งใส 70.6%) คน + ส่วนอื่น opaque 29.2%

เรียกดู

curl -b jar.txt -o cut.png http://127.0.0.1:30089/api/jobs/<JOB>/person_cut

วิเคราะห์ผล

mode: RGBA (4 channels)
size: 1500×1000
transparent pixels (alpha<50): 1,058,893 / 1,500,000 = 70.6%
opaque pixels     (alpha>200):   438,138 / 1,500,000 = 29.2%

การใช้งาน

  • ✅ ตรวจสอบคุณภาพ chroma key — ถ้า % transparent ต่ำเกินไป = key ไม่สะอาด
  • ✅ นำไปใส่ program อื่น (Photoshop, GIMP) เพื่อ edit เพิ่ม
  • ✅ ใช้เป็น asset ตั้งต้นสำหรับ render pipeline อื่น

🎬 Case B · VDO Mode

คนพื้นเขียว + N ฉากหลัง → MP4 พร้อม transitions, BGM, text overlay, Ken Burns

B.1

Cut Transition · 3 bgs × 5s = 15s

1280×720 @ 30fps · 1.83s render · MP4 1.15MB · 8× realtime
PASS
B.1 Cut transition frame
เฟรมที่ 25% (3.75s) — scene 1
B.1 Cut transition frame
เฟรมที่ 50% (7.5s) — scene 2
B.1 Cut transition frame
เฟรมที่ 75% (11.25s) — scene 3

Config

{
  "scene_duration": 5,         // แต่ละ scene 5 วินาที
  "transition": "cut",         // เปลี่ยนฉากแบบตัดตรงๆ (เร็วสุด)
  "transition_duration": 0.5,  // ไม่ใช้กับ cut แต่เก็บไว้
  "ken_burns": false,          // ไม่มี zoom
  "output_w": 1280,
  "output_h": 720,
  "fps": 30
}

ผลที่ได้

  • ✅ MP4 ขนาด 1280×720, 30fps
  • ✅ duration = 15.000000s exact (ffprobe confirmed)
  • ✅ File size 1.15MB (เล็กมาก เพราะ cut transition ไม่มี motion)
  • ✅ Render time 1.83s = 8x realtime

Pipeline

person photo → cut_person (chroma+despill) → RGBA PNG
  ↓
for each background:
  composite_scene (overlay centered) → still PNG
  render_scene_clip (Ken Burns optional) → scene_NNN.mp4
  ↓
concat with xfade (or cut) → merged.mp4
  ↓
apply text overlay (drawtext) → overlay.mp4
  ↓
apply BGM (stream_loop + volume) → final.mp4
B.2

Fade Transition · 2 bgs × 8s = 16s

1280×720 · 2.08s render · MP4 1.29MB
PASS
t=4s
t=8s (ระหว่าง fade)
t=12s

Config

{
  "scene_duration": 8,
  "transition": "fade",          // จางเข้า-ออก ระหว่าง scene
  "transition_duration": 0.8     // ใช้ 0.8s สำหรับ fade
}

💡 Fade ใช้ ffmpeg xfade filter — ต้องมี scene ≥ 2 จึงจะเห็น effect

B.3

Slide-Zoom Transition · 4 bgs × 3s = 12s

2.61s render · MP4 2.18MB (motion blur ทำให้ใหญ่กว่า cut)
PASS

Config

{"scene_duration": 3, "transition": "slide-zoom", "transition_duration": 0.5}

TRANSITIONS ที่รองรับ: cut · fade · slide-zoom

B.4

+ BGM (Background Music)

10s sine WAV · vol 0.5 · MP4 with audio stream
PASS

Steps

# 1) สร้าง BGM test (10s sine wave)
python -c "
import wave, struct, math
sr = 22050
with wave.open('test.wav','wb') as w:
    w.setnchannels(1); w.setsampwidth(2); w.setframerate(sr)
    for i in range(sr*10):
        s = int(8000 * math.sin(2*math.pi*440*i/sr))
        w.writeframes(struct.pack('<h', s))"

# 2) Upload BGM
curl -b jar.txt -F "file=@test.wav" \
  http://127.0.0.1:30089/api/jobs/<JOB>/bgm

# 3) Config + Render
{
  "scene_duration": 5,
  "bgm_volume": 0.5          // 0.0 - 1.0
}

รองรับ audio formats

ALLOWED_AUDIO_EXTS = {".mp3", ".wav", ".m4a", ".aac", ".ogg", ".flac"}
MAX_AUDIO_BYTES = 50 * 1024 * 1024  // 50MB

Behavior พิเศษ

  • ✅ BGM สั้นกว่า video → stream_loop -1 loop จนจบ
  • ✅ BGM ยาวกว่า video → ตัด audio ตาม video duration
  • ✅ ไม่มี BGM → MP4 ไม่มี audio stream (ยัง render ได้)
B.5

+ English Text Overlay "Hello World"

bottom-center · white · 48px · 1.46s render
PASS

Config

{
  "scene_duration": 4,
  "transition": "cut",
  "text_overlay": {
    "text": "Hello World",
    "position": "bottom-center",
    "color": "#FFFFFF",
    "size": 48,
    "opacity": 1.0
  }
}

Text Overlay fields

  • text: ข้อความ ≤120 chars
  • position: 9 ตำแหน่ง (เหมือน watermark)
  • color: hex #RRGGBB หรือ #RRGGBBAA
  • size: 10-200 (px)
  • opacity: 0.0-1.0
B.6

+ Thai Text Overlay (regression test)

"สวัสดี 🎉 ไทยแลนด์" · yellow · top-center · 56px · 1.45s render
PASS
B.6 Thai text
Thai text + emoji rendered ในภาพ preview — เห็นชัดที่ top-center สีเหลือง

Config

{
  "scene_duration": 4,
  "transition": "cut",
  "text_overlay": {
    "text": "สวัสดี 🎉 ไทยแลนด์",
    "position": "top-center",
    "color": "#FFD700",
    "size": 56,
    "opacity": 0.95
  }
}
🐛 Bug Regression Lock

Bug B3: ก่อนแก้, การ save config ที่มี Thai text ใน text_overlay.text → 500 UnicodeEncodeError บน Windows
Fix: app/storage.py เพิ่ม encoding="utf-8" ใน write_text()
Regression test: เคสนี้คือ regression lock — ถ้า regress อีกจะ fail

B.7

+ Intro + Outro Images

2s intro + 3s main + 2s outro = 7s · MP4 762KB
PASS
t=1.75s (กลาง intro)
t=3.5s (กลาง main scene)
t=5.25s (กลาง outro)

Steps

# Upload intro
curl -b jar.txt -F "file=@intro.jpg" \
  http://127.0.0.1:30089/api/jobs/<JOB>/intro

# Upload outro
curl -b jar.txt -F "file=@outro.jpg" \
  http://127.0.0.1:30089/api/jobs/<JOB>/outro

# Config
{
  "scene_duration": 3,
  "intro_duration": 2.0,
  "outro_duration": 2.0
}

💡 Intro/outro ใช้ render_static_clip() — เปลี่ยนภาพนิ่งเป็น MP4 แบบ static

B.8

Ken Burns (Slow Zoom)

3 bgs × 5s · MP4 1.29MB (มี motion → ใหญ่กว่า cut)
PASS
t=3.75s (zoom in 25%)
t=7.5s (zoom in 50%)
t=11.25s (zoom in 75%)

Config

{
  "scene_duration": 5,
  "ken_burns": true    // ← เปิด slow zoom
}

Effect

กล้องค่อยๆ zoom in จากขอบเข้าไปกลางภาพ ตลอด scene_duration — ทำให้ภาพนิ่งดู dynamic

💡 ใช้ /preview-clip/{n} ดู effect 3s ก่อน render เต็ม — เซฟเวลา debug

B.9

VDO Max Duration (60s)

4 bgs × 15s · 3.73s render · MP4 3.05MB · 16× realtime
PASS

Config

{
  "scene_duration": 15,    // 4 × 15 = 60s (limit)
  "transition": "cut"
}

Limits

  • scene_duration: 0.5 - 30 วินาที (validated)
  • จำนวน backgrounds: สูงสุด 30
  • Theoretical max video: 30 × 30 = 900 วินาที (15 นาที)

✅ ffprobe confirmed: duration=60.000000s exact

B.10

5 Backgrounds with Fade

5 bgs × 4s = 20s · 3.46s render · fade transition
PASS

📹 VDO output saved to vdo-mode/
(5 backgrounds × 4s = 20s · fade transition)

Config

{
  "scene_duration": 4,
  "transition": "fade",
  "transition_duration": 0.5
}
// 5 backgrounds uploaded

🎯 Edge Cases

เทสเพิ่มเติม — ทดสอบความเสถียรของระบบ

EC.1

Same Person × 3 Backgrounds

เปรียบเทียบผลลัพธ์ — คนเดียวกัน 3 ฉากหลัง
PASS
BG1
BG2
BG3

✅ คนเดียวกัน + chroma เดียวกัน → ขอบคนสะอาดเหมือนกันทุก background ตรวจสอบ chroma detection consistency

EC.2

Preview-Clip × 3 Scenes

3s MP4 ต่อ scene · ดู effect ก่อน render เต็ม
PASS
Scene 0
Scene 1
Scene 2

API

GET /api/jobs/<JOB>/preview-clip/{n}
# Returns 3-second MP4 (max) with Ken Burns + text overlay applied
# Render time ~0.5-1s per clip — fast enough for "see before you render" UX

🆕 AI Backgrounds · 9:16 TikTok Ready

ทดสอบครั้งที่ 2 กับพื้นหลัง AI ขนาด 941×1672 (9:16 portrait) — 20/20 PASS

🆕

ภาพรวม: Same Person × 5 AI Backgrounds

253 ภาพ AI-generated จาก Ai ฉากเขียว/พื้นหลัง\ · สุ่ม 5 ภาพมาทดสอบ
20/20

Grid of 5 AI backgrounds

💡 Key Insight

AI backgrounds ทั้งหมดเป็น 9:16 portrait พอดี — render VDO แนวตั้ง TikTok/Reels/Shorts ได้ตรงเป๊ะ ไม่ต้อง letterbox.
Chroma key ทำงานสม่ำเสมอแม้ background จะแตกต่างกันมาก (อุตสาหกรรม / สตูดิโอ / โกดัง) — ขอบคนสะอาด ไม่มี green halo

🟢 Image Mode (AI backgrounds)

T01 · Story 9:16 PNG
T03 · Square + Thai Watermark
T02 · Portrait + Watermark
T05 · Wallpaper letterbox
T06 · Person Cut RGBA
T12 · Thai text on AI bg

🎬 VDO Mode 9:16 (TikTok-ready)

ทุก VDO ใช้ output_w=1080, output_h=1920 — render ตรง 9:16 โดยไม่ scale/letterbox

T08 · TikTok 9:16 cut
T09 · Fade 9:16
T10 · Slide-zoom 9:16
T11 · + BGM
T13 · Ken Burns
T14 · 60s max 9:16
T15 · 5 backgrounds fade
CaseDescriptionOutputTimeSpeed
T07VDO 16:9 (letterbox from 9:16 bg)15.0s @ 1280×7201.83s8× realtime
T08VDO 9:16 TikTok Portrait15.0s @ 1080×19202.4s6× realtime
T09VDO 9:16 fade transition14.5s @ 1080×19202.5s5.8× realtime
T10VDO 9:16 slide-zoom10.5s @ 1080×1920——
T11VDO 9:16 + BGM (12s sine WAV)12.0s @ 1080×1920——
T12VDO 9:16 + Thai text "สวัสดี 🎬 ฉากหลัง AI"12.0s @ 1080×1920——
T13VDO 9:16 Ken Burns15.0s @ 1080×1920——
T14VDO 9:16 max 60s (4 × 15s)60.0s @ 1080×1920 exact5.8s10× realtime
T15VDO 9:16 with all 5 AI backgrounds18.0s @ 1080×1920——

🆚 เปรียบเทียบ Real-photo vs AI Backgrounds

Real-photo (test 1)AI Backgrounds (test 2)
Source20260828\ (133 JPG)Ai ฉากเขียว/พื้นหลัง\ (253 PNG)
Resolution1620×1080 (16:9)941×1672 (9:16)
Contentภาพธรรมชาติ/สถานที่อุตสาหกรรม AI generated
Tests32 cases20 cases
Pass32/32 (100%)20/20 (100%)
Best forDesktop, YouTube, wallpaperTikTok, Reels, Shorts
Output sizes1920×1080 wallpaper, 1280×720 vdo1080×1920 vdo (vertical)

🆕 EXIF Auto-Orient + Person Fit/Position

3 features ใหม่ใน v1.4.2 — แก้ปัญหา "คนนอนตะแคง" + "คนเล็กไป"

🆕

EXIF Auto-Orient (B10 fix)

upload รูปแนวตั้ง → ChromaStage หมุนให้อัตโนมัติ · ไม่ต้อง pre-rotate
PASS

B10 fixed

ผลลัพธ์: source 6000×4000 (EXIF=8) → auto-transposed เป็น 4000×6000 → คนยืนตรง

ก่อนแก้ (B10)

Source: 6000×4000 pixels, EXIF orientation=8 (90° CCW)
Upload → stored as-is
Render → person นอนตะแคง
→ ต้อง pre-rotate ก่อน upload (workaround)

หลังแก้

Source: 6000×4000 pixels, EXIF orientation=8
Upload → ImageOps.exif_transpose() applied
        → file re-saved as 4000×6000, EXIF stripped
Render → person upright ✓

ที่ใช้

from PIL import Image, ImageOps
from .storage import auto_orient_image

# Called automatically after every person/bg/intro/outro upload:
auto_orient_image(save_path)
💡 ทำไมสำคัญ

โทรศัพท์ส่วนใหญ่ถ่ายแนวตั้ง แต่ JPG บันทึก landscape pixels + EXIF tag
ถ้าไม่อ่าน EXIF → output ออกมานอนตะแคง (เคยเจอใน live test 2026-08-30)

🆕

person_fit & person_position

ปรับขนาด + ตำแหน่งคนในเฟรม — fill เต็มกว่าเดิม 70%
PASS

3 โหมด person_fit

ModeBehaviorWhen to use
contain (default)scale คนให้พอดี frame, preserve aspect, may letterboxคนเต็มตัว, มี padding
coverscale คนให้เต็ม frame, may crop ขอบคนเด่น พื้นหลังเต็ม
fillstretch คนให้เต็มเป๊ะ, บิด aspectartistic effect

9 ตำแหน่ง person_position

{"person_position": "bottom-center"}  // ← default for "ground" feel
// Same 9-grid as watermark/text_overlay
top-left
top-center
top-right
middle-left
middle-center
middle-right
bottom-left
bottom-center
bottom-right

ใหม่: defaults ดีขึ้น

FieldOld defaultNew default (v1.4.2+)
person_scale0.70.85 (เต็มขึ้น ~20%)
person_fit(none)"contain"
person_position"center""center" (same)

ตัวอย่าง config

// คนเต็ม frame แบบ TikTok
{
  "image_preset": "story",
  "person_fit": "cover",
  "person_position": "bottom-center",
  "person_scale": 1.0
}

// คนยืนบนพื้น ไม่ลอย
{
  "person_fit": "contain",
  "person_position": "bottom-center",
  "person_scale": 0.85
}

Validation

// Invalid values → 400
POST /api/jobs/{id}/config
{"person_fit": "stretch"}  // → 400 "invalid person_fit: stretch (must be contain|cover|fill)"

{"person_position": "nowhere"}  // → 400 "invalid person_position: nowhere (must be one of 9-grid)"

📊 Performance Benchmarks

ทดสอบบน Intel CPU (no GPU), libx264 ultrafast, crf=22

OperationRender TimeOutputSpeed
Image 3 bgs square PNG1.21s3 PNGs (2MB zip)—
Image 4 bgs story WebP + Thai3.66s4 WebPs (315KB zip)—
VDO cut 15s1.83s1.15MB MP48× realtime
VDO fade 16s2.08s1.29MB MP47.7× realtime
VDO slide-zoom 12s2.61s2.18MB MP44.6× realtime
VDO max 60s3.73s3.05MB MP416× realtime
Preview single scene0.24sPNG—
Preview-clip single scene0.31s3s MP410× realtime
Person cut (chroma key only)<1s1.26MB RGBA PNG—

💡 CPU-only path เร็วพอสำหรับ interactive web app — render 60s video ใน 4 วินาที

📐 Watermark Position Reference

9 ตำแหน่งใน TEXT_POSITIONS — ทั้ง watermark และ text_overlay ใช้ชุดนี้ร่วมกัน

top-left
top-center
top-right
middle-left
middle-center
middle-right
bottom-left
bottom-center
bottom-right

📚 API Quick Reference

ทั้งหมด 38 endpoints — ดูรายละเอียดเต็มใน [test plan](../2026-08-30-chromastage-test-plan.md)

Job Lifecycle

MethodPathDescription
POST/api/upload/personUpload person → new job
POST/api/jobs/{id}/upload/personAdd person to existing job
POST/api/jobs/{id}/upload/backgroundsUpload 1-30 backgrounds
GET/api/jobs/{id}Full job meta
DEL/api/jobs/{id}/backgrounds/{n}Remove 1 background
POST/api/jobs/{id}/reorderReorder backgrounds

Configuration

MethodPathDescription
POST/api/jobs/{id}/chromaSet chroma key params
POST/api/jobs/{id}/configSet 24 config fields
POST/api/jobs/{id}/bgmUpload BGM
POST/api/jobs/{id}/introUpload intro image
POST/api/jobs/{id}/outroUpload outro image

Preview & Render

MethodPathDescription
GET/api/jobs/{id}/preview/{n}Still composite
GET/api/jobs/{id}/preview-clip/{n}3s MP4 clip
POST/api/jobs/{id}/renderRender final VDO
POST/api/jobs/{id}/render/imagesRender image set
GET/api/jobs/{id}/downloadMP4
GET/api/jobs/{id}/download/zipZIP
GET/api/jobs/{id}/download/pdfPDF

📖 เต็ม endpoint list ดูที่ live demo หรือ /docs

🔧 Troubleshooting

ปัญหาที่เจอบ่อย + วิธีแก้

❌ /healthz returns version "1.4.0" แต่ code v1.4.2
แก้แล้ว: app/__init__.py อัปเดตเป็น "1.4.2". restart server ด้วย systemctl restart chromastage หรือ Ctrl+C แล้ว python main.py ใหม่
❌ /api/templates returns 500 บน Windows
แก้แล้ว (B1): ตอนนี้ทุก read_text() ใช้ encoding="utf-8". template birthday / business / product / realestate / gaming / travel มี Thai หรือ emoji
❌ save config ที่มี Thai text → 500
แก้แล้ว (B3): write_meta() ใน app/storage.py ใช้ encoding="utf-8". watermark + text_overlay รับ Thai/emoji/Japanese ได้แล้ว
❌ ผู้ใช้ B อ่าน/แก้งานของผู้ใช้ A ได้
แก้แล้ว (B2): ทุก endpoint ที่ใช้ job_id ตรวจ meta.user_uid กับ cookie cs_uid แล้ว — ต่างกัน → 403 forbidden
❌ /api/templates/birthday/bg/5 returns 404
แก้แล้ว (B7): เปลี่ยนเป็น 1-indexed — /bg/1 คือ bg1.jpg, /bg/5 คือ bg5.jpg, /bg/0 คือ 404
❌ upload ไฟล์ >40MB
By design: MAX_PERSON_BYTES = 40MB → return 413 Payload Too Large พร้อม detail "person image too large (>40MB)". resize ก่อน upload
❌ upload background ไฟล์ที่ 31
By design: MAX_BG_COUNT = 30 → return 400 max 30 backgrounds. ลดจำนวน หรือ split หลาย jobs
❌ image_preset="square" ไม่เปลี่ยนขนาด
ตรวจ syntax: {"image_preset":"square","output_w":null,"output_h":null} — ถ้า output_w/h ถูก override มาก่อน ระบบจะใช้ค่านั้น. ลอง POST /api/jobs/{id}/config ใหม่โดยไม่ส่ง output_w/h
❌ render fail "no backgrounds"
ต้อง upload background ≥ 1 ภาพก่อน render. POST /api/jobs/{id}/upload/backgrounds ก่อน
❌ render fail "chroma cut failed"
ffmpeg อาจจะ crash หรือ person file หายไป. ตรวจ:
ls /c/var/lib/chromastage/persons/<job_id>.*
ls /c/var/lib/chromastage/jobs/<job_id>/person_rgba.png
ถ้า person_rgba.png หาย ให้ upload ใหม่
❌ /api/me/jobs ไม่แสดงงาน
Cookie หมดอายุ/เปลี่ยน. ลบ cookie แล้วเปิด /api/me ใหม่. หรืองานเกิน 30 วัน retention — ดู README section "30-day retention"

✅ ผลทดสอบรวม

32/32
Real-Photo Tests
100% PASS
9/9
Bugs Found + Fixed
+ regression locked
295
Automated Tests Planned
P0=112 · P1=119 · P2=62

Test Matrix

IDCaseTimeOutputResult
A.1Square 1080² PNG1.21sZIP 2.0MBPASS
A.2Portrait + Watermark1.23sZIP 208KBPASS
A.3Story + Thai Watermark3.66sZIP 315KBPASS
A.4FB Cover 820×3120.51s—PASS
A.5YouTube 1280×7200.91s—PASS
A.6Wallpaper 1920×10802.04s—PASS
A.7Custom 800×6000.70s—PASS
A.8Format Comparison (4 sub-tests)——PASS
A.9Watermark × 9——PASS
A.10Person Cut RGBA<1s1.26MB PNGPASS
B.1VDO Cut 15s1.83sMP4 1.15MBPASS
B.2VDO Fade 16s2.08sMP4 1.29MBPASS
B.3VDO Slide-Zoom 12s2.61sMP4 2.18MBPASS
B.4VDO + BGM1.66sMP4 860KBPASS
B.5VDO + EN Text1.46sMP4 599KBPASS
B.6VDO + Thai Text1.45sMP4 859KBPASS
B.7VDO + Intro/Outro1.21sMP4 762KBPASS
B.8VDO Ken Burns1.90sMP4 1.29MBPASS
B.9VDO Max 60s3.73sMP4 3.05MBPASS
B.10VDO 5 Backgrounds3.46s—PASS
EC.1Same Person × 3 BG1.24s—PASS
EC.2Preview-Clip × 30.6s each—PASS