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
4 changes: 3 additions & 1 deletion app/api/internalization_room/back_translation.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,8 @@ async def finish(
state.clip_duration_ms = payload.clip_duration_ms
await room.save_back_translation(db, session, state)

if len(told) < len(final):
untold = room.first_untold(final)
if untold is not None:
waiting, _ = choose(
FailSafe.UNTOLD_STRETCH,
session.language,
Expand All @@ -206,6 +207,7 @@ async def finish(
audio_url=clip_url(spoken.key),
fixed_line="",
checked=False,
untold_segment_id=untold.id,
findings_remaining=0,
)

Expand Down
8 changes: 8 additions & 0 deletions app/models/internalization_room.py
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,14 @@ class BackTranslationVerdictResponse(BaseModel):
#: Which stretch the finding lands on, by its own address, so the room can take the team
#: straight to that slice of their recording instead of starting the whole passage over.
finding_segment_id: str | None = None
#: Which stretch was recorded in the mother tongue but never told back, by its own
#: address, when that is what stopped the reading. Its own field rather than
#: `finding_segment_id` because the two ask the room for different things: a finding is
#: a stretch the team told and the analyst has a correction about, and this is a stretch
#: with no telling-back at all. Reading one from the absence of the other is the
#: inference that cost a team their morning — the app had no address, inferred "start
#: over", and threw away every recording of the passage.
untold_segment_id: str | None = None
findings_remaining: int = 0
used_fail_safe: bool = False

Expand Down
2 changes: 2 additions & 0 deletions app/services/internalization_room/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
divide_segment,
divided_segments,
final_segments,
first_untold,
parent_of,
retired_segments,
segment_by_id,
Expand Down Expand Up @@ -70,6 +71,7 @@
"divided_segments",
"final_segments",
"findings_block",
"first_untold",
"get_session",
"get_session_for_facilitator",
"mark_needs_person",
Expand Down
20 changes: 20 additions & 0 deletions app/services/internalization_room/segments.py
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,26 @@ def told_back(segments: list[IRSegment]) -> list[IRSegment]:
return [segment for segment in segments if segment.transcript is not None]


def first_untold(segments: list[IRSegment]) -> IRSegment | None:
"""The earliest stretch that counts and carries no telling-back, or nothing if none does.

The complement of `told_back` over the same list, and it lives beside it for the same
reason the two are one function apart: which stretches count, and which of them are
evidence, are answered in one place so the count that stops the analyst and the address
the team is sent to cannot disagree.

Earliest, because a team tells a passage in its own sequence: sent to a hole in the
middle while an earlier one is still open, they work backwards through their own
recording. Earliest by position, which is not the same as earliest in the take — a
stretch re-recorded against a different slice keeps the position it had.

The caller decides which stretches are in scope, and none of the selection rules are
repeated here: `finish` passes `final_segments`, so what is walked is already current
and already in the order the team told in.
"""
return next((segment for segment in segments if segment.transcript is None), None)


async def retired_segments(db: AsyncSession, session_id: str) -> list[IRSegment]:
"""The stretches that stopped counting, oldest first.

Expand Down
Loading
Loading