EST. 2026 — บันทึกงานเซิร์ฟเวอร์จริง SYSADMIN · SECURITY · AI · DIRECTADMIN เล่มที่ 01
THE SYSADMIN ALMANAC
หน้าหลัก / AI / Whisper ถอดเสียงฮัมเป็นคำ และได้เวลาผิด ถ้าไม่ถอดจาก stem ที่ถูกต้อง
AI

Whisper ถอดเสียงฮัมเป็นคำ และได้เวลาผิด ถ้าไม่ถอดจาก stem ที่ถูกต้อง

โดย Ton-Or ·10 พ.ย. 2026·อ่าน 10 นาที
Whisper ถอดเสียงฮัมเป็นคำ และได้เวลาผิด ถ้าไม่ถอดจาก stem ที่ถูกต้อง

ผลถอดเสียงที่มีเวลากำกับดูน่าเชื่อถือเสมอ ทั้งที่ส่วนหนึ่งเป็นของที่ไม่มีอยู่จริง ผมเลยเก็บกับดักที่เจอจริงไว้เป็นรายการ พร้อมตัวเลขว่าเสียเวลาไปเท่าไรถ้าเชื่อโดยไม่ตรวจ

สรุปใน 1 นาที

อาการ/ปัญหา
Whisper ถอดเสียงทั้งมิกซ์แล้วเวลาคลาด มีข้อความหลอน และอ่านเสียงฮัมเป็นประโยค
ใครควรอ่าน
คนที่ใช้ Whisper ถอดเนื้อเพลงพร้อมเวลาเพื่อตัดต่อ
ทางแก้แบบสั้น
ถอดจากสเต็มที่ถูกต้องแทนมิกซ์ ตรวจช่วงบรรเลง เสียงฮัม และท้ายไฟล์ โดยเวลาเริ่มท่อนแรกขยับจาก 19.2 เป็น 18.5 วินาที
ข้ามไปขั้นตอน

พอดีผมถอดเสียงของเพลงทดลองเพลงหนึ่งยาว 280.85 วินาทีเพื่อเอาเวลาของวลีร้องไปวางแผนตัดต่อ รอบแรกผมถอดจากมิกซ์ทั้งเพลง ได้ไฟล์ที่ดูสมบูรณ์มาก มีเวลาเริ่มจบกำกับทุกเซกเมนต์ ก่อนเอาไปใช้ผมลองถอดอีกรอบจากสเต็มเสียงร้องที่แยกด้วย Demucs แล้วเอาสองรอบมาเทียบกัน ปรากฏว่ามีหลายจุดที่ไม่ตรงกันเลย และไม่ใช่แค่ต่างกันนิดเดียว บางจุดคือข้อความที่ไม่มีอยู่ในเสียงจริง

บทความนี้เล่าเฉพาะกับดักของ "ผลลัพธ์" ส่วนวิธีรันงานบนเครื่อง GPU กับการส่งมอบแบบตรวจ hash อยู่ใน Demucs + Whisper ทีละไฟล์บนเครื่อง GPU แล้วดึงผลกลับตรวจ SHA-256 ข้อความที่ Whisper ถอดได้ผมไม่ยกมาสักบรรทัด ที่ต้องการคือเวลากับรูปแบบของความผิดพลาด

ภาพรวม: ผลถอดจากมิกซ์ปนของหลอนเกือบหนึ่งในห้า

ผมเอาตัวเลขที่วัดได้จริงมาวางก่อน

280.85 sความยาวเพลง
57.5 sเซกเมนต์สัญลักษณ์โน้ต
ประมาณ 20%ของทั้งเพลง (คำนวณ)
0.7 sเวลาเริ่มท่อนแรกคลาด
21 เฟรมที่ 30 fps
13 sเซกเมนต์ข้อความซ้ำ

ตัวเลข "ประมาณ 20%" ผมบวกเองจากสองเซกเมนต์ (27.5 กับ 30.0 วินาที รวม 57.5 วินาที หารด้วย 280.85) ไม่ใช่ตัวเลขที่ระบบรายงานมา เลยขอระบุว่าเป็นการคำนวณ

ถอดจากมิกซ์ทั้งเพลงสัญลักษณ์โน้ต 27.5 sสัญลักษณ์โน้ต 30.0 sฮัม 3-19 s ถูกอ่านเป็นประโยคถอดจากสเต็มเสียงร้องนำเริ่มท่อนแรก 18.5 s (มิกซ์ว่า 19.2 s)วน 264.84-277.92 s และคำทักทายท้ายไฟล์ถอดจากสเต็มเสียงประสานท่อนร้องหมู่ 62.4-83.5 s อยู่ในสเต็มนี้เท่านั้น0 s140 s280.85 s
สีแดงคือส่วนที่เชื่อไม่ได้ สีน้ำเงินคือส่วนที่ต้องไปหาในสเต็มที่ถูกต้อง ทั้งสามแถวอยู่บนแกนเวลาเดียวกัน

กับดักที่ 1: ช่วงบรรเลงกลายเป็นสัญลักษณ์โน้ตยาวหลายสิบวินาที

ในผลถอดจากมิกซ์ มีเซกเมนต์ 96.72-124.24 วินาที (27.5 วินาที) และ 246.52-276.50 วินาที (30.0 วินาที) ที่ข้อความเป็นสตริงของสัญลักษณ์โน้ตดนตรีซ้ำยาว ๆ ช่วงนั้นเป็นเพลงบรรเลงล้วนไม่มีเสียงร้อง Whisper ออกแบบมาถอดเสียงพูด พอเจอเสียงที่ไม่ใช่คำพูดมันก็คายสตริงสัญลักษณ์นี้ออกมา (สาเหตุลึกกว่านั้นเป็นข้อสันนิษฐาน ผมไม่ได้พิสูจน์) เซกเมนต์เหล่านี้มีเวลาเริ่มจบสวยงามเหมือนของจริง ถ้าเอาไปใช้เป็น "ช่วงที่มีเสียงร้อง" จะผิดกว่าหนึ่งนาทีรวม

กับดักที่ 2: เสียงฮัมถูกอ่านเป็นประโยค

ต้นเพลงช่วง 3-19 วินาทีเป็นเสียงฮัมจริง ไม่มีคำ แต่ Whisper อ่านออกมาเป็นประโยคภาษาไทยที่ไม่มีอยู่ ผมยืนยันว่ามันเป็นฮัมได้จากสเต็มเสียงร้องและการตรวจระดับเสียง ไม่ใช่จากข้อความที่ถอดได้ ข้อความมั่นใจพอ ๆ กับคำพูดจริง นี่คือจุดที่อันตรายที่สุด เพราะผิดโดยไม่มีสัญญาณเตือน

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

วิธียืนยันที่ผมใช้คือ pitch tracking บนสเต็มเสียงร้อง ถ้าช่วงนั้นมีระดับเสียงต่อเนื่องแต่ข้อความไม่น่าเชื่อถือ ก็น่าจะเป็นเสียงร้องที่ไม่ใช่คำ

# hum_check.py — ช่วงนี้มีระดับเสียงต่อเนื่องไหม (ใช้ librosa.pyin บนสเต็มเสียงร้อง)
import numpy as np, librosa

def voiced_ratio(vocals_wav, t0, t1):
    y, sr = librosa.load(vocals_wav, sr=22050, offset=t0, duration=t1 - t0)
    f0, voiced_flag, voiced_prob = librosa.pyin(
        y, fmin=librosa.note_to_hz('C2'), fmax=librosa.note_to_hz('C6'), sr=sr)
    return float(np.mean(voiced_flag))          # สัดส่วนเฟรมที่มีระดับเสียงชัดเจน

r = voiced_ratio('sep/vocals.wav', 3.0, 19.0)
print(f'voiced {r:.0%} ของ 3-19 s')             # สูง = มีเสียงร้อง แม้ข้อความจะน่าสงสัย

กับดักที่ 3: คำทักทายท้ายไฟล์ที่ไม่มีอยู่จริง

ในผลถอดจากสเต็มเสียงร้อง มีเซกเมนต์ 279.44-280.85 วินาทีที่ข้อความเป็นประโยคทักทายสั้น ๆ ทั้งที่ช่วงนั้นเกือบเงียบ นี่เป็นพฤติกรรมหลอนของ Whisper ที่พบได้ทั่วไปกับเสียงเงียบท้ายไฟล์ ผมไม่ยกประโยคนั้นมาลง

อีกอาการที่ใกล้เคียงกันคือเซกเมนต์ยาว 264.84-277.92 วินาที (13 วินาที) ที่มีข้อความสั้นมากซ้ำไปซ้ำมา เป็นสัญญาณของลูปซ้ำ เซกเมนต์ยาวแต่ข้อความสั้นคือของน่าสงสัยเสมอ

ตัวกรองที่ผมเสนอ (ยังไม่ใช่ค่าที่ใช้จริง)

Whisper ให้ฟิลด์ตัวบอกความมั่นใจมาในทุกเซกเมนต์อยู่แล้ว คือ avg_logprob, no_speech_prob และ compression_ratio ค่า default ของ openai-whisper ตามที่ผมจำได้คือ compression_ratio_threshold 2.4, logprob_threshold -1.0 และ no_speech_threshold 0.6 ตรงนี้เป็นความรู้ทั่วไป ควรเปิดเอกสารของรุ่นที่ติดตั้งเช็กก่อนอ้าง ตัวกรองข้างล่างเป็นข้อเสนอที่เขียนขึ้นให้อ่านง่าย ผมไม่ได้ใช้ค่าเหล่านี้ตรง ๆ ในโปรเจกต์ และต้องลองปรับกับไฟล์ของตัวเอง

# filter_segments.py — ข้อเสนอ: ติดธงเซกเมนต์ที่น่าสงสัย ไม่ลบทิ้งเงียบ ๆ
import json, sys, unicodedata

def has_letters(text):
    return any(unicodedata.category(c)[0] in 'LN' for c in text)    # ตัวอักษรหรือตัวเลขจริง

def reasons(seg):
    r = []
    if not has_letters(seg['text']):          r.append('symbols-only')
    if seg['compression_ratio'] > 2.4:        r.append('repetitive')
    if seg['avg_logprob'] < -1.0:             r.append('low-confidence')
    if seg['no_speech_prob'] > 0.6:           r.append('probably-silence')
    if seg['end'] - seg['start'] > 10 and len(seg['text'].strip()) < 12:
        r.append('long-span-short-text')      # เกณฑ์ที่ผมเพิ่มเอง ใช้จับลูปซ้ำ
    return r

segs = json.load(open(sys.argv[1], encoding='utf-8'))
for s in segs:
    s['flags'] = reasons(s)
    if s['flags']:
        print(f"[WARN] {s['start']:.2f}-{s['end']:.2f}  {','.join(s['flags'])}")
json.dump(segs, open(sys.argv[2], 'w', encoding='utf-8'), ensure_ascii=False, indent=1)

ผมเลือกติดธงแทนการลบ เพราะตัวกรองอัตโนมัติผิดได้ทั้งสองทาง ถ้าลบทิ้งเงียบ ๆ จะไม่มีใครรู้ว่าเคยมีเซกเมนต์ตรงนั้น

ข้อความจาก ASR ไม่ใช่ข้อความจริง

ผมถือผลถอดเป็น "หลักฐานเวลา" เท่านั้น และใส่ human_listening_verified = false ในข้อมูลเสมอ จนกว่าจะมีคนฟังจริง ห้ามเขียนว่า "AI ฟังแล้ว" ให้เขียนว่าประมวลผลอัตโนมัติ

กับดักที่ 4: ท่อนร้องหมู่อยู่ผิดสเต็ม

ท่อนร้องหมู่ท่อนหนึ่งที่เวลา 62.4-83.5 วินาที (ราว 21 วินาที) ไม่ได้อยู่ในสเต็มเสียงร้องนำ แต่อยู่ในสเต็มเสียงประสาน ถ้าถอดจากสเต็มร้องนำอย่างเดียวจะไม่ได้เวลาของท่อนนั้นเลย ต้องถอดสเต็มเสียงประสานแยกอีกรอบ

คลื่นเสียงแยกเป็นสองเลนต่างสี เลนบนคือเสียงร้องนำและเลนล่างคือเสียงประสาน โดยมีช่วงที่ไฮไลต์ปรากฏเฉพาะในเลนล่าง
ช่วงที่ไฮไลต์มีเสียงแค่เลนล่าง ถ้าถอดเฉพาะเลนบนจะมองไม่เห็นช่วงนี้เลย

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

กับดักที่ 5: เวลาเริ่มท่อนแรกคลาด 0.7 วินาที

จากมิกซ์ ผมเชื่อว่าท่อนแรกเริ่มที่ 19.2 วินาที จากสเต็มเสียงร้องนำได้ 18.5 วินาที เร็วขึ้น 0.7 วินาที เท่ากับประมาณ 21 เฟรมที่ 30 fps ซึ่งเห็นได้ชัดในคัตที่ตัดตามจังหวะ ถ้าเชื่อค่าจากมิกซ์ คัตที่วางตามเวลานั้นจะช้าไป 0.7 วินาทีโดยไม่มีอะไรเตือน

กับดักที่ 6: ขอบเซกเมนต์ต่างกันเมื่อเสียงหนาแน่น

เมื่อเทียบสองรอบ (ถอดมิกซ์กับถอดสเต็ม) ท่อนที่ชัดตรงกันมาก ต่างกันแค่ 0.02-0.1 วินาที เช่น 124.20 กับ 124.24 หรือ 127.50 กับ 127.48 แต่ท่อนที่หนาแน่นแบ่งเซกเมนต์ต่างกัน ตัวอย่างที่เห็นชัดคือสเต็มรวม 158.54-166.42 วินาทีเป็นเซกเมนต์เดียว ส่วนมิกซ์แยกเป็นสอง (158.46-161.14 และ 162.78-166.44) ดังนั้นต้องเทียบสองแหล่งก่อนเชื่อขอบเซกเมนต์

# compare_runs.py — เทียบเวลาเริ่มของสองรอบ (มิกซ์ vs สเต็ม)
import json, sys

a = json.load(open(sys.argv[1], encoding='utf-8'))      # รอบถอดจากมิกซ์
b = json.load(open(sys.argv[2], encoding='utf-8'))      # รอบถอดจากสเต็ม
b_starts = [s['start'] for s in b]

for s in a:
    near = min(b_starts, key=lambda t: abs(t - s['start']))
    d = near - s['start']
    mark = 'OK ' if abs(d) <= 0.1 else 'DIFF'
    print(f'[{mark}] a={s["start"]:7.2f}  b={near:7.2f}  ต่าง {d:+.2f} s')

บรรทัดที่ขึ้น DIFF คือจุดที่ต้องไปฟังเอง ไม่ใช่จุดที่ต้องเลือกฝั่งใดฝั่งหนึ่งอัตโนมัติ

กับดักที่ 7: วลีที่ ASR ถอดไม่ออก ต้องทำเครื่องหมายว่าเป็นประมาณการ

มีช่วงที่ ASR ถอดคำไม่ออก แต่ pitch ยืนยันว่ามีเสียงร้องอยู่ ตัวอย่างคือช่วง 53.7-61.3 วินาที ซึ่งครอบสองบรรทัด ผมใช้การแบ่งเวลาเท่า ๆ กันและบังคับใส่ธง estimated ในข้อมูลเสมอ จะได้ไม่มีใครเข้าใจผิดว่าตัวเลขนั้นวัดมาจริง

# split_estimated.py — แบ่งช่วงที่ถอดไม่ได้เท่า ๆ กัน พร้อมธงบังคับ
def split_equal(t0, t1, n):
    step = (t1 - t0) / n
    return [{'start': round(t0 + i * step, 3),
             'end':   round(t0 + (i + 1) * step, 3),
             'timing': 'estimated'}               # ห้ามขาด ธงนี้คือความซื่อตรงของข้อมูล
            for i in range(n)]

# ตัวอย่างการคำนวณ: ถ้าส่วนที่ถอดไม่ได้คือ 53.7-60.0 s (คำที่จับได้เริ่มที่ 60.0 s) มี 2 บรรทัด
for seg in split_equal(53.7, 60.0, 2):
    print(seg)                                    # 53.7-56.85 และ 56.85-60.0

ตัวอย่างข้างบนเป็นการคำนวณประกอบ ไม่ใช่ผลวัด การแบ่งเท่า ๆ กันคือการเดาที่ซื่อตรงที่สุดเมื่อไม่มีข้อมูล แต่ต้องบอกว่าเดา

ลำดับที่ผมใช้แทนการเชื่อผลถอดจากมิกซ์แยกสเต็มชุดเสียงคนถอดทีละสเต็มนำ และประสานกรองเซกเมนต์ติดธง ไม่ลบยืนยันฮัมpitch pyinถอดไม่ได้แบ่งเท่า ๆ กันข้อมูลสุดท้ายมีธงกำกับช่วง 53.7-61.3 s ถอดคำไม่ได้แต่ pitch ยืนยันว่ามีเสียงร้องแบ่งเวลาเท่า ๆ กัน และใส่ธง estimated ในข้อมูลเสมอฮัมต้นเพลง 3-19 s ยืนยันด้วยระดับเสียง ไม่ใช่ด้วยข้อความทุกไฟล์ผลลัพธ์ human_listening_verified = false จนกว่าจะมีคนฟังจริง
ทุกขั้นเก็บธงไว้ในข้อมูล ไม่มีขั้นไหนลบหลักฐานทิ้งเงียบ ๆ

อ่านผลถอดก่อนเอาไปวางแผน: ลำดับที่ผมเช็ก

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

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

อีกข้อหนึ่งที่ผมได้จากงานนี้คือ ความมั่นใจของโมเดลไม่ใช่ความถูกต้อง ฟิลด์อย่าง avg_logprob บอกว่าโมเดลมั่นใจกับสิ่งที่มันพิมพ์แค่ไหน ไม่ได้บอกว่าเสียงตรงนั้นมีสิ่งนั้นอยู่จริง เสียงฮัมที่ถูกอ่านเป็นประโยคเป็นตัวอย่างที่ดีที่สุด เลยต้องมีหลักฐานจากอีกช่องทางหนึ่งเสมอ ในกรณีนี้คือระดับเสียงที่วัดได้จากตัวสัญญาณเอง

สรุปเป็นข้อที่นำไปใช้ได้

  1. อย่าถอดจากมิกซ์ทั้งเพลงเพียงอย่างเดียว ถอดจากสเต็มเสียงคนที่แยกแล้ว และถอดทุกสเต็มที่มีเสียงคน
  2. ติดธงเซกเมนต์ที่เป็นสัญลักษณ์ล้วน ยาวแต่ข้อความสั้น มั่นใจต่ำ หรือน่าจะเป็นความเงียบ แล้วไปฟังเอง
  3. ยืนยันเสียงฮัมด้วยระดับเสียง (pitch) ไม่ใช่ด้วยข้อความที่ถอดได้
  4. เทียบสองรอบก่อนเชื่อขอบเซกเมนต์ ที่ชัดตรงกันภายใน 0.1 วินาที ที่หนาแน่นให้ไปฟัง
  5. ช่วงที่ถอดไม่ได้ให้แบ่งเวลาเท่า ๆ กัน พร้อมธง estimated บังคับ
  6. ใส่ human_listening_verified เป็น false จนกว่าจะมีคนฟังจริง
ถอดจากมิกซ์

ดูครบแต่เชื่อไม่ได้

  • ช่วงบรรเลงกลายเป็นสัญลักษณ์โน้ต
  • ฮัมถูกอ่านเป็นประโยค
  • เริ่มท่อนแรกช้าไป 0.7 วินาที
ถอดจากสเต็ม

ถูกกว่าแต่ต้องเลือกให้ถูกชิ้น

  • ได้เวลาเริ่มท่อนแรก 18.5 วินาที
  • ท่อนร้องหมู่ต้องไปหาในสเต็มประสาน
  • ยังมีของหลอนท้ายไฟล์ ต้องกรอง
ตัวเลขนี้ของเพลงเดียว

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

บทเรียนที่สั้นที่สุดคือ ผลถอดที่มีเวลากำกับสวยงามไม่ได้แปลว่าเสียงตรงนั้นมีอะไร ต้องมีหลักฐานอีกชั้นเสมอ อย่างระดับเสียงหรือสเต็มที่ถูกต้อง และข้อมูลที่ไม่มั่นใจต้องมีธงกำกับในไฟล์เอง ไม่ใช่จำไว้ในหัว ตรงไหนผมอธิบายตกหล่นหรือพิมพ์ผิด เดี๋ยวกลับมาแก้เพิ่มให้ภายหลังครับ

เคยเจองานแบบนี้ไหม? ติดตรงไหน?

มาแลกวิธีกันต่อในบอร์ด LinuxThai

→ คุยต่อในบอร์ด

ความคิดเห็น 0

ยังไม่มีความคิดเห็น — เป็นคนแรกเลย