feat(stage11): audio integration — STT for interviews, podcasts, press conferences with timestamped claims

This commit is contained in:
NSCT Agent
2026-08-25 15:31:03 +00:00
parent 3ab875d2bc
commit 10a083aa02
8 changed files with 2832 additions and 65 deletions

637
tests/models/test_audio.py Normal file
View File

@@ -0,0 +1,637 @@
"""Tests für Pydantic v2 Schemas und SQLAlchemy Models der Audio Evidence Extraction (Stage 11)."""
from __future__ import annotations
import pytest
from pydantic import ValidationError
from nsct.models.audio import (
AudioClaimSchema,
AudioRequestSchema,
AudioReportSchema,
AudioSegmentType,
AudioSpeakerType,
AudioTranscriptSegmentSchema,
)
# ---------------------------------------------------------------------------
# Enums
# ---------------------------------------------------------------------------
class TestAudioSegmentTypeEnum:
"""Prüft die Enums AudioSegmentType und AudioSpeakerType."""
def test_audio_segment_type_values(self):
assert AudioSegmentType.INTERVIEW.value == "interview"
assert AudioSegmentType.PODCAST.value == "podcast"
assert AudioSegmentType.PRESSEKONFERENZ.value == "pressekonferenz"
assert AudioSegmentType.REDEN.value == "reden"
assert AudioSegmentType.SONSTIGE.value == "sonstige"
def test_audio_speaker_type_values(self):
assert AudioSpeakerType.SPOECHTENANTWORTER.value == "sprechantenworter"
assert AudioSpeakerType.FRAGENSTELLER.value == "fragensteller"
assert AudioSpeakerType.MODERATOR.value == "moderator"
assert AudioSpeakerType.SONSTIGE.value == "sonstige"
def test_invalid_segment_type(self):
with pytest.raises(ValidationError):
AudioTranscriptSegmentSchema(
text="test",
start_time=0.0,
end_time=1.0,
speaker_id="speaker_1",
segment_type="invalid", # type: ignore
)
def test_invalid_speaker_type(self):
with pytest.raises(ValidationError):
AudioTranscriptSegmentSchema(
text="test",
start_time=0.0,
end_time=1.0,
speaker_id="speaker_1",
speaker_type="invalid", # type: ignore
)
def test_all_segment_types(self):
for st in AudioSegmentType:
schema = AudioTranscriptSegmentSchema(
text="test content",
start_time=0.0,
end_time=1.0,
speaker_id="speaker_1",
)
assert isinstance(schema, AudioTranscriptSegmentSchema)
def test_all_speaker_types(self):
for st in AudioSpeakerType:
schema = AudioTranscriptSegmentSchema(
text="test content",
start_time=0.0,
end_time=1.0,
speaker_id="speaker_1",
)
assert isinstance(schema, AudioTranscriptSegmentSchema)
# ---------------------------------------------------------------------------
# AudioTranscriptSegmentSchema
# ---------------------------------------------------------------------------
class TestAudioTranscriptSegmentSchema:
"""Tests für AudioTranscriptSegmentSchema — Pflichtfelder, Defaults, Frozen."""
@pytest.fixture
def base_kwargs(self):
return {
"text": "Dies ist ein Testtranskript.",
"start_time": 0.0,
"end_time": 5.0,
"speaker_id": "speaker_1",
}
def test_create_valid_segment(self, base_kwargs):
segment = AudioTranscriptSegmentSchema(**base_kwargs)
assert segment.text == "Dies ist ein Testtranskript."
assert segment.start_time == 0.0
assert segment.end_time == 5.0
assert segment.speaker_id == "speaker_1"
assert segment.confidence == 0.5
def test_defaults(self, base_kwargs):
segment = AudioTranscriptSegmentSchema(**base_kwargs)
assert segment.confidence == 0.5
def test_frozen(self, base_kwargs):
segment = AudioTranscriptSegmentSchema(**base_kwargs)
with pytest.raises(Exception):
segment.text = "modified"
def test_missing_text(self, base_kwargs):
with pytest.raises(ValidationError):
AudioTranscriptSegmentSchema(
start_time=base_kwargs["start_time"],
end_time=base_kwargs["end_time"],
speaker_id=base_kwargs["speaker_id"],
)
def test_empty_text(self, base_kwargs):
with pytest.raises(ValidationError, match="text darf nicht nur aus Whitespaces bestehen"):
AudioTranscriptSegmentSchema(
text=" ",
start_time=base_kwargs["start_time"],
end_time=base_kwargs["end_time"],
speaker_id=base_kwargs["speaker_id"],
)
def test_missing_speaker_id(self, base_kwargs):
with pytest.raises(ValidationError):
AudioTranscriptSegmentSchema(
text=base_kwargs["text"],
start_time=base_kwargs["start_time"],
end_time=base_kwargs["end_time"],
)
def test_empty_speaker_id(self, base_kwargs):
with pytest.raises(ValidationError, match="speaker_id darf nicht leer sein"):
AudioTranscriptSegmentSchema(
text=base_kwargs["text"],
start_time=base_kwargs["start_time"],
end_time=base_kwargs["end_time"],
speaker_id=" ",
)
def test_missing_start_time(self, base_kwargs):
with pytest.raises(ValidationError):
AudioTranscriptSegmentSchema(
text=base_kwargs["text"],
end_time=base_kwargs["end_time"],
speaker_id=base_kwargs["speaker_id"],
)
def test_missing_end_time(self, base_kwargs):
with pytest.raises(ValidationError):
AudioTranscriptSegmentSchema(
text=base_kwargs["text"],
start_time=base_kwargs["start_time"],
speaker_id=base_kwargs["speaker_id"],
)
def test_negative_start_time(self, base_kwargs):
with pytest.raises(ValidationError):
AudioTranscriptSegmentSchema(
text=base_kwargs["text"],
start_time=-1.0,
end_time=base_kwargs["end_time"],
speaker_id=base_kwargs["speaker_id"],
)
def test_end_before_start(self):
with pytest.raises(ValidationError, match="end_time muss nach start_time liegen"):
AudioTranscriptSegmentSchema(
text="test",
start_time=10.0,
end_time=5.0,
speaker_id="speaker_1",
)
def test_confidence_bounds(self, base_kwargs):
schema_low = AudioTranscriptSegmentSchema(**base_kwargs, confidence=0.0)
assert schema_low.confidence == 0.0
schema_high = AudioTranscriptSegmentSchema(**base_kwargs, confidence=1.0)
assert schema_high.confidence == 1.0
def test_confidence_out_of_bounds_low(self, base_kwargs):
with pytest.raises(ValidationError):
AudioTranscriptSegmentSchema(**base_kwargs, confidence=-0.1)
def test_confidence_out_of_bounds_high(self, base_kwargs):
with pytest.raises(ValidationError):
AudioTranscriptSegmentSchema(**base_kwargs, confidence=1.1)
def test_high_confidence(self, base_kwargs):
segment = AudioTranscriptSegmentSchema(**base_kwargs, confidence=0.95)
assert segment.confidence == 0.95
# ---------------------------------------------------------------------------
# AudioClaimSchema
# ---------------------------------------------------------------------------
class TestAudioClaimSchema:
"""Tests für AudioClaimSchema — Claim mit Provenance und Timestamp."""
@pytest.fixture
def base_kwargs(self):
return {
"claim_text": "Die Regierung hat die Ausgaben erhöht.",
"timestamp_start": 120.0,
"timestamp_end": 125.0,
"speaker_id": "minister_1",
"source_url": "https://example.com/interview.mp3",
}
def test_create_valid_claim(self, base_kwargs):
claim = AudioClaimSchema(**base_kwargs)
assert claim.claim_text == "Die Regierung hat die Ausgaben erhöht."
assert claim.timestamp_start == 120.0
assert claim.timestamp_end == 125.0
assert claim.speaker_id == "minister_1"
assert claim.source_url == "https://example.com/interview.mp3"
assert claim.confidence == 0.5
assert claim.evidence_span is None
assert claim.claim_type is None
def test_defaults(self, base_kwargs):
claim = AudioClaimSchema(**base_kwargs)
assert claim.confidence == 0.5
assert claim.evidence_span is None
assert claim.claim_type is None
def test_frozen(self, base_kwargs):
claim = AudioClaimSchema(**base_kwargs)
with pytest.raises(Exception):
claim.claim_text = "modified"
def test_with_evidence_span(self, base_kwargs):
claim = AudioClaimSchema(
**base_kwargs,
evidence_span="Laut dem Haushaltsgesetz 2024 wurden die Ausgaben um 15% erhöht.",
)
assert claim.evidence_span == "Laut dem Haushaltsgesetz 2024 wurden die Ausgaben um 15% erhöht."
def test_with_claim_type(self, base_kwargs):
claim = AudioClaimSchema(**base_kwargs, claim_type="factual")
assert claim.claim_type == "factual"
def test_empty_claim_text(self, base_kwargs):
with pytest.raises(ValidationError, match="claim_text darf nicht nur aus Whitespaces bestehen"):
AudioClaimSchema(
claim_text=" ",
timestamp_start=base_kwargs["timestamp_start"],
timestamp_end=base_kwargs["timestamp_end"],
speaker_id=base_kwargs["speaker_id"],
source_url=base_kwargs["source_url"],
)
def test_missing_claim_text(self, base_kwargs):
with pytest.raises(ValidationError):
AudioClaimSchema(
timestamp_start=base_kwargs["timestamp_start"],
timestamp_end=base_kwargs["timestamp_end"],
speaker_id=base_kwargs["speaker_id"],
source_url=base_kwargs["source_url"],
)
def test_missing_speaker_id(self, base_kwargs):
with pytest.raises(ValidationError):
AudioClaimSchema(
claim_text=base_kwargs["claim_text"],
timestamp_start=base_kwargs["timestamp_start"],
timestamp_end=base_kwargs["timestamp_end"],
source_url=base_kwargs["source_url"],
)
def test_empty_speaker_id(self, base_kwargs):
with pytest.raises(ValidationError, match="speaker_id darf nicht leer sein"):
AudioClaimSchema(
claim_text=base_kwargs["claim_text"],
timestamp_start=base_kwargs["timestamp_start"],
timestamp_end=base_kwargs["timestamp_end"],
speaker_id=" ",
source_url=base_kwargs["source_url"],
)
def test_missing_source_url(self, base_kwargs):
with pytest.raises(ValidationError):
AudioClaimSchema(
claim_text=base_kwargs["claim_text"],
timestamp_start=base_kwargs["timestamp_start"],
timestamp_end=base_kwargs["timestamp_end"],
speaker_id=base_kwargs["speaker_id"],
)
def test_empty_source_url(self, base_kwargs):
with pytest.raises(ValidationError, match="source_url darf nicht leer sein"):
AudioClaimSchema(
claim_text=base_kwargs["claim_text"],
timestamp_start=base_kwargs["timestamp_start"],
timestamp_end=base_kwargs["timestamp_end"],
speaker_id=base_kwargs["speaker_id"],
source_url=" ",
)
def test_timestamp_end_before_start(self):
with pytest.raises(ValidationError, match="timestamp_end muss nach timestamp_start liegen"):
AudioClaimSchema(
claim_text="test",
timestamp_start=10.0,
timestamp_end=5.0,
speaker_id="speaker_1",
source_url="https://example.com",
)
def test_timestamp_bounds(self):
claim = AudioClaimSchema(
claim_text="test",
timestamp_start=0.0,
timestamp_end=0.0,
speaker_id="speaker_1",
source_url="https://example.com",
)
assert claim.timestamp_start == 0.0
assert claim.timestamp_end == 0.0
def test_negative_timestamp_start(self):
with pytest.raises(ValidationError):
AudioClaimSchema(
claim_text="test",
timestamp_start=-1.0,
timestamp_end=10.0,
speaker_id="speaker_1",
source_url="https://example.com",
)
def test_confidence_bounds(self, base_kwargs):
schema_low = AudioClaimSchema(**base_kwargs, confidence=0.0)
assert schema_low.confidence == 0.0
schema_high = AudioClaimSchema(**base_kwargs, confidence=1.0)
assert schema_high.confidence == 1.0
# ---------------------------------------------------------------------------
# AudioReportSchema
# ---------------------------------------------------------------------------
class TestAudioReportSchema:
"""Tests für AudioReportSchema — Zusammenfassung der Audio-Analyse."""
@pytest.fixture
def base_kwargs(self):
return {
"duration_seconds": 3600.0,
"language": "de",
}
def test_create_valid_report(self, base_kwargs):
report = AudioReportSchema(**base_kwargs)
assert report.duration_seconds == 3600.0
assert report.language == "de"
assert report.transcript_segments == []
assert report.claims == []
assert report.source_url is None
assert report.research_run_id is None
assert report.metadata == {}
def test_defaults(self, base_kwargs):
report = AudioReportSchema(**base_kwargs)
assert report.transcript_segments == []
assert report.claims == []
assert report.source_url is None
assert report.research_run_id is None
assert report.metadata == {}
def test_frozen(self, base_kwargs):
report = AudioReportSchema(**base_kwargs)
with pytest.raises(Exception):
report.duration_seconds = 7200.0
def test_with_transcript_segments(self, base_kwargs):
segment = AudioTranscriptSegmentSchema(
text="Guten Tag, ich möchte Sie etwas fragen.",
start_time=0.0,
end_time=3.0,
speaker_id="interviewer",
)
report = AudioReportSchema(
**base_kwargs,
transcript_segments=[segment],
)
assert len(report.transcript_segments) == 1
assert report.transcript_segments[0].text == "Guten Tag, ich möchte Sie etwas fragen."
def test_with_claims(self, base_kwargs):
claim = AudioClaimSchema(
claim_text="Die Regierung hat die Ausgaben erhöht.",
timestamp_start=120.0,
timestamp_end=125.0,
speaker_id="minister_1",
source_url="https://example.com/interview.mp3",
)
report = AudioReportSchema(
**base_kwargs,
claims=[claim],
)
assert len(report.claims) == 1
assert report.claims[0].claim_text == "Die Regierung hat die Ausgaben erhöht."
def test_with_source_url(self, base_kwargs):
report = AudioReportSchema(
**base_kwargs,
source_url="https://example.com/podcast.mp3",
)
assert report.source_url == "https://example.com/podcast.mp3"
def test_with_research_run_id(self, base_kwargs):
report = AudioReportSchema(
**base_kwargs,
research_run_id="run-uuid-001",
)
assert report.research_run_id == "run-uuid-001"
def test_empty_source_url(self, base_kwargs):
with pytest.raises(ValidationError, match="source_url darf nicht leer sein"):
AudioReportSchema(
**base_kwargs,
source_url=" ",
)
def test_empty_language(self, base_kwargs):
with pytest.raises(ValidationError, match="language darf nicht leer sein"):
AudioReportSchema(
**base_kwargs,
language=" ",
)
def test_language_normalized_to_lower(self, base_kwargs):
report = AudioReportSchema(
**base_kwargs,
language="DE",
)
assert report.language == "de"
def test_negative_duration(self):
with pytest.raises(ValidationError):
AudioReportSchema(
duration_seconds=-1.0,
language="de",
)
def test_zero_duration(self):
report = AudioReportSchema(
duration_seconds=0.0,
language="de",
)
assert report.duration_seconds == 0.0
def test_metadata_dict(self, base_kwargs):
report = AudioReportSchema(
**base_kwargs,
metadata={"model": "whisper-3", "processing_time": 12.5},
)
assert report.metadata["model"] == "whisper-3"
assert report.metadata["processing_time"] == 12.5
def test_full_report(self, base_kwargs):
segment = AudioTranscriptSegmentSchema(
text="Interview: Was denken Sie über die Wirtschaftslage?",
start_time=0.0,
end_time=5.0,
speaker_id="interviewer",
)
claim = AudioClaimSchema(
claim_text="Die Wirtschaftslage ist stabil.",
timestamp_start=5.0,
timestamp_end=10.0,
speaker_id="interviewee",
source_url="https://example.com/interview.mp3",
)
report = AudioReportSchema(
**base_kwargs,
transcript_segments=[segment],
claims=[claim],
source_url="https://example.com/interview.mp3",
research_run_id="run-uuid-001",
metadata={"model": "whisper-3"},
)
assert len(report.transcript_segments) == 1
assert len(report.claims) == 1
assert report.source_url == "https://example.com/interview.mp3"
assert report.research_run_id == "run-uuid-001"
def test_language_short_code(self, base_kwargs):
"""Kurze ISO 639-1 Codes sind erlaubt (min_length=2)."""
report = AudioReportSchema(**base_kwargs, language="en")
assert report.language == "en"
def test_language_long_code(self, base_kwargs):
"""Längere Codes bis max_length=5 sind erlaubt."""
report = AudioReportSchema(**base_kwargs, language="deu")
assert report.language == "deu"
# ---------------------------------------------------------------------------
# AudioRequestSchema
# ---------------------------------------------------------------------------
class TestAudioRequestSchema:
"""Tests für AudioRequestSchema — API-Request."""
@pytest.fixture
def base_kwargs(self):
return {
"research_run_id": "run-uuid-001",
"audio_file_url": "https://example.com/interview.mp3",
"segment_type": AudioSegmentType.INTERVIEW,
}
def test_create_valid_request(self, base_kwargs):
request = AudioRequestSchema(**base_kwargs)
assert request.research_run_id == "run-uuid-001"
assert request.audio_file_url == "https://example.com/interview.mp3"
assert request.audio_bytes_b64 is None
assert request.segment_type == AudioSegmentType.INTERVIEW
assert request.source_id is None
def test_defaults(self, base_kwargs):
request = AudioRequestSchema(**base_kwargs)
assert request.audio_bytes_b64 is None
assert request.segment_type == AudioSegmentType.INTERVIEW
assert request.source_id is None
def test_frozen(self, base_kwargs):
request = AudioRequestSchema(**base_kwargs)
with pytest.raises(Exception):
request.research_run_id = "new-id"
def test_with_audio_bytes_b64(self, base_kwargs):
request = AudioRequestSchema(
**base_kwargs,
audio_file_url=None,
audio_bytes_b64="base64encodedaudiodata==",
)
assert request.audio_file_url is None
assert request.audio_bytes_b64 == "base64encodedaudiodata=="
def test_with_source_id(self, base_kwargs):
request = AudioRequestSchema(
**base_kwargs,
source_id="source-uuid-001",
)
assert request.source_id == "source-uuid-001"
def test_missing_research_run_id(self):
with pytest.raises(ValidationError):
AudioRequestSchema(
audio_file_url="https://example.com/interview.mp3",
segment_type=AudioSegmentType.INTERVIEW,
)
def test_empty_research_run_id(self):
with pytest.raises(ValidationError):
AudioRequestSchema(
research_run_id=" ",
audio_file_url="https://example.com/interview.mp3",
segment_type=AudioSegmentType.INTERVIEW,
)
def test_empty_audio_file_url(self, base_kwargs):
with pytest.raises(ValidationError, match="audio_file_url darf nicht leer sein"):
AudioRequestSchema(
**base_kwargs,
audio_file_url=" ",
)
def test_empty_audio_bytes_b64(self, base_kwargs):
with pytest.raises(ValidationError, match="audio_bytes_b64 darf nicht leer sein"):
AudioRequestSchema(
**base_kwargs,
audio_file_url=None,
audio_bytes_b64=" ",
)
def test_podcast_segment_type(self, base_kwargs):
request = AudioRequestSchema(
**base_kwargs,
segment_type=AudioSegmentType.PODCAST,
)
assert request.segment_type == AudioSegmentType.PODCAST
def test_press_conference_segment_type(self, base_kwargs):
request = AudioRequestSchema(
**base_kwargs,
segment_type=AudioSegmentType.PRESSEKONFERENZ,
)
assert request.segment_type == AudioSegmentType.PRESSEKONFERENZ
def test_speeches_segment_type(self, base_kwargs):
request = AudioRequestSchema(
**base_kwargs,
segment_type=AudioSegmentType.REDEN,
)
assert request.segment_type == AudioSegmentType.REDEN
def test_other_segment_type(self, base_kwargs):
request = AudioRequestSchema(
**base_kwargs,
segment_type=AudioSegmentType.SONSTIGE,
)
assert request.segment_type == AudioSegmentType.SONSTIGE
def test_segment_type_override(self):
for st in AudioSegmentType:
request = AudioRequestSchema(
research_run_id="run-uuid-001",
audio_file_url="https://example.com/audio.mp3",
segment_type=st,
)
assert request.segment_type == st
def test_no_audio_url_or_bytes(self, base_kwargs):
"""Erlaubt: kein audio_file_url UND kein audio_bytes_b64 (beide optional)."""
request = AudioRequestSchema(
**base_kwargs,
audio_file_url=None,
audio_bytes_b64=None,
)
assert request.audio_file_url is None
assert request.audio_bytes_b64 is None

View File

@@ -84,7 +84,7 @@ class TestVisionCaptureSchema:
"""Tests für VisionCaptureSchema — Pflichtfelder, Defaults, Frozen."""
@pytest.fixture
def valid_kwargs(self):
def base_kwargs(self):
return {
"capture_type": VisionCaptureType.DIAGRAM,
"image_data_url": "data:image/png;base64,abc123",
@@ -93,8 +93,8 @@ class TestVisionCaptureSchema:
"source_url": "https://example.com/chart.png",
}
def test_create_valid_schema(self, valid_kwargs):
schema = VisionCaptureSchema(**valid_kwargs)
def test_create_valid_schema(self, base_kwargs):
schema = VisionCaptureSchema(**base_kwargs)
assert schema.capture_type == VisionCaptureType.DIAGRAM
assert schema.extracted_text == "This is a chart showing revenue growth."
assert schema.source_id == "source-uuid-001"
@@ -104,54 +104,80 @@ class TestVisionCaptureSchema:
assert schema.evidence_level == EvidenceLevel.MEDIUM
assert schema.metadata == {}
def test_defaults(self, valid_kwargs):
schema = VisionCaptureSchema(**valid_kwargs)
def test_defaults(self, base_kwargs):
schema = VisionCaptureSchema(**base_kwargs)
assert schema.entities == []
assert schema.confidence == 0.5
assert schema.confidence_label == VisionConfidence.MEDIUM
assert schema.evidence_level == EvidenceLevel.MEDIUM
assert schema.metadata == {}
def test_frozen(self, valid_kwargs):
schema = VisionCaptureSchema(**valid_kwargs)
def test_frozen(self, base_kwargs):
schema = VisionCaptureSchema(**base_kwargs)
with pytest.raises(Exception):
schema.capture_type = VisionCaptureType.CHART
def test_missing_required_field(self, valid_kwargs):
kwargs = {**valid_kwargs}
del kwargs["extracted_text"]
def test_missing_extracted_text(self, base_kwargs):
with pytest.raises(ValidationError):
VisionCaptureSchema(**kwargs)
VisionCaptureSchema(
capture_type=base_kwargs["capture_type"],
image_data_url=base_kwargs["image_data_url"],
source_id=base_kwargs["source_id"],
source_url=base_kwargs["source_url"],
)
def test_empty_extracted_text(self, valid_kwargs):
kwargs = {**valid_kwargs, "extracted_text": " "}
def test_empty_extracted_text(self, base_kwargs):
with pytest.raises(ValidationError, match="extracted_text darf nicht nur aus Whitespaces bestehen"):
VisionCaptureSchema(**kwargs)
VisionCaptureSchema(
capture_type=base_kwargs["capture_type"],
image_data_url=base_kwargs["image_data_url"],
extracted_text=" ",
source_id=base_kwargs["source_id"],
source_url=base_kwargs["source_url"],
)
def test_empty_source_url(self, valid_kwargs):
kwargs = {**valid_kwargs, "source_url": " "}
def test_empty_source_url(self, base_kwargs):
with pytest.raises(ValidationError, match="source_url darf nicht leer sein"):
VisionCaptureSchema(**kwargs)
VisionCaptureSchema(
capture_type=base_kwargs["capture_type"],
image_data_url=base_kwargs["image_data_url"],
extracted_text=base_kwargs["extracted_text"],
source_id=base_kwargs["source_id"],
source_url=" ",
)
def test_empty_image_data_url(self, valid_kwargs):
kwargs = {**valid_kwargs, "image_data_url": " "}
def test_empty_image_data_url(self, base_kwargs):
with pytest.raises(ValidationError, match="image_data_url darf nicht leer sein"):
VisionCaptureSchema(**kwargs)
VisionCaptureSchema(
capture_type=base_kwargs["capture_type"],
image_data_url=" ",
extracted_text=base_kwargs["extracted_text"],
source_id=base_kwargs["source_id"],
source_url=base_kwargs["source_url"],
)
def test_confidence_bounds(self, valid_kwargs):
schema_low = VisionCaptureSchema(**valid_kwargs, confidence=0.0)
def test_confidence_bounds(self, base_kwargs):
schema_low = VisionCaptureSchema(
**base_kwargs, confidence=0.0
)
assert schema_low.confidence == 0.0
schema_high = VisionCaptureSchema(**valid_kwargs, confidence=1.0)
schema_high = VisionCaptureSchema(
**base_kwargs, confidence=1.0
)
assert schema_high.confidence == 1.0
def test_confidence_out_of_bounds_low(self, valid_kwargs):
def test_confidence_out_of_bounds_low(self, base_kwargs):
with pytest.raises(ValidationError):
VisionCaptureSchema(**valid_kwargs, confidence=-0.1)
VisionCaptureSchema(
**base_kwargs, confidence=-0.1
)
def test_confidence_out_of_bounds_high(self, valid_kwargs):
def test_confidence_out_of_bounds_high(self, base_kwargs):
with pytest.raises(ValidationError):
VisionCaptureSchema(**valid_kwargs, confidence=1.1)
VisionCaptureSchema(
**base_kwargs, confidence=1.1
)
def test_all_capture_types(self):
for ct in VisionCaptureType:
@@ -188,9 +214,9 @@ class TestVisionCaptureSchema:
)
assert schema.confidence_label == label
def test_entities_list(self, valid_kwargs):
def test_entities_list(self, base_kwargs):
schema = VisionCaptureSchema(
**valid_kwargs,
**base_kwargs,
entities=[
{"type": "NUMBER", "value": "42", "confidence": 0.9},
{"type": "DATE", "value": "2024-01-15", "confidence": 0.8},
@@ -200,9 +226,9 @@ class TestVisionCaptureSchema:
assert schema.entities[0]["type"] == "NUMBER"
assert schema.entities[1]["value"] == "2024-01-15"
def test_metadata_dict(self, valid_kwargs):
def test_metadata_dict(self, base_kwargs):
schema = VisionCaptureSchema(
**valid_kwargs,
**base_kwargs,
metadata={"model": "qwen2.5-vl-3b", "processing_time": 2.3},
)
assert schema.metadata["model"] == "qwen2.5-vl-3b"
@@ -218,37 +244,40 @@ class TestVisionReportSchema:
"""Tests für VisionReportSchema — Zusammenfassung aller visuellen Evidenzen."""
@pytest.fixture
def valid_kwargs(self):
def base_kwargs(self):
return {
"research_run_id": "run-uuid-001",
"total_captures": 3,
}
def test_create_valid_report(self, valid_kwargs):
report = VisionReportSchema(**valid_kwargs)
def test_create_valid_report(self, base_kwargs):
report = VisionReportSchema(**base_kwargs)
assert report.research_run_id == "run-uuid-001"
assert report.total_captures == 3
assert report.captures == []
assert report.entity_summary == {}
assert report.summary_text == ""
def test_frozen(self, valid_kwargs):
report = VisionReportSchema(**valid_kwargs)
def test_frozen(self, base_kwargs):
report = VisionReportSchema(**base_kwargs)
with pytest.raises(Exception):
report.research_run_id = "new-id"
def test_empty_research_run_id(self, valid_kwargs):
def test_empty_research_run_id(self):
with pytest.raises(ValidationError, match="research_run_id darf nicht leer sein"):
VisionReportSchema(
**valid_kwargs,
research_run_id=" ",
total_captures=3,
)
def test_negative_total_captures(self, valid_kwargs):
def test_negative_total_captures(self):
with pytest.raises(ValidationError):
VisionReportSchema(**valid_kwargs, total_captures=-1)
VisionReportSchema(
research_run_id="run-uuid-001",
total_captures=-1,
)
def test_with_captures(self, valid_kwargs):
def test_with_captures(self):
capture = VisionCaptureSchema(
capture_type=VisionCaptureType.CHART,
image_data_url="data:image/png;base64,xyz",
@@ -257,17 +286,18 @@ class TestVisionReportSchema:
source_url="https://example.com",
)
report = VisionReportSchema(
**valid_kwargs,
research_run_id="run-uuid-001",
total_captures=1,
captures=[capture],
)
assert len(report.captures) == 1
assert report.captures[0].capture_type == VisionCaptureType.CHART
def test_political_summary_rejected(self, valid_kwargs):
def test_political_summary_rejected(self):
with pytest.raises(ValidationError, match="politische Empfehlung"):
VisionReportSchema(
**valid_kwargs,
research_run_id="run-uuid-001",
total_captures=0,
summary_text="Die Regierung sollte handeln.",
)
@@ -281,7 +311,7 @@ class TestVisionRequestSchema:
"""Tests für VisionRequestSchema — API-Request."""
@pytest.fixture
def valid_kwargs(self):
def base_kwargs(self):
return {
"research_run_id": "run-uuid-001",
"source_id": "source-uuid-001",
@@ -289,48 +319,74 @@ class TestVisionRequestSchema:
"image_data": "data:image/png;base64,iVBORw0KGgoAAA==",
}
def test_create_valid_request(self, valid_kwargs):
request = VisionRequestSchema(**valid_kwargs)
def test_create_valid_request(self, base_kwargs):
request = VisionRequestSchema(**base_kwargs)
assert request.research_run_id == "run-uuid-001"
assert request.source_id == "source-uuid-001"
assert request.source_url == "https://example.com/image.png"
assert request.capture_type == VisionCaptureType.RAW_IMAGE
assert request.prompt is None
def test_frozen(self, valid_kwargs):
request = VisionRequestSchema(**valid_kwargs)
def test_frozen(self, base_kwargs):
request = VisionRequestSchema(**base_kwargs)
with pytest.raises(Exception):
request.research_run_id = "new-id"
def test_defaults(self, valid_kwargs):
request = VisionRequestSchema(**valid_kwargs)
def test_defaults(self, base_kwargs):
request = VisionRequestSchema(**base_kwargs)
assert request.capture_type == VisionCaptureType.RAW_IMAGE
assert request.prompt is None
def test_with_prompt(self, valid_kwargs):
def test_with_prompt(self, base_kwargs):
request = VisionRequestSchema(
**valid_kwargs,
**base_kwargs,
prompt="Extrahiere alle Zahlen und Daten aus dem Diagramm.",
)
assert request.prompt == "Extrahiere alle Zahlen und Daten aus dem Diagramm."
def test_empty_image_data(self, valid_kwargs):
def test_empty_image_data(self):
with pytest.raises(ValidationError, match="image_data darf nicht leer sein"):
VisionRequestSchema(**valid_kwargs, image_data=" ")
VisionRequestSchema(
research_run_id="run-uuid-001",
source_id="source-uuid-001",
source_url="https://example.com/image.png",
image_data=" ",
)
def test_empty_source_url(self, valid_kwargs):
def test_empty_source_url(self):
with pytest.raises(ValidationError, match="source_url darf nicht leer sein"):
VisionRequestSchema(**valid_kwargs, source_url=" ")
VisionRequestSchema(
research_run_id="run-uuid-001",
source_id="source-uuid-001",
source_url=" ",
image_data="data:image/png;base64,abc",
)
def test_empty_research_run_id(self, valid_kwargs):
def test_empty_research_run_id(self):
with pytest.raises(ValidationError):
VisionRequestSchema(**valid_kwargs, research_run_id=" ")
VisionRequestSchema(
research_run_id=" ",
source_id="source-uuid-001",
source_url="https://example.com/image.png",
image_data="data:image/png;base64,abc",
)
def test_empty_source_id(self, valid_kwargs):
def test_empty_source_id(self):
with pytest.raises(ValidationError):
VisionRequestSchema(**valid_kwargs, source_id=" ")
VisionRequestSchema(
research_run_id="run-uuid-001",
source_id=" ",
source_url="https://example.com/image.png",
image_data="data:image/png;base64,abc",
)
def test_capture_type_override(self, valid_kwargs):
def test_capture_type_override(self):
for ct in VisionCaptureType:
request = VisionRequestSchema(**valid_kwargs, capture_type=ct)
request = VisionRequestSchema(
research_run_id="run-uuid-001",
source_id="source-uuid-001",
source_url="https://example.com/image.png",
image_data="data:image/png;base64,abc",
capture_type=ct,
)
assert request.capture_type == ct