Skip to content

Repository files navigation

meeting-notes

อัดประชุม → ถอดเสียงเป็นข้อความพร้อมแยกผู้พูด → สรุปประเด็นและ 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

ติดตั้ง

1. โคลนและสร้าง virtual environment

git clone https://github.com/wowBALL/meeting-notes.git
cd meeting-notes
python -m venv .venv
.\.venv\Scripts\python -m pip install -r requirements.txt

2. ติดตั้ง ffmpeg

winget install Gyan.FFmpeg

เปิด terminal ใหม่หลังติดตั้ง เพื่อให้ PATH อัปเดต แล้วเช็คว่าได้จริง:

ffmpeg -version

3. ขอ Hugging Face token และกดยอมรับ license

โมเดลแยกผู้พูดเป็น gated model ต้องกดยอมรับเงื่อนไขก่อน ทั้งสองหน้า ไม่งั้นดาวน์โหลดไม่ได้:

  1. https://huggingface.co/pyannote/speaker-diarization-community-1 → กด Agree (ค่าเริ่มต้น ถ้าตั้ง DIARIZATION_MODEL เป็น 3.1 ให้กดหน้า https://huggingface.co/pyannote/speaker-diarization-3.1 แทน)
  2. https://huggingface.co/pyannote/segmentation-3.0 → กด Agree
  3. สร้าง token ที่ https://huggingface.co/settings/tokens (แบบ Read พอ)

มีโมเดลที่สามที่ระบบใช้ด้วยคือ pyannote/wespeaker-voxceleb-resnet34-LM (26MB ตัวที่สร้าง เวกเตอร์น้ำเสียงสำหรับจำคนข้ามการประชุม) — ไม่ใช่ gated ไม่ต้องกด Agree ดาวน์โหลดเอง อัตโนมัติครั้งแรกที่ใช้ ถ้าเครื่องไม่มีเน็ตตอนนั้น ระบบจะเดินต่อโดยไม่มีการจำเสียง (ทุกคน เป็นป้าย "ผู้พูด N") และเขียนเหตุผลไว้ใน log ของ watcher

4. สร้างไฟล์ .env

ก๊อป .env.example เป็น .env แล้วใส่ค่าจริง:

copy .env.example .env
LLM_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/* แล้วปิดให้อัตโนมัติตอนเข้าขั้นแปลงไฟล์ — ดูหัวข้อถัดไป

COMPANION_COMMAND — รันโปรแกรมอื่นคู่ไปกับการอัด (ไม่บังคับ)

ไม่ตั้งค่า = ไม่มีอะไรเกิดขึ้น ซึ่งเป็นค่าเริ่มต้น

โปรเซสนี้ไม่ได้ถูกเปิดอัตโนมัติตอนเริ่มอัด ต้องสั่งเปิดด้วย 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, มีสมาชิกที่ไม่ใช่สตริง) ก็แปลว่าปิดฟีเจอร์ ไม่ใช่เปิดห้องไม่ได้

5. ตรวจว่าพร้อมอัด

.\.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 มุมขวาบนสลับภาษาทั้งหน้า รวมข้อความสถานะ ระบบจำภาษาที่เลือกไว้

แบบเมนูใน terminal

ดับเบิลคลิก start-meeting.bat หรือรันจาก terminal:

.\start-meeting.bat

จะเจอตามลำดับนี้:

  1. เมนูเลือกโมเดลสรุป[1] Qwen 3.6 ข้อมูลไม่ออกนอกบริษัทและเร็วที่สุด / [2] GLM 5.2 ข้อมูลไม่ออกนอกบริษัทเหมือนกันแต่ช้ากว่ามาก / [3] Opus 5 แม่นกว่า / [4] Sonnet 5 ถูกกว่า / [5] ถอดเสียงอย่างเดียว ไม่ส่งให้โมเดลไหนเลย (กด Enter = Qwen 3.6)
  2. ตรวจเสียงอัตโนมัติ — ถ้าไม่ผ่านจะถามก่อนว่าจะอัดต่อไหม
  3. ใส่ชื่อประชุม — ไม่ใส่ก็ได้ กด Enter ข้าม
  4. เริ่มอัด — ประชุมได้ตามปกติ พอจบกด 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 ไปสรุปเองได้

แก้คำที่ถอดเพี้ยน (glossary.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 ยังเก็บของดิบไว้เสมอ — ตัวกรองไม่แตะไฟล์นั้น)

ท้าย summary.md มีสองบรรทัดที่ความหมายต่างกัน

แก้คำตาม glossary: PostgreSQL 3 จุด, Railway 1 จุด
คำ fuzzy ที่เจอในห้อง: Electron 2 ครั้ง
  • บรรทัดแรก = แก้ไปแล้ว (ชั้น exact/aliases) ถ้าคำไหนจำนวนเฟ้อผิดปกติ แปลว่ามันไปแทนที่กลางคำอื่น ย้ายไป fuzzy
  • บรรทัดที่สอง = เจอ แต่ไม่ได้แก้ (ชั้น fuzzy) เป็นหลักฐานชิ้นเดียวที่บอกได้ว่าคำไหนเลิกใช้ไปแล้ว คำที่ไม่โผล่ติดกันหลายสัปดาห์ให้ลบออกจาก fuzzy เพราะมันกิน token ทุกครั้ง

ไม่มีคำถูกแก้และไม่เจอคำ fuzzy = ไม่มีบรรทัดพวกนี้เลย

teams.md (ไม่บังคับ)

ใช้บอกว่าใครอยู่ฝ่ายไหน เพื่อให้โมเดลตีความคำในหมวด ambiguous ได้ถูก คัดลอกจาก teams.example.md แล้วใส่ชื่อจริง ชื่อต้องตรงกับ speaker registry

glossary.md กับ teams.md ถูก gitignore ไว้ทั้งคู่ เพราะ repo นี้เป็น public — teams.md มีชื่อคนจริง และตารางศัพท์ของจริงมักโตไปทางชื่อโปรเจกต์ ชื่อระบบภายใน และคำย่อของงาน ทั้งคู่มีไฟล์ .example.md เป็นโครงตั้งต้นให้คัดลอก

ไม่มีทั้งสองไฟล์ = ระบบทำงานเหมือนเดิมทุกอย่าง ไม่มี error

summary.md มีอะไรอยู่ในนั้น

ประชุมที่ไม่มีวาระตายตัว ปัญหาไม่ใช่คำผิด แต่คือ สรุปออกมาแล้วแยกไม่ออกว่าอะไรตกลงแล้ว อะไรแค่ลอยๆ โครงสรุปจึงบังคับให้แยกออกจากกัน:

หัวข้อ มีไว้ทำไม
## หัวข้อที่คุยกัน ไม่มีวาระ → ให้โมเดลจัดกลุ่มเอง คนอ่านย้อนจะได้รู้ว่าคุยอะไรบ้าง
## ตกลงแล้ว เฉพาะที่มีคนสรุปปิดชัดเจนหรือทุกคนเห็นพ้อง + ระบุว่าใครสรุป
## เสนอไว้ ยังไม่สรุป ข้อเสนอมักเยอะกว่าข้อสรุปมาก ถ้าไม่แยกจะกลายเป็นสรุปที่เชื่อไม่ได้
## 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

⚠️ กลไกนี้ผูกสรุปเข้าด้วยกันเป็นลูกโซ่ ถ้าสรุปครั้งหนึ่งเพี้ยน เรื่องค้างที่ผิดจะถูกส่งต่อไปครั้งถัดไป สองสัปดาห์แรกที่ยังจูน prompt อยู่ควรอ่านหัวข้อ ## ต้องคุยต่อครั้งหน้า ทุกครั้งก่อนปล่อยผ่าน — หรือตั้ง CARRYOVER_ENABLED=false ไปก่อน

จูนคำสั่งสรุปโดยไม่แตะโค้ด (prompts/)

คำสั่งที่ส่งให้โมเดลอยู่ในไฟล์ ไม่ได้ฝังใน .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.main

start-meeting.bat เปิด watcher ให้อัตโนมัติอยู่แล้วถ้ายังไม่มีตัวไหนรัน

ใช้บน macOS / Linux

อัดประชุมไม่ได้ — ตัวอัดใช้ 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 ซึ่งเป็นชื่อที่ระบบรู้จักแน่นอน

log อยู่ที่ไหน

  • 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 เดิมตามปกติ (เพราะจับคู่เสร็จไปก่อนเขียนไฟล์แล้ว) แต่ผู้พูดที่ยังไม่รู้จักของการประชุมนั้นจะไม่ถูกเสนอให้ตั้งชื่ออีก เพราะเวกเตอร์เสียง มีอยู่แค่ในหน่วยความจำของรอบที่พังไปแล้วเท่านั้น

ลงทะเบียนเสียงล่วงหน้า (enrollment)

ปกติเสียงเข้าทะเบียนหลังประชุมจบ ผ่านคิว "ผู้พูดที่ยังไม่รู้จัก" ซึ่งแปลว่าประชุมครั้งแรก ของทุกคนได้ป้าย "ผู้พูด 1 / 2 / 3" เสมอ ถ้าอยากให้ระบบรู้จักใครตั้งแต่ประชุมแรก ลงทะเบียนเสียงไว้ก่อนได้:

  1. วางไฟล์เสียงไว้ที่ enroll\ ตั้งชื่อไฟล์เป็นชื่อคน เช่น enroll\สมชาย.ogg (รองรับ .wav .ogg .mp3 .m4a)
  2. เปิด http://127.0.0.1:8765/enroll
  3. กด "วิเคราะห์เสียง" — งานถูกส่งให้ watcher ถ้ากำลังถอดเทปประชุมอยู่ คิวนี้จะรอจนเสร็จ
  4. ตรวจชื่อแล้วกด "บันทึก" ไฟล์จะถูกย้ายไป 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/

License

MIT — ดู LICENSE

About

อัดประชุม → ถอดเสียงพร้อมแยกผู้พูด → สรุปด้วย LLM · เสียงไม่ออกจากเครื่อง อัดทั้งไมค์และเสียงลำโพงพร้อมกัน ใช้ได้กับแอปประชุมทุกตัวบน Windows

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages