diff --git a/lxc-frontend/app/core/mac_client.py b/lxc-frontend/app/core/mac_client.py index 079f407..a8ea06b 100644 --- a/lxc-frontend/app/core/mac_client.py +++ b/lxc-frontend/app/core/mac_client.py @@ -146,16 +146,28 @@ class MacClient: job_id, ) - async def export_docx(self, data: dict, profile: str | None = None, job_id: int | None = None) -> bytes: + async def export_docx( + self, + data: dict, + profile: str | None = None, + job_id: int | None = None, + full_transcript: list[dict] | None = None, + ) -> bytes: return await self._post_bytes_retry( f"{self.base_url}/api/export/docx", - {"data": data, "profile": profile}, + {"data": data, "profile": profile, "full_transcript": full_transcript or []}, job_id, ) - async def export_pdf(self, data: dict, profile: str | None = None, job_id: int | None = None) -> bytes: + async def export_pdf( + self, + data: dict, + profile: str | None = None, + job_id: int | None = None, + full_transcript: list[dict] | None = None, + ) -> bytes: return await self._post_bytes_retry( f"{self.base_url}/api/export/pdf", - {"data": data, "profile": profile}, + {"data": data, "profile": profile, "full_transcript": full_transcript or []}, job_id, ) diff --git a/lxc-frontend/app/services/pipeline.py b/lxc-frontend/app/services/pipeline.py index 9f3d029..1f880ac 100644 --- a/lxc-frontend/app/services/pipeline.py +++ b/lxc-frontend/app/services/pipeline.py @@ -166,6 +166,11 @@ async def run_pipeline(job_id: int) -> None: protocol_path.write_text(json.dumps(protocol, ensure_ascii=False, indent=2), encoding="utf-8") _update(job_id, protocol_path=str(protocol_path), progress=85) + # Volltranskript-Segmente: optionaler Anhang ans DOCX/PDF, falls das + # Profil `docx.include_full_transcript` setzt. Renderer ignoriert die + # Liste sonst — wir reichen sie trotzdem immer durch. + full_segments = tx.get("segments", []) or [] + # 4) DOCX if existing_dx_path: log.info("[job %s] reuse docx %s", job_id, existing_dx_path.name) @@ -174,7 +179,8 @@ async def run_pipeline(job_id: int) -> None: log.info("[job %s] export docx", job_id) docx_bytes = await _with_heartbeat( job_id, - client.export_docx(protocol, profile=profile, job_id=job_id), + client.export_docx(protocol, profile=profile, job_id=job_id, + full_transcript=full_segments), ) docx_path = job_dir / "protocol.docx" docx_path.write_bytes(docx_bytes) @@ -189,7 +195,8 @@ async def run_pipeline(job_id: int) -> None: log.info("[job %s] export pdf", job_id) pdf_bytes = await _with_heartbeat( job_id, - client.export_pdf(protocol, profile=profile, job_id=job_id), + client.export_pdf(protocol, profile=profile, job_id=job_id, + full_transcript=full_segments), ) pdf_path = job_dir / "protocol.pdf" pdf_path.write_bytes(pdf_bytes) diff --git a/mac-worker/app/core/docx_export.py b/mac-worker/app/core/docx_export.py index 587a058..292b8e6 100644 --- a/mac-worker/app/core/docx_export.py +++ b/mac-worker/app/core/docx_export.py @@ -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, "", [], {}): diff --git a/mac-worker/app/core/pdf_export.py b/mac-worker/app/core/pdf_export.py index 982134a..848498e 100644 --- a/mac-worker/app/core/pdf_export.py +++ b/mac-worker/app/core/pdf_export.py @@ -69,7 +69,11 @@ def _para(text: str, style_name: str = "Body2") -> Paragraph: return Paragraph(_esc(text).replace("\n", "
"), _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", "
"), transcript_style)) + + def _render_section(story: list, data: dict, section: dict) -> None: val = data.get(section["key"]) if val in (None, "", [], {}): diff --git a/mac-worker/app/main.py b/mac-worker/app/main.py index 6b045c2..48c6ae2 100644 --- a/mac-worker/app/main.py +++ b/mac-worker/app/main.py @@ -82,11 +82,15 @@ class ProtocolIn(BaseModel): class DocxIn(BaseModel): data: dict profile: str | None = None + # Optional: Whisper-Segmente; werden vom Renderer nur genutzt, wenn das + # Profil `docx.include_full_transcript: true` setzt. + full_transcript: list[dict] = [] class PdfIn(BaseModel): data: dict profile: str | None = None + full_transcript: list[dict] = [] class PreloadIn(BaseModel): @@ -170,7 +174,11 @@ async def protocol(payload: ProtocolIn): @app.post("/api/export/docx") async def export_docx(payload: DocxIn): - data = build_docx(payload.data, profile_name=payload.profile) + data = build_docx( + payload.data, + profile_name=payload.profile, + full_transcript=payload.full_transcript, + ) return Response( content=data, media_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document", @@ -180,7 +188,11 @@ async def export_docx(payload: DocxIn): @app.post("/api/export/pdf") async def export_pdf(payload: PdfIn): - data = build_pdf(payload.data, profile_name=payload.profile) + data = build_pdf( + payload.data, + profile_name=payload.profile, + full_transcript=payload.full_transcript, + ) return Response( content=data, media_type="application/pdf", diff --git a/mac-worker/profiles/interview.yaml b/mac-worker/profiles/interview.yaml index 711d2da..1a7e830 100644 --- a/mac-worker/profiles/interview.yaml +++ b/mac-worker/profiles/interview.yaml @@ -44,6 +44,7 @@ protocol_prompt: | docx: title_field: title default_title: Bewerbergespräch + include_full_transcript: true meta: - { key: date, label: "Datum" } - { key: candidate, label: "Bewerber/in" } diff --git a/mac-worker/profiles/meeting.yaml b/mac-worker/profiles/meeting.yaml index 9afcd47..450749e 100644 --- a/mac-worker/profiles/meeting.yaml +++ b/mac-worker/profiles/meeting.yaml @@ -50,6 +50,10 @@ protocol_prompt: | docx: title_field: title default_title: Sitzungsprotokoll + # Hängt das vollständige Whisper-Transkript als Anhang ans Dokument. + # Pipeline reicht die Segmente an den Renderer durch; LLM-Komprimierung + # im transcript_excerpt-Feld bleibt unverändert. + include_full_transcript: true meta: - { key: date, label: "Datum" } - { key: topic, label: "Thema" } diff --git a/mac-worker/profiles/phone-call.yaml b/mac-worker/profiles/phone-call.yaml index 1a713f2..085068d 100644 --- a/mac-worker/profiles/phone-call.yaml +++ b/mac-worker/profiles/phone-call.yaml @@ -45,6 +45,7 @@ protocol_prompt: | docx: title_field: title default_title: Telefonnotiz + include_full_transcript: true meta: - { key: date, label: "Datum" } - { key: caller, label: "Anrufer" }