File size: 3,051 Bytes
b5680ee
 
e23266a
 
b5680ee
 
e23266a
b5680ee
e23266a
b5680ee
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e23266a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b5680ee
 
 
 
 
e23266a
5b4386a
e23266a
b5680ee
 
e23266a
 
b5680ee
 
e23266a
 
 
b5680ee
 
5b4386a
e23266a
 
 
 
b5680ee
 
e23266a
 
 
 
5b4386a
e23266a
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
"""話者分離の呼び出し口。

pyannote は 3 系と 4 系で引数名も戻り値の形も違い、4 系は音声の読み込みに
torchcodec を要求する。モデル本体は動かさず、その差を吸収できているかを確かめる。
"""

import struct
import sys
import wave
from pathlib import Path

import pytest

sys.path.insert(0, str(Path(__file__).resolve().parents[1]))

import pipeline as pl  # noqa: E402


class FakeTurn:
    def __init__(self, start, end):
        self.start, self.end = start, end


class FakeAnnotation:
    """pyannote 3 系がそのまま返してくるもの。"""

    def __init__(self, rows):
        self.rows = rows

    def itertracks(self, yield_label=False):
        for start, end, label in self.rows:
            yield FakeTurn(start, end), None, label


class FakeDiarizeOutput:
    """pyannote 4 系が返す入れ物。中に Annotation が入っている。"""

    def __init__(self, annotation):
        self.speaker_diarization = annotation


ROWS = [(0.0, 8.5, "SPEAKER_00"), (8.5, 14.5, "SPEAKER_01")]


@pytest.fixture
def wav(tmp_path) -> Path:
    """16kHz モノラル 16bit の、1秒ぶんの WAV。"""
    path = tmp_path / "sample.wav"
    with wave.open(str(path), "wb") as f:
        f.setnchannels(1)
        f.setsampwidth(2)
        f.setframerate(16000)
        f.writeframes(b"".join(struct.pack("<h", (i % 200) * 100 - 10000) for i in range(16000)))
    return path


def test_WAVを自前で読める(wav):
    """torchcodec が無くても音声を渡せること。"""
    data = pl.load_waveform(wav)
    assert data["sample_rate"] == 16000
    assert tuple(data["waveform"].shape) == (1, 16000)      # (チャンネル, 時間)
    assert -1.0 <= float(data["waveform"].min()) <= float(data["waveform"].max()) <= 1.0


@pytest.mark.parametrize(
    "result",
    [FakeAnnotation(ROWS), FakeDiarizeOutput(FakeAnnotation(ROWS))],
    ids=["pyannote3", "pyannote4"],
)
def test_どちらの戻り値でも同じ区間になる(monkeypatch, wav, result):
    monkeypatch.setattr(pl, "load_diarizer", lambda token, device="": lambda audio, **kw: result)
    assert pl.diarize(wav, "token", None) == ROWS


def test_パスではなく波形を渡している(monkeypatch, wav):
    """パスを渡すと pyannote 4 が torchcodec を要求して落ちるため。"""
    seen = {}

    def fake_pipe(audio, **kwargs):
        seen["audio"] = audio
        seen["kwargs"] = kwargs
        return FakeAnnotation(ROWS)

    monkeypatch.setattr(pl, "load_diarizer", lambda token, device="": fake_pipe)
    pl.diarize(wav, "token", 2)

    assert set(seen["audio"]) == {"waveform", "sample_rate"}
    assert seen["kwargs"] == {"num_speakers": 2}


def test_話者人数を指定しなければ自動判定に任せる(monkeypatch, wav):
    seen = {}
    monkeypatch.setattr(
        pl, "load_diarizer",
        lambda token, device="": lambda audio, **kw: (seen.update(kw), FakeAnnotation(ROWS))[1],
    )
    pl.diarize(wav, "token", None)
    assert seen == {}