อัดประชุม → ถอดเสียงเป็นข้อความพร้อมแยกผู้พูด → สรุปประเด็นและ action items ด้วย LLM → เก็บเป็นไฟล์ Markdown
เสียงไม่เคยออกจากเครื่องนี้ ถอดเสียงและแยกผู้พูดทำในเครื่องทั้งหมด สิ่งเดียวที่ถูกส่งออกไปคือ transcript ที่เป็นข้อความ และปลายทางขึ้นกับโมเดลที่เลือก — ค่าเริ่มต้นคือ Qwen/Qwen3.6-35B-A3B บน endpoint ของบริษัท ซึ่งข้อมูลไม่ออกนอกองค์กรเลย เลือก Claude ได้ถ้าต้องการความแม่นกว่า หรือเลือกไม่สรุปเลยก็ได้
อัดทั้ง เสียงเราจากไมค์ และ เสียงคู่สนทนาจากลำโพง พร้อมกัน จึงใช้ได้กับแอปประชุมทุกตัว (Telegram, Zoom, Teams, Google Meet) โดยไม่ต้องให้แอปรองรับอะไรเป็นพิเศษ ปรับแต่งมาสำหรับการประชุมภาษาไทย
| ระบบปฏิบัติการ | อัดประชุมได้เฉพาะ Windows — ใช้ WASAPI loopback ดักเสียงลำโพง ซึ่งมีเฉพาะบน Windows ส่วนการประมวลผลไฟล์เสียงที่มีอยู่แล้วใช้ได้ทุกระบบ (ดู ใช้บน macOS / Linux) |
| Python | 3.12 ขึ้นไป |
| ffmpeg | ต้องเรียกจาก PATH ได้ |
| key ของโมเดลสรุป | ค่าเริ่มต้นใช้ endpoint ที่องค์กรรันเอง (LLM_API_KEY) เลือกใช้ Claude ได้ถ้ามี ANTHROPIC_API_KEY — ตัวนั้นเสียเงินตามการใช้งาน หรือเลือกไม่สรุปเลยก็ไม่ต้องมี key อะไรทั้งนั้น |
| Hugging Face token | สำหรับแยกผู้พูด — ฟรี |
| GPU (ไม่บังคับ) | NVIDIA จะถอดเสียงเร็วขึ้นมาก ดู docs/gpu-setup.md |
git clone https://github.com/wowBALL/meeting-notes.git
cd meeting-notes
python -m venv .venv
.\.venv\Scripts\python -m pip install -r requirements.txtwinget install Gyan.FFmpegเปิด terminal ใหม่หลังติดตั้ง เพื่อให้ PATH อัปเดต แล้วเช็คว่าได้จริง:
ffmpeg -versionโมเดลแยกผู้พูดเป็น gated model ต้องกดยอมรับเงื่อนไขก่อน ทั้งสองหน้า ไม่งั้นดาวน์โหลดไม่ได้:
- https://huggingface.co/pyannote/speaker-diarization-community-1 → กด Agree
(ค่าเริ่มต้น ถ้าตั้ง
DIARIZATION_MODELเป็น 3.1 ให้กดหน้า https://huggingface.co/pyannote/speaker-diarization-3.1 แทน) - https://huggingface.co/pyannote/segmentation-3.0 → กด Agree
- สร้าง token ที่ https://huggingface.co/settings/tokens (แบบ Read พอ)
มีโมเดลที่สามที่ระบบใช้ด้วยคือ pyannote/wespeaker-voxceleb-resnet34-LM (26MB ตัวที่สร้าง
เวกเตอร์น้ำเสียงสำหรับจำคนข้ามการประชุม) — ไม่ใช่ gated ไม่ต้องกด Agree ดาวน์โหลดเอง
อัตโนมัติครั้งแรกที่ใช้ ถ้าเครื่องไม่มีเน็ตตอนนั้น ระบบจะเดินต่อโดยไม่มีการจำเสียง (ทุกคน
เป็นป้าย "ผู้พูด N") และเขียนเหตุผลไว้ใน log ของ watcher
ก๊อป .env.example เป็น .env แล้วใส่ค่าจริง:
copy .env.example .envLLM_API_KEY=sk-...
LLM_BASE_URL=https://your-llm-endpoint.example/v1
HF_TOKEN=hf_...
ANTHROPIC_API_KEY=
CLAUDE_MODEL=Qwen/Qwen3.6-35B-A3B
WHISPER_MODEL=small.env อยู่ใน .gitignore แล้ว จะไม่หลุดขึ้น git
ต้องมีจริง ๆ แค่ HF_TOKEN กับ key ของโมเดลที่จะใช้สรุป ค่าเริ่มต้นคือ Qwen/Qwen3.6-35B-A3B บน endpoint ของบริษัท ซึ่งใช้ LLM_API_KEY — ถ้าไม่คิดจะเลือก Claude เลย ปล่อย ANTHROPIC_API_KEY ว่างไว้ได้ โปรแกรมจะเริ่มได้ปกติ error เรื่อง key จะเกิดตอนเลือกโมเดลที่ต้องใช้มันจริง ๆ ไม่ใช่ตอนเปิดโปรแกรม
อัปเกรดจากก่อนหน้านี้? ถ้า .env เดิมมี CLAUDE_MODEL=claude-opus-5 ค้างอยู่ (ค่าที่ .env.example เวอร์ชันเก่าเคยตั้งไว้) ไฟล์ที่ลากใส่ inbox/ เอง กับ session ที่ค้างจากรอบก่อนที่ manifest ไม่มีโมเดลระบุไว้ จะยังใช้ CLAUDE_MODEL เดิมนั้นและส่งไปที่ Anthropic ต่อไป — เป็นพฤติกรรมที่ถูกต้อง (ค่าที่ตั้งไว้ชัดเจนต้องมีผล) แต่ถ้าอยากให้ทางที่ข้อมูลไม่ออกนอกบริษัทเป็นค่าเริ่มต้นสำหรับสองทางนี้ด้วย ต้องแก้บรรทัดนั้นเป็น CLAUDE_MODEL=Qwen/Qwen3.6-35B-A3B เอง เมนูของ start-meeting.bat/start-ui.bat ไม่ได้อ่านค่านี้ (มันมีค่าเริ่มต้นของตัวเองอยู่แล้ว) จึงไม่ได้รับผลกระทบ
| ตัวแปร | ค่าเริ่มต้น | หมายเหตุ |
|---|---|---|
CLAUDE_MODEL |
Qwen/Qwen3.6-35B-A3B |
ใช้เมื่อไฟล์เสียงไม่ได้ระบุโมเดลมา (ดูหัวข้อถัดไป) ตั้งเป็น transcript-only ได้ ไฟล์ที่ลากใส่ inbox/ เองจะใช้ค่านี้ ชื่อตัวแปรยังเป็น CLAUDE_MODEL ตามเดิมทั้งที่รับ id ของ provider อื่นได้แล้ว — เปลี่ยนชื่อจะทำให้ .job.json ที่ค้างอยู่ใน inbox/ อ่านไม่ออก |
LLM_API_KEY |
— | key ของ endpoint บริษัท จำเป็นเมื่อสรุปด้วย GLM-5.2 หรือ Qwen/Qwen3.6-35B-A3B |
LLM_BASE_URL |
— (ต้องตั้งเอง) | URL ของ endpoint ที่องค์กรรัน จำเป็นเมื่อสรุปด้วย GLM-5.2 หรือ Qwen/Qwen3.6-35B-A3B ไม่มีค่า default ในโค้ด เพราะที่อยู่ของ endpoint ภายในไม่ควรถูกฝังลง repo สาธารณะ ใส่ / ปิดท้ายได้ โค้ดตัดให้เอง |
ANTHROPIC_API_KEY |
— | จำเป็นเฉพาะเมื่อเลือกสรุปด้วย Claude |
MEETING_PROFILE |
dev |
ประเภทประชุมที่ใช้เมื่อไฟล์เสียงไม่ได้ระบุมา (ไฟล์ที่ลากใส่ inbox/ เอง) ค่าที่รับคือ dev กับ cross — ตรงกับชื่อไฟล์ใน prompts/profiles/ ค่าที่พิมพ์ผิดจะเตือนแล้วใช้ dev ต่อ |
CARRYOVER_ENABLED |
true |
ส่งหัวข้อ "ต้องคุยต่อครั้งหน้า" ของประชุมครั้งก่อน (ประเภทเดียวกัน) เข้าไปในสรุปครั้งนี้ ตั้ง false เพื่อปิด (ดูหัวข้อ "ต่อเนื่องจากประชุมครั้งก่อน") |
CHUNK_OVERLAP_TOKENS |
1500 |
ส่วนที่ให้ chunk คาบเกี่ยวกันตอนหั่น transcript ยาว (10% ของขนาด chunk 15,000) กันบทสนทนาขาดกลางประโยคตรงรอยต่อ ตั้ง 0 = ไม่คาบเกี่ยว ค่าที่ใช้ไม่ได้ (ไม่ใช่เลข / ติดลบ / ≥ 15000) จะเตือนแล้วใช้ค่าเริ่มต้น |
MERGE_SPEAKER_TURNS |
true |
รวมบล็อกของผู้พูดคนเดียวกันที่อยู่ติดกันก่อนส่งให้โมเดล (transcript.md ไม่เปลี่ยน) ลดข้อความที่ส่ง ~43% และจำนวนคำขอ ~33% ตั้ง false เพื่อปิด (ดูหัวข้อ "ข้อความที่ส่งให้โมเดลถูกรวมบรรทัดก่อน") |
WHISPER_BATCHED |
false |
ถอดเสียงแบบ batch เร็วขึ้น ~4 เท่า แต่วัดแล้วว่าทำให้ faster-whisper วนซ้ำคำเดิมจนเนื้อหาหาย (โทเคนซ้ำมากสุดกิน 31.6% ของ transcript เทียบกับ 3.1%) จึงปิดไว้ ตัวเลขการวัดอยู่ใต้ BATCH_SIZE ใน src/transcribe.py |
WHISPER_MODEL |
small |
เร็ว ใช้ CPU ได้ เปลี่ยนเป็น large-v3 ได้ถ้ามี GPU |
UI_PORT |
8765 |
พอร์ตของหน้าจอตัวรัน (start-ui.bat) เปลี่ยนเมื่อพอร์ตนี้ชนกับโปรแกรมอื่น |
UI_LANG |
th |
ภาษาของข้อความฝั่ง terminal ตั้ง en ได้ — หน้าเว็บมีปุ่มสลับของตัวเองและจำค่าแยกต่างหาก |
COMPANION_COMMAND |
(ไม่ตั้ง) | โปรแกรมเสริมที่สั่งเปิด/ปิดเองได้ผ่าน /api/companion/* แล้วปิดให้อัตโนมัติตอนเข้าขั้นแปลงไฟล์ — ดูหัวข้อถัดไป |
ไม่ตั้งค่า = ไม่มีอะไรเกิดขึ้น ซึ่งเป็นค่าเริ่มต้น
โปรเซสนี้ไม่ได้ถูกเปิดอัตโนมัติตอนเริ่มอัด ต้องสั่งเปิดด้วย POST /api/companion/start
ซึ่งจะถูกปฏิเสธด้วย 409 gpu_busy ถ้ามีงานถอดเสียงหรือแยกผู้พูดเดินอยู่ · มันถูกปิด
อัตโนมัติเมื่อการอัดเข้าขั้นแปลงไฟล์ และสั่งปิดเองได้ทุกเมื่อด้วย POST /api/companion/stop
จังหวะปิดอัตโนมัติเลือกไว้ที่ขั้นแปลงไฟล์ (ไม่ใช่ตอนแปลงเสร็จ) เพื่อให้ทรัพยากรระดับเครื่องที่โปรแกรมนั้นถืออยู่ เช่นหน่วยความจำการ์ดจอ ถูกคืนทันก่อนขั้นถอดเสียงจะขอใช้ วัดจากประชุมจริงพบว่าช่วงแปลงไฟล์กว้าง 62–69 วินาที ส่วนช่วงหลังแปลงเสร็จถึงงานถัดไปมีแค่ราว 2 วินาที
ค่าต้องเป็น JSON array ของสตริง ไม่ใช่คำสั่งบรรทัดเดียว เพราะโปรแกรมถูกเรียกโดยไม่ผ่าน shell — พาธที่มีช่องว่างจึงไม่ต้องใส่เครื่องหมายคำพูด และ backslash ต้อง escape ตามกฎของ JSON:
COMPANION_COMMAND=["D:\\tools\\thing\\pythonw.exe", "-m", "thing"]
โปรแกรมลูกได้รับตัวแปร MEETING_ROOM เป็นชื่อห้อง ณ วินาทีที่สั่ง POST /api/companion/start เท่านั้น (ไม่อัปเดตอีกหลังจากนั้นแม้ห้องจะเปลี่ยนชื่อหรือปิดไปแล้ว) — ถ้าสั่งเปิดก่อนเปิดห้องประชุม ซึ่งเป็นลำดับปกติของปุ่มบนหัวแอป จะได้สตริงว่าง เพิ่มจากตัวแปรสภาพแวดล้อมทั้งหมดของ service
ข้อจำกัดที่ตั้งใจ: โปรแกรมนี้ถือเป็นของเสริม ถ้าเปิดไม่ติดหรือปิดไม่ลง การประชุมจะเดินต่อตามปกติและมีแค่บรรทัดใน log — จะไม่มีวันทำให้เปิดห้องไม่ได้ และถ้ามันไม่ยอมปิดเองภายใน 5 วินาทีจะถูกบังคับปิด ค่าที่ตั้งผิดรูป (ไม่ใช่ JSON, ไม่ใช่ list, มีสมาชิกที่ไม่ใช่สตริง) ก็แปลว่าปิดฟีเจอร์ ไม่ใช่เปิดห้องไม่ได้
.\.venv\Scripts\python -m src.preflightวัด 8 วินาที — พูดใส่ไมค์ และเปิดเสียงอะไรก็ได้ออกลำโพงระหว่างนั้น จะบอกว่าไมค์ดังพอไหม เสียงลำโพงไหลเข้ามาจริงไหม และไมค์กับลำโพงตั้ง sample rate ตรงกันไหม (ไม่ตรง = ตัวอัดปฏิเสธการทำงาน) ขั้นตอนนี้กันเคสที่ transcript ออกมามีแต่เสียงเราฝั่งเดียวโดยไม่รู้ตัวจนสายเกินไป
พร้อมกันนั้นจะเช็คว่าโมเดลที่ตั้งไว้ใน CLAUDE_MODEL เป็นตัวที่ระบบรู้จักไหม ถ้าไม่ใช่จะบอกทั้งค่าที่ตั้งผิดและรายชื่อที่รองรับ ข้อนี้เป็นได้แค่ เตือน ไม่เคยบล็อกการอัด เพราะโมเดลผิดยังได้ transcript ครบ เอาไปสรุปทีหลังได้ ส่วนประชุมที่ไม่ได้อัดนั้นหายถาวร
preflight ไม่ยิง API เพื่อตรวจ key — การตรวจนั้นถูกถอดออกไปแล้ว มันเสียเวลาก่อนอัดทุกครั้งและถ้า key เสียก็รู้ได้ตอนสรุปอยู่ดี ซึ่งเป็นตอนที่ transcript ปลอดภัยอยู่บนดิสก์แล้ว
มีสองทางเข้า ทั้งคู่ใช้ได้จริงและได้ผลลัพธ์เหมือนกัน เลือกอันที่ถนัด:
| ทางเข้า | เหมาะกับ |
|---|---|
start-ui.bat |
หน้าจอในเบราว์เซอร์ กดปุ่มเปิด/ปิดห้อง เห็นความคืบหน้าตั้งแต่อัดจนสรุปเสร็จ |
start-meeting.bat |
เมนูใน terminal แบบเดิม เบากว่า ไม่ต้องเปิดเบราว์เซอร์ |
อย่าเปิดสองทางพร้อมกัน — ไม่มีอะไรกันไว้ ทั้งคู่จะอัดพร้อมกันได้จริงและได้ไฟล์แยกกันสองไฟล์ เสียแรงประมวลผลกับค่า API เปล่า ๆ (เสียงไม่หาย แค่ซ้ำ)
.\start-ui.batเปิดตัวประมวลผลกับ service ให้ แล้วเปิดเบราว์เซอร์ไปที่ http://127.0.0.1:8765 — service ฟังเฉพาะ 127.0.0.1 เครื่องอื่นในเน็ตเวิร์กเข้าไม่ได้
ในหน้าจอ: เลือกรูปแบบการใช้ AI → ตั้งชื่อห้อง (ไม่ใส่ก็ได้) → เปิดห้อง → พอประชุมจบกด ปิดการประชุม แล้วยืนยัน จากนั้นจะเห็นขั้นตอนไล่ไปเอง (บีบอัด → ถอดเสียง → แยกผู้พูด → สรุป → เสร็จ)
รายละเอียดที่มีผลจริงตอนใช้งาน:
- ปิดแท็บเบราว์เซอร์ระหว่างประชุมได้ การอัดอยู่ใน service ไม่ได้อยู่ในหน้าเว็บ เปิดกลับมาเมื่อไหร่ก็เห็นสถานะเดิม
- ถ้าอุปกรณ์เสียงเปลี่ยนกลางประชุม จะมีคำเตือนค้างอยู่เหนือนาฬิกา ไม่ถูกซ่อนใน log — เป็นสาเหตุอันดับหนึ่งของเคสอัดแล้วไม่ได้เสียงคู่สนทนา
- ถ้าตัวประมวลผลไม่ได้เปิด จุดสถานะจะดับ แต่ยังเปิดห้องอัดได้ตามปกติ ไฟล์จะรอในคิวแล้วประมวลผลเมื่อมันกลับมา
- ปุ่ม EN/TH มุมขวาบนสลับภาษาทั้งหน้า รวมข้อความสถานะ ระบบจำภาษาที่เลือกไว้
ดับเบิลคลิก start-meeting.bat หรือรันจาก terminal:
.\start-meeting.batจะเจอตามลำดับนี้:
- เมนูเลือกโมเดลสรุป —
[1] Qwen 3.6ข้อมูลไม่ออกนอกบริษัทและเร็วที่สุด /[2] GLM 5.2ข้อมูลไม่ออกนอกบริษัทเหมือนกันแต่ช้ากว่ามาก /[3] Opus 5แม่นกว่า /[4] Sonnet 5ถูกกว่า /[5] ถอดเสียงอย่างเดียวไม่ส่งให้โมเดลไหนเลย (กด Enter = Qwen 3.6) - ตรวจเสียงอัตโนมัติ — ถ้าไม่ผ่านจะถามก่อนว่าจะอัดต่อไหม
- ใส่ชื่อประชุม — ไม่ใส่ก็ได้ กด Enter ข้าม
- เริ่มอัด — ประชุมได้ตามปกติ พอจบกด Ctrl+C
จากนั้นระบบทำงานต่อเองเบื้องหลัง ใช้เวลาสักพักขึ้นกับความยาวประชุมและว่ามี GPU ไหม ผลลัพธ์จะไปโผล่ที่:
meetings/2026-07-25_14-30-ชื่อประชุม/
├── summary.md สรุปแยก "ตกลงแล้ว" ออกจาก "เสนอไว้" + action items (เนื้อหาล้วน ส่งต่อให้คนอ่านได้ตรงๆ — ไม่มีไฟล์นี้ถ้าเลือก [5])
├── summary.meta.md โมเดลที่ใช้สรุป, ประเภทประชุม, คำที่แก้ตาม glossary, ตารางผู้พูด, รายงานคำที่น่าจะถอดเพี้ยน -- ข้อมูลของระบบ ไม่ใช่เนื้อหาประชุม
├── transcript.md บทสนทนาเต็มพร้อม timestamp และ ผู้พูด 1 / ผู้พูด 2 (ของดิบ ไม่ผ่านตัวกรองศัพท์)
└── <ชื่อ>.ogg ไฟล์เสียงต้นฉบับ
ชื่อโฟลเดอร์ไม่มีวินาที ถ้าอัดประชุมชื่อเดิมซ้ำภายในนาทีเดียวกัน ครั้งที่สองจะได้โฟลเดอร์ของตัวเองที่ลงท้ายด้วย -2 (-3 ไปเรื่อยๆ) ไม่ไปทับ transcript.md ของครั้งแรก
ค่าที่เลือกจากเมนู ผูกกับการประชุมครั้งนั้น ไม่ใช่ค่ากลาง อัดประชุมภายในด้วย Qwen แล้วอัดประชุมที่ต้องการความแม่นด้วย Opus 5 ต่อกันเลยได้ แต่ละอันจะถูกสรุปด้วยโมเดลที่เลือกไว้จริงๆ ถึงแม้คิวจะสลับกัน
summary.meta.md จะเขียนไว้เสมอว่าสรุปด้วยโมเดลอะไร เอาไว้ย้อนเทียบว่าตัวไหนคุ้ม — แยกไฟล์จาก summary.md โดยเจตนา เพราะ summary.md คือของที่ส่งต่อให้คนอ่าน (หัวหน้า, ทีม) ไม่ควรมีรายละเอียดเชิงระบบปนอยู่ท้ายไฟล์
ถ้า Qwen ล่ม ระบบจะไม่ไปเรียก Claude แทนให้ ประชุมนั้นจะได้แค่ transcript การสลับไปหา provider อื่นเองเท่ากับส่งบทสนทนาออกนอกบริษัทโดยที่ไม่มีใครสั่ง ซึ่งเป็นเหตุผลตั้งต้นที่มีตัวเลือกนี้อยู่ — [2] GLM 5.2 มีไว้สำหรับกรณีนี้: มันอยู่บน endpoint เดียวกัน ข้อมูลจึงไม่ออกนอกบริษัทเหมือนกัน แต่ต้องเป็นคนเลือกเอง ระบบไม่สลับให้เอง
ตัวเลือก [5] ถอดเสียงอย่างเดียว ผูกกับประชุมแบบเดียวกัน ใช้กับประชุมที่อยากได้แค่บทสนทนาไว้ค้นย้อนหลัง ผลลัพธ์จะมี transcript.md กับไฟล์เสียงครบ แต่ไม่มี summary.md โหมดนี้เลือกตอนเริ่มอัดเท่านั้น ระบบไม่มีทางกลับมาสรุปให้ทีหลังอัตโนมัติ — ถ้าเปลี่ยนใจให้เอา transcript.md ไปสรุปเองได้
ศัพท์เทคนิคกับชื่อเฉพาะเป็นของที่ faster-whisper ถอดผิดบ่อยที่สุด แก้ได้ที่ glossary.md ที่ root เพิ่มคำได้ตรงๆ ไม่ต้องแตะโค้ด ไม่ต้องรันเทสต์ใหม่ ประชุมครั้งถัดไปใช้ค่าใหม่เลย
เริ่มจากคัดลอกโครงตั้งต้น:
Copy-Item glossary.example.md glossary.mdไม่มี glossary.md = ระบบทำงานปกติ แค่ไม่กรองคำอะไรเลย ไม่มี error
ไฟล์มี 5 section แบ่งเป็นสองชั้นที่ทำงานคนละแบบ:
| section | ทำงานอย่างไร | ต้นทุน |
|---|---|---|
exact |
โค้ดแทนที่ตรงๆ ก่อนส่งให้โมเดล | ฟรี ไม่กิน token |
aliases |
โค้ดยุบชื่อที่คนละฝ่ายเรียกคนละชื่อ ให้เหลือชื่อกลาง | ฟรี ไม่กิน token |
fuzzy |
ยัดตารางเข้า prompt ให้โมเดลตีความตามบริบท | กิน token ทุกครั้งที่สรุป |
project-names |
เหมือน fuzzy แต่สั่งให้คงรูปชื่อโปรเจกต์ |
กิน token ทุกครั้งที่สรุป |
ambiguous |
คำเดียวกันที่คนละฝ่ายหมายคนละเรื่อง ต้องมี teams.md คู่กัน |
กิน token (เฉพาะประชุมข้ามฝ่าย) |
คำแบบไหนลงชั้นไหน — เพี้ยนแบบเดิมทุกครั้งและยาวพอ (ประมาณ 4 อักขระขึ้นไป) ให้ลง exact / เพี้ยนไม่แน่นอน หรือสั้น หรือไปตรงกับคำไทยทั่วไป ให้ลง fuzzy
ลังเลให้เลือก fuzzy — ภาษาไทยไม่มีช่องว่างระหว่างคำ การแทนที่จึงเป็น substring match ล้วน คำสั้นใน exact จะไปโดนกลางคำอื่น และเมื่อ exact แก้ผิด มันแก้ก่อนโมเดลเห็น คนอ่านสรุปจะไม่รู้เลยว่าเดิมพูดว่าอะไร (transcript.md ยังเก็บของดิบไว้เสมอ — ตัวกรองไม่แตะไฟล์นั้น)
แก้คำตาม glossary: PostgreSQL 3 จุด, Railway 1 จุด
คำ fuzzy ที่เจอในห้อง: Electron 2 ครั้ง
- บรรทัดแรก = แก้ไปแล้ว (ชั้น
exact/aliases) ถ้าคำไหนจำนวนเฟ้อผิดปกติ แปลว่ามันไปแทนที่กลางคำอื่น ย้ายไปfuzzy - บรรทัดที่สอง = เจอ แต่ไม่ได้แก้ (ชั้น
fuzzy) เป็นหลักฐานชิ้นเดียวที่บอกได้ว่าคำไหนเลิกใช้ไปแล้ว คำที่ไม่โผล่ติดกันหลายสัปดาห์ให้ลบออกจากfuzzyเพราะมันกิน token ทุกครั้ง
ไม่มีคำถูกแก้และไม่เจอคำ fuzzy = ไม่มีบรรทัดพวกนี้เลย
ใช้บอกว่าใครอยู่ฝ่ายไหน เพื่อให้โมเดลตีความคำในหมวด ambiguous ได้ถูก คัดลอกจาก teams.example.md แล้วใส่ชื่อจริง ชื่อต้องตรงกับ speaker registry
glossary.md กับ teams.md ถูก gitignore ไว้ทั้งคู่ เพราะ repo นี้เป็น public — teams.md มีชื่อคนจริง และตารางศัพท์ของจริงมักโตไปทางชื่อโปรเจกต์ ชื่อระบบภายใน และคำย่อของงาน ทั้งคู่มีไฟล์ .example.md เป็นโครงตั้งต้นให้คัดลอก
ไม่มีทั้งสองไฟล์ = ระบบทำงานเหมือนเดิมทุกอย่าง ไม่มี error
ประชุมที่ไม่มีวาระตายตัว ปัญหาไม่ใช่คำผิด แต่คือ สรุปออกมาแล้วแยกไม่ออกว่าอะไรตกลงแล้ว อะไรแค่ลอยๆ โครงสรุปจึงบังคับให้แยกออกจากกัน:
| หัวข้อ | มีไว้ทำไม |
|---|---|
## หัวข้อที่คุยกัน |
ไม่มีวาระ → ให้โมเดลจัดกลุ่มเอง คนอ่านย้อนจะได้รู้ว่าคุยอะไรบ้าง |
## ตกลงแล้ว |
เฉพาะที่มีคนสรุปปิดชัดเจนหรือทุกคนเห็นพ้อง + ระบุว่าใครสรุป |
## เสนอไว้ ยังไม่สรุป |
ข้อเสนอมักเยอะกว่าข้อสรุปมาก ถ้าไม่แยกจะกลายเป็นสรุปที่เชื่อไม่ได้ |
## Action items |
ตาราง งาน / ผู้รับผิดชอบ / กำหนด / ความชัดเจน |
## ความเห็นที่ยังไม่ตรงกัน |
ประชุมหลายคนมีเสมอ และเป็นข้อมูลที่มักหายไปในสรุป |
## ต้องคุยต่อครั้งหน้า |
เรื่องค้าง |
## คำที่น่าจะถอดเพี้ยน |
ทางเดียวที่คำที่ยังไม่อยู่ใน glossary.md จะโผล่มาให้เห็น |
## ติดขัดตรงไหน |
เฉพาะ dev — blocker ทางเทคนิคที่ทำให้งานไม่เดิน |
## สิ่งที่ฝ่าย Business สื่อกับลูกค้าได้ |
เฉพาะ cross — ดึงจาก "ตกลงแล้ว" เท่านั้น |
## จุดที่ควรตรวจเอง |
ตรงไหนที่โมเดลไม่มั่นใจ (เสียงทับกัน ผู้พูดไม่ชัด) — อยู่ท้ายสุดติดกับ footer |
## จุดที่ควรตรวจเอง มีเพราะ 6–8 คนคุยกันแล้วแยกเสียงผู้พูดพลาดได้บ่อย คนอ่านต้องรู้ว่าตรงไหนควรย้อนไปฟังเอง ไม่ใช่เชื่อสรุปทั้งหมดเท่ากัน — วางไว้ท้ายสุดติดกับ footer เพราะทั้งสองส่วนคือ "เรื่องที่ควรรู้เกี่ยวกับสรุปนี้" ไม่ใช่เนื้อหาของการประชุม
ช่อง "ความชัดเจน" สำคัญกว่าที่คิด — คนไทยไม่ค่อยพูดว่า "ผมจะทำ X ให้เสร็จวันศุกร์" มักพูดว่า "เดี๋ยวผมลองดู" คำพูดแบบหลังถูก mark เป็น คาดเดา ไม่ใช่ ชัดเจน
กฎเหล็กที่เขียนไว้ใน prompt — ห้ามยกระดับข้อเสนอเป็นข้อสรุป มีคนเสนอแล้วอีกคนตอบ "อืม" ไม่ใช่การตกลง / ไม่มีกำหนดเวลาที่พูดออกมาจริงให้เว้นว่าง ห้ามเดาวันที่ / ไม่แน่ใจว่าใครพูดให้เขียน "ไม่ชัด"
หัวเรื่องกับวันที่ของประชุม ไม่ได้มาจากโมเดล — มันเห็นแค่ transcript ถ้าให้มันเขียนเองก็เท่ากับเชิญให้เดาวันที่ ข้อมูลนั้นอยู่ในชื่อโฟลเดอร์อยู่แล้ว
ประชุมถี่ๆ เรื่องที่ค้างจากวันจันทร์มักถูกยกมาคุยต่อวันพุธ ระบบจะอ่านหัวข้อ ## ต้องคุยต่อครั้งหน้า จาก summary.md ของ ประชุมล่าสุดที่เป็นประเภทเดียวกัน แล้วให้โมเดลเขียนหัวข้อเพิ่ม:
## คืบหน้าจากครั้งก่อน
- สรุปงบ Railway ให้จบ → ตกลงกันแล้วว่าใช้แผน Hobby ก่อน
- ตัดสินใจเรื่อง Redis → ยังไม่ได้แตะ- ประเภทเดียวกันเท่านั้น เรื่องค้างของประชุมข้ามฝ่ายไม่โผล่ในสรุป dev ล้วน มันคนละวงคุย
- ไม่มีประชุมก่อนหน้า / อ่านไฟล์ไม่ได้ / ไม่มีเรื่องค้าง → ข้ามหัวข้อนี้ไปเลย ไม่มี error และไม่มีหัวข้อว่างโผล่มา
- ประชุมที่กำลังสรุปถูกกันออกจากการค้นหาเสมอ — ตอนลองใหม่หลังจากพัง โฟลเดอร์เดิมมี
summary.mdของรอบก่อนอยู่ ถ้าไม่กันไว้มันจะยกเรื่องค้างของตัวเองมาเป็น "คืบหน้าจากครั้งก่อน" ## คืบหน้าจากครั้งก่อนไม่เข้าขั้นสรุปรายช่วง (map.md) เพราะเรื่องค้างของประชุมก่อนไม่เกี่ยวกับการสรุปแต่ละช่วงของประชุมนี้ และmapถูกเรียกทุก chunk
## ต้องคุยต่อครั้งหน้า ทุกครั้งก่อนปล่อยผ่าน — หรือตั้ง CARRYOVER_ENABLED=false ไปก่อน
คำสั่งที่ส่งให้โมเดลอยู่ในไฟล์ ไม่ได้ฝังใน .py แก้ไฟล์แล้วประชุมครั้งถัดไปใช้ค่าใหม่เลย — ไม่ต้องรันเทสต์ใหม่
prompts/
├── map.md สรุปทีละช่วง (ใช้เมื่อ transcript ยาวเกิน ~20k token)
├── reduce.md รวมสรุปย่อยเป็นฉบับเดียว
├── single.md สรุปรอบเดียว (transcript สั้น)
└── profiles/
├── dev.md กฎเฉพาะประชุม dev ล้วน
└── cross.md กฎเฉพาะประชุมข้ามฝ่าย (Business + dev)
กฎที่ใช้ทุกประชุม แก้ใน map.md / reduce.md / single.md
กฎเฉพาะประเภทประชุม แก้ใน profiles/*.md — ไม่ต้องทำ prompt สองชุดเต็ม เพราะกฎที่ใช้ร่วมกันจะ drift ออกจากกันแน่นอนเมื่อแก้ชุดหนึ่งแล้วลืมอีกชุด
มี placeholder สองตัวที่โค้ดแทนที่ให้:
| placeholder | ถูกแทนด้วย |
|---|---|
{glossary} |
ตารางศัพท์จาก glossary.md (ชั้นที่ให้โมเดลตีความ) |
{profile_rules} |
เนื้อหาทั้งไฟล์ของ profiles/<ประเภทประชุม>.md |
{carryover} |
เรื่องค้างจากประชุมครั้งก่อน — มีใน reduce.md / single.md เท่านั้น |
{chunk} / {partials} ไม่มี — บทสนทนาถูกส่งเป็น user message แยกจาก system prompt ไม่ได้แทรกเข้าไปในตัว prompt
เขียน { } ในไฟล์ได้ตามปกติ (ตัวอย่าง JSON ฯลฯ) โค้ดใช้ str.replace ไม่ใช่ str.format จึงไม่ระเบิด
ไฟล์หายไม่ทำให้สรุปไม่ได้ — ตกไปใช้ prompt สำรองที่ฝังใน src/prompts.py พร้อมเตือนใน log (การจูนถ้อยคำจะไม่มีผลจนกว่าไฟล์จะกลับมา)
ต่อจากเมนูเลือกโมเดล จะมีเมนูถามประเภทประชุม (ทั้งใน start-meeting.bat และหน้าเว็บ):
ประเภทประชุม:
[1] dev ล้วน - ศัพท์เทคนิคตรงๆ
[2] Business + dev - แยก "ทำได้" ออกจาก "จะทำ" ขยายศัพท์ให้คนนอกทีม
กด Enter ผ่านได้เลย — dev เป็นค่าเริ่มต้นเพราะเป็นประชุมส่วนใหญ่ ค่าที่เลือก ผูกกับการประชุมครั้งนั้น ผ่าน .job.json เหมือนที่โมเดลทำ คิวสลับกันก็ไม่ผิดอัน
dev |
cross |
|
|---|---|---|
ตาราง ambiguous + teams เข้า prompt |
ไม่ | ใช่ |
| กฎ "ทำได้ ≠ จะทำ" | ไม่ | ใช่ |
| น้ำเสียง | ศัพท์เทคนิคตรงๆ | ขยายศัพท์ให้คนนอกทีมอ่านรู้เรื่อง |
ทำไมไม่ใส่ cross ให้ทุกประชุมไปเลย — ไม่ใช่แค่เปลืองโทเคน (prompt ใหญ่ขึ้น ~2.5 เท่า) แต่มันบอกโมเดลว่าคำอย่าง "เสร็จ" กำกวมระหว่างสองฝ่าย ทั้งที่ในห้องมีแต่ dev โมเดลจะไป qualify คำพูดปกติเกินจำเป็นจนสรุปอ่านแล้วอ้อมค้อม
เมนูนี้ ไม่ถามเมื่อเลือก [5] ถอดเสียงอย่างเดียว เพราะไม่มีสรุปให้ใช้กฎกับมัน
summary.meta.md เขียนไว้ว่าใช้ประเภทไหน (ประเภทประชุม: dev) — เผลอเลือกผิดกู้ได้ ลาก .job.json กับไฟล์เสียงกลับเข้า inbox/ ตามวิธี "ลองใหม่หลังจากพัง" แล้วแก้ค่า profile ใน .job.json ก่อน ระบบใช้ transcript เดิม ไม่ถอดเสียงซ้ำ
วางไฟล์ .mp3 .m4a .wav .ogg ลงในโฟลเดอร์ inbox/ ได้เลย ระบบจะเก็บไปประมวลผลเอง (ใช้ CLAUDE_MODEL จาก .env) ต้องให้ watcher รันอยู่ — เปิดด้วย:
.\.venv\Scripts\python -m src.mainstart-meeting.bat เปิด watcher ให้อัตโนมัติอยู่แล้วถ้ายังไม่มีตัวไหนรัน
อัดประชุมไม่ได้ — ตัวอัดใช้ WASAPI loopback ซึ่งเป็น API ของ Windows ล้วน บน macOS การดักเสียงลำโพงต้องผ่าน virtual audio device (BlackHole, Loopback) หรือ ScreenCaptureKit ซึ่งเป็นคนละกลไกและยังไม่ได้เขียนไว้
แต่ประมวลผลไฟล์เสียงที่มีอยู่แล้วได้ครบ — ถอดข้อความ แยกผู้พูด สรุป ทำงานเหมือนกันทุกอย่าง ใช้กับไฟล์ที่อัดจากมือถือ จาก Zoom หรือจากเครื่อง Windows อีกเครื่องได้
python -m venv .venv
./.venv/bin/python -m pip install -r requirements.txt # ข้าม pyaudiowpatch ให้เอง
brew install ffmpeg # หรือ apt install ffmpeg
cp .env.example .env # แล้วใส่ค่าจริง
./.venv/bin/python -m src.main # เฝ้า inbox/จากนั้นวางไฟล์เสียงลงใน inbox/ ได้เลย ผลลัพธ์ไปโผล่ที่ meetings/ เหมือนกัน
ข้อควรรู้: การเลือกอุปกรณ์เร่งความเร็วดูแค่ CUDA (transcribe.py, diarize.py) เครื่อง Mac จึงตกไปใช้ CPU เสมอ แม้เป็น Apple Silicon ที่มี MPS ก็ตาม แนะนำให้อยู่ที่ WHISPER_MODEL=small ไปก่อน ถ้าจะดัน large-v3 บน CPU จะช้ามาก
มีสอง process แยกกัน
start-meeting.bat
│
├─ เปิด watcher (src.main) ถ้ายังไม่มีตัวไหนรัน ─────────┐
│ │
└─ อัดเสียง (src.record) │
mic + ลำโพง ผสมกัน → เขียนเป็นไฟล์ย่อยทุก 30 นาที │
กด Ctrl+C → ffmpeg รวมเป็น .ogg → วางใน inbox/ │
▼
watcher เจอไฟล์ใหม่
│
Whisper ถอดข้อความ ─┐ │
├─ รวมด้วย timestamp ┤
pyannote แยกผู้พูด ─┘ │
│
transcript.md ◄───────────────┤
│
LLM สรุป → summary.md ◄───────┘
การอัดเขียนเป็นไฟล์ย่อยทุก 30 นาที ถ้าเครื่องดับกลางประชุม รอบหน้าที่เปิด start-meeting.bat ระบบจะกู้ส่วนที่อัดไว้แล้วให้อัตโนมัติ
ถ้าแยกผู้พูดล้มเหลว ระบบจะไปต่อโดยไม่มีป้ายผู้พูด ไม่ล้มทั้งงาน ถ้าขั้นตอนไหนพังจริง ไฟล์เสียงจะถูกย้ายไป failed/ พร้อมไฟล์ .error.log บอกสาเหตุ
ถ้าพังที่ขั้นสรุป (เครดิตหมด key หมดอายุ เน็ตหลุด) transcript.md จะถูกเขียนไว้เรียบร้อยแล้วก่อนหน้านั้น แก้ต้นเหตุแล้วลากไฟล์ใน failed/ กลับเข้า inbox/ ทั้ง .mp3/.ogg และ .job.json ระบบจะหยิบ transcript เดิมมาใช้ต่อ ไม่ถอดเสียงซ้ำ และไม่สร้างโฟลเดอร์ประชุมใหม่
.job.json คือไฟล์ที่จำว่าประชุมนี้เลือกโมเดลอะไรและ transcript อยู่ที่ไหน ถ้าลากกลับแต่ไฟล์เสียงอย่างเดียวก็ยังทำงานได้ แต่จะถอดเสียงใหม่ทั้งไฟล์และกลับไปใช้ CLAUDE_MODEL จาก .env
ถ้า .job.json ระบุชื่อโมเดลที่ระบบไม่รู้จักอีกแล้ว (เช่นพิมพ์ผิด หรือเป็นชื่อรุ่นเก่าที่เลิกรองรับ) การลากกลับเข้า inbox/ ทั้งสองไฟล์จะพังซ้ำทันทีด้วยสาเหตุเดิม — ให้ลากกลับแต่ไฟล์เสียงอย่างเดียวตามย่อหน้าข้างบนแทน จะได้ถอดเสียงใหม่และตกไปใช้ CLAUDE_MODEL จาก .env ซึ่งเป็นชื่อที่ระบบรู้จักแน่นอน
state/watcher.log— ทุกอย่างที่ตัวประมวลผล (src.main) ทำstate/ui.log— ฝั่งหน้าเว็บ (src.session_service)
สองไฟล์นี้หมุนเองที่ 5MB เก็บย้อนหลัง 3 ไฟล์ และเขียนเป็น UTF-8 เสมอ ไม่ต้อง redirect อะไรเอง — เดิม log อยู่ใน console window อย่างเดียวและหายไปพร้อมกับหน้าต่างที่ปิด ทำให้ไล่หาสาเหตุย้อนหลังไม่ได้เลย
ขั้นสรุปบันทึกทุก chunk พร้อมเวลาที่ใช้ และบันทึกทุกรอบที่ลองใหม่ ถ้าประชุมไหนดูค้าง ให้เปิด state/watcher.log — จะเห็นว่าค้างอยู่ที่ chunk ไหนและรอมานานเท่าไร
บล็อกของผู้พูดคนเดียวกันที่อยู่ติดกัน (ไม่มีคนอื่นคั่น) จะถูกต่อเป็นบล็อกเดียวก่อนส่งให้โมเดล วัดจริงบนประชุม 84 นาที: 1,618 บล็อกเหลือ 282 ข้อความสั้นลง 43% และคำขอไปที่โมเดล 6 ครั้งเหลือ 4
transcript.md ไม่ถูกแตะ — ที่รวมคือสำเนาในหน่วยความจำเท่านั้น เปิดไฟล์ย้อนดูของดิบได้เสมอ เหมือนกับที่ตัวกรองศัพท์ทำ
ปิดด้วย MERGE_SPEAKER_TURNS=false ใน .env เมื่ออยากเทียบคุณภาพสรุปกับข้อความดิบ หรือเมื่อการแยกผู้พูดของประชุมนั้นมั่วจนการรวมบล็อกยิ่งกลบความผิดพลาด
ก่อนเริ่มสรุปจริง ระบบยิงคำขอเล็ก ๆ ไปที่โมเดลก่อน (timeout 30 วินาที) ถ้าไปไม่ถึง ประชุมนั้นจะตกไป failed/ ทันทีพร้อม transcript ที่ถอดเสร็จแล้ว แทนที่จะรอ 900 วินาที × 3 รอบ × ทุก chunk ซึ่งกินเวลาได้เป็นชั่วโมงกว่าจะยอมแพ้
แก้ต้นเหตุแล้วลากกลับเข้า inbox/ ตามขั้นตอนข้างบนได้เลย ไม่ต้องถอดเสียงใหม่
ระบบจำน้ำเสียงข้ามการประชุมได้ หลังประชุมจบ ผู้พูดที่ยังไม่รู้จักจะขึ้นในหน้าเว็บให้ตั้งชื่อ
(มีประโยคตัวอย่าง ปุ่มฟังเสียง และชื่อที่โมเดลเดาให้เป็นตัวช่วย) ตั้งชื่อเมื่อไหร่ก็ได้ ไม่ต้อง
ทำทันที การประชุมครั้งถัดไปจะใส่ชื่อจริงให้เองทั้งใน transcript.md และในสรุป
- ทะเบียนเสียงอยู่ที่
speakers/registry.jsonในเครื่องเท่านั้น ไม่เคยถูกส่งออกนอกเครื่อง และถูก gitignore ไว้ — เวกเตอร์น้ำเสียงเป็นข้อมูล biometric - ลบคนออกจากทะเบียนได้ผ่าน
DELETE /api/speakers/<id>หรือลบทั้งโฟลเดอร์speakers/เพื่อเริ่มใหม่ทั้งหมด - โหมด
transcript-onlyจะไม่เรียกโมเดลเดาชื่อ (ข้อความไม่ออกนอกเครื่องตามที่เลือกไว้) แต่ยังเก็บคิวให้ตั้งชื่อเองได้ตามปกติ - ปรับความเข้มของการจำเสียงได้ที่
SPEAKER_MATCH_HIGH/SPEAKER_MATCH_LOWใน.envค่าสูงขึ้น = ใส่ชื่อให้น้อยลงแต่ผิดคนยากขึ้น - เลือกโมเดลแยกผู้พูดได้ที่
DIARIZATION_MODELใน.env— ค่าเริ่มต้นคือpyannote/speaker-diarization-community-1กลับไปใช้pyannote/speaker-diarization-3.1ได้ทุกเมื่อ สลับได้อย่างอิสระ ไม่กระทบทะเบียนเสียง เพราะ voiceprint ไม่ได้มาจาก โมเดลแยกผู้พูดอีกแล้ว - ค่าที่กระทบทะเบียนคือ
EMBEDDING_MODEL(ค่าเริ่มต้นpyannote/wespeaker-voxceleb-resnet34-LMปกติไม่ต้องแตะ) — มันคือโมเดลที่สร้างเวกเตอร์ น้ำเสียงจริง ๆ เวกเตอร์จากคนละโมเดลอยู่คนละพื้นที่ เอามาเทียบกันได้ตัวเลขที่ดูใช้ได้แต่ ไม่มีความหมาย ตัวอย่างเสียงทุกตัวจึงถูกติดป้ายโมเดลที่สร้างมัน และถูกใช้เฉพาะตอนที่ป้าย ตรงกันเท่านั้น เปลี่ยนค่านี้แล้วทุกคนต้องลงทะเบียนใหม่ และต้องวัดเกณฑ์SPEAKER_MATCH_*ใหม่ด้วย (python -m tools.calibrate_voiceprintsดูtools/README.md) — ข้อมูลเดิมไม่หาย สลับกลับมาก็จำได้เหมือนเดิม - ถ้าประชุมพังที่ขั้นสรุปแล้วลากกลับเข้า
inbox/ตามหัวข้อ "ลองใหม่หลังจากพัง" ด้านบน ระบบจะใช้ transcript เดิมต่อโดยไม่ผ่านขั้นตอนคิวตั้งชื่ออีกครั้ง — ผู้พูดที่จำได้มั่นใจ แล้วยังมีชื่อจริงอยู่ใน transcript เดิมตามปกติ (เพราะจับคู่เสร็จไปก่อนเขียนไฟล์แล้ว) แต่ผู้พูดที่ยังไม่รู้จักของการประชุมนั้นจะไม่ถูกเสนอให้ตั้งชื่ออีก เพราะเวกเตอร์เสียง มีอยู่แค่ในหน่วยความจำของรอบที่พังไปแล้วเท่านั้น
ปกติเสียงเข้าทะเบียนหลังประชุมจบ ผ่านคิว "ผู้พูดที่ยังไม่รู้จัก" ซึ่งแปลว่าประชุมครั้งแรก ของทุกคนได้ป้าย "ผู้พูด 1 / 2 / 3" เสมอ ถ้าอยากให้ระบบรู้จักใครตั้งแต่ประชุมแรก ลงทะเบียนเสียงไว้ก่อนได้:
- วางไฟล์เสียงไว้ที่
enroll\ตั้งชื่อไฟล์เป็นชื่อคน เช่นenroll\สมชาย.ogg(รองรับ.wav.ogg.mp3.m4a) - เปิด
http://127.0.0.1:8765/enroll - กด "วิเคราะห์เสียง" — งานถูกส่งให้ watcher ถ้ากำลังถอดเทปประชุมอยู่ คิวนี้จะรอจนเสร็จ
- ตรวจชื่อแล้วกด "บันทึก" ไฟล์จะถูกย้ายไป
enroll\done\ไม่ถูกลบ
ไฟล์ที่ใช้ไม่ได้: ไฟล์ที่มีคนพูดมากกว่าหนึ่งคน หรือมีเสียงพูดจริงไม่ถึงค่าต่ำสุดที่
ตั้งไว้ (ดู MIN_SPEAKING_SECONDS ใน src/speakers.py — หน้าเว็บ /enroll แสดงค่านี้
ให้เห็นตรง ๆ ในข้อความเตือน) จะถูกปฏิเสธ ไม่ใช่แค่เตือน — ตัวอย่างเสียงผิดคนที่หลุดเข้า
ทะเบียนจะฝังอยู่ถาวรและไปโผล่เป็นชื่อผิดในสรุปการประชุมครั้งหน้าโดยไม่มีอะไรฟ้อง
แนะนำให้อัดยาวเกิน 30 วินาที
หมายเหตุเรื่องความเป็นส่วนตัว: enroll\ ถูก gitignore ไว้เหมือน speakers\
ไฟล์ในนั้นคือเสียงพูดของคนจริง อย่า commit เข้า repo
หมายเหตุเรื่องคุณภาพ: เสียงคนเดียวกันผ่านไมค์ตรงหน้ากับผ่าน codec ของแอปประชุม ให้เวกเตอร์ต่างกันจริง ตัวอย่างที่อัดจากไมค์ตรงหน้าจึงจำคนที่นั่งในห้องได้ดีกว่าคนที่ ต่อเข้ามาทางออนไลน์ ระบบเก็บได้ถึง 10 ตัวอย่างต่อคน — ลงทะเบียนเพิ่มจากทั้งสองทางได้
| อาการ | สาเหตุที่พบบ่อย |
|---|---|
ffmpeg not found on PATH |
ยังไม่ได้ติดตั้ง หรือติดตั้งแล้วแต่ไม่ได้เปิด terminal ใหม่ |
ไม่พบ loopback device ของลำโพง default |
ลำโพง default ของ Windows ไม่รองรับ loopback ลองเปลี่ยนอุปกรณ์เสียงหลักที่ mmsys.cpl |
sample rate ของไมค์กับลำโพงไม่ตรงกัน |
ตั้งให้เท่ากันที่ mmsys.cpl → อุปกรณ์ → Advanced (แนะนำ 48000 Hz) |
| transcript มีแต่เสียงเราฝั่งเดียว | แอปประชุมส่งเสียงออกอุปกรณ์อื่นที่ไม่ใช่ default ของ Windows — รัน src.preflight เช็คก่อน |
| ดาวน์โหลดโมเดลแยกผู้พูดไม่ได้ | ยังไม่ได้กดยอมรับ license ครบทั้งสองหน้าในขั้นตอนที่ 3 |
| เตือนว่าอุปกรณ์เสียงเปลี่ยนกลางประชุม | ต่อ/ถอดหูฟังระหว่างอัด เสียงคู่สนทนาที่ออกอุปกรณ์ใหม่จะไม่ถูกอัด ให้สลับกลับหรือหยุดแล้วเริ่มใหม่ |
.\.venv\Scripts\python -m pytestโครงสร้างโค้ดแบ่งเป็นโมดูลตามขั้นตอนของ pipeline — record อัด, segments รวมและเข้ารหัสไฟล์,
watcher เฝ้า inbox/, transcribe / diarize / merge ทำ transcript, llm รู้ว่าแต่ละ
provider คุยด้วยโปรโตคอลอะไร, summarize ทำ map-reduce ผ่าน llm โดยไม่รู้ว่าปลายทางเป็นใคร,
storage เขียนผลลัพธ์ แต่ละโมดูลมีชุดเทสต์ของตัวเองใน tests/
MIT — ดู LICENSE