Add post-diarization speaker review workflow

This commit is contained in:
2026-09-17 19:16:57 +02:00
parent bd48aec5aa
commit 2215414282
8 changed files with 240 additions and 24 deletions
+32
View File
@@ -92,6 +92,8 @@ class ProcessingOutcome:
failed_stage: str | None = None
error_message: str | None = None
speaker_attribution_available: bool | None = None
awaiting_speaker_review: bool = False
detected_speaker_count: int | None = None
@dataclass(frozen=True)
@@ -249,6 +251,7 @@ class MeetingProcessingService:
"diarization_runtime": self.settings.diarization_runtime,
"diarization_container_image": (self.settings.diarization_container_image),
"diarization_container_args": self.settings.diarization_container_args,
"stop_after_diarization": options.diarization_enabled,
}
)
current_stage: str | None = None
@@ -299,6 +302,15 @@ class MeetingProcessingService:
failed_stage=failed_stage or current_stage or "preparing",
error_message=failure_message,
)
if result.protocol_path is None:
return ProcessingOutcome(
succeeded=True,
run_dir=result.run_dir,
original_protocol=None,
protocol_path=None,
awaiting_speaker_review=True,
detected_speaker_count=self._detected_speaker_count(result.run_dir),
)
protocol_path = Path(result.protocol_path)
return ProcessingOutcome(
succeeded=True,
@@ -401,6 +413,13 @@ class MeetingProcessingService:
context_data["speaker_mappings"] = dict(sorted(speaker_mappings.items()))
self._apply_glossary(context_data)
context = self.meeting_lab.create_context(context_data)
# A checkpoint mapping is user input, not a derived protocol artifact. Save it
# before inference so a failed generation remains retryable with the same review.
if context_path.is_symlink():
context_path.unlink()
context_path.write_text(
yaml.safe_dump(context.data, allow_unicode=True, sort_keys=False), encoding="utf-8"
)
def relay(event: Any) -> None:
self._update_timer(event)
@@ -432,6 +451,7 @@ class MeetingProcessingService:
self.timer.finish_run()
raise
self.timer.finish_run()
self._persist_timing(Path(result.run_dir))
protocol_path = Path(result.protocol_path)
return ProcessingOutcome(
succeeded=True,
@@ -473,6 +493,18 @@ class MeetingProcessingService:
value = metadata.get("speaker_attribution_available")
return value if isinstance(value, bool) else None
@staticmethod
def _detected_speaker_count(run_dir: Path | None) -> int | None:
if run_dir is None:
return None
metadata_path = Path(run_dir) / "run_metadata.json"
try:
metadata = json.loads(metadata_path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
return None
value = metadata.get("diarization", {}).get("speaker_count")
return value if isinstance(value, int) else None
@staticmethod
def _read_failure(run_dir: Path | None) -> dict[str, str]:
if run_dir is None:
+25 -16
View File
@@ -554,7 +554,15 @@ def _render_result() -> None:
st.caption("Intermediate artifacts and run metadata were preserved here.")
return
st.success("Processing completed. Review the generated protocol before use.")
awaiting_review = getattr(outcome, "awaiting_speaker_review", False)
if awaiting_review:
count = getattr(outcome, "detected_speaker_count", None)
detected = f"{count} speakers detected" if count is not None else "speakers detected"
st.info(
f"Diarization complete — {detected}. Assign speakers if desired, then generate the protocol."
)
else:
st.success("Processing completed. Review the generated protocol before use.")
st.caption(f"Run artifacts: {outcome.run_dir}")
if outcome.speaker_attribution_available is False:
st.warning(
@@ -563,17 +571,18 @@ def _render_result() -> None:
)
if message := st.session_state.pop("speaker_mapping_message", None):
st.success(message)
with st.expander("Original generated protocol", expanded=False):
st.markdown(outcome.original_protocol or "")
edited = st.text_area(
"Editable protocol",
key="edited_protocol",
height=500,
help="The original protocol.md remains unchanged.",
)
if st.button("Save edited protocol", type="primary"):
path = MeetingProcessingService.save_edited_protocol(outcome.run_dir, edited)
st.success(f"Saved edited protocol to {path}")
if not awaiting_review:
with st.expander("Original generated protocol", expanded=False):
st.markdown(outcome.original_protocol or "")
edited = st.text_area(
"Editable protocol",
key="edited_protocol",
height=500,
help="The original protocol.md remains unchanged.",
)
if st.button("Save edited protocol", type="primary"):
path = MeetingProcessingService.save_edited_protocol(outcome.run_dir, edited)
st.success(f"Saved edited protocol to {path}")
try:
service = MeetingProcessingService(AppSettings.from_environment(), MeetingLabGateway())
@@ -590,7 +599,7 @@ def _render_result() -> None:
if duplicate_assignments:
st.error("Each participant can be assigned to only one detected speaker.")
if st.button(
"Regenerate protocol with confirmed speakers",
"Generate protocol" if awaiting_review else "Regenerate protocol with confirmed speakers",
disabled=duplicate_assignments,
type="primary",
):
@@ -610,7 +619,7 @@ def _render_result() -> None:
performance_profile=selected_profile,
),
states,
"Starting protocol regeneration",
"Starting protocol generation" if awaiting_review else "Starting protocol regeneration",
)
except (OSError, RuntimeError, ValueError) as exc:
_remember_regeneration_timing(
@@ -618,13 +627,13 @@ def _render_result() -> None:
"Protocol regeneration failed",
service.timer.snapshot(),
)
st.error(f"Protocol regeneration failed: {exc}")
st.error(f"Protocol generation failed: {exc}")
else:
_remember_regeneration_timing(states, message, timing)
st.session_state.outcome = regenerated
_queue_edited_protocol(regenerated.original_protocol or "")
st.session_state.speaker_mapping_message = (
"Speaker mappings saved and protocol regenerated."
"Speaker mappings saved and protocol generated."
)
st.rerun()