TutorialMaker / pipeline /docx_builder.py
vivekchakraverty's picture
Replace keyword density with primary/secondary keyword placement + AEO structure
01abd01
Raw
History Blame Contribute Delete
3.71 kB
"""Stage 9: assemble the .docx tutorial (text + captioned screenshots).
Layout follows answer-engine best practices: H1 title, an SEO metadata line
(slug / meta description / last-updated), an answer-first "Quick answer" block, the
intro, H2 step sections with captioned screenshots, an FAQ section, and a sources/
trust footer citing the source video.
"""
from __future__ import annotations
import datetime
import os
from docx import Document
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.shared import Inches, Pt, RGBColor
_GREY = RGBColor(0x66, 0x66, 0x66)
_LIGHT = RGBColor(0x88, 0x88, 0x88)
def _fmt_ts(seconds: float) -> str:
m, s = divmod(int(seconds), 60)
h, m = divmod(m, 60)
return f"{h:d}:{m:02d}:{s:02d}" if h else f"{m:d}:{s:02d}"
def _grey_line(doc, text: str, size: int = 9, color: RGBColor = _GREY):
p = doc.add_paragraph()
run = p.add_run(text)
run.italic = True
run.font.size = Pt(size)
run.font.color.rgb = color
return p
def build_docx(tutorial: dict, selected: dict[int, dict], captions: dict[int, str],
out_path: str, source_url: str = "") -> str:
"""Write the tutorial to ``out_path`` and return it.
``tutorial`` = {title, slug, meta_description, answer, intro, steps, faqs};
``selected`` = {step_idx: {time, path}}; ``captions`` = {step_idx: caption}.
"""
today = datetime.date.today().isoformat()
doc = Document()
doc.add_heading(tutorial["title"], level=0)
# SEO metadata line (slug + last-updated) and meta description.
meta_bits = []
if tutorial.get("slug"):
meta_bits.append(f"Slug: /{tutorial['slug']}/")
meta_bits.append(f"Last updated: {today}")
_grey_line(doc, " • ".join(meta_bits))
if tutorial.get("meta_description"):
_grey_line(doc, f"Meta description: {tutorial['meta_description']}")
# Answer-first block (quotable by AI assistants).
if tutorial.get("answer"):
doc.add_heading("Quick answer", level=2)
ans = doc.add_paragraph()
ans.add_run(tutorial["answer"]).bold = True
if tutorial.get("intro"):
doc.add_paragraph(tutorial["intro"])
# Step sections.
for i, step in enumerate(tutorial["steps"]):
doc.add_heading(f"{i + 1}. {step['heading']}", level=1)
if step.get("body"):
doc.add_paragraph(step["body"])
sel = selected.get(i)
if sel and os.path.exists(sel["path"]):
pic_par = doc.add_paragraph()
pic_par.alignment = WD_ALIGN_PARAGRAPH.CENTER
pic_par.add_run().add_picture(sel["path"], width=Inches(6))
cap_par = doc.add_paragraph()
cap_par.alignment = WD_ALIGN_PARAGRAPH.CENTER
cap_run = cap_par.add_run(f"{captions.get(i, '')} ")
cap_run.italic = True
cap_run.font.size = Pt(9)
ts_run = cap_par.add_run(f"(@ {_fmt_ts(sel['time'])})")
ts_run.italic = True
ts_run.font.size = Pt(9)
ts_run.font.color.rgb = _LIGHT
# FAQ section (visible Q&A — AEO/FAQPage-friendly).
if tutorial.get("faqs"):
doc.add_heading("Frequently asked questions", level=1)
for f in tutorial["faqs"]:
doc.add_heading(f["q"], level=2)
doc.add_paragraph(f["a"])
# Sources / trust footer (E-E-A-T: citation + freshness).
doc.add_heading("Sources", level=1)
if source_url:
_grey_line(doc, f"Adapted from the source video: {source_url}", color=_GREY)
_grey_line(doc, f"Generated and last updated: {today}.", color=_GREY)
os.makedirs(os.path.dirname(os.path.abspath(out_path)), exist_ok=True)
doc.save(out_path)
return out_path