macOS · Bash + Swift · Vision.framework

Drive Auto-Sort
ไม่อยากจ่าย iCloud เลยสอน HDD ให้รู้ว่าอะไรคืออะไร

โยนรูปลงโฟลเดอร์ Inbox โฟลเดอร์เดียว แล้วเครื่องแยกให้เองว่าอันไหนคือสลิปโอนเงิน อันไหนคือสกรีนช็อต อันไหนคือแมว — ด้วย Vision framework ที่ติดมากับ macOS อยู่แล้ว — และรูปเดินทางจาก iPhone มาถึงฮาร์ดดิสก์ที่บ้านเองผ่าน Tailscale โดยไม่ต้องเสียบสาย ไม่ต้องเปิดพอร์ตออกอินเทอร์เน็ต และไม่มีค่าบริการรายเดือน

Bash Swift C Vision.framework OCR ภาษาไทย Tailscale SMB launchd on-device
Empathize · Define

Pain point ที่แท้จริงไม่ใช่ค่าเช่าพื้นที่

โจทย์ตั้งต้นคือ “ไม่อยากจ่าย iCloud” แต่พอถามต่ออีกชั้นว่าจ่ายไปเพื่ออะไร คำตอบมันเปลี่ยนรูปปัญหาทั้งหมด

ผมมีฮาร์ดดิสก์ที่ใหญ่พอและจ่ายจบไปแล้วเสียบอยู่กับ Mac แต่มันเป็นแค่ ที่ทิ้งไฟล์ ไม่ใช่ คลังที่หาของเจอ — สิ่งที่ iCloud ขายจริง ๆ ไม่ใช่กิกะไบต์ แต่คือการที่ของเข้าที่ของมันเองโดยผมไม่ต้องทำอะไร

— Point of view ที่ทำให้เปลี่ยนโจทย์ทั้งหมด

พอนิยามปัญหาใหม่แบบนี้ ทางแก้ที่ดูสมเหตุสมผลที่สุดตอนแรก — คือ “ก็ลากไฟล์มาลง HDD สิ” — กลายเป็นทางที่แย่กว่าเดิม เพราะมันย้ายต้นทุนจากค่าเช่ารายเดือน ไปเป็นแรงคนที่ต้องจ่ายทุกครั้งที่มีไฟล์เข้า

ค่าที่แท้จริงคือค่าแรงจัดไฟล์ ไม่ใช่ค่าพื้นที่

ค่าบริการรายเดือนจ่ายครั้งเดียวจบต่อรอบ แต่การจัดไฟล์เองต้องจ่ายด้วยความตั้งใจทุกครั้งที่มีของเข้า และเป็นต้นทุนที่ไม่มีใครจ่ายต่อเนื่องได้จริง

โฟลเดอร์ที่ไม่มีคนจัด จะกลายเป็นที่ที่ไม่มีใครเปิด

ย้ายของขึ้น HDD เฉย ๆ โดยไม่มีตัวจัด ผลลัพธ์คือกองไฟล์ก้อนเดียวหลายพันชิ้นที่ค้นไม่เจอ สุดท้ายไม่กล้าลบและไม่เคยเปิด แย่กว่าตอนอยู่ในมือถือด้วยซ้ำ

นามสกุลไฟล์บอกได้แค่ว่ามันเป็นรูป ไม่ได้บอกว่ารูปอะไร

ตัวจัดไฟล์เดิมแยกได้แค่ .jpg เข้า Photos และ .pdf เข้า Documents ซึ่งพอสำหรับเอกสาร แต่ไม่พอสำหรับรูป เพราะรูปหลายพันใบในโฟลเดอร์เดียวก็ยังหาสลิปเดือนที่แล้วไม่เจออยู่ดี

ทางแก้ที่มีอยู่ทุกเจ้า แลกด้วยการอัปรูปส่วนตัวขึ้นคลาวด์

บริการที่ค้นรูปเก่งจริงทำงานบนเซิร์ฟเวอร์ของเจ้าของบริการทั้งหมด แปลว่าสลิปการเงินและรูปในบ้านต้องออกจากเครื่องไปก่อน จึงจะได้ความสะดวกกลับมา


Ideate

สี่ทางที่ชั่งจริง ก่อนตัดสินใจเขียนเอง

เกณฑ์มี 3 ข้อ — ต้องแยกรูปตามเนื้อในรูปได้ · ไม่มีค่าใช้จ่ายต่อเนื่อง · รูปไม่ออกจากเครื่อง

ทางเลือกแยกตามเนื้อในรูปค่าใช้จ่ายเหตุที่ตก / รับ
iCloud+ ค้นได้ แต่ไม่จัดโฟลเดอร์ให้ รายเดือน ตลอดไป รูปอยู่บนเซิร์ฟเวอร์ Apple · จ่ายแล้วก็ยังไม่ได้โครงโฟลเดอร์ที่เอาไปใช้ต่อได้ และเป็นโจทย์ตั้งต้นที่อยากเลี่ยง
Google Photos ค้นเก่งที่สุดในกลุ่ม รายเดือนเมื่อเกินโควตา ต้องอัปสลิปการเงินและรูปส่วนตัวทั้งหมดขึ้นเซิร์ฟเวอร์ Google — ราคาที่ไม่อยากจ่ายในความหมายอื่น
NAS + แอปจัดรูป ได้ ถ้าลงปลั๊กอินเพิ่ม ค่าเครื่อง + ค่าดูแล อยู่ในบ้านก็จริง แต่ต้องซื้อฮาร์ดแวร์ใหม่ทั้งชุด เพื่อแก้ปัญหาที่ HDD เดิมกับ Mac เดิมทำได้อยู่แล้ว
ต่อยอดตัวจัดไฟล์เดิม เลือก ได้ ด้วย Vision ที่ติดมากับ macOS ไม่มี รูปอยู่บน HDD ที่มีอยู่ · มีตัวจัดตามนามสกุลทำงานอยู่แล้ว เหลือแค่ต่ออีกชั้นให้ดูเนื้อในรูป
ตัวชี้ขาด

Vision framework ติดมากับ macOS อยู่แล้ว — จำแนกภาพได้ 1,303 ชนิด และอ่านตัวอักษรได้ 30 ภาษารวมภาษาไทย ทำงานบนเครื่อง ไม่ต้องต่อเน็ต ไม่ต้องมี API key ไม่มีค่าเรียกต่อครั้ง ของที่จำเป็นทั้งหมดจึงมีอยู่ในเครื่องตั้งแต่ต้น เหลือแค่ต่อสายให้ถูก


Ideate · Prototype

จะแทน iCloud ได้จริง รูปต้องเดินทางจากมือถือมาถึงฮาร์ดดิสก์เองได้

ตัวจัดไฟล์จะฉลาดแค่ไหนก็ไม่มีความหมาย ถ้าคนต้องเสียบสาย iPhone เข้าคอมก่อนทุกครั้ง — ชิ้นส่วนที่ทำให้ทั้งระบบใช้แทนคลาวด์ได้จริง อยู่ตรงเส้นทางนี้

เสียบสายโอนเองทุกครั้ง

เป็น workflow เดิมที่ทำให้ไม่มีใครทำจริงตั้งแต่แรก ถ้าต้องรอกลับถึงบ้านแล้วหาสาย รูปก็จะกองอยู่ในมือถือต่อไปเหมือนเดิม

Forward พอร์ต 445 ออกอินเทอร์เน็ตที่เราเตอร์

SMB เป็นหนึ่งในพอร์ตที่ถูกสแกนหนักที่สุดบนอินเทอร์เน็ต การเปิดมันตรง ๆ เพื่อความสะดวกส่วนตัว คือการแลกที่ไม่คุ้มเลย และยังต้องมี dynamic DNS คอยตามเมื่อ IP บ้านเปลี่ยน

ตั้ง VPN เข้าบ้านเอง

ได้ผลลัพธ์ถูกทาง แต่ต้องแตะตั้งค่าเราเตอร์ เปิดพอร์ต และดูแลกุญแจ/ใบรับรองเอง เป็นงานดูแลระบบที่ต้องจ่ายต่อเนื่อง เพื่อแก้ปัญหาโอนรูป

ทางที่เลือก — Tailscale

mesh VPN บน WireGuard ที่ทำให้ iPhone กับ Mac คุยกันตรง ๆ ผ่าน IP ส่วนตัวของ tailnet ซึ่งคงที่ตลอด ไม่ว่าเครื่องจะอยู่เครือข่ายไหน — ไม่ต้อง forward พอร์ต ไม่ต้องมี dynamic DNS ไม่ต้องแตะเราเตอร์สักบรรทัด และไม่มีอะไรถูกเปิดออกสู่อินเทอร์เน็ตสาธารณะ

iPhone — อยู่ที่ไหนก็ได้
แอป FilesConnect to Serverเลือกรูปแล้ววาง
Tailscale — WireGuard mesh
IP ส่วนตัวคงที่ไม่ผ่านเซิร์ฟเวอร์กลางไม่ต้อง forward พอร์ต
macOS File Sharing — share ชื่อ Drive
SMB พอร์ต 445ฟังเฉพาะบน tailnetguest access ปิด
HDD ที่บ้าน — Drive/Inbox
launchd หยิบไปจัดใน 5 นาทีVision แยกหมวดลง Photos/<หมวด>

Prototype

สามไฟล์ แต่ละไฟล์มีเหตุผลที่ต้องแยกออกมา

ไม่ได้แยกเพราะอยากให้ดูเป็นระบบ แต่เพราะข้อจำกัดของ macOS บังคับให้แยก

drive — bash

ตรรกะทั้งหมดอยู่ที่นี่ · sort จัดไฟล์ · offload ย้ายของขึ้น HDD แล้วทิ้ง symlink ไว้ที่เดิม · selftest ตรวจตัวเองก่อนแก้

drivesort — C, 8 บรรทัด

macOS ไม่ยอมผูก Full Disk Access กับ shell script จึงต้องมี binary จริงไว้รับสิทธิ์ ให้เฉพาะตัวนี้เข้าถึงดิสก์ได้ แทนการเปิดสิทธิ์ให้ /bin/bash ทั้งระบบ

imgclass — Swift

bash เรียก framework ของระบบเองไม่ได้ ตัวนี้ทำหน้าที่เดียวคือเปิด Vision แล้วพิมพ์ผลดิบออกมา — label ของสิ่งที่อยู่ในรูป หรือตัวอักษรที่อ่านได้

launchd — ปลุกทุก 5 นาที
ไม่เฝ้าโฟลเดอร์แบบ real-timeHDD ไม่เสียบ = จบเงียบ ไม่ใช่ error
drive sort — แยกชั้นแรกตามนามสกุล
PhotosVideosDocumentsDesignไม่รู้จัก = ค้างใน Inbox
imgclass — Vision บนเครื่อง
VNClassifyImageRequestVNRecognizeTextRequestth-TH + en-US
Photos/ — 6 โฟลเดอร์ย่อย
slipscreenshotdogcatplantsky

Prototype

วิธีทำงาน — 5 จังหวะ

จากไฟล์ที่เพิ่งตกลง Inbox ถึงโฟลเดอร์ปลายทาง โดยไม่มีใครกดอะไรเลย

01

ปลุกทุก 5 นาที

ไม่เฝ้าโฟลเดอร์แบบ real-time เพราะงานนี้ไม่ต้องการความเร็วระดับวินาที และการปลุกเป็นรอบถูกกว่าการค้าง process ไว้ตลอดเวลา

02

หยิบเฉพาะไฟล์ที่นิ่งเกิน 1 นาที

ข้ามไฟล์ที่เพิ่งถูกแตะ กันย้ายไฟล์ที่มือถือกำลังอัปยังไม่เสร็จ แล้วได้ไฟล์ครึ่ง ๆ กลาง ๆ ไปอยู่ผิดที่

03

แยกชั้นแรกตามนามสกุล

Photos · Videos · Documents · Design — นามสกุลที่ไม่รู้จักไม่เดา ปล่อยค้างไว้ใน Inbox ให้คนตัดสิน

04

เฉพาะรูป ส่งเข้า Vision

ได้ label ของสิ่งที่อยู่ในรูปเรียงตามความมั่นใจ · ถ้าดูแล้วเป็นภาพหน้าจอ จึงค่อยเรียก OCR อ่านตัวอักษรต่อ เพราะ OCR ช้ากว่าหลายเท่า

05

ลงโฟลเดอร์ย่อย หรือไม่ลงเลย

ไม่มั่นใจ = ไม่เดา ค้างไว้ที่ Photos/ ให้คนตัดสิน หลักเดียวกับนามสกุลที่ไม่รู้จัก


Define

ลำดับการเช็คคือตัวตัดสินผลลัพธ์ ไม่ใช่ความแม่นของโมเดล

รูปหนึ่งใบติดหลาย label พร้อมกันเสมอ สิ่งที่กำหนดว่ามันจะไปโฟลเดอร์ไหน คือใครถูกถามก่อน

ภาพหน้าจอ + อ่านเจอคำการเงิน
→ slip
สลิปแคปจากแอปติดทั้ง slip และ screenshot — slip ต้องถูกถามก่อน
ภาพหน้าจอ
→ screenshot
เหลือจากด่านบนแล้วคือสกรีนช็อตธรรมดา
มี label หมา
→ dog
สัตว์ถูกถามก่อนต้นไม้เสมอ
มี label แมว
→ cat
รูปแมวยืนหน้าพุ่มไม้ติดทั้ง cat และ plant — ถ้าถามต้นไม้ก่อน รูปแมวจะหายไปอยู่ในโฟลเดอร์ต้นไม้ทั้งหมด
มี label พืช
→ plant
ท้องฟ้า และต้องติด 2 อันดับแรก
→ sky
รูปกลางแจ้งเกือบทุกรูปมี sky ติดมาด้วย ถ้าไม่บังคับว่าต้องเด่นจริง ทุกอย่างจะกลายเป็นรูปท้องฟ้า
ไม่เข้าเงื่อนไขไหนเลย
→ ไม่ย้าย
ค้างที่ Photos/ ให้คนตัดสิน
พิสูจน์กับรูปจริง

รูปแมวดำยืนหน้ากอต้นไม้ Vision คืน animal mammal cat feline พร้อมกัน และเพราะแมวถูกถามก่อนพืช ไฟล์จึงลง Photos/cat ไม่ใช่ Photos/plant


Test & Iterate

สามเรื่องที่รู้ก็ต่อเมื่อเอาไปวัดกับไฟล์จริง

ทุกข้อคือสมมติฐานที่ฟังดูสมเหตุสมผลตอนคิด แล้วผิดตอนวัด

คิดว่า Vision จะรู้จักสลิป เพราะในรายการมี label ชื่อ receipt อยู่

เอาสลิปจริง 60 ใบไปทดสอบ ปรากฏว่า label receipt ไม่เคยถูกคืนมาเลยสักครั้ง — 59 ใบได้ document กับ screenshot เหมือนกันหมด เพราะสลิปของจริงไม่ใช่ใบเสร็จกระดาษที่ถ่ายด้วยกล้อง แต่เป็นภาพแคปหน้าจอจากแอปธนาคาร ซึ่งหน้าตาเหมือนสกรีนช็อตทุกประการ

แก้: ถ้าดูจากภาพแยกไม่ออก ก็ต้องอ่านตัวอักษรบนภาพแทน Vision รองรับ OCR ภาษาไทย จึงให้มันอ่านแล้วมองหาคำอย่าง รหัสอ้างอิง พร้อมเพย์ ยอดชำระ และเพราะ OCR ช้ากว่าการจำแนกภาพมาก จึงเรียกเฉพาะรูปที่ถูกตีว่าเป็นภาพหน้าจอเท่านั้น

คำว่า “ค่าธรรมเนียม” ดูดใบยืนยันซื้อหุ้นเข้ามาเป็นสลิป

ตอนตั้งรายการคำที่บ่งบอกว่าเป็นสลิป ใส่ ค่าธรรมเนียม ไปด้วยเพราะสลิปโอนเงินมักมีคำนี้ พอเอาไปวัดกับสกรีนช็อตทั่วไป 25 รูป มีใบยืนยันการซื้อหุ้น 3 ใบถูกดึงมาเป็นสลิป เพราะมันมีคำว่าค่าธรรมเนียมเหมือนกัน

แก้: ตัดคำนั้นทิ้ง เก็บเฉพาะคำที่เจาะจงพอจะไม่ไปโผล่ในเอกสารการเงินประเภทอื่น หลังตัดแล้ววัดซ้ำ สกรีนช็อตทั่วไปไม่มีใบไหนหลุดมาเป็นสลิปเลย — บทเรียน: คำที่ “มักจะมี” ไม่เท่ากับคำที่ “มีเฉพาะที่นี่”

ใน 1,303 label มีคำที่หน้าตาเหมือนกันแต่คนละเรื่อง

รายการของ Vision มีทั้ง hotdog prairie_dog eggplant cauliflower sunflower_seeds bobcat ถ้าจับคู่แบบหาคำย่อย รูปฮอทดอกจะกลายเป็นรูปหมา และมะเขือม่วงจะกลายเป็นต้นไม้

แก้: เทียบทั้งบรรทัดเท่านั้น ไม่เทียบแบบคำย่อย ใช้เวลาแก้ไม่ถึงนาที แต่ถ้าไม่ได้ไล่อ่านรายการ label ทั้งหมดก่อน ก็จะไม่มีทางรู้ว่าต้องแก้

Test

วัดกับไฟล์ของตัวเอง ไม่ใช่กับชุดข้อมูลตัวอย่าง

โฟลเดอร์ที่เคยจัดด้วยมือไว้แล้ว กลายเป็นชุดเฉลยที่ใช้ตรวจคำตอบได้ฟรี

ชุดทดสอบจำนวนผล
โฟลเดอร์ slip ที่จัดด้วยมือไว้67 รูปตีเป็น slip 53 · screenshot 8 · ไม่แยก 1 — เปิดดู 8 ใบที่หลุดแล้วพบว่าเป็นแชท คลิป TikTok และลายเซ็นที่ปนอยู่ในโฟลเดอร์จริง ๆ ไม่ใช่สลิป
โฟลเดอร์ screenshot ที่จัดด้วยมือไว้5 รูปถูกทั้งหมด 5 จาก 5
สกรีนช็อตทั่วไปนอกไดรฟ์25 รูปไม่มีใบไหนถูกตีเป็นสลิปผิด
Inbox จริง ตอนรันของจริง18 ไฟล์ตรงกับที่ทดลองไว้ก่อนย้ายทั้ง 18 ไฟล์ — cat 8 · slip 8 · plant 1 · screenshot 1
18/18
ไฟล์ที่ลงถูกโฟลเดอร์ตอนรันกับ Inbox จริง
0
สกรีนช็อตธรรมดาที่หลุดไปเป็นสลิป
5/5
ชุดเฉลย screenshot ที่จัดด้วยมือไว้ก่อน
1
หมวดที่ยังไม่มีรูปจริงมาทดสอบ คือหมา
ตัวเลขที่มีค่าที่สุดในตารางนี้คือเลขศูนย์

เครื่องมือจัดไฟล์ที่แยกผิดบ้างยังพอรับได้ เพราะเปิดโฟลเดอร์ก็เจอ แต่เครื่องมือที่ เอาของไปซ่อนผิดที่ แย่กว่าไม่มีเครื่องมือเลย หลักที่ยึดตลอดทั้งงานจึงเหลือข้อเดียว คือไม่แน่ใจแล้วไม่ต้องเดา


Define

ขอบเขตที่ตั้งใจไม่ทำ

สิ่งที่ตัดออกมีผลกับความน่าเชื่อถือของเครื่องมือ มากกว่าสิ่งที่ใส่เข้าไป

เครื่องมือที่ดีที่สุดคือเครื่องมือที่ไม่ต้องเปิดใช้

ทุกวันนี้มันทำงานเงียบ ๆ ทุก 5 นาทีโดยไม่มีหน้าจอ ไม่มีการแจ้งเตือน และไม่มีอะไรให้กด — โยนรูปลง Inbox แล้วเดินไปทำอย่างอื่น กลับมาอีกทีของก็อยู่ในโฟลเดอร์ของมันแล้ว

Bash Swift C Vision.framework VNClassifyImageRequest VNRecognizeTextRequest launchd on-device
Projects