feat(export): Volltranskript als Anhang in DOCX/PDF (meeting/phone-call/interview)
LLM komprimiert das Transkript bisher auf wenige Sätze + 20 Excerpt-Einträge — der Rest fiel aus dem Dokument. Pipeline reicht Whisper-Segmente jetzt an build_docx/build_pdf durch; Profile mit `docx.include_full_transcript: true` hängen das komplette Transkript (9 pt) am Ende an. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -19,7 +19,11 @@ from docx.shared import Pt
|
||||
from . import profiles
|
||||
|
||||
|
||||
def build_docx(data: dict[str, Any], profile_name: str | None = None) -> bytes:
|
||||
def build_docx(
|
||||
data: dict[str, Any],
|
||||
profile_name: str | None = None,
|
||||
full_transcript: list[dict] | None = None,
|
||||
) -> bytes:
|
||||
profile = profiles.get(profile_name)
|
||||
cfg = profile.docx or {}
|
||||
|
||||
@@ -37,6 +41,9 @@ def build_docx(data: dict[str, Any], profile_name: str | None = None) -> bytes:
|
||||
for section in cfg.get("sections", []) or []:
|
||||
_render_section(doc, data, section)
|
||||
|
||||
if cfg.get("include_full_transcript") and full_transcript:
|
||||
_render_full_transcript(doc, full_transcript)
|
||||
|
||||
if data.get("_parse_error") and data.get("_raw"):
|
||||
doc.add_heading("Roh-Antwort des Modells", level=1)
|
||||
doc.add_paragraph(data["_raw"])
|
||||
@@ -46,6 +53,23 @@ def build_docx(data: dict[str, Any], profile_name: str | None = None) -> bytes:
|
||||
return buf.getvalue()
|
||||
|
||||
|
||||
def _render_full_transcript(doc, segments: list[dict]) -> None:
|
||||
"""Hängt den Volltranskript-Anhang an: eine Überschrift, dann ein Absatz
|
||||
pro Whisper-Segment in 9 pt (damit das Dokument nicht explodiert).
|
||||
Leere Segmente werden übersprungen.
|
||||
"""
|
||||
doc.add_page_break()
|
||||
doc.add_heading("Volltranskript", level=1)
|
||||
for seg in segments:
|
||||
text = (seg.get("text") if isinstance(seg, dict) else str(seg)) or ""
|
||||
text = text.strip()
|
||||
if not text:
|
||||
continue
|
||||
p = doc.add_paragraph()
|
||||
run = p.add_run(text)
|
||||
run.font.size = Pt(9)
|
||||
|
||||
|
||||
def _render_section(doc, data: dict, section: dict) -> None:
|
||||
val = data.get(section["key"])
|
||||
if val in (None, "", [], {}):
|
||||
|
||||
@@ -69,7 +69,11 @@ def _para(text: str, style_name: str = "Body2") -> Paragraph:
|
||||
return Paragraph(_esc(text).replace("\n", "<br/>"), _STYLES[style_name])
|
||||
|
||||
|
||||
def build_pdf(data: dict[str, Any], profile_name: str | None = None) -> bytes:
|
||||
def build_pdf(
|
||||
data: dict[str, Any],
|
||||
profile_name: str | None = None,
|
||||
full_transcript: list[dict] | None = None,
|
||||
) -> bytes:
|
||||
profile = profiles.get(profile_name)
|
||||
cfg = profile.docx or {}
|
||||
|
||||
@@ -108,6 +112,9 @@ def build_pdf(data: dict[str, Any], profile_name: str | None = None) -> bytes:
|
||||
for section in cfg.get("sections", []) or []:
|
||||
_render_section(story, data, section)
|
||||
|
||||
if cfg.get("include_full_transcript") and full_transcript:
|
||||
_render_full_transcript(story, full_transcript)
|
||||
|
||||
# Parse-Error-Fallback
|
||||
if data.get("_parse_error") and data.get("_raw"):
|
||||
story.append(Paragraph("Roh-Antwort des Modells", _STYLES["SectionHeading"]))
|
||||
@@ -117,6 +124,27 @@ def build_pdf(data: dict[str, Any], profile_name: str | None = None) -> bytes:
|
||||
return buf.getvalue()
|
||||
|
||||
|
||||
def _render_full_transcript(story: list, segments: list[dict]) -> None:
|
||||
"""Anhang nach allen Sektionen: ein Absatz pro Segment in 9 pt.
|
||||
Vor dem Block ein PageBreak, damit das Volltranskript auf einer
|
||||
eigenen Seite beginnt und das Protokoll nicht zerreißt.
|
||||
"""
|
||||
from reportlab.platypus import PageBreak # local import — selten genutzt
|
||||
|
||||
story.append(PageBreak())
|
||||
story.append(Paragraph("Volltranskript", _STYLES["SectionHeading"]))
|
||||
transcript_style = ParagraphStyle(
|
||||
name="TranscriptLine", parent=_STYLES["Body2"],
|
||||
fontSize=9, leading=12, spaceAfter=3,
|
||||
)
|
||||
for seg in segments:
|
||||
text = (seg.get("text") if isinstance(seg, dict) else str(seg)) or ""
|
||||
text = text.strip()
|
||||
if not text:
|
||||
continue
|
||||
story.append(Paragraph(_esc(text).replace("\n", "<br/>"), transcript_style))
|
||||
|
||||
|
||||
def _render_section(story: list, data: dict, section: dict) -> None:
|
||||
val = data.get(section["key"])
|
||||
if val in (None, "", [], {}):
|
||||
|
||||
Reference in New Issue
Block a user