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 @@ -84,3 +84,4 @@ dmypy.json
/.kilocode/
/data/whisper-models/
/.qwen/
docs/
55 changes: 0 additions & 55 deletions .qwen/settings.json

This file was deleted.

54 changes: 0 additions & 54 deletions .qwen/settings.json.orig

This file was deleted.

4 changes: 2 additions & 2 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -628,7 +628,7 @@ Run attempts are rate-limited per task (`CODING_MAX_RUNS_PER_TASK`, default 20).

## Data Flow: Dictation WebSocket

Separate from answer/evaluation WS. Requires active interview and loaded transcriber (`app.state.speech_transcriber`).
Separate from answer/evaluation WS. Requires active interview and loaded transcriber (from the in-process Whisper runtime).

```
Client → WS connect /interview/{id}/dictation
Expand All @@ -655,7 +655,7 @@ User → GET /config (speech_model_size, locale)
User → POST /speech/model/download
→ WhisperModelService.start_download(size from config)
→ Hugging Face snapshot → data/whisper-models/<size>/
→ WhisperRuntime.load_size(size) → app.state.speech_transcriber
→ WhisperRuntime.load_size(size) → transcriber in the in-process runtime
User → GET /speech/model/status (HTMX poll while downloading)
```

Expand Down
8 changes: 8 additions & 0 deletions app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@
# SPDX-License-Identifier: Apache-2.0
"""GrillKit application package."""

from importlib.metadata import PackageNotFoundError, version

from app.shared.infrastructure.hf_hub_runtime import configure_hf_hub

try:
__version__ = version("grillkit")
except PackageNotFoundError:
__version__ = "2026.8.9"


configure_hf_hub()
15 changes: 14 additions & 1 deletion app/coding/api/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
# Copyright 2026 GrillKit Contributors
# SPDX-License-Identifier: Apache-2.0
"""Coding API transport layer."""
"""Coding API transport layer.

This package aggregates all coding sub-routers into a single ``router``
that the application factory mounts via :func:`app.include_router`.
"""

from fastapi import APIRouter

from app.coding.api import routes

router = APIRouter()
router.include_router(routes.router)

__all__ = ["router"]
19 changes: 18 additions & 1 deletion app/interview/api/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,20 @@
# Copyright 2026 GrillKit Contributors
# SPDX-License-Identifier: Apache-2.0
"""Interview feature HTTP and WebSocket endpoints."""
"""Interview feature HTTP and WebSocket endpoints.

This package aggregates all interview sub-routers into a single ``router``
that the application factory mounts via :func:`app.include_router`.
"""

from fastapi import APIRouter

from app.interview.api import dashboard, known_questions, results, routes, setup

router = APIRouter()
router.include_router(dashboard.router)
router.include_router(setup.router)
router.include_router(known_questions.router)
router.include_router(routes.router)
router.include_router(results.router)

__all__ = ["router"]
18 changes: 8 additions & 10 deletions app/interview/api/deps.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from collections.abc import AsyncIterator
from typing import Annotated

from fastapi import Depends, HTTPException, Request
from fastapi import Depends, HTTPException

from app.ai.base import AIProvider
from app.ai.speech_transcriber import SpeechTranscriber
Expand All @@ -21,7 +21,7 @@
from app.interview.queries.session_page import ActiveSessionPage
from app.interview.use_cases.complete_session import CompleteInterviewSession
from app.interview.use_cases.create_session import CreateInterviewSession
from app.platform.api.deps import ConfigServiceDep
from app.platform.api.deps import SpeechRuntimeDep
from app.shared.application.uow_deps import UoWAutoCommitDep, UoWDep
from app.shared.infrastructure.gateways.ai_context import ai_provider_from_config
from app.speech.domain.transcriber_resolver import (
Expand Down Expand Up @@ -216,26 +216,24 @@ def get_coding_review_service(uow: UoWDep) -> CodingReviewService:


async def get_speech_transcriber(
request: Request,
config_service: ConfigServiceDep,
coordinator: SpeechRuntimeDep,
) -> SpeechTranscriber:
"""Resolve a loaded Whisper transcriber from application state.
"""Resolve a loaded speech transcriber through the speech runtime.

Args:
request: FastAPI request with ASGI app state.
config_service: Provider configuration service.
coordinator: App-lifetime speech runtime coordinator.

Returns:
Loaded speech transcriber.

Raises:
HTTPException: When Whisper is not installed or loaded.
HTTPException: When a speech model is not installed or loaded.
"""
transcriber = await resolve_speech_transcriber(request.app, config_service)
transcriber = await resolve_speech_transcriber(coordinator)
if transcriber is None:
raise HTTPException(
status_code=503,
detail=speech_transcriber_unavailable_message(),
detail=speech_transcriber_unavailable_message(coordinator),
)
return transcriber

Expand Down
17 changes: 11 additions & 6 deletions app/interview/api/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@
from app.interview.api.deps import ActiveSessionPageDep
from app.interview.api.errors import http_exception_from_domain_error
from app.interview.domain.exceptions import InterviewDomainError
from app.platform.api.deps import ConfigServiceDep
from app.platform.domain.speech_runtime import SpeechRuntimeCoordinator
from app.platform.api.deps import ConfigServiceDep, SpeechRuntimeDep
from app.question_voice.use_cases.generate_question_audio import GenerateQuestionAudio
from app.shared.infrastructure.gateways.tts_exceptions import (
QuestionVoiceDisabledError,
Expand Down Expand Up @@ -46,6 +45,7 @@ async def interview_page(
config_service: ConfigServiceDep,
whisper_model_service: WhisperModelServiceDep,
page_service: ActiveSessionPageDep,
coordinator: SpeechRuntimeDep,
) -> Response:
"""View an interview session.

Expand All @@ -57,6 +57,8 @@ async def interview_page(
interview_id: The session UUID.
config_service: Provider configuration service.
whisper_model_service: Whisper model download service.
page_service: Active session page service.
coordinator: App-lifetime speech runtime coordinator.

Returns:
HTML response with interview view, or redirect if not found.
Expand All @@ -77,8 +79,7 @@ async def interview_page(
status_code=303,
)

await SpeechRuntimeCoordinator.preload_whisper_for_active_interview(
request.app,
await coordinator.preload_whisper_for_active_interview(
config,
interview_active=page.interview_active,
)
Expand All @@ -92,22 +93,26 @@ async def interview_page(
@router.get("/{interview_id}/question-audio")
async def question_audio(
interview_id: str,
coordinator: SpeechRuntimeDep,
answer_id: int | None = None,
) -> FileResponse:
"""Stream WAV audio for the current or specified unanswered question.

Args:
interview_id: Interview session UUID.
coordinator: App-lifetime speech runtime coordinator.
answer_id: Optional answer row id; defaults to the first unanswered question.

Returns:
``audio/wav`` file from cache or Piper synthesis.
``audio/wav`` file from cache or TTS synthesis.

Raises:
HTTPException: When voice is disabled, the session is invalid, or TTS fails.
"""
try:
path = await GenerateQuestionAudio.execute(interview_id, answer_id)
path = await GenerateQuestionAudio.execute(
coordinator.tts, interview_id, answer_id
)
except QuestionVoiceDisabledError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
except QuestionVoiceSynthesisError as exc:
Expand Down
Loading
Loading