Redesign cover page layout for transcript and summary DOCX
- Remove old title-only style. - New layout (centered): - 5 blank lines - Logo from COVER_PAGE_LOGO_URL - 3 blank lines - Title (TRANSCRIPT or SUMMARY) - Date of transcription - Created by + COVER_PAGE_ORGANIZATION - 3 blank lines - Disclaimer - Insert page break after cover page; content starts on page 2.
This commit is contained in:
+87
-72
@@ -1,28 +1,58 @@
|
||||
"""
|
||||
Reusable cover-page generator for transcript and summary DOCX files.
|
||||
Cover-page generator for transcript and summary DOCX files.
|
||||
|
||||
Configuration (env):
|
||||
Layout (all centered):
|
||||
- 5 blank lines
|
||||
- Logo from COVER_PAGE_LOGO_URL (if set)
|
||||
- 3 blank lines
|
||||
- Title in uppercase: "TRANSCRIPT" or "SUMMARY"
|
||||
- Next line: date of transcription (e.g. "June 19, 2026")
|
||||
- Next line: "Created by <COVER_PAGE_ORGANIZATION>"
|
||||
- 3 blank lines
|
||||
- Disclaimer text
|
||||
|
||||
A page break is inserted after the cover page so the main content starts on page 2.
|
||||
|
||||
Environment variables:
|
||||
- COVER_PAGE_ENABLED: "true"/"false" (default: false)
|
||||
- COVER_PAGE_ORGANIZATION: e.g., "A.P.Strom"
|
||||
- COVER_PAGE_TITLE_PREFIX: e.g., "TRANSCRIPT" or "SUMMARY"
|
||||
- COVER_PAGE_LOGO_URL: optional URL
|
||||
- COVER_PAGE_LOGO_PATH: optional local path
|
||||
- COVER_PAGE_LOGO_URL: URL of logo image to include on the cover page
|
||||
"""
|
||||
|
||||
import os
|
||||
import io
|
||||
import requests
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from docx import Document
|
||||
from docx.shared import Pt, Inches
|
||||
from docx.shared import Pt
|
||||
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
||||
from docx.oxml import OxmlElement
|
||||
from docx.oxml.ns import qn
|
||||
|
||||
|
||||
def _add_centered_paragraph(doc: Document, text: str = "", font_size: Pt = Pt(12), bold: bool = False):
|
||||
"""Add a centered paragraph with given text and style."""
|
||||
p = doc.add_paragraph()
|
||||
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
if text:
|
||||
run = p.add_run(text)
|
||||
run.font.name = "Courier"
|
||||
run.font.size = font_size
|
||||
run.bold = bold
|
||||
return p
|
||||
|
||||
|
||||
def _add_blank_lines(doc: Document, count: int):
|
||||
"""Add specified number of blank lines (empty centered paragraphs)."""
|
||||
for _ in range(count):
|
||||
_add_centered_paragraph(doc)
|
||||
|
||||
|
||||
def _add_page_break(doc: Document):
|
||||
"""Insert a page break paragraph."""
|
||||
p = doc.add_paragraph()
|
||||
pPr = p._p.get_or_add_pPr()
|
||||
# Clear spacing/tabs
|
||||
for child in list(pPr):
|
||||
tag = child.tag.split("}")[-1] if "}" in child.tag else child.tag
|
||||
if tag in ("tabs", "spacing", "ind"):
|
||||
@@ -32,87 +62,72 @@ def _add_page_break(doc: Document):
|
||||
pPr.append(page_break)
|
||||
|
||||
|
||||
def _download_image_bytes(url: str) -> Optional[bytes]:
|
||||
"""Download image from URL and return bytes, or None on failure."""
|
||||
try:
|
||||
r = requests.get(url, timeout=10)
|
||||
if r.status_code == 200:
|
||||
return r.content
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def add_cover_page(
|
||||
doc: Document,
|
||||
title: str,
|
||||
subtitle: Optional[str] = None,
|
||||
metadata: Optional[dict] = None,
|
||||
include_logo: bool = False,
|
||||
date_str: Optional[str] = None,
|
||||
):
|
||||
"""
|
||||
Insert a cover page at the current cursor position.
|
||||
|
||||
- title: e.g., "TRANSCRIPT" or "SUMMARY"
|
||||
- subtitle: e.g., "Meeting of 16 June 2026"
|
||||
- metadata: optional dict with keys like:
|
||||
- "Organization"
|
||||
- "Date"
|
||||
- "Prepared by"
|
||||
- "Reference"
|
||||
- date_str: optional date string (e.g., "June 19, 2026");
|
||||
if not provided, uses today's date.
|
||||
"""
|
||||
|
||||
org = (os.getenv("COVER_PAGE_ORGANIZATION") or "").strip() or metadata.get("Organization") if metadata else None
|
||||
date = (metadata.get("Date") if metadata else None) or ""
|
||||
prepared_by = (metadata.get("Prepared by") if metadata else None) or ""
|
||||
reference = (metadata.get("Reference") if metadata else None) or ""
|
||||
# 5 blank lines
|
||||
_add_blank_lines(doc, 5)
|
||||
|
||||
# Title
|
||||
p = doc.add_paragraph()
|
||||
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
p.paragraph_format.space_after = Pt(6)
|
||||
run = p.add_run(title.upper())
|
||||
run.bold = True
|
||||
run.font.name = "Courier"
|
||||
run.font.size = Pt(18)
|
||||
|
||||
# Subtitle
|
||||
if subtitle:
|
||||
p = doc.add_paragraph()
|
||||
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
p.paragraph_format.space_after = Pt(12)
|
||||
run = p.add_run(subtitle)
|
||||
run.font.name = "Courier"
|
||||
run.font.size = Pt(14)
|
||||
|
||||
# Optional logo placeholder (text-only for now; can be extended)
|
||||
if include_logo:
|
||||
# Logo (if COVER_PAGE_LOGO_URL is set)
|
||||
logo_url = (os.getenv("COVER_PAGE_LOGO_URL") or "").strip()
|
||||
logo_path = (os.getenv("COVER_PAGE_LOGO_PATH") or "").strip()
|
||||
# For now, just reserve space; image insertion can be added later.
|
||||
if logo_url:
|
||||
img_bytes = _download_image_bytes(logo_url)
|
||||
if img_bytes:
|
||||
p = doc.add_paragraph()
|
||||
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
p.paragraph_format.space_after = Pt(12)
|
||||
try:
|
||||
# Add image centered
|
||||
p.add_run().add_picture(io.BytesIO(img_bytes), width=None)
|
||||
except Exception:
|
||||
# Fallback: do nothing if image fails
|
||||
pass
|
||||
|
||||
# Metadata lines
|
||||
if org or date or prepared_by or reference:
|
||||
p = doc.add_paragraph()
|
||||
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
p.paragraph_format.space_after = Pt(4)
|
||||
# 3 blank lines
|
||||
_add_blank_lines(doc, 3)
|
||||
|
||||
# Title in uppercase
|
||||
_add_centered_paragraph(doc, text=title.upper().strip(), font_size=Pt(18), bold=True)
|
||||
|
||||
# Date of transcription
|
||||
if not date_str:
|
||||
date_str = datetime.utcnow().strftime("%B %d, %Y")
|
||||
_add_centered_paragraph(doc, text=date_str, font_size=Pt(14))
|
||||
|
||||
# Created by + organization
|
||||
org = (os.getenv("COVER_PAGE_ORGANIZATION") or "").strip()
|
||||
if org:
|
||||
r = p.add_run(org)
|
||||
r.font.name = "Courier"
|
||||
r.font.size = Pt(12)
|
||||
if date:
|
||||
if org:
|
||||
p.add_run("\n")
|
||||
r = p.add_run(date)
|
||||
r.font.name = "Courier"
|
||||
r.font.size = Pt(12)
|
||||
_add_centered_paragraph(doc, text=f"Created by {org}", font_size=Pt(12))
|
||||
|
||||
if prepared_by or reference:
|
||||
p = doc.add_paragraph()
|
||||
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
p.paragraph_format.space_after = Pt(4)
|
||||
if prepared_by:
|
||||
r = p.add_run(f"Prepared by: {prepared_by}")
|
||||
r.font.name = "Courier"
|
||||
r.font.size = Pt(11)
|
||||
if reference:
|
||||
if prepared_by:
|
||||
p.add_run("\n")
|
||||
r = p.add_run(f"Reference: {reference}")
|
||||
r.font.name = "Courier"
|
||||
r.font.size = Pt(11)
|
||||
# 3 blank lines
|
||||
_add_blank_lines(doc, 3)
|
||||
|
||||
# Page break after cover page
|
||||
# Disclaimer
|
||||
disclaimer = (
|
||||
"Disclaimer\n"
|
||||
"This transcription and any summary derived therefrom may contain errors or inaccuracies."
|
||||
)
|
||||
_add_centered_paragraph(doc, text=disclaimer, font_size=Pt(10))
|
||||
|
||||
# Page break after cover page so main content starts on page 2
|
||||
_add_page_break(doc)
|
||||
|
||||
@@ -507,14 +507,14 @@ def create_transcript_docx(text: str, filename: str):
|
||||
|
||||
# Step 3: Optionally add cover page
|
||||
from . import docx_cover
|
||||
from datetime import datetime
|
||||
cover_enabled = os.getenv("COVER_PAGE_ENABLED", "false").strip().lower() in ("true", "1", "yes")
|
||||
if cover_enabled:
|
||||
date_str = datetime.utcnow().strftime("%B %d, %Y")
|
||||
docx_cover.add_cover_page(
|
||||
doc,
|
||||
title="TRANSCRIPT",
|
||||
subtitle=None,
|
||||
metadata=None,
|
||||
include_logo=True,
|
||||
date_str=date_str,
|
||||
)
|
||||
|
||||
# Step 4: Write prepared pages into DOCX
|
||||
@@ -600,14 +600,14 @@ def create_summary_docx(text: str, filename: str):
|
||||
|
||||
# Optionally add cover page
|
||||
from . import docx_cover
|
||||
from datetime import datetime
|
||||
cover_enabled = os.getenv("COVER_PAGE_ENABLED", "false").strip().lower() in ("true", "1", "yes")
|
||||
if cover_enabled:
|
||||
date_str = datetime.utcnow().strftime("%B %d, %Y")
|
||||
docx_cover.add_cover_page(
|
||||
doc,
|
||||
title="SUMMARY",
|
||||
subtitle=None,
|
||||
metadata=None,
|
||||
include_logo=True,
|
||||
date_str=date_str,
|
||||
)
|
||||
|
||||
# Add summary content using markdown-aware styling
|
||||
|
||||
Reference in New Issue
Block a user