G7 증명과 G8 clean-head 승격 준비
This commit is contained in:
parent
94c681d450
commit
5221f79e3f
52 changed files with 6876 additions and 506 deletions
|
|
@ -153,7 +153,7 @@ class Settings(BaseSettings):
|
|||
validation_alias="VIGNETTE_LOCAL_WHISPER_STT_URL",
|
||||
)
|
||||
local_whisper_stt_model: str = Field(
|
||||
default="large-v3",
|
||||
default="small",
|
||||
validation_alias="VIGNETTE_LOCAL_WHISPER_STT_MODEL",
|
||||
)
|
||||
local_whisper_stt_language: str = Field(
|
||||
|
|
|
|||
|
|
@ -206,9 +206,13 @@ async def voice_health() -> JSONResponse:
|
|||
"stt_model": voice_service.stt_model(),
|
||||
"stt_batch_fallback_available": voice_service.batch_stt_available(),
|
||||
"tts_model": (
|
||||
voice_svc.HIGGS_TTS_MODEL
|
||||
if tts_provider == "higgs"
|
||||
else voice_svc.TTS_MODEL
|
||||
voice_svc.MELOTTS_TTS_MODEL
|
||||
if tts_provider == "melotts"
|
||||
else (
|
||||
voice_svc.HIGGS_TTS_MODEL
|
||||
if tts_provider == "higgs"
|
||||
else voice_svc.TTS_MODEL
|
||||
)
|
||||
),
|
||||
"tts_provider": tts_provider,
|
||||
"limits": {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, Callable
|
||||
|
||||
from ..store import InProcSession
|
||||
|
|
@ -44,7 +44,7 @@ class LearnerGrowthMetrics:
|
|||
def iso_datetime(ts: float | None) -> str | None:
|
||||
if ts is None:
|
||||
return None
|
||||
return datetime.fromtimestamp(ts).isoformat(timespec="seconds")
|
||||
return datetime.fromtimestamp(ts, tz=timezone.utc).isoformat(timespec="seconds")
|
||||
|
||||
|
||||
def session_activity_time(sess: InProcSession) -> float:
|
||||
|
|
|
|||
|
|
@ -998,7 +998,7 @@ class VoiceService:
|
|||
local_whisper_stt_model
|
||||
if local_whisper_stt_model is not None
|
||||
else settings.local_whisper_stt_model
|
||||
).strip() or "large-v3"
|
||||
).strip() or "small"
|
||||
self._local_whisper_stt_language = (
|
||||
local_whisper_stt_language
|
||||
if local_whisper_stt_language is not None
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ from __future__ import annotations
|
|||
import re
|
||||
from collections import Counter
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Any, Literal, Optional, cast
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
|
@ -31,6 +31,7 @@ WorksheetItemSpec = tuple[str, str, list[str], WorksheetSpeaker | None]
|
|||
WorksheetSectionSpec = tuple[str, str, list[WorksheetItemSpec]]
|
||||
|
||||
LEARNER_VISIBLE_AI_ROLE = "counselor"
|
||||
KOREA_STANDARD_TIME = timezone(timedelta(hours=9), name="KST")
|
||||
MISSING_SESSION_EVALUATION_GRACE_SECONDS = 30.0
|
||||
MISSING_SESSION_EVALUATION_ERROR = (
|
||||
"회기말 평가가 제한 시간 이후에도 저장되지 않았습니다. AI 평가 재시도가 필요합니다."
|
||||
|
|
@ -478,7 +479,11 @@ def stage_label(stage: object) -> StageLabel:
|
|||
def iso(ts: float | None) -> str | None:
|
||||
if ts is None:
|
||||
return None
|
||||
return datetime.fromtimestamp(ts).isoformat(timespec="seconds")
|
||||
return datetime.fromtimestamp(ts, tz=timezone.utc).isoformat(timespec="seconds")
|
||||
|
||||
|
||||
def session_calendar_date(ts: float) -> str:
|
||||
return datetime.fromtimestamp(ts, tz=KOREA_STANDARD_TIME).strftime("%Y-%m-%d")
|
||||
|
||||
|
||||
def learner_visible_turns(sess: InProcSession) -> list[TurnRecord]:
|
||||
|
|
@ -1757,7 +1762,7 @@ def build_session_review(read_input: SessionReviewReadInput) -> SessionReviewRes
|
|||
initial=client_initial,
|
||||
persona=_persona_label(sess.persona_code, str(sess.persona.difficulty)),
|
||||
),
|
||||
date=datetime.fromtimestamp(sess.created_at).strftime("%Y-%m-%d"),
|
||||
date=session_calendar_date(sess.created_at),
|
||||
durationLabel=_duration_label(duration_seconds),
|
||||
durationSeconds=duration_seconds,
|
||||
reachedPhase=reached_phase,
|
||||
|
|
|
|||
49
apps/api/app/test_session_read_model.py
Normal file
49
apps/api/app/test_session_read_model.py
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
"""회기 읽기 모델의 시간 직렬화 계약 테스트."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from .session_read_model import iso, session_calendar_date
|
||||
from .services.session_metrics import iso_datetime
|
||||
|
||||
|
||||
class SessionReadModelIsoTest(unittest.TestCase):
|
||||
def test_none_remains_none(self) -> None:
|
||||
self.assertIsNone(iso(None))
|
||||
|
||||
def test_epoch_is_serialized_as_utc_with_offset(self) -> None:
|
||||
self.assertEqual(iso(0), "1970-01-01T00:00:00+00:00")
|
||||
|
||||
def test_timestamp_round_trips_without_local_timezone_drift(self) -> None:
|
||||
timestamp = 1_800_000_120.0
|
||||
|
||||
rendered = iso(timestamp)
|
||||
|
||||
self.assertIsNotNone(rendered)
|
||||
assert rendered is not None
|
||||
parsed = datetime.fromisoformat(rendered)
|
||||
self.assertEqual(parsed.tzinfo, timezone.utc)
|
||||
self.assertEqual(parsed.timestamp(), timestamp)
|
||||
|
||||
def test_serialized_timestamp_has_explicit_utc_suffix(self) -> None:
|
||||
rendered = iso(1_800_000_120.0)
|
||||
|
||||
self.assertIsNotNone(rendered)
|
||||
assert rendered is not None
|
||||
self.assertTrue(rendered.endswith("+00:00"))
|
||||
|
||||
def test_dashboard_metrics_use_the_same_utc_contract(self) -> None:
|
||||
timestamp = 1_800_000_120.0
|
||||
|
||||
self.assertEqual(iso_datetime(timestamp), iso(timestamp))
|
||||
|
||||
def test_review_calendar_date_uses_korea_standard_time(self) -> None:
|
||||
utc_late_afternoon = datetime(2026, 8, 8, 15, 30, tzinfo=timezone.utc)
|
||||
|
||||
self.assertEqual(session_calendar_date(utc_late_afternoon.timestamp()), "2026-08-09")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
@ -693,7 +693,7 @@ class LocalWhisperStreamingTest(unittest.IsolatedAsyncioTestCase):
|
|||
query = parse_qs(urlsplit(url).query)
|
||||
self.assertEqual(urlsplit(url).scheme, "ws")
|
||||
self.assertEqual(urlsplit(url).hostname, "127.0.0.1")
|
||||
self.assertEqual(query["model"], ["large-v3"])
|
||||
self.assertEqual(query["model"], ["small"])
|
||||
self.assertEqual(query["language"], ["ko"])
|
||||
self.assertEqual(query["sample_rate"], ["16000"])
|
||||
self.assertEqual(query["channels"], ["1"])
|
||||
|
|
@ -701,7 +701,7 @@ class LocalWhisperStreamingTest(unittest.IsolatedAsyncioTestCase):
|
|||
self.assertEqual(query["utterance_end_ms"], ["1200"])
|
||||
|
||||
self.assertEqual(result.text, "안녕하세요")
|
||||
self.assertEqual(result.model, "large-v3")
|
||||
self.assertEqual(result.model, "small")
|
||||
self.assertEqual(result.language, "ko")
|
||||
self.assertEqual([word.word for word in result.words], ["안녕하세요"])
|
||||
self.assertAlmostEqual(result.words[0].start, 0.05, places=3)
|
||||
|
|
@ -927,7 +927,7 @@ class LocalWhisperProviderSelectionTest(unittest.TestCase):
|
|||
service = VoiceService(api_key="", stt_provider="local_whisper")
|
||||
self.assertTrue(service.streaming_stt_enabled())
|
||||
self.assertEqual(service.stt_provider(), "local_whisper")
|
||||
self.assertEqual(service.stt_model(), "large-v3")
|
||||
self.assertEqual(service.stt_model(), "small")
|
||||
|
||||
def test_missing_sidecar_url_falls_back_to_batch(self) -> None:
|
||||
service = VoiceService(
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ from .persona_repository import PersonaVoiceMap
|
|||
from .routes import voice as voice_routes
|
||||
from .services import multimodal_alliance_store
|
||||
from .services.voice import (
|
||||
MELOTTS_TTS_MODEL,
|
||||
StreamingTranscriptEvent,
|
||||
TranscriptResult,
|
||||
TranscriptWord,
|
||||
|
|
@ -150,6 +151,27 @@ class _FakeRouteStreamingSession:
|
|||
self.aborted = True
|
||||
|
||||
|
||||
class VoiceHealthContractTest(unittest.IsolatedAsyncioTestCase):
|
||||
async def test_melotts_health_reports_the_operated_model(self) -> None:
|
||||
service = SimpleNamespace(
|
||||
is_available=lambda: True,
|
||||
stt_available=lambda: True,
|
||||
tts_available=lambda: True,
|
||||
tts_provider=lambda: "melotts",
|
||||
stt_provider=lambda: "local_whisper",
|
||||
stt_model=lambda: "small",
|
||||
batch_stt_available=lambda: False,
|
||||
)
|
||||
|
||||
with patch.object(voice_routes, "voice_service", service):
|
||||
response = await voice_routes.voice_health()
|
||||
|
||||
body = json.loads(bytes(response.body))
|
||||
self.assertEqual(body["status"], "ok")
|
||||
self.assertEqual(body["tts_provider"], "melotts")
|
||||
self.assertEqual(body["tts_model"], MELOTTS_TTS_MODEL)
|
||||
|
||||
|
||||
class VoiceWebSocketContractTest(unittest.IsolatedAsyncioTestCase):
|
||||
def _bind_result(self) -> tuple[str, VoicePreset, None, dict[str, object]]:
|
||||
return (
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue