Some checks failed
Tests / Go Tests (push) Has been cancelled
Tests / Python Tests (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / Go Lint (push) Has been cancelled
Tests / Python Lint (push) Has been cancelled
Tests / Security Scan (push) Has been cancelled
Tests / All Checks Passed (push) Has been cancelled
Security Scanning / Secret Scanning (push) Has been cancelled
Security Scanning / Dependency Vulnerability Scan (push) Has been cancelled
Security Scanning / Go Security Scan (push) Has been cancelled
Security Scanning / Python Security Scan (push) Has been cancelled
Security Scanning / Node.js Security Scan (push) Has been cancelled
Security Scanning / Docker Image Security (push) Has been cancelled
Security Scanning / Security Summary (push) Has been cancelled
CI/CD Pipeline / Go Tests (push) Has been cancelled
CI/CD Pipeline / Python Tests (push) Has been cancelled
CI/CD Pipeline / Website Tests (push) Has been cancelled
CI/CD Pipeline / Linting (push) Has been cancelled
CI/CD Pipeline / Security Scan (push) Has been cancelled
CI/CD Pipeline / Docker Build & Push (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / CI Summary (push) Has been cancelled
ci/woodpecker/manual/build-ci-image Pipeline was successful
ci/woodpecker/manual/main Pipeline failed
All services: admin-v2, studio-v2, website, ai-compliance-sdk, consent-service, klausur-service, voice-service, and infrastructure. Large PDFs and compiled binaries excluded via .gitignore.
326 lines
11 KiB
Python
326 lines
11 KiB
Python
"""
|
|
Tests for Transcription Worker Components
|
|
|
|
Tests for the transcription pipeline including:
|
|
- Whisper transcription
|
|
- Speaker diarization
|
|
- Segment alignment
|
|
- Export formats (VTT, SRT, JSON)
|
|
"""
|
|
|
|
import pytest
|
|
from unittest.mock import Mock, patch, MagicMock
|
|
from datetime import datetime
|
|
import json
|
|
|
|
|
|
class TestTranscriberModule:
|
|
"""Tests for the Whisper transcription module."""
|
|
|
|
def test_transcription_result_structure(self):
|
|
"""Test that transcription results have the expected structure."""
|
|
# Mock transcription result
|
|
result = {
|
|
"text": "Dies ist ein Test.",
|
|
"segments": [
|
|
{
|
|
"id": 0,
|
|
"start": 0.0,
|
|
"end": 2.5,
|
|
"text": " Dies ist ein Test.",
|
|
"avg_logprob": -0.25,
|
|
"no_speech_prob": 0.01
|
|
}
|
|
],
|
|
"language": "de"
|
|
}
|
|
|
|
assert "text" in result
|
|
assert "segments" in result
|
|
assert len(result["segments"]) > 0
|
|
assert "start" in result["segments"][0]
|
|
assert "end" in result["segments"][0]
|
|
assert "text" in result["segments"][0]
|
|
|
|
def test_confidence_calculation(self):
|
|
"""Test confidence score calculation from log probabilities."""
|
|
# avg_logprob of -0.25 should give ~78% confidence
|
|
import math
|
|
avg_logprob = -0.25
|
|
confidence = math.exp(avg_logprob)
|
|
|
|
assert 0.7 < confidence < 0.8
|
|
|
|
def test_segment_timing_validation(self):
|
|
"""Test that segment timings are valid."""
|
|
segments = [
|
|
{"start": 0.0, "end": 2.5, "text": "First segment"},
|
|
{"start": 2.5, "end": 5.0, "text": "Second segment"},
|
|
{"start": 5.0, "end": 7.5, "text": "Third segment"}
|
|
]
|
|
|
|
for i, segment in enumerate(segments):
|
|
# End must be after start
|
|
assert segment["end"] > segment["start"]
|
|
|
|
# No negative times
|
|
assert segment["start"] >= 0
|
|
assert segment["end"] >= 0
|
|
|
|
# Segments should be sequential
|
|
if i > 0:
|
|
assert segment["start"] >= segments[i - 1]["end"]
|
|
|
|
|
|
class TestDiarizationModule:
|
|
"""Tests for speaker diarization module."""
|
|
|
|
def test_speaker_segment_structure(self):
|
|
"""Test speaker segment structure."""
|
|
speaker_segments = [
|
|
{"speaker": "SPEAKER_00", "start": 0.0, "end": 3.0},
|
|
{"speaker": "SPEAKER_01", "start": 3.0, "end": 6.0},
|
|
{"speaker": "SPEAKER_00", "start": 6.0, "end": 9.0}
|
|
]
|
|
|
|
for segment in speaker_segments:
|
|
assert "speaker" in segment
|
|
assert "start" in segment
|
|
assert "end" in segment
|
|
assert segment["speaker"].startswith("SPEAKER_")
|
|
|
|
def test_multiple_speakers_detection(self):
|
|
"""Test that multiple speakers are detected."""
|
|
speaker_segments = [
|
|
{"speaker": "SPEAKER_00", "start": 0.0, "end": 3.0},
|
|
{"speaker": "SPEAKER_01", "start": 3.0, "end": 6.0},
|
|
{"speaker": "SPEAKER_02", "start": 6.0, "end": 9.0}
|
|
]
|
|
|
|
unique_speakers = set(s["speaker"] for s in speaker_segments)
|
|
assert len(unique_speakers) == 3
|
|
|
|
def test_overlapping_speech_handling(self):
|
|
"""Test handling of overlapping speech."""
|
|
# In real diarization, overlapping speech is split
|
|
segments = [
|
|
{"speaker": "SPEAKER_00", "start": 0.0, "end": 5.0},
|
|
{"speaker": "SPEAKER_01", "start": 4.0, "end": 8.0} # Overlap at 4-5
|
|
]
|
|
|
|
# Detect overlap
|
|
overlap_detected = False
|
|
for i in range(len(segments) - 1):
|
|
if segments[i]["end"] > segments[i + 1]["start"]:
|
|
overlap_detected = True
|
|
break
|
|
|
|
assert overlap_detected
|
|
|
|
|
|
class TestAlignmentModule:
|
|
"""Tests for text-speaker alignment."""
|
|
|
|
def test_align_transcription_with_speakers(self):
|
|
"""Test aligning transcription segments with speaker segments."""
|
|
transcription_segments = [
|
|
{"start": 0.0, "end": 2.0, "text": "Guten Tag."},
|
|
{"start": 2.0, "end": 4.0, "text": "Wie geht es Ihnen?"},
|
|
{"start": 4.0, "end": 6.0, "text": "Mir geht es gut, danke."}
|
|
]
|
|
|
|
speaker_segments = [
|
|
{"speaker": "SPEAKER_00", "start": 0.0, "end": 2.5},
|
|
{"speaker": "SPEAKER_01", "start": 2.5, "end": 6.0}
|
|
]
|
|
|
|
# Simple alignment by overlap
|
|
def find_speaker(text_start, text_end, speakers):
|
|
max_overlap = 0
|
|
best_speaker = None
|
|
for sp in speakers:
|
|
overlap_start = max(text_start, sp["start"])
|
|
overlap_end = min(text_end, sp["end"])
|
|
overlap = max(0, overlap_end - overlap_start)
|
|
if overlap > max_overlap:
|
|
max_overlap = overlap
|
|
best_speaker = sp["speaker"]
|
|
return best_speaker
|
|
|
|
aligned = []
|
|
for seg in transcription_segments:
|
|
speaker = find_speaker(seg["start"], seg["end"], speaker_segments)
|
|
aligned.append({**seg, "speaker": speaker})
|
|
|
|
assert aligned[0]["speaker"] == "SPEAKER_00"
|
|
assert aligned[1]["speaker"] == "SPEAKER_01"
|
|
assert aligned[2]["speaker"] == "SPEAKER_01"
|
|
|
|
|
|
class TestVTTExport:
|
|
"""Tests for WebVTT export format."""
|
|
|
|
def test_vtt_header(self):
|
|
"""Test VTT file starts with correct header."""
|
|
vtt_content = "WEBVTT\n\n00:00:00.000 --> 00:00:02.000\nTest"
|
|
|
|
assert vtt_content.startswith("WEBVTT")
|
|
|
|
def test_vtt_timestamp_format(self):
|
|
"""Test VTT timestamp format is correct."""
|
|
def format_vtt_time(ms):
|
|
hours = ms // 3600000
|
|
minutes = (ms % 3600000) // 60000
|
|
seconds = (ms % 60000) // 1000
|
|
millis = ms % 1000
|
|
return f"{hours:02d}:{minutes:02d}:{seconds:02d}.{millis:03d}"
|
|
|
|
assert format_vtt_time(0) == "00:00:00.000"
|
|
assert format_vtt_time(1500) == "00:00:01.500"
|
|
assert format_vtt_time(3661500) == "01:01:01.500"
|
|
|
|
def test_vtt_cue_format(self):
|
|
"""Test VTT cue block format."""
|
|
def create_vtt_cue(start_ms, end_ms, text):
|
|
start = f"{start_ms // 3600000:02d}:{(start_ms % 3600000) // 60000:02d}:{(start_ms % 60000) // 1000:02d}.{start_ms % 1000:03d}"
|
|
end = f"{end_ms // 3600000:02d}:{(end_ms % 3600000) // 60000:02d}:{(end_ms % 60000) // 1000:02d}.{end_ms % 1000:03d}"
|
|
return f"{start} --> {end}\n{text}"
|
|
|
|
cue = create_vtt_cue(0, 2500, "Test subtitle")
|
|
assert "-->" in cue
|
|
assert "Test subtitle" in cue
|
|
|
|
|
|
class TestSRTExport:
|
|
"""Tests for SRT subtitle export format."""
|
|
|
|
def test_srt_timestamp_format(self):
|
|
"""Test SRT timestamp format (uses comma instead of period)."""
|
|
def format_srt_time(ms):
|
|
hours = ms // 3600000
|
|
minutes = (ms % 3600000) // 60000
|
|
seconds = (ms % 60000) // 1000
|
|
millis = ms % 1000
|
|
return f"{hours:02d}:{minutes:02d}:{seconds:02d},{millis:03d}"
|
|
|
|
assert format_srt_time(0) == "00:00:00,000"
|
|
assert format_srt_time(1500) == "00:00:01,500"
|
|
|
|
def test_srt_entry_format(self):
|
|
"""Test SRT entry format with index."""
|
|
def create_srt_entry(index, start_ms, end_ms, text):
|
|
def fmt(ms):
|
|
return f"{ms // 3600000:02d}:{(ms % 3600000) // 60000:02d}:{(ms % 60000) // 1000:02d},{ms % 1000:03d}"
|
|
return f"{index}\n{fmt(start_ms)} --> {fmt(end_ms)}\n{text}\n"
|
|
|
|
entry = create_srt_entry(1, 0, 2500, "Test")
|
|
|
|
lines = entry.strip().split("\n")
|
|
assert lines[0] == "1"
|
|
assert "-->" in lines[1]
|
|
assert lines[2] == "Test"
|
|
|
|
|
|
class TestJSONExport:
|
|
"""Tests for JSON export format."""
|
|
|
|
def test_json_structure(self):
|
|
"""Test JSON export structure."""
|
|
export = {
|
|
"transcription_id": "abc123",
|
|
"recording_id": "xyz789",
|
|
"language": "de",
|
|
"model": "large-v3",
|
|
"created_at": datetime.utcnow().isoformat(),
|
|
"duration_seconds": 300,
|
|
"word_count": 500,
|
|
"confidence_score": 0.92,
|
|
"segments": [
|
|
{
|
|
"id": 0,
|
|
"start_ms": 0,
|
|
"end_ms": 2500,
|
|
"text": "Test segment",
|
|
"speaker": "SPEAKER_00",
|
|
"confidence": 0.95
|
|
}
|
|
]
|
|
}
|
|
|
|
# Verify structure
|
|
assert "transcription_id" in export
|
|
assert "segments" in export
|
|
assert len(export["segments"]) > 0
|
|
|
|
# Verify serializable
|
|
json_str = json.dumps(export)
|
|
assert len(json_str) > 0
|
|
|
|
|
|
class TestMinIOStorage:
|
|
"""Tests for MinIO storage operations."""
|
|
|
|
def test_recording_path_format(self):
|
|
"""Test recording storage path format."""
|
|
recording_name = "test-meeting_20260115_120000"
|
|
base_path = f"recordings/{recording_name}"
|
|
|
|
video_path = f"{base_path}/video.mp4"
|
|
audio_path = f"{base_path}/audio.wav"
|
|
vtt_path = f"{base_path}/transcript.vtt"
|
|
|
|
assert video_path == "recordings/test-meeting_20260115_120000/video.mp4"
|
|
assert audio_path == "recordings/test-meeting_20260115_120000/audio.wav"
|
|
assert vtt_path == "recordings/test-meeting_20260115_120000/transcript.vtt"
|
|
|
|
def test_bucket_name_validation(self):
|
|
"""Test MinIO bucket name is valid."""
|
|
bucket = "breakpilot-recordings"
|
|
|
|
# MinIO bucket name rules
|
|
assert len(bucket) >= 3
|
|
assert len(bucket) <= 63
|
|
assert bucket.islower() or "-" in bucket
|
|
assert not bucket.startswith("-")
|
|
assert not bucket.endswith("-")
|
|
|
|
|
|
class TestQueueProcessing:
|
|
"""Tests for RQ queue processing."""
|
|
|
|
def test_job_payload_structure(self):
|
|
"""Test transcription job payload structure."""
|
|
job_payload = {
|
|
"transcription_id": "abc123",
|
|
"recording_id": "xyz789",
|
|
"audio_path": "recordings/test/audio.wav",
|
|
"language": "de",
|
|
"model": "large-v3",
|
|
"priority": 0
|
|
}
|
|
|
|
required_fields = ["transcription_id", "recording_id", "audio_path", "language", "model"]
|
|
for field in required_fields:
|
|
assert field in job_payload
|
|
|
|
def test_job_status_transitions(self):
|
|
"""Test valid job status transitions."""
|
|
valid_transitions = {
|
|
"pending": ["queued", "cancelled"],
|
|
"queued": ["processing", "cancelled"],
|
|
"processing": ["completed", "failed"],
|
|
"completed": [],
|
|
"failed": ["pending"], # Can retry
|
|
"cancelled": []
|
|
}
|
|
|
|
# Test a valid workflow
|
|
status = "pending"
|
|
assert "queued" in valid_transitions[status]
|
|
|
|
status = "queued"
|
|
assert "processing" in valid_transitions[status]
|
|
|
|
status = "processing"
|
|
assert "completed" in valid_transitions[status]
|