AudioSumma/pipeline.py
Pat Wendorf 9cf3f879d0 Improve UI, processing pipeline, and test coverage
Replace FFmpeg silence removal with dependency-free PCM processing, add in-app activity and status indicators, extract pipeline orchestration, prevent duplicate daily summaries, remove obsolete image assets, and add audio, pipeline, and Qt tests.
2026-08-13 10:44:52 -04:00

95 lines
3.8 KiB
Python

"""Transcription, summarization, and cleanup orchestration for AudioSumma."""
from __future__ import annotations
import datetime
import os
from collections.abc import Callable
from pathlib import Path
from typing import Any
class ProcessingPipeline:
"""Run the file-processing workflow independently of the Qt UI."""
def __init__(
self,
config: dict[str, Any],
*,
log: Callable[[str], None] = lambda _message: None,
trim_silence: Callable[[str], Any] | None = None,
transcribe: Callable[[Any], str] | None = None,
summarize: Callable[[str], str] | None = None,
) -> None:
self.config = config
self.log = log
self.trim_silence = trim_silence
self.transcribe = transcribe
self.summarize = summarize
@property
def output_dir(self) -> Path:
return Path(self.config["OUTPUT_DIR"])
def process_wav_files(self) -> None:
"""Trim and transcribe WAV files without an existing transcript."""
self.output_dir.mkdir(parents=True, exist_ok=True)
for wav_path in sorted(self.output_dir.glob("*.wav")):
transcript_path = wav_path.with_suffix(".tns")
if transcript_path.exists():
self.log(f"Skipping {wav_path.name}, transcript already exists")
continue
if self.trim_silence is None or self.transcribe is None:
raise RuntimeError("transcription services are not configured")
self.log(f"Trimming silence: {wav_path}")
self.trim_silence(str(wav_path))
self.log(f"Transcribing: {wav_path}")
with wav_path.open("rb") as audio, transcript_path.open("w") as output:
output.write(self.transcribe(audio))
def summarize_transcripts(self) -> None:
"""Summarize every transcript into today's Markdown summary."""
if self.summarize is None:
raise RuntimeError("summarization service is not configured")
summary_path = self.output_dir / (
f"summary-{datetime.datetime.now():%Y%m%d}.md"
)
existing_summary = summary_path.read_text() if summary_path.exists() else ""
for transcript_path in sorted(self.output_dir.glob("*.tns")):
heading = f"# Call Transcript - {transcript_path.name}\n"
if heading in existing_summary:
self.log(
f"Skipping {transcript_path.name}, already summarized today"
)
continue
self.log(f"Summarizing: {transcript_path}")
transcript = transcript_path.read_text()
with summary_path.open("a") as summary:
summary.write(f"# Call Transcript - {transcript_path.name}\n\n")
for step in self.config["SUMMARIZATION_STEPS"]:
self.log(f" Running step: {step['name']}")
result = self.summarize(step["prompt"].format(chunk=transcript))
summary.write(f"## {step['name']}\n\n{result}\n\n")
summary.write("---\n")
self.log("Summarizing complete")
def run(self) -> None:
self.log("Starting transcription and summarization")
self.process_wav_files()
self.summarize_transcripts()
self.log("Processing complete")
def clean(self) -> None:
"""Delete intermediate WAV and transcript files, retaining Markdown."""
self.log("Cleaning files...")
try:
for path in self.output_dir.iterdir():
if path.suffix in {".wav", ".tns"}:
path.unlink()
self.log(f"Deleted: {path}")
self.log("Cleaning complete.")
except Exception as error:
self.log(f"An error occurred while cleaning files: {error}")