A release-readiness QA pass over the whole product. The commits split into defects a user would hit and gates that were reporting green while measuring nothing. ## Fixes that change behaviour Rate limiting was bypassable on every install: TRUST_PROXY defaulted to true, so request.ip came from a client-set header and a forged X-Forwarded-For got past the login limiter. The default is now a private-network trust list. A transient Postgres outage stranded in-flight jobs, leaving finished output on disk with no row pointing at it. A reconciler now resolves those rows and adopts the bytes rather than dropping the work. A Redis connection that moved to a new address wedged every read-blocked consumer, so completions stopped signalling while health still answered 200. Socket timeouts plus subscriber pings recover it. Installing more than one AI bundle left the shared venv multi-versioned and silently broke three tools. The installer now reconciles distributions to one version each. Converting an image to JXL at quality 1 through 4 returned a 500, because libjxl 0.7 rejects the distance those values compute. The quality is floored at what the encoder honours. A missing ffmpeg was also reported to the user as a corrupt upload; it now says the engine is unavailable. RAW uploads reached an unpatched LibRaw on arm64, so it is built from source at 0.22.2, and the release scan was split so it can fail on an unfixed critical instead of hiding it behind ignore-unfixed. ## Gates that could not fail Two mutation lanes ran zero mutants because Stryker crawled the gitignored docs build; coverage discarded its whole report on any failing test; the lint gate skipped root tests, scripts, and two workspaces; and several generated matrices counted a host missing ffmpeg as a passing tool. Each now measures what it claims. Full evidence and the outstanding release items are tracked locally and are not part of this branch.
18 KiB
description, i18n_source_hash, i18n_provenance, i18n_output_hash, i18n_hash_version
| description | i18n_source_hash | i18n_provenance | i18n_output_hash | i18n_hash_version |
|---|---|---|---|---|
| โครงสร้าง monorepo, สถาปัตยกรรมของแอปและแพ็กเกจ, วงจรชีวิตของคำขอ และรอยเท้าทรัพยากรของ SnapOtter | 50e076925c4b | human | b3755300060a | 2 |
Architecture
SnapOtter เป็น monorepo ที่จัดการด้วย pnpm workspaces และ Turborepo ปรับใช้เป็นสแตก Docker Compose 3 คอนเทนเนอร์: อิมเมจแอป SnapOtter, PostgreSQL 17 และ Redis 8
Project structure
snapotter/
├── apps/
│ ├── api/ # Fastify backend
│ ├── web/ # React + Vite frontend
│ └── docs/ # This VitePress site
├── packages/
│ ├── image-engine/ # Sharp-based image operations
│ ├── media-engine/ # FFmpeg spawn + progress parsing
│ ├── doc-engine/ # qpdf, LibreOffice, ghostscript wrappers
│ ├── ai/ # Python AI model bridge
│ └── shared/ # Types, constants, i18n
└── docker/ # Dockerfile and Compose config
Packages
@snapotter/image-engine
ไลบรารีประมวลผลรูปภาพหลักที่สร้างบน Sharp จัดการการดำเนินการที่ไม่ใช่ AI ทั้งหมด: ปรับขนาด, ครอบตัด, หมุน, พลิก, แปลง, บีบอัด, ลบเมทาดาทา และปรับสี (ความสว่าง, ความเปรียบต่าง, ความอิ่มตัว, ขาวดำ, ซีเปีย, กลับสี, ช่องสี)
แพ็กเกจนี้ไม่มี dependency ของเครือข่ายและทำงานภายในกระบวนการทั้งหมด
@snapotter/ai
เลเยอร์บริดจ์ที่เรียกเนทีฟและรันไทม์ Python ML เครื่องมือ Python ส่วนใหญ่ใช้ dispatcher แบบถาวรที่นำเข้าไลบรารีขนาดใหญ่ล่วงหน้า (PIL, NumPy, MediaPipe, rembg) ดังนั้นการโทรครั้งต่อไปจะข้ามค่าใช้จ่ายในการนำเข้า OCR ถูกแยกออกจากสภาพแวดล้อมที่ใช้ร่วมกันที่ไม่แน่นอน: fast เรียกใช้ Tesseract ดั้งเดิม ในขณะที่ balanced และ best ใช้ JSONL dispatcher ถาวรโดยเฉพาะที่ปักหมุดไว้กับรุ่น RapidOCR/ONNX ที่ไม่เปลี่ยนรูปแบบที่ใช้งานอยู่ แต่ละคำขอจะมี generation lease การเปิดใช้งานจะรัน smoke test บนตัวเลือกแรก จากนั้นจึงสลับไปที่ dispatcher แบบอะตอมมิก dispatcher รุ่นก่อนหน้าจะระบายออกก่อนที่จะมีการรวบรวมขยะ
โมเดลไม่ได้ถูกโหลดล่วงหน้า สคริปต์ของแต่ละเครื่องมือโหลดน้ำหนักโมเดลจากดิสก์ ณ เวลาที่ขอ และทิ้งเมื่อคำขอเสร็จสิ้น ดู Resource footprint สำหรับโปรไฟล์หน่วยความจำทั้งหมด
การดำเนินการที่รองรับ: การลบพื้นหลัง (rembg/BiRefNet), การลดขนาด (RealESRGAN), การเบลอใบหน้า (MediaPipe), การปรับปรุงใบหน้า (GFPGAN/CodeFormer), การลบวัตถุ (LaMa ONNX), OCR (Tesseract และ RapidOCR พร้อมรุ่น PP-OCR ONNX), การปรับสี (DDColor), การกำจัดสัญญาณรบกวน, การลบตาแดง, การฟื้นฟูภาพถ่าย, การสร้างภาพถ่ายหนังสือเดินทาง การแก้ไขความโปร่งใส (BiRefNet HR-matting) และการปรับขนาดการรับรู้เนื้อหา (Go caire binary)
สคริปต์ Python ใช้งานจริงใน packages/ai/python/ แพ็กแบบจำลองเสริมขนาดใหญ่ได้รับการติดตั้งตามความต้องการในไดรฟ์ข้อมูล /data/ai แบบถาวร OCR ที่แม่นยำใช้สิ่งประดิษฐ์เฉพาะแพลตฟอร์มที่มีการลงนาม ระดับ Tesseract ในตัวไม่จำเป็นต้องดาวน์โหลดแพ็คโมเดล
@snapotter/shared
ประเภท TypeScript ที่ใช้ร่วมกัน, ค่าคงที่ (เช่น APP_VERSION และการกำหนดเครื่องมือ) และสตริงการแปล i18n ที่ใช้ทั้งส่วนหน้าและส่วนหลัง
Applications
API (apps/api)
เซิร์ฟเวอร์ Fastify v5 ที่เปิดเผยเส้นทางเครื่องมือ 243 เส้นทางครอบคลุมห้ารูปแบบ (image, video, audio, PDF, file) ที่จัดการ:
- การอัปโหลดไฟล์, การจัดการพื้นที่ทำงานชั่วคราว และที่จัดเก็บไฟล์แบบถาวร
- คลังไฟล์ผู้ใช้ (ตาราง
user_files): โดยค่าเริ่มต้น การแก้ไขที่บันทึกไว้จะถูกจัดเก็บเป็นไฟล์ใหม่อิสระ หรือเป็นเวอร์ชันที่เชื่อมโยงกับแถวแม่เมื่อคุณเขียนทับไฟล์ต้นฉบับ โดยจะบันทึกว่าใช้เครื่องมือใดบ้าง (toolChain) และได้ภาพขนาดย่อที่สร้างอัตโนมัติสำหรับหน้า Files - การเรียกใช้เครื่องมือ (กำหนดเส้นทางคำขอเครื่องมือแต่ละรายการไปยังเอนจินรูปภาพหรือบริดจ์ AI)
- การประสานงานไปป์ไลน์ (เชื่อมโยงเครื่องมือหลายตัวตามลำดับ)
- การประมวลผลเป็นชุดพร้อมการควบคุมการทำงานพร้อมกันผ่านคิวงาน BullMQ (pool: image, media, ai, docs, system)
- การยืนยันตัวตนผู้ใช้, RBAC (บทบาท admin/user พร้อมชุดสิทธิ์เต็ม), การจัดการคีย์ API และการจำกัดอัตรา
- การจัดการทีม - CRUD เฉพาะ admin ผู้ใช้ถูกกำหนดให้อยู่ในทีมผ่านฟิลด์
teamบนโปรไฟล์ของพวกเขา - การตั้งค่ารันไทม์ - ที่จัดเก็บแบบคีย์-ค่าในตาราง
settingsที่ควบคุมdisabledTools,enableExperimentalTools,loginAttemptLimitและปุ่มปรับการทำงานอื่น ๆ โดยไม่ต้องปรับใช้ใหม่ - การสร้างแบรนด์กำหนดเองและการตั้งค่ารันไทม์ผ่านการตั้งค่าที่รองรับด้วยฐานข้อมูล
- เอกสาร Scalar/OpenAPI ที่
/api/docs - การเสิร์ฟส่วนหน้าที่สร้างแล้วเป็น SPA ในการใช้งานจริง
Dependency หลัก: Fastify, Drizzle ORM (pg-core, node-postgres), Sharp, BullMQ, ioredis, Zod สำหรับการตรวจสอบ
เซิร์ฟเวอร์จัดการการปิดตัวอย่างสง่างามเมื่อได้รับ SIGTERM/SIGINT: มันระบายการเชื่อมต่อ HTTP, หยุด worker ของ BullMQ, ปิด Python dispatcher และปิดการเชื่อมต่อฐานข้อมูล
Web (apps/web)
แอปหน้าเดียว React 19 ที่สร้างด้วย Vite ใช้ Zustand สำหรับการจัดการสถานะ, Tailwind CSS v4 สำหรับการจัดสไตล์ และ Lucide สำหรับไอคอน สื่อสารกับ API ผ่าน REST และ SSE (สำหรับการติดตามความคืบหน้า)
หน้าต่าง ๆ รวมถึงพื้นที่ทำงานเครื่องมือ, หน้า Files สำหรับจัดการการอัปโหลดและผลลัพธ์แบบถาวร, ตัวสร้างระบบอัตโนมัติ/ไปป์ไลน์ และแผงการตั้งค่าผู้ดูแลระบบ
ส่วนหน้าที่สร้างแล้วถูกเสิร์ฟโดยส่วนหลัง Fastify ในการใช้งานจริง จึงไม่มีเว็บเซิร์ฟเวอร์แยกต่างหากในคอนเทนเนอร์ Docker
Docs (apps/docs)
ไซต์ VitePress นี้ ปรับใช้ไปยัง Cloudflare Pages โดยอัตโนมัติเมื่อ push ไปยัง main
How a request flows
- ผู้ใช้เลือกเครื่องมือใน UI เว็บและอัปโหลดไฟล์
- ส่วนหน้าส่ง multipart POST ไปยัง
/api/v1/tools/:section/:toolIdพร้อมไฟล์และการตั้งค่า - เส้นทาง API ตรวจสอบอินพุตด้วย Zod จากนั้นส่งต่อการประมวลผล
- สำหรับเครื่องมือมาตรฐาน งานจะถูกจัดคิวไปยัง BullMQ pool ที่เหมาะสม (image, media หรือ docs ตามรูปแบบ) worker BullMQ ในกระบวนการจะปรับทิศทางภาพอัตโนมัติตามเมทาดาทา EXIF, รันฟังก์ชันการประมวลผลของเครื่องมือ และส่งคืนผลลัพธ์
- สำหรับเครื่องมือ AI ส่วนใหญ่ สะพาน TypeScript จะส่งคำขอไปยัง Python dispatcher แบบถาวร Fast OCR จะเรียกใช้ Tesseract แทน และ OCR ที่แม่นยำจะเริ่มต้นการดำเนินการที่ปักหมุดไว้จากรุ่น OCR ที่ไม่เปลี่ยนรูปแบบที่ใช้งานอยู่ ระดับ OCR ที่ร้องขอได้รับการแก้ไขที่ทางเข้าและจะไม่มีการเปลี่ยนแปลงอย่างเงียบๆ ในระหว่างการดำเนินการ
- ความคืบหน้าของงานจะถูกบันทึกลงในตาราง
jobsใน PostgreSQL เพื่อให้สถานะอยู่รอดจากการรีสตาร์ตคอนเทนเนอร์ การอัปเดตแบบเรียลไทม์ถูกส่งผ่าน SSE ที่/api/v1/jobs/:jobId/progress - API ส่งคืน
jobIdและdownloadUrlผู้ใช้ดาวน์โหลดไฟล์ที่ประมวลผลแล้วจาก/api/v1/download/:jobId/:filename
สำหรับไปป์ไลน์ API จะป้อนเอาต์พุตของแต่ละขั้นตอนเป็นอินพุตให้ขั้นตอนถัดไป โดยรันตามลำดับ
สำหรับการประมวลผลเป็นชุด API ใช้ BullMQ flow พร้อม child job ต่อขั้นตอน และส่งคืนไฟล์ ZIP พร้อมไฟล์ที่ประมวลผลแล้วทั้งหมด
Resource footprint
SnapOtter ออกแบบมาเพื่อการใช้หน่วยความจำขณะว่างต่ำ ไม่มีสิ่งใดถูกโหลดล่วงหน้าหรืออุ่นไว้ตอนเริ่มต้น
At idle
กระบวนการ Node.js/Fastify, PostgreSQL และ Redis กำลังทำงาน RAM ขณะว่างโดยทั่วไปอยู่ที่ ~200-300 MB รวมทั้งสามคอนเทนเนอร์ (กระบวนการ Node.js, Postgres และ Redis) ไม่มีกระบวนการ Python ไม่มีน้ำหนักโมเดลในหน่วยความจำ
What starts, and when
| Component | Starts when | Memory while active |
|---|---|---|
| เซิร์ฟเวอร์ Fastify + Postgres + Redis | เมื่อคอนเทนเนอร์เริ่ม | ~200-300 MB รวม |
| worker BullMQ | เมื่อคอนเทนเนอร์เริ่ม (ในกระบวนการ) | หนึ่ง worker ต่อ pool (image, media, ai, docs, system) |
| Python dispatcher | คำขอเครื่องมือ AI ครั้งแรก | ตัวแปล Python + ไลบรารีที่นำเข้าล่วงหน้า (PIL, NumPy, MediaPipe, rembg) - ไม่มีน้ำหนักโมเดล |
| น้ำหนักโมเดล AI | ระหว่างคำขอของเครื่องมือนั้น ๆ | โหลดจากดิสก์ ปล่อยเมื่อคำขอเสร็จสิ้น |
Model loading
ไฟล์น้ำหนักโมเดลทั้งหมด (รวมหลาย GB) อยู่บนดิสก์ใน /opt/models/ ตลอดเวลา สคริปต์เครื่องมือ AI แต่ละตัวโหลดเฉพาะโมเดลของตัวเองเข้าหน่วยความจำตลอดระยะเวลาของคำขอ แล้วปล่อยออก บางสคริปต์เรียก del model และ torch.cuda.empty_cache() อย่างชัดเจนหลังการอนุมานเพื่อให้แน่ใจว่าหน่วยความจำถูกคืนทันที
ไม่มีแคชโมเดลระหว่างคำขอ การรันเครื่องมือ AI เดียวกันติดต่อกันจะโหลดโมเดลใหม่ทุกครั้ง สิ่งนี้ทำให้หน่วยความจำขณะว่างเข้าใกล้ศูนย์ โดยแลกกับความล่าช้าในการโหลดโมเดลในทุกคำขอ AI
First AI request cold start
Python dispatcher ไม่ทำงานเมื่อคอนเทนเนอร์เริ่มต้น คำขอ AI ครั้งแรกกระตุ้นสองสิ่งพร้อมกัน: dispatcher เริ่มอุ่นเครื่องในเบื้องหลัง และคำขอนั้นเองจะย้อนกลับไปสร้าง Python subprocess แบบครั้งเดียว เมื่อ dispatcher ส่งสัญญาณว่าพร้อม คำขอ AI ที่ตามมาทั้งหมดจะใช้มันโดยตรงและข้ามค่าใช้จ่ายในการสร้าง subprocess