Phase 2
This commit is contained in:
+121
-10
@@ -42,20 +42,131 @@ pub struct MarkdownNotes;
|
||||
impl NotesRenderer for MarkdownNotes {
|
||||
fn to_markdown(
|
||||
&self,
|
||||
_segments: &[TranscriptSegment],
|
||||
_speakers: &[SpeakerInfo],
|
||||
_summary_md: Option<&str>,
|
||||
segments: &[TranscriptSegment],
|
||||
speakers: &[SpeakerInfo],
|
||||
summary_md: Option<&str>,
|
||||
) -> String {
|
||||
// T2.4: resolve speaker names, group dialogue, prepend summary section.
|
||||
todo!("Phase 2 — assemble Markdown")
|
||||
let mut out = String::new();
|
||||
if let Some(summary) = summary_md {
|
||||
let summary = summary.trim();
|
||||
if !summary.is_empty() {
|
||||
out.push_str(summary);
|
||||
out.push_str("\n\n---\n\n");
|
||||
}
|
||||
}
|
||||
|
||||
let name_for = |label: &str| -> String {
|
||||
speakers
|
||||
.iter()
|
||||
.find(|s| s.label == label)
|
||||
.and_then(|s| s.display_name.clone())
|
||||
.unwrap_or_else(|| label.to_string())
|
||||
};
|
||||
|
||||
// Group consecutive segments from the same speaker into one paragraph
|
||||
// (matters once Phase 4 diarization produces more than one speaker).
|
||||
let mut current_speaker: Option<&str> = None;
|
||||
let mut buffer = String::new();
|
||||
for seg in segments {
|
||||
let text = seg.text.trim();
|
||||
if text.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if current_speaker != Some(seg.speaker.as_str()) {
|
||||
if let Some(speaker) = current_speaker {
|
||||
out.push_str(&format!("**{}:** {}\n\n", name_for(speaker), buffer.trim()));
|
||||
}
|
||||
current_speaker = Some(seg.speaker.as_str());
|
||||
buffer.clear();
|
||||
}
|
||||
if !buffer.is_empty() {
|
||||
buffer.push(' ');
|
||||
}
|
||||
buffer.push_str(text);
|
||||
}
|
||||
if let Some(speaker) = current_speaker {
|
||||
out.push_str(&format!("**{}:** {}\n\n", name_for(speaker), buffer.trim()));
|
||||
}
|
||||
|
||||
out.trim_end().to_string()
|
||||
}
|
||||
|
||||
fn export(
|
||||
&self,
|
||||
_markdown: &str,
|
||||
_dest: &Path,
|
||||
_fmt: ExportFormat,
|
||||
markdown: &str,
|
||||
dest: &Path,
|
||||
fmt: ExportFormat,
|
||||
) -> Result<PathBuf, NotesError> {
|
||||
// T2.6 (.md/bundle); T8.4 (pdf/docx via local conversion).
|
||||
todo!("Phase 2/8 — export notes")
|
||||
match fmt {
|
||||
ExportFormat::Md => {
|
||||
if let Some(parent) = dest.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
std::fs::write(dest, markdown)?;
|
||||
Ok(dest.to_path_buf())
|
||||
}
|
||||
// Bundling pulls in audio.wav/transcript.json alongside notes.md —
|
||||
// that's meeting-folder orchestration, not Markdown rendering, so
|
||||
// `commands::export_meeting` handles it directly rather than here.
|
||||
ExportFormat::Bundle => Err(NotesError::Export(
|
||||
"bundle export is assembled in commands::export_meeting, not NotesRenderer".into(),
|
||||
)),
|
||||
ExportFormat::Pdf | ExportFormat::Docx => Err(NotesError::Export(
|
||||
"PDF/Word export lands in Phase 8".into(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn seg(id: u64, speaker: &str, text: &str) -> TranscriptSegment {
|
||||
TranscriptSegment {
|
||||
id,
|
||||
start_ms: id * 1000,
|
||||
end_ms: id * 1000 + 900,
|
||||
speaker: speaker.to_string(),
|
||||
text: text.to_string(),
|
||||
confidence: Some(0.9),
|
||||
interim: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn groups_consecutive_same_speaker_segments_into_one_paragraph() {
|
||||
let segments = vec![
|
||||
seg(0, "S1", "Hello"),
|
||||
seg(1, "S1", "world."),
|
||||
seg(2, "S2", "Hi there."),
|
||||
];
|
||||
let md = MarkdownNotes.to_markdown(&segments, &[], None);
|
||||
assert_eq!(md, "**S1:** Hello world.\n\n**S2:** Hi there.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolves_display_name_and_falls_back_to_label() {
|
||||
let segments = vec![seg(0, "S1", "Hi")];
|
||||
let speakers = [SpeakerInfo {
|
||||
label: "S1".to_string(),
|
||||
display_name: Some("Alex".to_string()),
|
||||
participant_id: None,
|
||||
}];
|
||||
assert_eq!(
|
||||
MarkdownNotes.to_markdown(&segments, &speakers, None),
|
||||
"**Alex:** Hi"
|
||||
);
|
||||
assert_eq!(
|
||||
MarkdownNotes.to_markdown(&segments, &[], None),
|
||||
"**S1:** Hi"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skips_blank_segments_and_prepends_summary() {
|
||||
let segments = vec![seg(0, "S1", " "), seg(1, "S1", "Real text.")];
|
||||
let md = MarkdownNotes.to_markdown(&segments, &[], Some("## Summary\nDone."));
|
||||
assert_eq!(md, "## Summary\nDone.\n\n---\n\n**S1:** Real text.");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user