diff --git a/drb-c2-core/app/routers/upload.py b/drb-c2-core/app/routers/upload.py index 21d7716..1ea9598 100644 --- a/drb-c2-core/app/routers/upload.py +++ b/drb-c2-core/app/routers/upload.py @@ -522,11 +522,18 @@ async def _run_intelligence_pipeline( # Correlator also runs for calls with no scenes (unclassified) to attempt # talkgroup-based linking even when no transcript could be produced. - # Skip when extraction flagged the call — garbage or too-short transcripts - # carry no signal and would only attach spuriously via the thin path. + # transcript_too_short (<=5 words: "10-8", "show me clear", a unit + # check-in) still carries a real transcript and talkgroup — exactly the + # brief follow-up/clearance traffic an incident needs, and the thin-path + # merge below already requires a same-talkgroup, recently-active + # incident before attaching anything, same guard already trusted for + # no-transcript calls. Previously excluded here, so these calls never + # attached to anything at all. garbage_transcript (Whisper + # hallucination) has no real content behind it and stays excluded. if not scenes: _call_doc = await fstore.doc_get("calls", call_id) - if not (_call_doc or {}).get("skip_reason"): + skip_reason = (_call_doc or {}).get("skip_reason") + if not skip_reason or skip_reason == "transcript_too_short": incident_id = await _correlate_with_consensus( call_id=call_id, node_id=node_id, diff --git a/drb-c2-core/tests/test_upload_short_transcript_correlation.py b/drb-c2-core/tests/test_upload_short_transcript_correlation.py new file mode 100644 index 0000000..a70a3a3 --- /dev/null +++ b/drb-c2-core/tests/test_upload_short_transcript_correlation.py @@ -0,0 +1,62 @@ +""" +server-26# — a transcript_too_short call (<=5 words: "10-8", "show me +clear", a unit check-in) never reached correlation at all. upload.py's +no-scenes fallback (the path that lets a no-transcript call still thin-link +by talkgroup) explicitly excluded ANY skip_reason, so short-but-real follow-up +and clearance traffic was permanently unlinkable — not just unextracted by +GPT, but never even attempted against the fast/thin path that already exists +for exactly this kind of content-free signal. garbage_transcript (Whisper +hallucination) has no real content behind it and should stay excluded. +""" +from unittest.mock import AsyncMock, patch + +import pytest + +from app.routers import upload + +ALL_ON = { + "stt_enabled": True, + "correlation_enabled": True, + "summaries_enabled": True, + "vocabulary_learning_enabled": True, + "transcript_correction_enabled": True, +} + + +async def _run_ingest(skip_reason): + with patch("app.internal.feature_flags.get_flags", + AsyncMock(return_value=ALL_ON)), \ + patch("app.internal.firestore.doc_get_cached", + AsyncMock(return_value={"system_id": "sys-1", "ai_flags": {}})), \ + patch.object(upload, "fstore") as fs, \ + patch.object(upload, "_correlate_with_consensus", AsyncMock(return_value=None)) as corr, \ + patch("app.internal.transcription.transcribe_call", + AsyncMock(return_value=("10-8", []))), \ + patch("app.internal.intelligence.extract_scenes", AsyncMock(return_value=[])), \ + patch("app.internal.alerter.check_and_dispatch", AsyncMock()): + fs.doc_get = AsyncMock(return_value={"skip_reason": skip_reason} if skip_reason else {}) + fs.doc_set = AsyncMock() + await upload._run_intelligence_pipeline( + call_id="call-1", node_id="node-1", system_id="sys-1", + talkgroup_id=101, talkgroup_name="PD Dispatch", + gcs_uri="gs://bucket/call-1.mp3", + ) + return corr + + +@pytest.mark.asyncio +async def test_transcript_too_short_now_attempts_correlation(): + corr = await _run_ingest("transcript_too_short") + corr.assert_awaited_once() + + +@pytest.mark.asyncio +async def test_garbage_transcript_still_skips_correlation(): + corr = await _run_ingest("garbage_transcript") + corr.assert_not_awaited() + + +@pytest.mark.asyncio +async def test_no_skip_reason_still_attempts_correlation(): + corr = await _run_ingest(None) + corr.assert_awaited_once()