This commit is contained in:
iamdoubz
2026-06-30 23:18:29 -05:00
parent 849fa79647
commit f9b172aff4
24 changed files with 1867 additions and 177 deletions
+121 -10
View File
@@ -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.");
}
}