Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,4 @@ dmypy.json

/.kilocode/
/data/whisper-models/
/.qwen/
2 changes: 1 addition & 1 deletion app/coding/domain/entities.py
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,7 @@ def start_timer_for_task(
def with_submit_test_summary(
self,
task_row_id: int,
summary: dict[str, Any],
summary: dict[str, Any] | None,
*,
source_code: str,
) -> CodingSection:
Expand Down
4 changes: 2 additions & 2 deletions app/coding/domain/value_objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class PlannedCodingTask:


@dataclass(frozen=True, slots=True)
class TestCaseRunResult:
class CaseRunResult:
"""Outcome of executing one public test case via Judge0.

Attributes:
Expand Down Expand Up @@ -77,5 +77,5 @@ class CodingRunResult:
compile_output: str | None
tests_passed: int
tests_total: int
test_results: tuple[TestCaseRunResult, ...]
test_results: tuple[CaseRunResult, ...]
duration_ms: int | None
5 changes: 5 additions & 0 deletions app/coding/services/evaluation_persistence.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,11 +121,16 @@ def persist(
raise CodingSectionNotFoundError(interview_id)
section.ensure_active()

existing_task = section.find_task(task_id, round_num)
updated = section.with_evaluation(
task_id,
round_num,
evaluation.score,
evaluation.feedback,
).with_submit_test_summary(
existing_task.id,
submit_test_summary,
source_code=existing_task.submitted_code or submitted_source_code,
)
follow_up_round: int | None = None
if follow_up_needed:
Expand Down
4 changes: 2 additions & 2 deletions app/coding/services/run_execution.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
CodingRunLimitExceededError,
CodingSectionNotFoundError,
)
from app.coding.domain.value_objects import CodingRunResult, TestCaseRunResult
from app.coding.domain.value_objects import CaseRunResult, CodingRunResult
from app.coding.services.runner import CodingRunnerService
from app.interview.domain.exceptions import InterviewNotFoundError
from app.interview.repositories.uow import InterviewUnitOfWork
Expand Down Expand Up @@ -50,7 +50,7 @@ def _ensure_interview_active(interview_id: str) -> None:
aggregate.ensure_active()


def _serialize_test_result(result: TestCaseRunResult) -> dict[str, Any]:
def _serialize_test_result(result: CaseRunResult) -> dict[str, Any]:
"""Convert a domain test result into an API/persistence payload.

Args:
Expand Down
10 changes: 5 additions & 5 deletions app/coding/services/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
from typing import Any

from app.coding.domain.value_objects import (
CaseRunResult,
CodingRunResult,
RunOutcomeStatus,
TestCaseRunResult,
)
from app.coding.services.harness import build_python_script
from app.coding.services.judge0_client import (
Expand Down Expand Up @@ -109,7 +109,7 @@ async def run_public_tests(
client=judge0,
)

results: list[TestCaseRunResult] = []
results: list[CaseRunResult] = []
total_duration_ms = 0
last_stdout: str | None = None
last_stderr: str | None = None
Expand Down Expand Up @@ -218,7 +218,7 @@ def _case_result_from_submission(
name: str,
expected_stdout: str,
submission: Judge0SubmissionResult,
) -> TestCaseRunResult:
) -> CaseRunResult:
"""Map one Judge0 submission to a public test case result.

Args:
Expand All @@ -234,7 +234,7 @@ def _case_result_from_submission(
submission.status_id == JUDGE0_STATUS_ACCEPTED
and actual_stdout == expected_stdout
)
return TestCaseRunResult(
return CaseRunResult(
name=name,
passed=passed,
expected_stdout=expected_stdout,
Expand Down Expand Up @@ -272,7 +272,7 @@ def _status_from_submission(
return "tests_failed"

@staticmethod
def _aggregate_status(results: list[TestCaseRunResult]) -> RunOutcomeStatus:
def _aggregate_status(results: list[CaseRunResult]) -> RunOutcomeStatus:
"""Derive the aggregate run status from per-test results.

Args:
Expand Down
2 changes: 1 addition & 1 deletion app/interview/services/section_prefetch.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ async def prefetch_section_feedback(

try:
provider = ConfigService.create_provider_from_config()
except ValueError:
except Exception:
logger.warning(
"Skipping %s section prefetch for %s: provider not configured",
section_name,
Expand Down
137 changes: 137 additions & 0 deletions tests/ai/test_faster_whisper_transcriber.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
# Copyright 2026 GrillKit Contributors
# SPDX-License-Identifier: Apache-2.0
"""Tests for FasterWhisperTranscriber."""

from unittest.mock import MagicMock, patch

import numpy as np
import pytest

from app.ai.faster_whisper_transcriber import FasterWhisperTranscriber


class FakeSegment:
"""Fake segment matching faster-whisper segment interface."""

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


class TestFasterWhisperTranscriber:
"""Tests for the faster-whisper adapter."""

@pytest.mark.asyncio
async def test_transcribe_calls_model_transcribe(self):
"""Adapter delegates to WhisperModel.transcribe with correct arguments."""
segment = FakeSegment(" hello world")
model = MagicMock()
model.transcribe.return_value = ([segment], None)

transcriber = FasterWhisperTranscriber(model)
audio = np.zeros(16000, dtype=np.float32)
text = await transcriber.transcribe(audio, "en")

assert text == "hello world"
model.transcribe.assert_called_once()
call_args, call_kwargs = model.transcribe.call_args
assert call_args[0] is audio
assert call_kwargs["language"] == "en"
assert call_kwargs["task"] == "transcribe"

@pytest.mark.asyncio
async def test_uses_vad_filter_true(self):
"""Transcription enables VAD filtering."""
segment = FakeSegment(" test")
model = MagicMock()
model.transcribe.return_value = ([segment], None)

transcriber = FasterWhisperTranscriber(model)
audio = np.zeros(8000, dtype=np.float32)
await transcriber.transcribe(audio, "ru")

call_kwargs = model.transcribe.call_args.kwargs
assert call_kwargs["vad_filter"] is True

@pytest.mark.asyncio
async def test_normalizes_locale(self):
"""Locale is normalized before being passed to the model."""
segment = FakeSegment(" result")
model = MagicMock()
model.transcribe.return_value = ([segment], None)

transcriber = FasterWhisperTranscriber(model)
audio = np.zeros(8000, dtype=np.float32)
text = await transcriber.transcribe(audio, " RU ")

call_kwargs = model.transcribe.call_args.kwargs
assert call_kwargs["language"] == "ru"
assert text == "result"

@pytest.mark.asyncio
async def test_joins_multiple_segments(self):
"""Multiple segments are concatenated and stripped."""
segments = [FakeSegment(" Hello"), FakeSegment(" world")]
model = MagicMock()
model.transcribe.return_value = (segments, None)

transcriber = FasterWhisperTranscriber(model)
audio = np.zeros(8000, dtype=np.float32)
text = await transcriber.transcribe(audio, "en")

assert text == "Hello world"

@pytest.mark.asyncio
async def test_returns_empty_string_when_no_segments(self):
"""Empty segment list produces empty transcript."""
model = MagicMock()
model.transcribe.return_value = ([], None)

transcriber = FasterWhisperTranscriber(model)
audio = np.zeros(8000, dtype=np.float32)
text = await transcriber.transcribe(audio, "en")

assert text == ""

@pytest.mark.asyncio
async def test_runs_in_thread(self):
"""Transcription runs the blocking model in a thread."""
segment = FakeSegment(" async result")
model = MagicMock()
model.transcribe.return_value = ([segment], None)

with patch(
"asyncio.to_thread", side_effect=lambda f, *a, **k: f(*a, **k)
) as mock_to_thread: # noqa: E501
transcriber = FasterWhisperTranscriber(model)
audio = np.zeros(8000, dtype=np.float32)
await transcriber.transcribe(audio, "en")

mock_to_thread.assert_called_once()

@pytest.mark.asyncio
async def test_strips_whitespace(self):
"""Trailing and leading whitespace is stripped from result."""
segments = [FakeSegment(" padded ")]
model = MagicMock()
model.transcribe.return_value = (segments, None)

transcriber = FasterWhisperTranscriber(model)
audio = np.zeros(8000, dtype=np.float32)
text = await transcriber.transcribe(audio, "en")

assert text == "padded"

@pytest.mark.asyncio
async def test_ignores_info_return_value(self):
"""Second return value from transcribe (info) is ignored."""
segment = FakeSegment("text")
info = MagicMock()
info.language = "en"
model = MagicMock()
model.transcribe.return_value = ([segment], info)

transcriber = FasterWhisperTranscriber(model)
audio = np.zeros(8000, dtype=np.float32)
text = await transcriber.transcribe(audio, "en")

assert text == "text"
Loading
Loading